返回提交历史
Modified
g4f/mcp/pa_provider.py
+9
-6
Modified
g4f/mcp/server.py
+74
-1
Modified
g4f/mcp/tools.py
+5
-1
XFEstudio/gpt4free
Add PA file handling and secure serving in MCPServer; update FileWriteTool to include file URL
b09317fe
代码差异
3 个文件
+88
-8
@@ -188,7 +188,12 @@ def _exec_in_thread(
188
188
prev = sys.getrecursionlimit()
189
189
sys.setrecursionlimit(max_depth)
190
190
try:
191
exec(compiled, safe_globals, local_vars) # noqa: S102
191
# Use safe_globals as both globals and locals so that top-level
192
# imports (e.g. ``from aiohttp import ClientSession``) are stored in
193
# the same namespace that class method ``__globals__`` points to.
194
# Otherwise imports land only in the locals dict and are invisible
195
# to class methods (NameError at call time).
196
exec(compiled, safe_globals, safe_globals) # noqa: S102
192
197
except Exception: # noqa: BLE001
193
198
exc_box.append(traceback.format_exc())
194
199
finally:
@@ -349,8 +354,6 @@ def execute_safe_code(
349
354
if extra_globals:
350
355
safe_globals.update(extra_globals)
351
356
352
local_vars: Dict[str, Any] = {}
353
354
357
# Compile outside the thread so SyntaxErrors surface immediately.
355
358
try:
356
359
compiled = compile(code, "<pa_provider>", "exec")
@@ -369,7 +372,7 @@ def execute_safe_code(
369
372
exc_box: List = []
370
373
thread = threading.Thread(
371
374
target=_exec_in_thread,
372
args=(compiled, safe_globals, local_vars, max_depth, exc_box),
375
args=(compiled, safe_globals, safe_globals, max_depth, exc_box),
373
376
daemon=True,
374
377
name="g4f-sandbox",
375
378
)
@@ -410,8 +413,8 @@ def execute_safe_code(
410
413
success=True,
411
414
stdout=stdout,
412
415
stderr=stderr,
413
result=local_vars.get("result"),
414
locals=local_vars,
416
result=safe_globals.get("result"),
417
locals=safe_globals,
415
418
)
416
419
417
420
@@ -392,6 +392,75 @@ class MCPServer:
392
392
sys.stderr.write(f"Synthesize error: {e}\n")
393
393
return web.Response(status=500, text=f"Synthesize error: {str(e)}")
394
394
395
_WORKSPACE_SAFE_TYPES: Dict[str, str] = {
396
"html": "text/html; charset=utf-8",
397
"htm": "text/html; charset=utf-8",
398
"css": "text/css; charset=utf-8",
399
"js": "application/javascript; charset=utf-8",
400
"mjs": "application/javascript; charset=utf-8",
401
"json": "application/json; charset=utf-8",
402
"txt": "text/plain; charset=utf-8",
403
"md": "text/markdown; charset=utf-8",
404
"svg": "image/svg+xml",
405
"png": "image/png",
406
"jpg": "image/jpeg",
407
"jpeg": "image/jpeg",
408
"gif": "image/gif",
409
"webp": "image/webp",
410
"ico": "image/x-icon",
411
"woff": "font/woff",
412
"woff2": "font/woff2",
413
"ttf": "font/ttf",
414
"otf": "font/otf",
415
}
416
417
async def handle_pa_providers(request: web.Request) -> web.Response:
418
"""List all PA providers from workspace."""
419
from .pa_provider import get_pa_registry
420
providers = get_pa_registry().list_providers()
421
return web.json_response(providers, headers={"access-control-allow-origin": "*"})
422
423
async def handle_pa_file(request: web.Request) -> web.Response:
424
"""Securely serve a workspace file for browser rendering.
425
426
Only files within ``~/.g4f/workspace`` are served. Path traversal
427
is blocked. Only the MIME types in ``_WORKSPACE_SAFE_TYPES`` are
428
allowed — ``.py``, ``.env``, and other sensitive types return 403.
429
HTML files are served with a ``Content-Security-Policy: sandbox``
430
header so they run in an isolated null origin.
431
"""
432
from .pa_provider import get_workspace_dir
433
workspace = get_workspace_dir()
434
435
file_path = request.match_info.get("file_path", "")
436
try:
437
resolved = (workspace / file_path).resolve()
438
resolved.relative_to(workspace.resolve())
439
except (ValueError, Exception):
440
return web.Response(status=403, text="Path traversal is not allowed")
441
442
if not resolved.exists() or not resolved.is_file():
443
return web.Response(status=404, text=f"File not found: {file_path}")
444
445
ext = resolved.suffix.lstrip(".").lower()
446
mime = _WORKSPACE_SAFE_TYPES.get(ext)
447
if mime is None:
448
return web.Response(status=403, text=f"File type not allowed: .{ext}")
449
450
content = resolved.read_bytes()
451
headers: Dict[str, str] = {"access-control-allow-origin": "*"}
452
if ext in ("html", "htm"):
453
req_origin = f"{request.scheme}://{request.host}"
454
headers["content-security-policy"] = (
455
f"sandbox allow-scripts allow-forms allow-popups; "
456
f"default-src {req_origin}; "
457
f"img-src {req_origin} data: blob:; "
458
f"font-src {req_origin}; "
459
f"style-src {req_origin} 'unsafe-inline'; "
460
f"script-src {req_origin} 'unsafe-inline'"
461
)
462
return web.Response(body=content, content_type=mime.split(";")[0].strip(), headers=headers)
463
395
464
# Create aiohttp application
396
465
app = web.Application()
397
466
app.router.add_options('/mcp', lambda request: web.Response(headers={"access-control-allow-origin": "*", "access-control-allow-methods": "POST, OPTIONS", "access-control-allow-headers": "Content-Type"}))
@@ -399,13 +468,17 @@ class MCPServer:
399
468
app.router.add_get('/health', handle_health)
400
469
app.router.add_get('/media/{filename:.*}', handle_media)
401
470
app.router.add_get('/backend-api/v2/synthesize/{provider}', handle_synthesize)
402
471
app.router.add_get('/pa/providers', handle_pa_providers)
472
app.router.add_get('/pa/files/{file_path:.*}', handle_pa_file)
473
403
474
# Start server
404
475
sys.stderr.write(f"Starting {self.server_info['name']} v{self.server_info['version']} (HTTP mode)\n")
405
476
sys.stderr.write(f"Listening on http://{host}:{port}\n")
406
477
sys.stderr.write(f"MCP endpoint: http://{host}:{port}/mcp\n")
407
478
sys.stderr.write(f"Health check: http://{host}:{port}/health\n")
408
479
sys.stderr.write(f"Media files: http://{host}:{port}/media/{{filename}}\n")
480
sys.stderr.write(f"PA providers: http://{host}:{port}/pa/providers\n")
481
sys.stderr.write(f"PA files: http://{host}:{port}/pa/files/{{path}}\n")
409
482
sys.stderr.flush()
410
483
411
484
runner = web.AppRunner(app)
@@ -682,11 +682,15 @@ class FileWriteTool(MCPTool):
682
682
f.write(content)
683
683
else:
684
684
target.write_text(content, encoding="utf-8")
685
return {
685
result: Dict[str, Any] = {
686
686
"path": rel_path,
687
687
"size": len(content),
688
688
"appended": append,
689
689
}
690
origin = arguments.get("origin")
691
if origin:
692
result["url"] = f"{origin}/pa/files/{rel_path}"
693
return result
690
694
except Exception as exc:
691
695
return {"error": f"Write failed: {exc}"}
692
696