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
1 change: 1 addition & 0 deletions .github/workflows/python-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ jobs:
packages/hyperlight/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/python-merge-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
- 'python/packages/ollama/**'
- 'python/packages/core/agent_framework/_mcp.py'
- 'python/packages/core/tests/core/test_mcp.py'
- 'python/packages/hosting-mcp/**'
- 'python/scripts/local_mcp_streamable_http_server.py'
- '.github/actions/setup-local-mcp-server/**'
- '.github/workflows/python-merge-tests.yml'
Expand Down Expand Up @@ -345,6 +346,7 @@ jobs:
packages/hyperlight/tests
packages/ollama/tests
packages/core/tests/core/test_mcp.py
packages/hosting-mcp/tests
-m integration
-n logical --dist worksteal
--timeout=120 --session-timeout=900 --timeout_method thread
Expand Down
73 changes: 73 additions & 0 deletions docs/specs/002-python-hosting-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ must be aligned with the helper-first model before implementation. Old vocabular
|---|---|---|
| `agent-framework-hosting` | `agent_framework_hosting` | `AgentState`, `WorkflowState`, `SessionStore`, and run-argument `TypedDict`s. |
| `agent-framework-hosting-a2a` | `agent_framework_hosting_a2a` | A2A `Message` to run conversion and Agent Framework output to A2A `Part` conversion. |
| `agent-framework-hosting-mcp` | `agent_framework_hosting_mcp` | Agent and workflow MCP tool adapters, MCP tool arguments to run conversion, and Agent Framework output to MCP `ContentBlock` conversion. |
| `agent-framework-hosting-responses` | `agent_framework_hosting_responses` | Responses helpers: request parsing, session id extraction, response id creation, response rendering, streaming rendering. |
| `agent-framework-hosting-telegram` | `agent_framework_hosting_telegram` | Telegram Bot API helpers: update parsing, chat/session/command/media extraction, final rendering, and streaming edit rendering. |
| Future protocol packages | e.g. `agent_framework_hosting_activity_protocol` | Protocol-specific helpers such as `activity_to_run(...)`, `activity_from_run(...)`, `activity_session_id(...)`, and command/media helpers when useful. |
Expand Down Expand Up @@ -272,6 +273,78 @@ artifact-id policy, or session-key policy. Application code composes the two
helpers with those native A2A SDK constructs and may use any server framework
supported by the SDK.

## `agent-framework-hosting-mcp`

The MCP package provides only the conversion seam between native MCP SDK values
and Agent Framework:

- `MCPAgentTool(target, ...)`
- `MCPWorkflowTool(target, ...)`
- `mcp_to_run(arguments, *, argument_name="task", chat_option_arguments=()) -> AgentRunArgs`
- `mcp_from_run(result) -> list[mcp.types.ContentBlock]`

`MCPAgentTool` represents one Agent Framework agent as one native MCP tool. It
derives the default tool name and description from the agent, accepts
overrides for those values and the main text parameter, includes app-owned
additional parameter schemas, and explicitly maps selected parameter schemas
to ChatOptions. Its asynchronous `list_tools()` returns the native `Tool` list,
and `call_tool(...)` performs conversion, agent execution, and final result
conversion.

The adapter accepts either an agent or an existing `AgentState`. With a
configured `session_id_parameter`, it loads and stores the corresponding
`AgentSession`. The application remains responsible for deriving and
authorizing the session id and preventing concurrent updates to the same
session.

`MCPWorkflowTool` represents one Agent Framework workflow as one native MCP
tool. It derives the tool name and description from the workflow and derives
the input schema from the start executor's single declared input type.
Object-shaped dataclass and Pydantic inputs become top-level MCP arguments;
primitive inputs are wrapped in one configurable argument. The adapter
validates the arguments against that type, runs the workflow, and converts
terminal outputs to MCP content blocks.

Workflow instances preserve state and reject concurrent runs. Applications
that need independent calls should provide a `WorkflowState` factory with
`cache_target=False`. Checkpoint restoration, human-in-the-loop responses, and
continuation identifiers remain application-owned contracts. If a workflow
stops to request external input, the adapter raises rather than returning an
empty successful tool result.

`mcp_to_run(...)` accepts the argument mapping from a native MCP `call_tool`
handler. The application owns the tool schema and may select which required
string argument contains the user request. The application should define that
argument name once and use the same value in the native tool schema and the
`argument_name` parameter so those two sides of the contract remain aligned.
Applications may also expose selected ChatOptions fields in their native tool
schema and pass those names through `chat_option_arguments`. Only explicitly
selected names are copied to run options; the helper does not forward all MCP
arguments or own their JSON Schema validation.

MCP `tools/call` arguments are JSON-only and do not have a native multimodal
content-block union. The package does not impose a non-standard JSON
representation for multimodal tool arguments.

