fast-agent-mcp 0.2.36__py3-none-any.whl → 0.2.37__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.37.dist-info}/METADATA +10 -7
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/RECORD +46 -47
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.dist-info}/licenses/LICENSE +1 -1
- mcp_agent/cli/commands/quickstart.py +59 -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/README.md +157 -0
- 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.37.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.36.dist-info → fast_agent_mcp-0.2.37.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,157 @@
|
|
|
1
|
+
# Quick Start: MCP Elicitations
|
|
2
|
+
|
|
3
|
+
This quickstart demonstrates **fast-agent**'s elicitation feature - a powerful way to collect structured data from users through forms and interactive prompts.
|
|
4
|
+
|
|
5
|
+
## What are Elicitations?
|
|
6
|
+
|
|
7
|
+
Elicitations allow MCP servers to request structured input from users through type-safe forms. This enables:
|
|
8
|
+
- User preference collection
|
|
9
|
+
- Account registration flows
|
|
10
|
+
- Configuration wizards
|
|
11
|
+
- Interactive feedback forms
|
|
12
|
+
- Any scenario requiring structured user input
|
|
13
|
+
|
|
14
|
+
## Examples Included
|
|
15
|
+
|
|
16
|
+
### 1. Forms Demo (`forms_demo.py`)
|
|
17
|
+
A showcase of different form types with beautiful rich console output. Uses the passthrough model to display forms directly to users.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv run forms_demo.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This example demonstrates:
|
|
24
|
+
- User profile collection
|
|
25
|
+
- Preference settings
|
|
26
|
+
- Simple yes/no ratings
|
|
27
|
+
- Detailed feedback forms
|
|
28
|
+
|
|
29
|
+
### 2. Account Creation Assistant (`account_creation.py`)
|
|
30
|
+
An AI-powered account creation workflow where the LLM initiates the account signup process and the user fills out the form.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Configure your LLM first (edit fastagent.config.yaml)
|
|
34
|
+
uv run account_creation.py --model gpt-4o
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This example shows:
|
|
38
|
+
- LLM initiating elicitation via tool calls
|
|
39
|
+
- User filling out the form (not the LLM)
|
|
40
|
+
- Tool call errors when user cancels/declines
|
|
41
|
+
- Success handling when form is completed
|
|
42
|
+
|
|
43
|
+
### 3. Game Character Creator (`game_character.py` + `game_character_handler.py`)
|
|
44
|
+
A whimsical example with custom elicitation handling, featuring animated dice rolls and visual effects.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv run game_character.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Features:
|
|
51
|
+
- Custom elicitation handler (in separate module for clarity)
|
|
52
|
+
- Animated progress bars and typewriter effects
|
|
53
|
+
- Epic dice roll mechanics with cosmic bonuses
|
|
54
|
+
- Interactive character creation with theatrical flair
|
|
55
|
+
- Demonstrates proper handler file organization
|
|
56
|
+
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
1. **Setup your environment:**
|
|
60
|
+
```bash
|
|
61
|
+
# Activate your Python environment
|
|
62
|
+
source .venv/bin/activate # or .venv\Scripts\activate on Windows
|
|
63
|
+
|
|
64
|
+
# Install dependencies if needed
|
|
65
|
+
uv pip install fast-agent-mcp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. **For the account creation example**, rename `fastagent.secrets.yaml.example` to `fastagent.secrets.yaml` and add your API keys.
|
|
69
|
+
|
|
70
|
+
3. **Run an example:**
|
|
71
|
+
```bash
|
|
72
|
+
# Try the forms demo first (quiet mode enabled programmatically)
|
|
73
|
+
uv run forms_demo.py
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## How Elicitations Work
|
|
77
|
+
|
|
78
|
+
### Elicitation Modes
|
|
79
|
+
|
|
80
|
+
**fast-agent** supports these elicitation modes:
|
|
81
|
+
|
|
82
|
+
1. **`forms`** - Shows forms to users (great with passthrough model)
|
|
83
|
+
2. **`auto_cancel`** - Automatically cancels all elicitations
|
|
84
|
+
3. **`none`** - No elicitation handling
|
|
85
|
+
4. **Custom handler** - Use your own handler function (overrides mode setting)
|
|
86
|
+
|
|
87
|
+
Configure modes in `fastagent.config.yaml`:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
mcp:
|
|
91
|
+
servers:
|
|
92
|
+
my_server:
|
|
93
|
+
command: "uv"
|
|
94
|
+
args: ["run", "server.py"]
|
|
95
|
+
elicitation:
|
|
96
|
+
mode: "forms" # or "auto" or "custom"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Creating Your Own Elicitations
|
|
100
|
+
|
|
101
|
+
#### Basic Server-Side Elicitation
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pydantic import BaseModel, Field
|
|
105
|
+
|
|
106
|
+
class UserPrefs(BaseModel):
|
|
107
|
+
theme: str = Field(
|
|
108
|
+
description="Color theme",
|
|
109
|
+
json_schema_extra={
|
|
110
|
+
"enum": ["light", "dark"],
|
|
111
|
+
"enumNames": ["Light Mode", "Dark Mode"]
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
notifications: bool = Field(True, description="Enable notifications?")
|
|
115
|
+
|
|
116
|
+
# In your MCP server:
|
|
117
|
+
result = await mcp.get_context().elicit(
|
|
118
|
+
"Configure your preferences",
|
|
119
|
+
schema=UserPrefs
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Custom Elicitation Handler
|
|
124
|
+
|
|
125
|
+
For advanced interactive experiences, create a custom handler:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
async def my_custom_handler(context, params) -> ElicitResult:
|
|
129
|
+
# Your custom logic here - animations, special effects, etc.
|
|
130
|
+
content = {"field": "value"}
|
|
131
|
+
return ElicitResult(action="accept", content=content)
|
|
132
|
+
|
|
133
|
+
# Register with your agent:
|
|
134
|
+
@fast.agent(
|
|
135
|
+
"my-agent",
|
|
136
|
+
servers=["my_server"],
|
|
137
|
+
elicitation_handler=my_custom_handler
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
See `game_character_handler.py` for a complete example with animations and effects.
|
|
142
|
+
|
|
143
|
+
## Next Steps
|
|
144
|
+
|
|
145
|
+
- Explore the example code to understand different patterns
|
|
146
|
+
- Try modifying the forms in `elicitation_server.py`
|
|
147
|
+
- Create your own custom elicitation handlers
|
|
148
|
+
- Check the [documentation](https://fast-agent.ai) for advanced features
|
|
149
|
+
|
|
150
|
+
## Tips
|
|
151
|
+
|
|
152
|
+
- Use `rich` for beautiful console output
|
|
153
|
+
- Test with passthrough model first, then try real LLMs
|
|
154
|
+
- Custom handlers enable creative interactions
|
|
155
|
+
- Validate user input in your schemas using Pydantic
|
|
156
|
+
|
|
157
|
+
Happy form building! 🚀
|
|
@@ -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()
|