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.
- agent_framework_hosting_mcp-1.0.0a260721/LICENSE +21 -0
- agent_framework_hosting_mcp-1.0.0a260721/PKG-INFO +202 -0
- agent_framework_hosting_mcp-1.0.0a260721/README.md +174 -0
- agent_framework_hosting_mcp-1.0.0a260721/agent_framework_hosting_mcp/__init__.py +22 -0
- agent_framework_hosting_mcp-1.0.0a260721/agent_framework_hosting_mcp/_agent_tool.py +179 -0
- agent_framework_hosting_mcp-1.0.0a260721/agent_framework_hosting_mcp/_conversion.py +155 -0
- agent_framework_hosting_mcp-1.0.0a260721/agent_framework_hosting_mcp/_workflow_tool.py +143 -0
- agent_framework_hosting_mcp-1.0.0a260721/agent_framework_hosting_mcp/py.typed +0 -0
- agent_framework_hosting_mcp-1.0.0a260721/pyproject.toml +81 -0
|
@@ -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)
|
|
File without changes
|
|
@@ -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"
|