`mcp_from_run(...)` accepts an `AgentResponse` or `Message`. It converts text,
URI, image data, audio data, and other binary data into native MCP content
blocks.

Its output is specifically the content union accepted by `CallToolResult`.
Sampling-only values such as `ToolUseContent` belong to the separate MCP
sampling response path and are not emitted by this hosting helper.

MCP `tools/call` returns one final `CallToolResult`. Streamable HTTP can carry
multiple MCP messages and progress notifications can report operation status,
but the protocol does not define partial tool-result content chunks.
Experimental MCP tasks defer retrieval of the same final result. Therefore the
conversion helpers do not expose Agent Framework streaming updates.

The package does not provide an MCP `Server`, handler registration, transport, route,
session policy, authentication, authorization, or deployment wrapper.
Application code composes the adapters and conversion helpers with native MCP SDK constructs and
may use stdio, streamable HTTP, or another transport supported by the SDK.

## `agent-framework-hosting-telegram`

The Telegram package provides side-effect-free helpers around Telegram Bot API
Expand Down
1 change: 1 addition & 0 deletions python/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ python/
### Protocols & UI
- [a2a](packages/a2a/AGENTS.md) - Agent-to-Agent protocol
- [hosting-a2a](packages/hosting-a2a/AGENTS.md) - A2A hosting conversion helpers
- [hosting-mcp](packages/hosting-mcp/AGENTS.md) - MCP hosting conversion helpers
- [ag-ui](packages/ag-ui/AGENTS.md) - AG-UI protocol
- [chatkit](packages/chatkit/AGENTS.md) - OpenAI ChatKit integration
- [devui](packages/devui/AGENTS.md) - Developer UI for testing
Expand Down
1 change: 1 addition & 0 deletions python/PACKAGE_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Status is grouped into these buckets:
| `agent-framework-github-copilot` | `python/packages/github_copilot` | `rc` |
| `agent-framework-hosting` | `python/packages/hosting` | `alpha` |
| `agent-framework-hosting-a2a` | `python/packages/hosting-a2a` | `alpha` |
| `agent-framework-hosting-mcp` | `python/packages/hosting-mcp` | `alpha` |
| `agent-framework-hosting-responses` | `python/packages/hosting-responses` | `alpha` |
| `agent-framework-hosting-telegram` | `python/packages/hosting-telegram` | `alpha` |
| `agent-framework-hyperlight` | `python/packages/hyperlight` | `beta` |
Expand Down
53 changes: 53 additions & 0 deletions python/packages/hosting-mcp/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# MCP Hosting Helpers (`agent-framework-hosting-mcp`)

Side-effect-free adapters and conversion helpers for hosting Agent Framework
agents and workflows through the native MCP SDK.

## Public API

- `AgentMCPTool(target, ...)` generates one native MCP `Tool` from an agent,
converts and executes calls, and optionally persists sessions through an
existing `AgentState`.
- `WorkflowMCPTool(target, ...)` derives one native MCP `Tool` from a workflow's
single start-executor input type and converts completed workflow outputs.
- `mcp_to_run(arguments, *, argument_name="task",
chat_option_arguments=())` converts MCP tool arguments to `AgentRunArgs` and
copies only explicitly selected arguments into chat options.
- `mcp_from_run(result)` converts an Agent Framework response or message to MCP
`ContentBlock` values.

## Boundary

This package does not provide a server, routes, transport lifecycle,
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.

`WorkflowMCPTool` owns only the schema derived from the start executor. It
requires exactly one input type. Workflow factories and continuation policy
remain application-owned. Pending external-input requests raise because the
adapter does not own a human-in-the-loop continuation contract.

When configured with `AgentState`, the adapter performs session get/run/set.
Applications still derive and authorize the session identifier and serialize
concurrent calls for the same session.

MCP tool arguments are JSON-only and have no native multimodal content-block
union. Do not add a package-owned JSON convention for image or audio input.

`mcp_from_run(...)` intentionally returns a flat content block list. It
preserves content-level metadata, while applications own MCP result-level
metadata and structured content.

The helper targets `CallToolResult.content`. Do not add sampling-only
`ToolUseContent` to its output; MCP sampling has a separate response content
union.

`CallToolResult` is a single final result. Streamable HTTP transports MCP
messages rather than partial result content; progress notifications and
experimental tasks remain application-owned protocol concerns.
21 changes: 21 additions & 0 deletions python/packages/hosting-mcp/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
174 changes: 174 additions & 0 deletions python/packages/hosting-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# agent-framework-hosting-mcp

MCP conversion helpers for app-owned Agent Framework hosting.

The package deliberately does not choose a web framework or wrap the MCP SDK
server lifecycle. It provides two conversion functions and small adapters:

