agent-framework-hosting-mcp 1.0.0a260721__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-framework-hosting-mcp
3
+ Version: 1.0.0a260721
4
+ Summary: Agent and workflow MCP tool adapters for app-owned hosting.
5
+ Author-email: Microsoft <af-support@microsoft.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ License-File: LICENSE
19
+ Requires-Dist: agent-framework-core>=1.11.0,<2
20
+ Requires-Dist: agent-framework-hosting==1.0.0a260721
21
+ Requires-Dist: mcp>=1.11.0,<2
22
+ Requires-Dist: pydantic>=2,<3
23
+ Project-URL: homepage, https://aka.ms/agent-framework
24
+ Project-URL: issues, https://github.com/microsoft/agent-framework/issues
25
+ Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
26
+ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
27
+
28
+ # agent-framework-hosting-mcp
29
+
30
+ MCP conversion helpers for app-owned Agent Framework hosting.
31
+
32
+ The package deliberately does not choose a web framework or wrap the MCP SDK
33
+ server lifecycle. It provides two conversion functions and small adapters:
34
+
35
+ - `mcp_to_run(...)` converts native MCP tool arguments into Agent Framework run
36
+ arguments.
37
+ - `mcp_from_run(...)` converts an `AgentResponse` or `Message` into native MCP
38
+ `ContentBlock` values.
39
+ - `AgentMCPTool(...)` generates the native `Tool` definition from an agent and
40
+ keeps listing, parsing, execution, result conversion, and optional
41
+ `AgentState` session persistence aligned.
42
+ - `WorkflowMCPTool(...)` generates the native `Tool` definition from a
43
+ workflow's start-executor input type and converts completed workflow outputs.
44
+
45
+ Application code keeps ownership of the MCP SDK's `Server`, handler
46
+ registration, request context, transport, session-key policy, authentication,
47
+ authorization, and deployment.
48
+
49
+ For direct conversion, the argument name is part of the app-owned MCP tool
50
+ contract. Define it once and use the same value in both the native tool schema
51
+ and `mcp_to_run(...)`:
52
+
53
+ ```python
54
+ agent_input_argument = "task"
55
+ chat_option_arguments = {
56
+ "reasoning_effort": {
57
+ "type": "string",
58
+ "enum": ["low", "medium", "high"],
59
+ },
60
+ }
61
+
62
+ tool = Tool(
63
+ name="run_agent",
64
+ inputSchema={
65
+ "type": "object",
66
+ "properties": {
67
+ agent_input_argument: {"type": "string"},
68
+ **chat_option_arguments,
69
+ },
70
+ "required": [agent_input_argument],
71
+ },
72
+ )
73
+ run = mcp_to_run(
74
+ arguments,
75
+ argument_name=agent_input_argument,
76
+ chat_option_arguments=chat_option_arguments,
77
+ )
78
+ ```
79
+
80
+ Only names listed in `chat_option_arguments` are copied to `run["options"]`;
81
+ other MCP arguments remain available in the message's raw representation but
82
+ are not forwarded to the model client. The native MCP schema remains
83
+ responsible for validating exposed option types and ranges.
84
+
85
+ For an agent exposed as one MCP tool, use the adapter so the schema and
86
+ conversion cannot drift:
87
+
88
+ ```python
89
+ agent_tool = AgentMCPTool(
90
+ agent,
91
+ name="run_agent",
92
+ argument_description="The request for the hosted agent.",
93
+ parameters={"audience": {"type": "string"}},
94
+ chat_option_parameters={
95
+ "reasoning_effort": {
96
+ "type": "string",
97
+ "enum": ["low", "medium", "high"],
98
+ }
99
+ },
100
+ )
101
+
102
+ @server.list_tools()
103
+ async def list_tools():
104
+ return await agent_tool.list_tools()
105
+
106
+ @server.call_tool()
107
+ async def call_tool(name, arguments):
108
+ return await agent_tool.call_tool(name, arguments)
109
+ ```
110
+
111
+ `AgentMCPTool` uses the agent's name and description unless overridden.
112
+ `parameters` adds app-owned JSON Schema properties that remain available in the
113
+ raw MCP arguments. `chat_option_parameters` adds properties and explicitly
114
+ copies their values into Agent Framework chat options.
115
+
116
+ For a workflow exposed as one MCP tool, use `WorkflowMCPTool`:
117
+
118
+ ```python
119
+ workflow_tool = WorkflowMCPTool(
120
+ WorkflowState(create_workflow, cache_target=False),
121
+ name="run_workflow",
122
+ )
123
+ ```
124
+
125
+ The start executor must declare exactly one input type. Dataclass, Pydantic, and
126
+ other object-shaped inputs become the MCP tool's top-level arguments. Primitive
127
+ inputs are wrapped in the configurable `argument_name` property. The adapter
128
+ validates MCP arguments against that derived type before calling
129
+ `workflow.run(...)`.
130
+
131
+ Workflow instances preserve execution state, so applications that need
132
+ independent calls should supply a `WorkflowState` factory with
133
+ `cache_target=False`, as above. Checkpoint restoration, human-in-the-loop
134
+ responses, and continuation identifiers remain application-owned contracts.
135
+ If a workflow requests external input, the adapter raises instead of returning
136
+ an empty successful tool result.
137
+
138
+ Pass an existing `AgentState` plus `session_id_parameter` to persist an
139
+ `AgentSession`:
140
+
141
+ ```python
142
+ state = AgentState(agent)
143
+ agent_tool = AgentMCPTool(
144
+ state,
145
+ parameters={"session_id": {"type": "string", "minLength": 1}},
146
+ required_parameters={"session_id"},
147
+ session_id_parameter="session_id",
148
+ )
149
+ ```
150
+
151
+ The application must authenticate or authorize that session identifier and
152
+ serialize concurrent calls for the same session. The adapter only performs the
153
+ `AgentState` session-store get/run/set sequence. A configured session parameter
154
+ is always marked required in the generated MCP schema.
155
+
156
+ The session identifier is an opaque, application-defined key. Neither MCP nor
157
+ Agent Framework prescribes its format. `AgentMCPTool` treats it as the key for
158
+ one mutable conversation: each call loads that session and stores the updated
159
+ session under the same key. It does not implement
160
+ `previous_response_id`-style branching. Branching requires an app-owned
161
+ contract with separate source and destination identifiers so the application
162
+ can authorize both, copy the source session, and store the result under the
163
+ destination key.
164
+
165
+ MCP `tools/call` inputs are JSON objects defined by the app's `inputSchema`;
166
+ the protocol does not define image, audio, or resource content blocks for tool
167
+ arguments. This helper therefore converts one selected string argument and
168
+ does not impose a non-standard multimodal JSON convention.
169
+
170
+ For non-image/audio binary output, `mcp_from_run(...)` uses an app-provided
171
+ `content.additional_properties["uri"]` when present and otherwise uses the
172
+ short fallback `af://binary`; the payload itself is stored only in the MCP
173
+ resource's `blob` field.
174
+
175
+ `mcp_from_run(...)` targets `CallToolResult.content`, whose MCP content union
176
+ does not include sampling-only `ToolUseContent`. Agent Framework
177
+ `function_call` content is therefore omitted from tool results. MCP sampling
178
+ callbacks use a separate response contract and may convert function calls to
179
+ `ToolUseContent`.
180
+
181
+ MCP tool calls return one final `CallToolResult`; they do not stream partial
182
+ content blocks. Streamable HTTP may carry multiple MCP messages, and apps may
183
+ send progress notifications while work runs, but neither mechanism turns
184
+ Agent Framework response updates into incremental tool results. Experimental
185
+ MCP tasks defer retrieval of the same final result.
186
+
187
+ ```python
188
+ run = mcp_to_run(arguments)
189
+ result = await agent.run(
190
+ run["messages"],
191
+ options=run["options"],
192
+ )
193
+ content = mcp_from_run(result)
194
+
195
+ # Native MCP SDK application code returns `content` from its call_tool handler.
196
+ ```
197
+
198
+ The surrounding MCP application still owns the low-level `Server`, handler
199
+ registration, Starlette/FastAPI composition, stdio or streamable HTTP
200
+ transport, request authentication, session-key trust, concurrency, and
201
+ deployment.
202
+
@@ -0,0 +1,174 @@
1
+ # agent-framework-hosting-mcp
2
+
3
+ MCP conversion helpers for app-owned Agent Framework hosting.
4
+
5
+ The package deliberately does not choose a web framework or wrap the MCP SDK
6
+ server lifecycle. It provides two conversion functions and small adapters:
7
+
8
+ - `mcp_to_run(...)` converts native MCP tool arguments into Agent Framework run
9
+ arguments.
10
+ - `mcp_from_run(...)` converts an `AgentResponse` or `Message` into native MCP
11
+ `ContentBlock` values.
12
+ - `AgentMCPTool(...)` generates the native `Tool` definition from an agent and
13
+ keeps listing, parsing, execution, result conversion, and optional
14
+ `AgentState` session persistence aligned.
15
+ - `WorkflowMCPTool(...)` generates the native `Tool` definition from a
16
+ workflow's start-executor input type and converts completed workflow outputs.
17
+
18
+ Application code keeps ownership of the MCP SDK's `Server`, handler
19
+ registration, request context, transport, session-key policy, authentication,
20
+ authorization, and deployment.
21
+
22
+ For direct conversion, the argument name is part of the app-owned MCP tool
23
+ contract. Define it once and use the same value in both the native tool schema
24
+ and `mcp_to_run(...)`:
25
+
26
+ ```python
27
+ agent_input_argument = "task"
28
+ chat_option_arguments = {
29
+ "reasoning_effort": {
30
+ "type": "string",
31
+ "enum": ["low", "medium", "high"],
32
+ },
33
+ }
34
+
35
+ tool = Tool(
36
+ name="run_agent",
37
+ inputSchema={
38
+ "type": "object",
39
+ "properties": {
40
+ agent_input_argument: {"type": "string"},
41
+ **chat_option_arguments,
42
+ },
43
+ "required": [agent_input_argument],
44
+ },
45
+ )
46
+ run = mcp_to_run(
47
+ arguments,
48
+ argument_name=agent_input_argument,
49
+ chat_option_arguments=chat_option_arguments,
50
+ )
51
+ ```
52
+
53
+ Only names listed in `chat_option_arguments` are copied to `run["options"]`;
54
+ other MCP arguments remain available in the message's raw representation but
55
+ are not forwarded to the model client. The native MCP schema remains
56
+ responsible for validating exposed option types and ranges.
57
+
58
+ For an agent exposed as one MCP tool, use the adapter so the schema and
59
+ conversion cannot drift:
60
+
61
+ ```python
62
+ agent_tool = AgentMCPTool(
63
+ agent,
64
+ name="run_agent",
65
+ argument_description="The request for the hosted agent.",
66
+ parameters={"audience": {"type": "string"}},
67
+ chat_option_parameters={
68
+ "reasoning_effort": {
69
+ "type": "string",
70
+ "enum": ["low", "medium", "high"],
71
+ }
72
+ },
73
+ )
74
+
75
+ @server.list_tools()
76
+ async def list_tools():
77
+ return await agent_tool.list_tools()
78
+
79
+ @server.call_tool()
80
+ async def call_tool(name, arguments):
81
+ return await agent_tool.call_tool(name, arguments)
82
+ ```
83
+
84
+ `AgentMCPTool` uses the agent's name and description unless overridden.
85
+ `parameters` adds app-owned JSON Schema properties that remain available in the
86
+ raw MCP arguments. `chat_option_parameters` adds properties and explicitly
87
+ copies their values into Agent Framework chat options.
88
+
89
+ For a workflow exposed as one MCP tool, use `WorkflowMCPTool`:
90
+
91
+ ```python
92
+ workflow_tool = WorkflowMCPTool(
93
+ WorkflowState(create_workflow, cache_target=False),
94
+ name="run_workflow",
95
+ )
96
+ ```
97
+
98
+ The start executor must declare exactly one input type. Dataclass, Pydantic, and
99
+ other object-shaped inputs become the MCP tool's top-level arguments. Primitive
100
+ inputs are wrapped in the configurable `argument_name` property. The adapter
101
+ validates MCP arguments against that derived type before calling
102
+ `workflow.run(...)`.
103
+
104
+ Workflow instances preserve execution state, so applications that need
105
+ independent calls should supply a `WorkflowState` factory with
106
+ `cache_target=False`, as above. Checkpoint restoration, human-in-the-loop
107
+ responses, and continuation identifiers remain application-owned contracts.
108
+ If a workflow requests external input, the adapter raises instead of returning
109
+ an empty successful tool result.
110
+
111
+ Pass an existing `AgentState` plus `session_id_parameter` to persist an
112
+ `AgentSession`:
113
+
114
+ ```python
115
+ state = AgentState(agent)
116
+ agent_tool = AgentMCPTool(
117
+ state,
118
+ parameters={"session_id": {"type": "string", "minLength": 1}},
119
+ required_parameters={"session_id"},
120
+ session_id_parameter="session_id",
121
+ )
122
+ ```
123
+
124
+ The application must authenticate or authorize that session identifier and
125
+ serialize concurrent calls for the same session. The adapter only performs the
126
+ `AgentState` session-store get/run/set sequence. A configured session parameter
127
+ is always marked required in the generated MCP schema.
128
+
129
+ The session identifier is an opaque, application-defined key. Neither MCP nor
130
+ Agent Framework prescribes its format. `AgentMCPTool` treats it as the key for
131
+ one mutable conversation: each call loads that session and stores the updated
132
+ session under the same key. It does not implement
133
+ `previous_response_id`-style branching. Branching requires an app-owned
134
+ contract with separate source and destination identifiers so the application
135
+ can authorize both, copy the source session, and store the result under the
136
+ destination key.
137
+
138
+ MCP `tools/call` inputs are JSON objects defined by the app's `inputSchema`;
139
+ the protocol does not define image, audio, or resource content blocks for tool
140
+ arguments. This helper therefore converts one selected string argument and
141
+ does not impose a non-standard multimodal JSON convention.
142
+
143
+ For non-image/audio binary output, `mcp_from_run(...)` uses an app-provided
144
+ `content.additional_properties["uri"]` when present and otherwise uses the
145
+ short fallback `af://binary`; the payload itself is stored only in the MCP
146
+ resource's `blob` field.
147
+
148
+ `mcp_from_run(...)` targets `CallToolResult.content`, whose MCP content union
149
+ does not include sampling-only `ToolUseContent`. Agent Framework
150
+ `function_call` content is therefore omitted from tool results. MCP sampling
151
+ callbacks use a separate response contract and may convert function calls to
152
+ `ToolUseContent`.
153
+
154
+ MCP tool calls return one final `CallToolResult`; they do not stream partial
155
+ content blocks. Streamable HTTP may carry multiple MCP messages, and apps may
156
+ send progress notifications while work runs, but neither mechanism turns
157
+ Agent Framework response updates into incremental tool results. Experimental
158
+ MCP tasks defer retrieval of the same final result.
159
+
160
+ ```python
161
+ run = mcp_to_run(arguments)
162
+ result = await agent.run(
163
+ run["messages"],
164
+ options=run["options"],
165
+ )
166
+ content = mcp_from_run(result)
167
+
168
+ # Native MCP SDK application code returns `content` from its call_tool handler.
169
+ ```
170
+
171
+ The surrounding MCP application still owns the low-level `Server`, handler
172
+ registration, Starlette/FastAPI composition, stdio or streamable HTTP
173
+ transport, request authentication, session-key trust, concurrency, and
174
+ deployment.
@@ -0,0 +1,22 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Agent and workflow MCP tool adapters for app-owned hosting."""
4
+
5
+ import importlib.metadata
6
+
7
+ from ._agent_tool import AgentMCPTool
8
+ from ._conversion import mcp_from_run, mcp_to_run
9
+ from ._workflow_tool import WorkflowMCPTool
10
+
11
+ try:
12
+ __version__ = importlib.metadata.version(__name__)
13
+ except importlib.metadata.PackageNotFoundError:
14
+ __version__ = "0.0.0"
15
+
16
+ __all__ = [
17
+ "AgentMCPTool",
18
+ "WorkflowMCPTool",
19
+ "__version__",
20
+ "mcp_from_run",
21
+ "mcp_to_run",
22
+ ]
@@ -0,0 +1,179 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Agent-backed MCP tool adapter for app-owned servers."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from collections.abc import Collection, Mapping
9
+ from typing import Any, Generic, TypeVar, cast
10
+
11
+ from agent_framework import AgentResponse, Message, SupportsAgentRun
12
+ from agent_framework_hosting import AgentRunArgs, AgentState
13
+ from mcp import types
14
+
15
+ from ._conversion import mcp_from_run, mcp_to_run
16
+
17
+ AgentT = TypeVar("AgentT", bound=SupportsAgentRun)
18
+
19
+
20
+ class AgentMCPTool(Generic[AgentT]):
21
+ """Expose one Agent Framework agent through an app-owned MCP server.
22
+
23
+ The adapter generates the native MCP tool definition and keeps its argument
24
+ parsing aligned with agent execution. It does not create an MCP server,
25
+ register handlers, derive trusted session identifiers, or choose a
26
+ transport.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ target: AgentT | AgentState[AgentT],
32
+ *,
33
+ name: str | None = None,
34
+ description: str | None = None,
35
+ argument_name: str = "task",
36
+ argument_description: str | None = None,
37
+ parameters: Mapping[str, Mapping[str, Any]] | None = None,
38
+ required_parameters: Collection[str] = (),
39
+ chat_option_parameters: Mapping[str, Mapping[str, Any]] | None = None,
40
+ session_id_parameter: str | None = None,
41
+ ) -> None:
42
+ """Create an agent-backed MCP tool adapter.
43
+
44
+ Args:
45
+ target: Agent target or existing ``AgentState``.
46
+
47
+ Keyword Args:
48
+ name: MCP tool name override. Defaults to a sanitized agent name.
49
+ description: MCP tool description override. Defaults to the agent description.
50
+ argument_name: Name of the main text argument.
51
+ argument_description: Description of the main text argument.
52
+ parameters: Additional app-owned MCP parameter schemas.
53
+ required_parameters: Additional parameter names that are required.
54
+ chat_option_parameters: MCP parameter schemas whose values are copied to chat options.
55
+ session_id_parameter: Additional string parameter used as the ``AgentState`` session key.
56
+
57
+ Raises:
58
+ ValueError: If parameter names overlap or required/session parameters are not defined.
59
+ """
60
+ self.state = target if isinstance(target, AgentState) else AgentState(target)
61
+ self._name = name
62
+ self._description = description
63
+ self.argument_name = argument_name
64
+ self.argument_description = argument_description
65
+ self.parameters = {key: dict(value) for key, value in (parameters or {}).items()}
66
+ self.chat_option_parameters = {key: dict(value) for key, value in (chat_option_parameters or {}).items()}
67
+ self.session_id_parameter = session_id_parameter
68
+
69
+ parameter_names = set(self.parameters)
70
+ chat_option_names = set(self.chat_option_parameters)
71
+ if self.argument_name in parameter_names | chat_option_names:
72
+ raise ValueError(f"Main argument '{self.argument_name}' must not be repeated in additional parameters.")
73
+ if parameter_names & chat_option_names:
74
+ raise ValueError("Additional parameters and chat option parameters must have distinct names.")
75
+ required_names = set(required_parameters)
76
+ if session_id_parameter is not None:
77
+ required_names.add(session_id_parameter)
78
+ if self.session_id_parameter is not None and self.session_id_parameter not in parameter_names:
79
+ raise ValueError("session_id_parameter must name an additional parameter.")
80
+ undefined_required = required_names - parameter_names - chat_option_names
81
+ if undefined_required:
82
+ raise ValueError(f"Required parameters are not defined: {sorted(undefined_required)}")
83
+ self.required_parameters = tuple(
84
+ name for name in (*self.parameters, *self.chat_option_parameters) if name in required_names
85
+ )
86
+
87
+ async def list_tools(self) -> list[types.Tool]:
88
+ """Return the native MCP tool definition for the target agent."""
89
+ target = await self.state.get_target()
90
+ return [self._tool_for_target(target)]
91
+
92
+ def _tool_for_target(self, target: AgentT) -> types.Tool:
93
+ """Create the native MCP tool definition for a resolved target."""
94
+ tool_name = self._name
95
+ if tool_name is None:
96
+ if target.name is None:
97
+ raise ValueError("MCP tool name requires either an override or an agent name.")
98
+ tool_name = re.sub(r"[^a-zA-Z0-9_.-]+", "_", target.name).strip("_") or "agent"
99
+
100
+ properties: dict[str, Any] = {
101
+ self.argument_name: {
102
+ "type": "string",
103
+ "description": self.argument_description or f"Task for {tool_name}",
104
+ },
105
+ **self.parameters,
106
+ **self.chat_option_parameters,
107
+ }
108
+ return types.Tool(
109
+ name=tool_name,
110
+ description=self._description if self._description is not None else target.description or "",
111
+ inputSchema={
112
+ "type": "object",
113
+ "properties": properties,
114
+ "required": [self.argument_name, *self.required_parameters],
115
+ "additionalProperties": False,
116
+ },
117
+ )
118
+
119
+ def mcp_to_run(self, arguments: Mapping[str, Any] | None) -> AgentRunArgs:
120
+ """Convert this tool's MCP arguments into Agent Framework run arguments."""
121
+ return mcp_to_run(
122
+ arguments,
123
+ argument_name=self.argument_name,
124
+ chat_option_arguments=self.chat_option_parameters,
125
+ )
126
+
127
+ def mcp_from_run(self, result: AgentResponse[Any] | Message) -> list[types.ContentBlock]:
128
+ """Convert Agent Framework output into this tool's MCP result content."""
129
+ return mcp_from_run(result)
130
+
131
+ async def call_tool(
132
+ self,
133
+ name: str,
134
+ arguments: Mapping[str, Any] | None,
135
+ ) -> list[types.ContentBlock]:
136
+ """Run the target agent for a native MCP ``call_tool`` handler.
137
+
138
+ Args:
139
+ name: MCP tool name selected by the client.
140
+ arguments: Native MCP tool arguments.
141
+
142
+ Returns:
143
+ Native MCP content blocks for the completed tool result.
144
+
145
+ Raises:
146
+ ValueError: If the tool name or configured session id is invalid.
147
+ """
148
+ target = await self.state.get_target()
149
+ tool = self._tool_for_target(target)
150
+ if name != tool.name:
151
+ raise ValueError(f"Unknown MCP tool: {name}")
152
+
153
+ run = self.mcp_to_run(arguments)
154
+ if self.session_id_parameter is None:
155
+ result = cast(
156
+ "AgentResponse[Any]",
157
+ await target.run( # pyright: ignore[reportCallIssue]
158
+ run["messages"],
159
+ options=run["options"],
160
+ stream=False,
161
+ ),
162
+ )
163
+ return self.mcp_from_run(result)
164
+
165
+ session_id = arguments.get(self.session_id_parameter) if arguments else None
166
+ if not isinstance(session_id, str) or not session_id:
167
+ raise ValueError(f"MCP tool argument '{self.session_id_parameter}' must be a non-empty string.")
168
+ session = await self.state.get_or_create_session(session_id)
169
+ result = cast(
170
+ "AgentResponse[Any]",
171
+ await target.run( # pyright: ignore[reportCallIssue]
172
+ run["messages"],
173
+ options=run["options"],
174
+ session=session,
175
+ stream=False,
176
+ ),
177
+ )
178
+ await self.state.set_session(session_id, session)
179
+ return self.mcp_from_run(result)
@@ -0,0 +1,155 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Conversion between native MCP values and Agent Framework run values."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ import logging
9
+ from collections.abc import Collection, Mapping, Sequence
10
+ from typing import Any, cast
11
+
12
+ from agent_framework import AgentResponse, ChatOptions, Content, Message
13
+ from agent_framework_hosting import AgentRunArgs
14
+ from mcp import types
15
+
16
+ logger = logging.getLogger("agent_framework.hosting.mcp")
17
+
18
+
19
+ def mcp_to_run(
20
+ arguments: Mapping[str, Any] | None,
21
+ *,
22
+ argument_name: str = "task",
23
+ chat_option_arguments: Collection[str] = (),
24
+ ) -> AgentRunArgs:
25
+ """Convert native MCP tool arguments into Agent Framework run arguments.
26
+
27
+ The application owns the MCP tool schema and chooses which string argument
28
+ contains the user request. This helper does not create sessions, inspect
29
+ request context, or interact with an MCP server.
30
+
31
+ Args:
32
+ arguments: Arguments supplied to an MCP ``call_tool`` handler.
33
+
34
+ Keyword Args:
35
+ argument_name: Name of the required string argument containing the user request.
36
+ chat_option_arguments: MCP argument names to copy into Agent Framework chat options when present.
37
+
38
+ Returns:
39
+ Arguments corresponding to ``Agent.run(...)``.
40
+
41
+ Raises:
42
+ ValueError: If the selected argument is missing or is not a string.
43
+ """
44
+ if arguments is None or argument_name not in arguments:
45
+ raise ValueError(f"MCP tool arguments must include a '{argument_name}' string.")
46
+
47
+ message_value = arguments[argument_name]
48
+ if not isinstance(message_value, str):
49
+ raise ValueError(f"MCP tool argument '{argument_name}' must be a string.")
50
+
51
+ options = {name: arguments[name] for name in chat_option_arguments if name in arguments}
52
+ return AgentRunArgs(
53
+ messages=[
54
+ Message(
55
+ "user",
56
+ [Content.from_text(message_value)],
57
+ raw_representation=dict(arguments),
58
+ )
59
+ ],
60
+ options=cast("ChatOptions[Any]", options),
61
+ stream=False,
62
+ )
63
+
64
+
65
+ def mcp_from_run(
66
+ result: AgentResponse[Any] | Message,
67
+ ) -> list[types.ContentBlock]:
68
+ """Convert Agent Framework output into native MCP content blocks.
69
+
70
+ ``AgentResponse`` values are flattened in message order. User-role
71
+ messages are omitted. Text, external URI, and inline data content become
72
+ the corresponding native MCP content block types. Content-level metadata
73
+ is preserved in each block's ``_meta`` field.
74
+
75
+ Args:
76
+ result: A completed response or response message.
77
+
78
+ Returns:
79
+ Native MCP content blocks ready for a ``CallToolResult``.
80
+
81
+ Raises:
82
+ ValueError: If Agent Framework data content contains an invalid data URI.
83
+ """
84
+ items: Sequence[Message] = result.messages if isinstance(result, AgentResponse) else [result]
85
+
86
+ blocks: list[types.ContentBlock] = []
87
+ for item in items:
88
+ if item.role == "user":
89
+ continue
90
+ for content in item.contents:
91
+ metadata = content.additional_properties or None
92
+ match content.type:
93
+ case "text" if content.text is not None:
94
+ blocks.append(types.TextContent(type="text", text=content.text, _meta=metadata))
95
+ case "uri" if content.uri is not None:
96
+ name = content.uri.rsplit("/", maxsplit=1)[-1] or content.uri
97
+ blocks.append(
98
+ types.ResourceLink(
99
+ type="resource_link",
100
+ name=name,
101
+ uri=content.uri, # pyright: ignore[reportArgumentType]
102
+ mimeType=content.media_type,
103
+ _meta=metadata,
104
+ )
105
+ )
106
+ case "data" if content.uri is not None:
107
+ prefix, separator, encoded = content.uri.partition(",")
108
+ if not separator or not prefix.startswith("data:") or ";base64" not in prefix:
109
+ raise ValueError("Agent Framework data content must contain a base64 data URI.")
110
+ try:
111
+ base64.b64decode(encoded, validate=True)
112
+ except ValueError as exc:
113
+ raise ValueError("Agent Framework data content contains invalid base64 data.") from exc
114
+
115
+ if content.media_type and content.media_type.startswith("image/"):
116
+ blocks.append(
117
+ types.ImageContent(
118
+ type="image",
119
+ data=encoded,
120
+ mimeType=content.media_type,
121
+ _meta=metadata,
122
+ )
123
+ )
124
+ elif content.media_type and content.media_type.startswith("audio/"):
125
+ blocks.append(
126
+ types.AudioContent(
127
+ type="audio",
128
+ data=encoded,
129
+ mimeType=content.media_type,
130
+ _meta=metadata,
131
+ )
132
+ )
133
+ else:
134
+ resource_uri = (
135
+ content.additional_properties.get("uri") if content.additional_properties else None
136
+ )
137
+ if not isinstance(resource_uri, str):
138
+ resource_uri = "af://binary"
139
+ blocks.append(
140
+ types.EmbeddedResource(
141
+ type="resource",
142
+ resource=types.BlobResourceContents(
143
+ uri=resource_uri, # pyright: ignore[reportArgumentType]
144
+ blob=encoded,
145
+ mimeType=content.media_type,
146
+ ),
147
+ _meta=metadata,
148
+ )
149
+ )
150
+ case _:
151
+ logger.warning(
152
+ "Agent Framework content type %s is not supported in MCP tool results and was omitted.",
153
+ content.type,
154
+ )
155
+ return blocks
@@ -0,0 +1,143 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Workflow-backed MCP tool adapter for app-owned servers."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from collections.abc import Mapping
9
+ from typing import Any, Generic, TypeVar, cast
10
+
11
+ from agent_framework import AgentResponse, Message, Workflow, WorkflowRunResult
12
+ from agent_framework_hosting import WorkflowState
13
+ from mcp import types
14
+ from pydantic import TypeAdapter
15
+
16
+ from ._conversion import mcp_from_run
17
+
18
+ WorkflowT = TypeVar("WorkflowT", bound=Workflow)
19
+
20
+
21
+ class WorkflowMCPTool(Generic[WorkflowT]):
22
+ """Expose one Agent Framework workflow through an app-owned MCP server.
23
+
24
+ The adapter derives the native MCP input schema from the workflow's start
25
+ executor. It does not create an MCP server, register handlers, choose a
26
+ transport, or manage workflow checkpoints and human-in-the-loop responses.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ target: WorkflowT | WorkflowState[WorkflowT],
32
+ *,
33
+ name: str | None = None,
34
+ description: str | None = None,
35
+ argument_name: str = "input",
36
+ ) -> None:
37
+ """Create a workflow-backed MCP tool adapter.
38
+
39
+ Args:
40
+ target: Workflow target or existing ``WorkflowState``. Use a
41
+ ``WorkflowState`` factory with ``cache_target=False`` when
42
+ concurrent calls need independent workflow instances.
43
+
44
+ Keyword Args:
45
+ name: MCP tool name override. Defaults to a sanitized workflow name.
46
+ description: MCP tool description override. Defaults to the workflow description.
47
+ argument_name: MCP property used when the workflow input is not an object.
48
+ """
49
+ self.state = target if isinstance(target, WorkflowState) else WorkflowState(target)
50
+ self._name = name
51
+ self._description = description
52
+ self.argument_name = argument_name
53
+
54
+ async def list_tools(self) -> list[types.Tool]:
55
+ """Return the native MCP tool definition for the target workflow."""
56
+ workflow = await self.state.get_target()
57
+ return [self._tool_for_workflow(workflow)]
58
+
59
+ def _tool_for_workflow(self, workflow: WorkflowT) -> types.Tool:
60
+ tool_name = self._name
61
+ if tool_name is None:
62
+ tool_name = re.sub(r"[^a-zA-Z0-9_.-]+", "_", workflow.name).strip("_") or "workflow"
63
+
64
+ input_adapter = self._input_adapter(workflow)
65
+ input_schema = input_adapter.json_schema()
66
+ if input_schema.get("type") != "object":
67
+ input_schema = {
68
+ "type": "object",
69
+ "properties": {self.argument_name: input_schema},
70
+ "required": [self.argument_name],
71
+ "additionalProperties": False,
72
+ }
73
+
74
+ return types.Tool(
75
+ name=tool_name,
76
+ description=self._description if self._description is not None else workflow.description or "",
77
+ inputSchema=input_schema,
78
+ )
79
+
80
+ def _input_adapter(self, workflow: WorkflowT) -> TypeAdapter[Any]:
81
+ input_types = workflow.get_start_executor().input_types
82
+ if len(input_types) != 1:
83
+ raise ValueError(
84
+ f"MCP workflow tools require exactly one start-executor input type; found {len(input_types)}."
85
+ )
86
+ return TypeAdapter(input_types[0])
87
+
88
+ def _workflow_input(self, workflow: WorkflowT, arguments: Mapping[str, Any] | None) -> Any:
89
+ input_adapter = self._input_adapter(workflow)
90
+ input_schema = input_adapter.json_schema()
91
+ if input_schema.get("type") == "object":
92
+ return input_adapter.validate_python(dict(arguments or {}))
93
+ if arguments is None or self.argument_name not in arguments:
94
+ raise ValueError(f"MCP tool arguments must include '{self.argument_name}'.")
95
+ return input_adapter.validate_python(arguments[self.argument_name])
96
+
97
+ def mcp_from_run(self, result: WorkflowRunResult) -> list[types.ContentBlock]:
98
+ """Convert completed workflow outputs into native MCP content blocks."""
99
+ if result.get_request_info_events():
100
+ raise ValueError(
101
+ "The workflow requires external input. WorkflowMCPTool does not manage "
102
+ "human-in-the-loop continuation; handle it in the application contract."
103
+ )
104
+
105
+ blocks: list[types.ContentBlock] = []
106
+ for output in result.get_outputs():
107
+ if isinstance(output, (AgentResponse, Message)):
108
+ blocks.extend(mcp_from_run(cast("AgentResponse[Any] | Message", output)))
109
+ elif isinstance(output, str):
110
+ blocks.append(types.TextContent(type="text", text=output))
111
+ else:
112
+ blocks.append(
113
+ types.TextContent(
114
+ type="text",
115
+ text=TypeAdapter[Any](Any).dump_json(output, serialize_as_any=True).decode(),
116
+ )
117
+ )
118
+ return blocks
119
+
120
+ async def call_tool(
121
+ self,
122
+ name: str,
123
+ arguments: Mapping[str, Any] | None,
124
+ ) -> list[types.ContentBlock]:
125
+ """Run the target workflow for a native MCP ``call_tool`` handler.
126
+
127
+ Args:
128
+ name: MCP tool name selected by the client.
129
+ arguments: Native MCP tool arguments.
130
+
131
+ Returns:
132
+ Native MCP content blocks for the completed workflow result.
133
+
134
+ Raises:
135
+ ValueError: If the tool name or workflow input contract is invalid.
136
+ """
137
+ workflow = await self.state.get_target()
138
+ tool = self._tool_for_workflow(workflow)
139
+ if name != tool.name:
140
+ raise ValueError(f"Unknown MCP tool: {name}")
141
+
142
+ result = await workflow.run(self._workflow_input(workflow, arguments), stream=False)
143
+ return self.mcp_from_run(result)
@@ -0,0 +1,81 @@
1
+ [project]
2
+ name = "agent-framework-hosting-mcp"
3
+ description = "Agent and workflow MCP tool adapters for app-owned hosting."
4
+ authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ version = "1.0.0a260721"
8
+ license-files = ["LICENSE"]
9
+ urls.homepage = "https://aka.ms/agent-framework"
10
+ urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
11
+ urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
12
+ urls.issues = "https://github.com/microsoft/agent-framework/issues"
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "agent-framework-core>=1.11.0,<2",
27
+ "agent-framework-hosting==1.0.0a260721",
28
+ "mcp>=1.11.0,<2",
29
+ "pydantic>=2,<3",
30
+ ]
31
+
32
+ [tool.uv]
33
+ prerelease = "if-necessary-or-explicit"
34
+ environments = [
35
+ "sys_platform == 'darwin'",
36
+ "sys_platform == 'linux'",
37
+ "sys_platform == 'win32'"
38
+ ]
39
+
40
+ [tool.uv-dynamic-versioning]
41
+ fallback-version = "0.0.0"
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = 'tests'
45
+ addopts = "-ra -q -r fEX"
46
+ asyncio_mode = "auto"
47
+ asyncio_default_fixture_loop_scope = "function"
48
+ filterwarnings = []
49
+ timeout = 120
50
+ markers = [
51
+ "integration: marks tests as integration tests that require external services",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ extend = "../../pyproject.toml"
56
+
57
+ [tool.coverage.run]
58
+ omit = [
59
+ "**/__init__.py"
60
+ ]
61
+
62
+ [tool.pyright]
63
+ extends = "../../pyproject.toml"
64
+ include = ["agent_framework_hosting_mcp"]
65
+ exclude = ['tests']
66
+
67
+ [tool.bandit]
68
+ targets = ["agent_framework_hosting_mcp"]
69
+ exclude_dirs = ["tests"]
70
+
71
+ [tool.poe]
72
+ executor.type = "uv"
73
+ include = "../../shared_tasks.toml"
74
+
75
+ [tool.poe.tasks.test]
76
+ help = "Run the default unit test suite for this package."
77
+ cmd = 'pytest -m "not integration" --cov=agent_framework_hosting_mcp --cov-report=term-missing:skip-covered tests'
78
+
79
+ [build-system]
80
+ requires = ["flit-core >= 3.11,<4.0"]
81
+ build-backend = "flit_core.buildapi"