mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-06 02:48:22 +00:00
* feat(service): expose MCP through HTTP backend Serve JSON/SSE job endpoints and streamable HTTP MCP from one FastAPI application, sharing the same jobs and application lifecycle. Preserve the standalone MCP backend, add configurable MCP HTTP settings, update startup metadata and integration docs, and cover routing, lifecycle, configuration, and compatibility behavior with unit tests. * fix(service): preserve MCP request protections Route the exact MCP path through the complete FastMCP ASGI application so its middleware and state remain active. Reject non-literal MCP paths and validate reserved Job conflicts before tolerant service registration. Add regression coverage for middleware preservation, route syntax, and startup failure. * fix(service): reject encoded MCP paths Reject percent signs in mcp_path so ASGI path decoding cannot turn an accepted configuration into an unreachable route. Cover encoded slash, space, and double-encoded slash inputs.
52 lines
1.6 KiB
Python
52 lines
1.6 KiB
Python
"""Shared MCP tool registration for services that expose ReMe jobs."""
|
|
|
|
from typing import Any
|
|
|
|
from ..job import BaseJob, StreamJob
|
|
|
|
|
|
def add_mcp_job(
|
|
server: Any,
|
|
job: BaseJob,
|
|
*,
|
|
injected_job_kwargs: dict[str, Any],
|
|
tool_error_on_failure: bool,
|
|
) -> bool:
|
|
"""Register a non-stream job as an MCP tool on ``server``."""
|
|
from fastmcp.exceptions import ToolError
|
|
from fastmcp.tools import FunctionTool
|
|
|
|
if isinstance(job, StreamJob):
|
|
return False
|
|
|
|
async def execute_tool(**kwargs):
|
|
conflicts = sorted(injected_job_kwargs.keys() & kwargs.keys())
|
|
if conflicts:
|
|
names = ", ".join(conflicts)
|
|
raise ToolError(
|
|
f"{names} injected by the MCP server and cannot be provided by the caller",
|
|
)
|
|
kwargs.update(injected_job_kwargs)
|
|
response = await job(**kwargs)
|
|
if tool_error_on_failure and not response.success:
|
|
raise ToolError(str(response.answer))
|
|
return response.answer
|
|
|
|
parameters = dict(job.parameters or {})
|
|
injected_names = injected_job_kwargs.keys()
|
|
if "properties" in parameters:
|
|
parameters["properties"] = {
|
|
name: schema for name, schema in parameters["properties"].items() if name not in injected_names
|
|
}
|
|
if "required" in parameters:
|
|
parameters["required"] = [name for name in parameters["required"] if name not in injected_names]
|
|
|
|
server.add_tool(
|
|
FunctionTool(
|
|
name=job.name,
|
|
description=job.description,
|
|
fn=execute_tool,
|
|
parameters=parameters,
|
|
),
|
|
)
|
|
return True
|