- `mcp_to_run(...)` converts native MCP tool arguments into Agent Framework run
arguments.
- `mcp_from_run(...)` converts an `AgentResponse` or `Message` into native MCP
`ContentBlock` values.
- `AgentMCPTool(...)` generates the native `Tool` definition from an agent and
keeps listing, parsing, execution, result conversion, and optional
`AgentState` session persistence aligned.
- `WorkflowMCPTool(...)` generates the native `Tool` definition from a
workflow's start-executor input type and converts completed workflow outputs.

Application code keeps ownership of the MCP SDK's `Server`, handler
registration, request context, transport, session-key policy, authentication,
authorization, and deployment.

For direct conversion, the argument name is part of the app-owned MCP tool
contract. Define it once and use the same value in both the native tool schema
and `mcp_to_run(...)`:

```python
agent_input_argument = "task"
chat_option_arguments = {
"reasoning_effort": {
"type": "string",
"enum": ["low", "medium", "high"],
},
}

tool = Tool(
name="run_agent",
inputSchema={
"type": "object",
"properties": {
agent_input_argument: {"type": "string"},
**chat_option_arguments,
},
"required": [agent_input_argument],
},
)
run = mcp_to_run(
arguments,
argument_name=agent_input_argument,
chat_option_arguments=chat_option_arguments,
)
```

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.

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

```python
agent_tool = AgentMCPTool(
agent,
name="run_agent",
argument_description="The request for the hosted agent.",
parameters={"audience": {"type": "string"}},
chat_option_parameters={
"reasoning_effort": {
"type": "string",
"enum": ["low", "medium", "high"],
}
},
)

@server.list_tools()
async def list_tools():
return await agent_tool.list_tools()

@server.call_tool()
async def call_tool(name, arguments):
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 remain available in the
raw MCP arguments. `chat_option_parameters` adds properties and explicitly
copies their values into Agent Framework chat options.

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

```python
workflow_tool = WorkflowMCPTool(
WorkflowState(create_workflow, cache_target=False),
name="run_workflow",
)
```

The start executor must declare exactly one input type. Dataclass, Pydantic, and
other object-shaped inputs become the MCP tool's top-level arguments. Primitive
inputs are wrapped in the configurable `argument_name` property. The adapter
validates MCP arguments against that derived type before calling
`workflow.run(...)`.

Workflow instances preserve execution state, so applications that need
independent calls should supply a `WorkflowState` factory with
`cache_target=False`, as above. Checkpoint restoration, human-in-the-loop
responses, and continuation identifiers remain application-owned contracts.
If a workflow requests external input, the adapter raises instead of returning
an empty successful tool result.

Pass an existing `AgentState` plus `session_id_parameter` to persist an
`AgentSession`:

```python
state = AgentState(agent)
agent_tool = AgentMCPTool(
state,
parameters={"session_id": {"type": "string", "minLength": 1}},
required_parameters={"session_id"},
session_id_parameter="session_id",
)
```

The application must authenticate or authorize that session identifier and
serialize concurrent calls for the same session. The adapter only performs the
`AgentState` session-store get/run/set sequence. A configured session parameter
is always marked required in the generated MCP schema.

The session identifier is an opaque, application-defined key. Neither MCP nor
Agent Framework prescribes its format. `AgentMCPTool` treats it as the key for
one mutable conversation: each call loads that session and stores the updated
session under the same key. It does not implement
`previous_response_id`-style branching. Branching requires an app-owned
contract with separate source and destination identifiers so the application
can authorize both, copy the source session, and store the result under the
destination key.

MCP `tools/call` inputs are JSON objects defined by the app's `inputSchema`;
the protocol does not define image, audio, or resource content blocks for tool
arguments. This helper therefore converts one selected string argument and
does not impose a non-standard multimodal JSON convention.

For non-image/audio binary output, `mcp_from_run(...)` uses an app-provided
`content.additional_properties["uri"]` when present and otherwise uses the
short fallback `af://binary`; the payload itself is stored only in the MCP
resource's `blob` field.

`mcp_from_run(...)` targets `CallToolResult.content`, whose MCP content union
does not include sampling-only `ToolUseContent`. Agent Framework
`function_call` content is therefore omitted from tool results. MCP sampling
callbacks use a separate response contract and may convert function calls to
`ToolUseContent`.

MCP tool calls return one final `CallToolResult`; they do not stream partial
content blocks. Streamable HTTP may carry multiple MCP messages, and apps may
send progress notifications while work runs, but neither mechanism turns
Agent Framework response updates into incremental tool results. Experimental
MCP tasks defer retrieval of the same final result.

```python
run = mcp_to_run(arguments)
result = await agent.run(
run["messages"],
options=run["options"],
)
content = mcp_from_run(result)

# Native MCP SDK application code returns `content` from its call_tool handler.
```

The surrounding MCP application still owns the low-level `Server`, handler
registration, Starlette/FastAPI composition, stdio or streamable HTTP
transport, request authentication, session-key trust, concurrency, and
deployment.
Loading
Loading