fast-agent-mcp 0.2.36__py3-none-any.whl → 0.2.38__py3-none-any.whl
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.
Potentially problematic release.
This version of fast-agent-mcp might be problematic. Click here for more details.
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/METADATA +10 -7
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/RECORD +45 -47
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/licenses/LICENSE +1 -1
- mcp_agent/cli/commands/quickstart.py +60 -5
- mcp_agent/config.py +10 -0
- mcp_agent/context.py +1 -4
- mcp_agent/core/agent_types.py +7 -6
- mcp_agent/core/direct_decorators.py +14 -0
- mcp_agent/core/direct_factory.py +1 -0
- mcp_agent/core/fastagent.py +23 -2
- mcp_agent/human_input/elicitation_form.py +723 -0
- mcp_agent/human_input/elicitation_forms.py +59 -0
- mcp_agent/human_input/elicitation_handler.py +88 -0
- mcp_agent/human_input/elicitation_state.py +34 -0
- mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
- mcp_agent/llm/providers/augmented_llm_openai.py +1 -1
- mcp_agent/mcp/elicitation_factory.py +84 -0
- mcp_agent/mcp/elicitation_handlers.py +155 -0
- mcp_agent/mcp/helpers/content_helpers.py +27 -0
- mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
- mcp_agent/mcp/mcp_agent_client_session.py +44 -1
- mcp_agent/mcp/mcp_aggregator.py +56 -11
- mcp_agent/mcp/mcp_connection_manager.py +30 -18
- mcp_agent/mcp_server/agent_server.py +2 -0
- mcp_agent/mcp_server_registry.py +16 -8
- mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
- mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
- mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
- mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
- mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
- mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
- mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
- mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
- mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
- mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
- mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
- mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
- mcp_agent/resources/examples/workflows/evaluator.py +1 -1
- mcp_agent/resources/examples/workflows/graded_report.md +89 -0
- mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
- mcp_agent/resources/examples/workflows/parallel.py +0 -2
- mcp_agent/resources/examples/workflows/short_story.md +13 -0
- mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
- mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
- mcp_agent/resources/examples/in_dev/slides.py +0 -110
- mcp_agent/resources/examples/internal/agent.py +0 -20
- mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
- mcp_agent/resources/examples/internal/history_transfer.py +0 -35
- mcp_agent/resources/examples/internal/job.py +0 -84
- mcp_agent/resources/examples/internal/prompt_category.py +0 -21
- mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
- mcp_agent/resources/examples/internal/simple.txt +0 -2
- mcp_agent/resources/examples/internal/sizer.py +0 -20
- mcp_agent/resources/examples/internal/social.py +0 -67
- mcp_agent/resources/examples/prompting/__init__.py +0 -3
- mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
- mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
- mcp_agent/resources/examples/prompting/image_server.py +0 -52
- mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
- mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/entry_points.txt +0 -0
mcp_agent/mcp/mcp_aggregator.py
CHANGED
|
@@ -217,19 +217,30 @@ class MCPAggregator(ContextDependent):
|
|
|
217
217
|
|
|
218
218
|
# Create a wrapper to capture the parameters for the client session
|
|
219
219
|
def session_factory(read_stream, write_stream, read_timeout, **kwargs):
|
|
220
|
-
# Get agent's model if this aggregator is part of an agent
|
|
221
|
-
agent_model = None
|
|
222
|
-
|
|
220
|
+
# Get agent's model and name if this aggregator is part of an agent
|
|
221
|
+
agent_model: str | None = None
|
|
222
|
+
agent_name: str | None = None
|
|
223
|
+
elicitation_handler = None
|
|
224
|
+
|
|
225
|
+
# Check if this aggregator is part of an Agent (which has config)
|
|
226
|
+
# Import here to avoid circular dependency
|
|
227
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
|
228
|
+
|
|
229
|
+
if isinstance(self, BaseAgent):
|
|
223
230
|
agent_model = self.config.model
|
|
224
|
-
|
|
231
|
+
agent_name = self.config.name
|
|
232
|
+
elicitation_handler = self.config.elicitation_handler
|
|
233
|
+
|
|
225
234
|
return MCPAgentClientSession(
|
|
226
235
|
read_stream,
|
|
227
236
|
write_stream,
|
|
228
237
|
read_timeout,
|
|
229
238
|
server_name=server_name,
|
|
230
239
|
agent_model=agent_model,
|
|
240
|
+
agent_name=agent_name,
|
|
241
|
+
elicitation_handler=elicitation_handler,
|
|
231
242
|
tool_list_changed_callback=self._handle_tool_list_changed,
|
|
232
|
-
**kwargs # Pass through any additional kwargs like server_config
|
|
243
|
+
**kwargs, # Pass through any additional kwargs like server_config
|
|
233
244
|
)
|
|
234
245
|
|
|
235
246
|
await self._persistent_connection_manager.get_server(
|
|
@@ -278,19 +289,27 @@ class MCPAggregator(ContextDependent):
|
|
|
278
289
|
else:
|
|
279
290
|
# Create a factory function for the client session
|
|
280
291
|
def create_session(read_stream, write_stream, read_timeout, **kwargs):
|
|
281
|
-
# Get agent's model if this aggregator is part of an agent
|
|
282
|
-
agent_model = None
|
|
283
|
-
|
|
292
|
+
# Get agent's model and name if this aggregator is part of an agent
|
|
293
|
+
agent_model: str | None = None
|
|
294
|
+
agent_name: str | None = None
|
|
295
|
+
|
|
296
|
+
# Check if this aggregator is part of an Agent (which has config)
|
|
297
|
+
# Import here to avoid circular dependency
|
|
298
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
|
299
|
+
|
|
300
|
+
if isinstance(self, BaseAgent):
|
|
284
301
|
agent_model = self.config.model
|
|
285
|
-
|
|
302
|
+
agent_name = self.config.name
|
|
303
|
+
|
|
286
304
|
return MCPAgentClientSession(
|
|
287
305
|
read_stream,
|
|
288
306
|
write_stream,
|
|
289
307
|
read_timeout,
|
|
290
308
|
server_name=server_name,
|
|
291
309
|
agent_model=agent_model,
|
|
310
|
+
agent_name=agent_name,
|
|
292
311
|
tool_list_changed_callback=self._handle_tool_list_changed,
|
|
293
|
-
**kwargs # Pass through any additional kwargs like server_config
|
|
312
|
+
**kwargs, # Pass through any additional kwargs like server_config
|
|
294
313
|
)
|
|
295
314
|
|
|
296
315
|
async with gen_client(
|
|
@@ -812,7 +831,9 @@ class MCPAggregator(ContextDependent):
|
|
|
812
831
|
messages=[],
|
|
813
832
|
)
|
|
814
833
|
|
|
815
|
-
async def list_prompts(
|
|
834
|
+
async def list_prompts(
|
|
835
|
+
self, server_name: str | None = None, agent_name: str | None = None
|
|
836
|
+
) -> Mapping[str, List[Prompt]]:
|
|
816
837
|
"""
|
|
817
838
|
List available prompts from one or all servers.
|
|
818
839
|
|
|
@@ -940,11 +961,23 @@ class MCPAggregator(ContextDependent):
|
|
|
940
961
|
if self.connection_persistence:
|
|
941
962
|
# Create a factory function that will include our parameters
|
|
942
963
|
def create_session(read_stream, write_stream, read_timeout):
|
|
964
|
+
# Get agent name if available
|
|
965
|
+
agent_name: str | None = None
|
|
966
|
+
|
|
967
|
+
# Import here to avoid circular dependency
|
|
968
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
|
969
|
+
|
|
970
|
+
if isinstance(self, BaseAgent):
|
|
971
|
+
agent_name = self.config.name
|
|
972
|
+
elicitation_handler = self.config.elicitation_handler
|
|
973
|
+
|
|
943
974
|
return MCPAgentClientSession(
|
|
944
975
|
read_stream,
|
|
945
976
|
write_stream,
|
|
946
977
|
read_timeout,
|
|
947
978
|
server_name=server_name,
|
|
979
|
+
agent_name=agent_name,
|
|
980
|
+
elicitation_handler=elicitation_handler,
|
|
948
981
|
tool_list_changed_callback=self._handle_tool_list_changed,
|
|
949
982
|
)
|
|
950
983
|
|
|
@@ -956,11 +989,23 @@ class MCPAggregator(ContextDependent):
|
|
|
956
989
|
else:
|
|
957
990
|
# Create a factory function for the client session
|
|
958
991
|
def create_session(read_stream, write_stream, read_timeout):
|
|
992
|
+
# Get agent name if available
|
|
993
|
+
agent_name: str | None = None
|
|
994
|
+
|
|
995
|
+
# Import here to avoid circular dependency
|
|
996
|
+
from mcp_agent.agents.base_agent import BaseAgent
|
|
997
|
+
|
|
998
|
+
if isinstance(self, BaseAgent):
|
|
999
|
+
agent_name = self.config.name
|
|
1000
|
+
elicitation_handler = self.config.elicitation_handler
|
|
1001
|
+
|
|
959
1002
|
return MCPAgentClientSession(
|
|
960
1003
|
read_stream,
|
|
961
1004
|
write_stream,
|
|
962
1005
|
read_timeout,
|
|
963
1006
|
server_name=server_name,
|
|
1007
|
+
agent_name=agent_name,
|
|
1008
|
+
elicitation_handler=elicitation_handler,
|
|
964
1009
|
tool_list_changed_callback=self._handle_tool_list_changed,
|
|
965
1010
|
)
|
|
966
1011
|
|
|
@@ -166,10 +166,7 @@ class ServerConnection:
|
|
|
166
166
|
)
|
|
167
167
|
|
|
168
168
|
session = self._client_session_factory(
|
|
169
|
-
read_stream,
|
|
170
|
-
send_stream,
|
|
171
|
-
read_timeout,
|
|
172
|
-
server_config=self.server_config
|
|
169
|
+
read_stream, send_stream, read_timeout, server_config=self.server_config
|
|
173
170
|
)
|
|
174
171
|
|
|
175
172
|
self.session = session
|
|
@@ -220,19 +217,34 @@ async def _server_lifecycle_task(server_conn: ServerConnection) -> None:
|
|
|
220
217
|
|
|
221
218
|
if "ExceptionGroup" in type(exc).__name__ and hasattr(exc, "exceptions"):
|
|
222
219
|
# Handle ExceptionGroup better by extracting the actual errors
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
220
|
+
def extract_errors(exception_group):
|
|
221
|
+
"""Recursively extract meaningful errors from ExceptionGroups"""
|
|
222
|
+
messages = []
|
|
223
|
+
for subexc in exception_group.exceptions:
|
|
224
|
+
if "ExceptionGroup" in type(subexc).__name__ and hasattr(subexc, "exceptions"):
|
|
225
|
+
# Recursively handle nested ExceptionGroups
|
|
226
|
+
messages.extend(extract_errors(subexc))
|
|
227
|
+
elif isinstance(subexc, HTTPStatusError):
|
|
228
|
+
# Special handling for HTTP errors to make them more user-friendly
|
|
229
|
+
messages.append(
|
|
230
|
+
f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
# Show the exception type and message, plus the root cause if available
|
|
234
|
+
error_msg = f"{type(subexc).__name__}: {subexc}"
|
|
235
|
+
messages.append(error_msg)
|
|
236
|
+
|
|
237
|
+
# If there's a root cause, show that too as it's often the most informative
|
|
238
|
+
if hasattr(subexc, "__cause__") and subexc.__cause__:
|
|
239
|
+
messages.append(
|
|
240
|
+
f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
|
|
241
|
+
)
|
|
242
|
+
return messages
|
|
243
|
+
|
|
244
|
+
error_messages = extract_errors(exc)
|
|
245
|
+
# If we didn't extract any meaningful errors, fall back to the original exception
|
|
246
|
+
if not error_messages:
|
|
247
|
+
error_messages = [f"{type(exc).__name__}: {exc}"]
|
|
236
248
|
server_conn._error_message = error_messages
|
|
237
249
|
else:
|
|
238
250
|
# For regular exceptions, keep the traceback but format it more cleanly
|
|
@@ -309,7 +321,7 @@ class MCPConnectionManager(ContextDependent):
|
|
|
309
321
|
self._tg = self._task_group
|
|
310
322
|
logger.info(f"Auto-created task group for server: {server_name}")
|
|
311
323
|
|
|
312
|
-
config = self.server_registry.
|
|
324
|
+
config = self.server_registry.get_server_config(server_name)
|
|
313
325
|
if not config:
|
|
314
326
|
raise ValueError(f"Server '{server_name}' not found in registry.")
|
|
315
327
|
|
|
@@ -65,6 +65,8 @@ class AgentMCPServer:
|
|
|
65
65
|
@self.mcp_server.tool(
|
|
66
66
|
name=f"{agent_name}_send",
|
|
67
67
|
description=f"Send a message to the {agent_name} agent",
|
|
68
|
+
structured_output=False,
|
|
69
|
+
# MCP 1.10.1 turns every tool in to a structured output
|
|
68
70
|
)
|
|
69
71
|
async def send_message(message: str, ctx: MCPContext) -> str:
|
|
70
72
|
"""Send a message to the agent and return its response."""
|
mcp_agent/mcp_server_registry.py
CHANGED
|
@@ -73,7 +73,11 @@ class ServerRegistry:
|
|
|
73
73
|
"""
|
|
74
74
|
if config is None:
|
|
75
75
|
self.registry = self.load_registry_from_file(config_path)
|
|
76
|
-
elif
|
|
76
|
+
elif (
|
|
77
|
+
config.mcp is not None
|
|
78
|
+
and hasattr(config.mcp, "servers")
|
|
79
|
+
and config.mcp.servers is not None
|
|
80
|
+
):
|
|
77
81
|
# Ensure config.mcp exists, has a 'servers' attribute, and it's not None
|
|
78
82
|
self.registry = config.mcp.servers
|
|
79
83
|
else:
|
|
@@ -95,13 +99,17 @@ class ServerRegistry:
|
|
|
95
99
|
Raises:
|
|
96
100
|
ValueError: If the configuration is invalid.
|
|
97
101
|
"""
|
|
98
|
-
servers = {}
|
|
102
|
+
servers = {}
|
|
99
103
|
|
|
100
104
|
settings = get_settings(config_path)
|
|
101
|
-
|
|
102
|
-
if
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
settings.mcp is not None
|
|
108
|
+
and hasattr(settings.mcp, "servers")
|
|
109
|
+
and settings.mcp.servers is not None
|
|
110
|
+
):
|
|
103
111
|
return settings.mcp.servers
|
|
104
|
-
|
|
112
|
+
|
|
105
113
|
return servers
|
|
106
114
|
|
|
107
115
|
@asynccontextmanager
|
|
@@ -164,7 +172,7 @@ class ServerRegistry:
|
|
|
164
172
|
read_stream,
|
|
165
173
|
write_stream,
|
|
166
174
|
read_timeout_seconds,
|
|
167
|
-
|
|
175
|
+
server_config=config,
|
|
168
176
|
)
|
|
169
177
|
async with session:
|
|
170
178
|
logger.info(f"{server_name}: Connected to server using stdio transport.")
|
|
@@ -192,7 +200,7 @@ class ServerRegistry:
|
|
|
192
200
|
read_stream,
|
|
193
201
|
write_stream,
|
|
194
202
|
read_timeout_seconds,
|
|
195
|
-
|
|
203
|
+
server_config=config,
|
|
196
204
|
)
|
|
197
205
|
async with session:
|
|
198
206
|
logger.info(f"{server_name}: Connected to server using SSE transport.")
|
|
@@ -216,7 +224,7 @@ class ServerRegistry:
|
|
|
216
224
|
read_stream,
|
|
217
225
|
write_stream,
|
|
218
226
|
read_timeout_seconds,
|
|
219
|
-
|
|
227
|
+
server_config=config,
|
|
220
228
|
)
|
|
221
229
|
async with session:
|
|
222
230
|
logger.info(f"{server_name}: Connected to server using HTTP transport.")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from mcp_agent.core.fastagent import FastAgent
|
|
4
|
-
from mcp_agent.llm.augmented_llm import RequestParams
|
|
5
4
|
|
|
6
5
|
# Create the application
|
|
7
6
|
fast = FastAgent("Data Analysis (Roots)")
|
|
@@ -21,8 +20,8 @@ Data files are accessible from the /mnt/data/ directory (this is the current wor
|
|
|
21
20
|
Visualisations should be saved as .png files in the current working directory.
|
|
22
21
|
""",
|
|
23
22
|
servers=["interpreter"],
|
|
24
|
-
request_params=RequestParams(maxTokens=8192),
|
|
25
23
|
)
|
|
24
|
+
@fast.agent(name="another_test", instruction="", servers=["filesystem"])
|
|
26
25
|
async def main() -> None:
|
|
27
26
|
# Use the app's context manager
|
|
28
27
|
async with fast.run() as agent:
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server for Account Creation Demo
|
|
3
|
+
|
|
4
|
+
This server provides an account signup form that can be triggered
|
|
5
|
+
by tools, demonstrating LLM-initiated elicitations.
|
|
6
|
+
|
|
7
|
+
Note: Following MCP spec, we don't collect sensitive information like passwords.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from mcp.server.elicitation import (
|
|
14
|
+
AcceptedElicitation,
|
|
15
|
+
CancelledElicitation,
|
|
16
|
+
DeclinedElicitation,
|
|
17
|
+
)
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
# Configure logging
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
level=logging.INFO,
|
|
24
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
25
|
+
stream=sys.stderr,
|
|
26
|
+
)
|
|
27
|
+
logger = logging.getLogger("elicitation_account_server")
|
|
28
|
+
|
|
29
|
+
# Create MCP server
|
|
30
|
+
mcp = FastMCP("Account Creation Server", log_level="INFO")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
async def create_user_account(service_name: str = "MyApp") -> str:
|
|
35
|
+
"""
|
|
36
|
+
Create a new user account for the specified service.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
service_name: The name of the service to create an account for
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Status message about the account creation
|
|
43
|
+
"""
|
|
44
|
+
# This tool triggers the elicitation form
|
|
45
|
+
logger.info(f"Creating account for service: {service_name}")
|
|
46
|
+
|
|
47
|
+
class AccountSignup(BaseModel):
|
|
48
|
+
username: str = Field(description="Choose a username", min_length=3, max_length=20)
|
|
49
|
+
email: str = Field(description="Your email address", json_schema_extra={"format": "email"})
|
|
50
|
+
full_name: str = Field(description="Your full name", max_length=30)
|
|
51
|
+
|
|
52
|
+
language: str = Field(
|
|
53
|
+
default="en",
|
|
54
|
+
description="Preferred language",
|
|
55
|
+
json_schema_extra={
|
|
56
|
+
"enum": [
|
|
57
|
+
"en",
|
|
58
|
+
"zh",
|
|
59
|
+
"es",
|
|
60
|
+
"fr",
|
|
61
|
+
"de",
|
|
62
|
+
"ja",
|
|
63
|
+
],
|
|
64
|
+
"enumNames": ["English", "中文", "Español", "Français", "Deutsch", "日本語"],
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
agree_terms: bool = Field(description="I agree to the terms of service")
|
|
68
|
+
marketing_emails: bool = Field(False, description="Send me product updates")
|
|
69
|
+
|
|
70
|
+
result = await mcp.get_context().elicit(
|
|
71
|
+
f"Create Your {service_name} Account", schema=AccountSignup
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
match result:
|
|
75
|
+
case AcceptedElicitation(data=data):
|
|
76
|
+
if not data.agree_terms:
|
|
77
|
+
return "❌ Account creation failed: You must agree to the terms of service"
|
|
78
|
+
else:
|
|
79
|
+
return f"✅ Account created successfully for {service_name}!\nUsername: {data.username}\nEmail: {data.email}"
|
|
80
|
+
case DeclinedElicitation():
|
|
81
|
+
return f"❌ Account creation for {service_name} was declined by user"
|
|
82
|
+
case CancelledElicitation():
|
|
83
|
+
return f"❌ Account creation for {service_name} was cancelled by user"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
logger.info("Starting account creation server...")
|
|
88
|
+
mcp.run()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server for Basic Elicitation Forms Demo
|
|
3
|
+
|
|
4
|
+
This server provides various elicitation resources that demonstrate
|
|
5
|
+
different form types and validation patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from mcp import ReadResourceResult
|
|
13
|
+
from mcp.server.elicitation import (
|
|
14
|
+
AcceptedElicitation,
|
|
15
|
+
CancelledElicitation,
|
|
16
|
+
DeclinedElicitation,
|
|
17
|
+
)
|
|
18
|
+
from mcp.server.fastmcp import FastMCP
|
|
19
|
+
from mcp.types import TextResourceContents
|
|
20
|
+
from pydantic import AnyUrl, BaseModel, Field
|
|
21
|
+
|
|
22
|
+
# Configure logging
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
level=logging.INFO,
|
|
25
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
26
|
+
stream=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
logger = logging.getLogger("elicitation_forms_server")
|
|
29
|
+
|
|
30
|
+
# Create MCP server
|
|
31
|
+
mcp = FastMCP("Elicitation Forms Demo Server", log_level="INFO")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.resource(uri="elicitation://event-registration")
|
|
35
|
+
async def event_registration() -> ReadResourceResult:
|
|
36
|
+
"""Register for a tech conference event."""
|
|
37
|
+
|
|
38
|
+
class EventRegistration(BaseModel):
|
|
39
|
+
name: str = Field(description="Your full name", min_length=2, max_length=100)
|
|
40
|
+
email: str = Field(description="Your email address", json_schema_extra={"format": "email"})
|
|
41
|
+
company_website: Optional[str] = Field(
|
|
42
|
+
None, description="Your company website (optional)", json_schema_extra={"format": "uri"}
|
|
43
|
+
)
|
|
44
|
+
event_date: str = Field(
|
|
45
|
+
description="Which event date works for you?", json_schema_extra={"format": "date"}
|
|
46
|
+
)
|
|
47
|
+
dietary_requirements: Optional[str] = Field(
|
|
48
|
+
None, description="Any dietary requirements? (optional)", max_length=200
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
result = await mcp.get_context().elicit(
|
|
52
|
+
"Register for the fast-agent conference - fill out your details",
|
|
53
|
+
schema=EventRegistration,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
match result:
|
|
57
|
+
case AcceptedElicitation(data=data):
|
|
58
|
+
lines = [
|
|
59
|
+
f"✅ Registration confirmed for {data.name}",
|
|
60
|
+
f"📧 Email: {data.email}",
|
|
61
|
+
f"🏢 Company: {data.company_website or 'Not provided'}",
|
|
62
|
+
f"📅 Event Date: {data.event_date}",
|
|
63
|
+
f"🍽️ Dietary Requirements: {data.dietary_requirements or 'None'}",
|
|
64
|
+
]
|
|
65
|
+
response = "\n".join(lines)
|
|
66
|
+
case DeclinedElicitation():
|
|
67
|
+
response = "Registration declined - no ticket reserved"
|
|
68
|
+
case CancelledElicitation():
|
|
69
|
+
response = "Registration cancelled - please try again later"
|
|
70
|
+
|
|
71
|
+
return ReadResourceResult(
|
|
72
|
+
contents=[
|
|
73
|
+
TextResourceContents(
|
|
74
|
+
mimeType="text/plain", uri=AnyUrl("elicitation://event-registration"), text=response
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mcp.resource(uri="elicitation://product-review")
|
|
81
|
+
async def product_review() -> ReadResourceResult:
|
|
82
|
+
"""Submit a product review with rating and comments."""
|
|
83
|
+
|
|
84
|
+
class ProductReview(BaseModel):
|
|
85
|
+
rating: int = Field(description="Rate this product (1-5 stars)", ge=1, le=5)
|
|
86
|
+
satisfaction: float = Field(
|
|
87
|
+
description="Overall satisfaction score (0.0-10.0)", ge=0.0, le=10.0
|
|
88
|
+
)
|
|
89
|
+
category: str = Field(
|
|
90
|
+
description="What type of product is this?",
|
|
91
|
+
json_schema_extra={
|
|
92
|
+
"enum": ["electronics", "books", "clothing", "home", "sports"],
|
|
93
|
+
"enumNames": [
|
|
94
|
+
"Electronics",
|
|
95
|
+
"Books & Media",
|
|
96
|
+
"Clothing",
|
|
97
|
+
"Home & Garden",
|
|
98
|
+
"Sports & Outdoors",
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
review_text: str = Field(
|
|
103
|
+
description="Tell us about your experience", min_length=10, max_length=1000
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
result = await mcp.get_context().elicit(
|
|
107
|
+
"Share your product review - Help others make informed decisions!", schema=ProductReview
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
match result:
|
|
111
|
+
case AcceptedElicitation(data=data):
|
|
112
|
+
stars = "⭐" * data.rating
|
|
113
|
+
lines = [
|
|
114
|
+
"🎯 Product Review Submitted!",
|
|
115
|
+
f"⭐ Rating: {stars} ({data.rating}/5)",
|
|
116
|
+
f"📊 Satisfaction: {data.satisfaction}/10.0",
|
|
117
|
+
f"📦 Category: {data.category.replace('_', ' ').title()}",
|
|
118
|
+
f"💬 Review: {data.review_text}",
|
|
119
|
+
]
|
|
120
|
+
response = "\n".join(lines)
|
|
121
|
+
case DeclinedElicitation():
|
|
122
|
+
response = "Review declined - no feedback submitted"
|
|
123
|
+
case CancelledElicitation():
|
|
124
|
+
response = "Review cancelled - you can submit it later"
|
|
125
|
+
|
|
126
|
+
return ReadResourceResult(
|
|
127
|
+
contents=[
|
|
128
|
+
TextResourceContents(
|
|
129
|
+
mimeType="text/plain", uri=AnyUrl("elicitation://product-review"), text=response
|
|
130
|
+
)
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.resource(uri="elicitation://account-settings")
|
|
136
|
+
async def account_settings() -> ReadResourceResult:
|
|
137
|
+
"""Configure your account settings and preferences."""
|
|
138
|
+
|
|
139
|
+
class AccountSettings(BaseModel):
|
|
140
|
+
email_notifications: bool = Field(True, description="Receive email notifications?")
|
|
141
|
+
marketing_emails: bool = Field(False, description="Subscribe to marketing emails?")
|
|
142
|
+
theme: str = Field(
|
|
143
|
+
description="Choose your preferred theme",
|
|
144
|
+
json_schema_extra={
|
|
145
|
+
"enum": ["light", "dark", "auto"],
|
|
146
|
+
"enumNames": ["Light Theme", "Dark Theme", "Auto (System)"],
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
privacy_public: bool = Field(False, description="Make your profile public?")
|
|
150
|
+
items_per_page: int = Field(description="Items to show per page (10-100)", ge=10, le=100)
|
|
151
|
+
|
|
152
|
+
result = await mcp.get_context().elicit("Update your account settings", schema=AccountSettings)
|
|
153
|
+
|
|
154
|
+
match result:
|
|
155
|
+
case AcceptedElicitation(data=data):
|
|
156
|
+
lines = [
|
|
157
|
+
"⚙️ Account Settings Updated!",
|
|
158
|
+
f"📧 Email notifications: {'On' if data.email_notifications else 'Off'}",
|
|
159
|
+
f"📬 Marketing emails: {'On' if data.marketing_emails else 'Off'}",
|
|
160
|
+
f"🎨 Theme: {data.theme.title()}",
|
|
161
|
+
f"👥 Public profile: {'Yes' if data.privacy_public else 'No'}",
|
|
162
|
+
f"📄 Items per page: {data.items_per_page}",
|
|
163
|
+
]
|
|
164
|
+
response = "\n".join(lines)
|
|
165
|
+
case DeclinedElicitation():
|
|
166
|
+
response = "Settings unchanged - keeping current preferences"
|
|
167
|
+
case CancelledElicitation():
|
|
168
|
+
response = "Settings update cancelled"
|
|
169
|
+
|
|
170
|
+
return ReadResourceResult(
|
|
171
|
+
contents=[
|
|
172
|
+
TextResourceContents(
|
|
173
|
+
mimeType="text/plain", uri=AnyUrl("elicitation://account-settings"), text=response
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@mcp.resource(uri="elicitation://service-appointment")
|
|
180
|
+
async def service_appointment() -> ReadResourceResult:
|
|
181
|
+
"""Schedule a car service appointment."""
|
|
182
|
+
|
|
183
|
+
class ServiceAppointment(BaseModel):
|
|
184
|
+
customer_name: str = Field(description="Your full name", min_length=2, max_length=50)
|
|
185
|
+
vehicle_type: str = Field(
|
|
186
|
+
description="What type of vehicle do you have?",
|
|
187
|
+
json_schema_extra={
|
|
188
|
+
"enum": ["sedan", "suv", "truck", "motorcycle", "other"],
|
|
189
|
+
"enumNames": ["Sedan", "SUV/Crossover", "Truck", "Motorcycle", "Other"],
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
needs_loaner: bool = Field(description="Do you need a loaner vehicle?")
|
|
193
|
+
appointment_time: str = Field(
|
|
194
|
+
description="Preferred appointment date and time",
|
|
195
|
+
json_schema_extra={"format": "date-time"},
|
|
196
|
+
)
|
|
197
|
+
priority_service: bool = Field(False, description="Is this an urgent repair?")
|
|
198
|
+
|
|
199
|
+
result = await mcp.get_context().elicit(
|
|
200
|
+
"Schedule your vehicle service appointment", schema=ServiceAppointment
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
match result:
|
|
204
|
+
case AcceptedElicitation(data=data):
|
|
205
|
+
lines = [
|
|
206
|
+
"🔧 Service Appointment Scheduled!",
|
|
207
|
+
f"👤 Customer: {data.customer_name}",
|
|
208
|
+
f"🚗 Vehicle: {data.vehicle_type.title()}",
|
|
209
|
+
f"🚙 Loaner needed: {'Yes' if data.needs_loaner else 'No'}",
|
|
210
|
+
f"📅 Appointment: {data.appointment_time}",
|
|
211
|
+
f"⚡ Priority service: {'Yes' if data.priority_service else 'No'}",
|
|
212
|
+
]
|
|
213
|
+
response = "\n".join(lines)
|
|
214
|
+
case DeclinedElicitation():
|
|
215
|
+
response = "Appointment cancelled - call us when you're ready!"
|
|
216
|
+
case CancelledElicitation():
|
|
217
|
+
response = "Appointment scheduling cancelled"
|
|
218
|
+
|
|
219
|
+
return ReadResourceResult(
|
|
220
|
+
contents=[
|
|
221
|
+
TextResourceContents(
|
|
222
|
+
mimeType="text/plain",
|
|
223
|
+
uri=AnyUrl("elicitation://service-appointment"),
|
|
224
|
+
text=response,
|
|
225
|
+
)
|
|
226
|
+
]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
if __name__ == "__main__":
|
|
231
|
+
logger.info("Starting elicitation forms demo server...")
|
|
232
|
+
mcp.run()
|