Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions python/packages/hosting-mcp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ authentication, authorization, session-key policy, concurrency policy, or
outbound delivery. Applications compose the adapter and conversion helpers
with native MCP SDK constructs.

`AgentMCPTool` owns only the schema for its single generated agent tool. It
does not register that schema with a server. Applications call
`await adapter.list_tools()` and `await adapter.call_tool(...)` from native MCP
handlers.
`AgentMCPTool` generates and validates the schema for its single agent tool.
`call_tool(...)` rejects schema-invalid arguments before conversion, agent
execution, or session-store access. It does not register that schema with a
server. Applications call `await adapter.list_tools()` and
`await adapter.call_tool(...)` from native MCP handlers. Direct
`mcp_to_run(...)` conversion does not perform schema validation.

Parameter schemas are trusted application configuration. Validation is
synchronous and has no execution timeout; applications must choose regex
patterns with predictable matching time for caller-controlled values and
property names. Schema enforcement does not provide execution isolation.

`WorkflowMCPTool` owns only the schema derived from the start executor. It
requires exactly one input type. Workflow factories and continuation policy
Expand Down
55 changes: 47 additions & 8 deletions python/packages/hosting-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ contract. Define it once and use the same value in both the native tool schema
and `mcp_to_run(...)`:

```python
from jsonschema import validate
from mcp.types import Tool

from agent_framework_hosting_mcp import mcp_to_run

agent_input_argument = "task"
chat_option_arguments = {
"reasoning_effort": {
Expand All @@ -34,7 +39,7 @@ chat_option_arguments = {

tool = Tool(
name="run_agent",
inputSchema={
input_schema={
"type": "object",
"properties": {
agent_input_argument: {"type": "string"},
Expand All @@ -43,6 +48,7 @@ tool = Tool(
"required": [agent_input_argument],
},
)
validate(instance=arguments, schema=tool.input_schema)
run = mcp_to_run(
arguments,
argument_name=agent_input_argument,
Expand All @@ -52,13 +58,19 @@ run = mcp_to_run(

Only names listed in `chat_option_arguments` are copied to `run["options"]`;
other MCP arguments remain available in the message's raw representation but
are not forwarded to the model client. The native MCP schema remains
responsible for validating exposed option types and ranges.
are not forwarded to the model client. Applications using the conversion
helper directly must validate arguments against their tool schema before
conversion. Declaring a schema does not enforce it in the low-level MCP server.

For an agent exposed as one MCP tool, use the adapter so the schema and
conversion cannot drift:

```python
from mcp import types
from mcp.server import Server, ServerRequestContext

from agent_framework_hosting_mcp import AgentMCPTool

agent_tool = AgentMCPTool(
agent,
name="run_agent",
Expand All @@ -72,20 +84,47 @@ agent_tool = AgentMCPTool(
},
)

@server.list_tools()
async def list_tools():

async def list_tools(
_ctx: ServerRequestContext[dict[str, object]],
_params: types.PaginatedRequestParams | None,
) -> types.ListToolsResult:
return await agent_tool.list_tools()

@server.call_tool()
async def call_tool(name, arguments):
return await agent_tool.call_tool(name, arguments)

async def call_tool(
_ctx: ServerRequestContext[dict[str, object]],
params: types.CallToolRequestParams,
) -> types.CallToolResult:
return await agent_tool.call_tool(params.name, params.arguments)


server = Server("agent-server", on_list_tools=list_tools, on_call_tool=call_tool)
```

`AgentMCPTool` uses the agent's name and description unless overridden.
`parameters` adds app-owned JSON Schema properties that remain available in the
raw MCP arguments. `chat_option_parameters` adds properties and explicitly
copies their values into Agent Framework chat options.

`call_tool(...)` validates the complete generated input schema, including
nested constraints, before conversion, agent execution, or session-store
access. Invalid arguments raise `MCPError` with `INVALID_PARAMS`. Valid exposed
chat options still override the agent's defaults. The adapter's
`mcp_to_run(...)` method remains a conversion helper, not a schema validator.

Treat `parameters` and `chat_option_parameters` as trusted application
configuration, not client-supplied schemas. Validation runs synchronously on
the server event loop and has no execution timeout. JSON Schema `pattern` and
`patternProperties` use Python's backtracking regular-expression engine.
Unsafe configured patterns can let caller-controlled values or property names
stall unrelated requests. Choose and test patterns with predictable matching
time for untrusted inputs; request-size limits alone do not make a
backtracking pattern safe. Applications requiring a hard validation deadline
must use an execution boundary that can be terminated, such as a separate
process. An asyncio timeout or worker thread cannot interrupt a running
standard-library regex match.

For a workflow exposed as one MCP tool, use `WorkflowMCPTool`:

```python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from agent_framework import AgentResponse, Message, SupportsAgentRun
from agent_framework._telemetry import mark_feature_used
from agent_framework_hosting import AgentRunArgs, AgentState
from jsonschema import ValidationError, validate
from mcp import MCPError, types

from ._conversion import mcp_from_run, mcp_to_run
Expand All @@ -22,10 +23,10 @@
class AgentMCPTool(Generic[AgentT]):
"""Expose one Agent Framework agent through an app-owned MCP server.

The adapter generates the native MCP tool definition and keeps its argument
parsing aligned with agent execution. It does not create an MCP server,
register handlers, derive trusted session identifiers, or choose a
transport.
The adapter generates the native MCP tool definition and validates its
arguments before agent execution or session-store access. It does not
create an MCP server, register handlers, derive trusted session identifiers,
or choose a transport.
"""

def __init__(
Expand Down Expand Up @@ -146,14 +147,20 @@ async def call_tool(
Native MCP ``CallToolResult`` for the completed tool result.

Raises:
MCPError: If the tool name or configured session id is invalid.
MCPError: If the tool name or arguments are invalid.
jsonschema.SchemaError: If the configured input schema is invalid.
"""
mark_feature_used(FeatureIndex.HOSTING_MCP)
target = await self.state.get_target()
tool = self._tool_for_target(target)
if name != tool.name:
raise MCPError(types.INVALID_PARAMS, f"Unknown MCP tool: {name}")

try:
validate(instance=dict(arguments or {}), schema=tool.input_schema)
Comment thread
eavanvalkenburg marked this conversation as resolved.
Resolved
except ValidationError as exc:
raise MCPError(types.INVALID_PARAMS, f"Invalid MCP tool arguments: {exc.message}") from exc

run = self.mcp_to_run(arguments)
if self.session_id_parameter is None:
result = cast(
Expand Down
1 change: 1 addition & 0 deletions python/packages/hosting-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers = [
dependencies = [
"agent-framework-core>=1.19.0,<2",
"agent-framework-hosting==1.0.0a261030",
"jsonschema>=4.20.0,<5",
"mcp>=2.2.0,<3",
"pydantic>=2,<3",
]
Expand Down
Loading
Loading