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.

Files changed (63) hide show
  1. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/METADATA +10 -7
  2. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/RECORD +45 -47
  3. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/licenses/LICENSE +1 -1
  4. mcp_agent/cli/commands/quickstart.py +60 -5
  5. mcp_agent/config.py +10 -0
  6. mcp_agent/context.py +1 -4
  7. mcp_agent/core/agent_types.py +7 -6
  8. mcp_agent/core/direct_decorators.py +14 -0
  9. mcp_agent/core/direct_factory.py +1 -0
  10. mcp_agent/core/fastagent.py +23 -2
  11. mcp_agent/human_input/elicitation_form.py +723 -0
  12. mcp_agent/human_input/elicitation_forms.py +59 -0
  13. mcp_agent/human_input/elicitation_handler.py +88 -0
  14. mcp_agent/human_input/elicitation_state.py +34 -0
  15. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
  16. mcp_agent/llm/providers/augmented_llm_openai.py +1 -1
  17. mcp_agent/mcp/elicitation_factory.py +84 -0
  18. mcp_agent/mcp/elicitation_handlers.py +155 -0
  19. mcp_agent/mcp/helpers/content_helpers.py +27 -0
  20. mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
  21. mcp_agent/mcp/mcp_agent_client_session.py +44 -1
  22. mcp_agent/mcp/mcp_aggregator.py +56 -11
  23. mcp_agent/mcp/mcp_connection_manager.py +30 -18
  24. mcp_agent/mcp_server/agent_server.py +2 -0
  25. mcp_agent/mcp_server_registry.py +16 -8
  26. mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
  27. mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  28. mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
  29. mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  30. mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  31. mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  32. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
  33. mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  34. mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  35. mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
  36. mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  37. mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
  38. mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
  39. mcp_agent/resources/examples/workflows/evaluator.py +1 -1
  40. mcp_agent/resources/examples/workflows/graded_report.md +89 -0
  41. mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
  42. mcp_agent/resources/examples/workflows/parallel.py +0 -2
  43. mcp_agent/resources/examples/workflows/short_story.md +13 -0
  44. mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
  45. mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
  46. mcp_agent/resources/examples/in_dev/slides.py +0 -110
  47. mcp_agent/resources/examples/internal/agent.py +0 -20
  48. mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
  49. mcp_agent/resources/examples/internal/history_transfer.py +0 -35
  50. mcp_agent/resources/examples/internal/job.py +0 -84
  51. mcp_agent/resources/examples/internal/prompt_category.py +0 -21
  52. mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
  53. mcp_agent/resources/examples/internal/simple.txt +0 -2
  54. mcp_agent/resources/examples/internal/sizer.py +0 -20
  55. mcp_agent/resources/examples/internal/social.py +0 -67
  56. mcp_agent/resources/examples/prompting/__init__.py +0 -3
  57. mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
  58. mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
  59. mcp_agent/resources/examples/prompting/image_server.py +0 -52
  60. mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
  61. mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
  62. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/WHEEL +0 -0
  63. {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.38.dist-info}/entry_points.txt +0 -0
@@ -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
- if hasattr(self, 'config') and self.config and hasattr(self.config, 'model'):
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
- if hasattr(self, 'config') and self.config and hasattr(self.config, 'model'):
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(self, server_name: str | None = None, agent_name: str | None = None) -> Mapping[str, List[Prompt]]:
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
- error_messages = []
224
- for subexc in exc.exceptions:
225
- if isinstance(subexc, HTTPStatusError):
226
- # Special handling for HTTP errors to make them more user-friendly
227
- error_messages.append(
228
- f"HTTP Error: {subexc.response.status_code} {subexc.response.reason_phrase} for URL: {subexc.request.url}"
229
- )
230
- else:
231
- error_messages.append(f"Error: {type(subexc).__name__}: {subexc}")
232
- if hasattr(subexc, "__cause__") and subexc.__cause__:
233
- error_messages.append(
234
- f"Caused by: {type(subexc.__cause__).__name__}: {subexc.__cause__}"
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.registry.get(server_name)
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."""
@@ -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 config.mcp is not None and hasattr(config.mcp, 'servers') and config.mcp.servers is not None:
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 settings.mcp is not None and hasattr(settings.mcp, 'servers') and settings.mcp.servers is not None:
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
- None, # No callback for stdio
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
- None, # No callback for stdio
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
- None, # No callback for stdio
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()