Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
Self-hosting MCP tool support in .NET is coming soon.
Note
Self-hosting MCP tool support is not currently available for Go.
Use agent-framework-hosting-mcp to expose an Agent Framework agent or workflow as a tool on the native Model Context Protocol SDK. The package does not choose a web framework or wrap the MCP SDK server lifecycle; your application still owns the Server, handler registration, transport, session-key policy, authentication, authorization, and deployment.
pip install --pre agent-framework-hosting-mcp
Convert at the protocol boundary
mcp_to_run(...) converts validated MCP tool arguments into Agent Framework messages and selected chat options, and mcp_from_run(...) converts a completed response into native MCP ContentBlock values. Use these two functions directly when an application's tool contract needs a fully custom native schema and handler:
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""Return the app-owned native MCP tool definition."""
return [
types.Tool(
name="run_agent_manually",
description=agent.description or "",
inputSchema={
"type": "object",
"properties": {
TASK_ARGUMENT: {
"type": "string",
"description": "The request for the hosted agent.",
},
**CHAT_OPTION_ARGUMENTS,
},
"required": [TASK_ARGUMENT],
"additionalProperties": False,
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, object] | None) -> list[types.ContentBlock]:
"""Convert, run, and render without the agent-backed adapter."""
if name != "run_agent_manually":
raise ValueError(f"Unknown MCP tool: {name}")
run = mcp_to_run(
arguments,
argument_name=TASK_ARGUMENT,
chat_option_arguments=CHAT_OPTION_ARGUMENTS,
)
result = await agent.run(run["messages"], options=run["options"])
return mcp_from_run(result)
Only argument names listed in chat_option_arguments are copied into run["options"]; other MCP arguments stay available on the message's raw representation but aren't forwarded to the model client.
Host an agent as one generated tool
AgentMCPTool derives the native tool name, description, and schema from an agent, and keeps listing, parsing, execution, and result conversion aligned so the two can't drift:
agent_tool = AgentMCPTool(
agent,
name="run_agent",
argument_description="The request for the hosted agent.",
chat_option_parameters={
"reasoning_effort": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Optional reasoning effort for models that support it.",
}
},
)
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""Describe the app-owned MCP tool schema."""
return await agent_tool.list_tools()
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, object] | None) -> list[types.ContentBlock]:
"""Run the app-owned tool with native MCP and Agent Framework values."""
return await agent_tool.call_tool(name, arguments)
AgentMCPTool uses the agent's name and description unless overridden. parameters adds app-owned JSON Schema properties that stay available in the raw MCP arguments, and chat_option_parameters adds properties whose values are explicitly copied into Agent Framework chat options.
Persist a session per call
Pass an existing AgentState and a session_id_parameter to let repeated calls with the same opaque, app-defined session_id continue one conversation:
session_locks: dict[str, asyncio.Lock] = {}
@server.list_tools()
async def list_tools() -> list[types.Tool]:
"""Return the agent-derived MCP tool definition."""
return await agent_tool.list_tools()
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, object] | None) -> list[types.ContentBlock]:
"""Serialize calls per app-owned session before using ``AgentState``."""
session_id = arguments.get("session_id") if arguments else None
if not isinstance(session_id, str) or not session_id:
raise ValueError("MCP tool argument 'session_id' must be a non-empty string.")
lock = session_locks.setdefault(session_id, asyncio.Lock())
async with lock:
return await agent_tool.call_tool(name, arguments)
AgentMCPTool only performs the AgentState session get/run/set sequence; your application must authenticate or authorize the session identifier and serialize concurrent calls for the same session, as the sample does with a per-session asyncio.Lock. This isn't previous_response_id-style branching — an application that needs to fork a conversation should accept separate source and destination IDs, copy the source session, and store the result under the destination key.
Host a workflow as a tool
WorkflowMCPTool derives one native MCP tool from a workflow's start-executor input type and converts completed workflow outputs. Dataclass, Pydantic, and other object-shaped inputs become top-level MCP arguments; primitive inputs are wrapped in a configurable argument name:
server = Server("agent-framework-hosting-mcp-workflow-sample")
workflow_tool = WorkflowMCPTool(
WorkflowState(create_workflow, cache_target=False),
name="draft_content",
)
Workflow instances preserve execution state, so applications that need independent calls should supply a WorkflowState factory with cache_target=False, as shown above. Checkpoint restoration, human-in-the-loop responses, and continuation identifiers remain application-owned; if a workflow requests external input, the adapter raises instead of returning an empty successful tool result.
For the complete set of runnable servers — including the FastMCP variant that derives its schema from a decorated function — see the MCP hosting samples.
Important
Treat the MCP session identifier and any app-defined session_id argument as untrusted input. Authenticate and authorize the caller before using either to load or save session state, and derive durable partitioning from the authenticated tenant, user, or workspace rather than the raw value.
Next steps
Go deeper: