fast-agent-mcp 0.2.44__py3-none-any.whl → 0.2.46__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.44.dist-info → fast_agent_mcp-0.2.46.dist-info}/METADATA +5 -5
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.46.dist-info}/RECORD +28 -25
- mcp_agent/__init__.py +40 -0
- mcp_agent/agents/workflow/evaluator_optimizer.py +39 -63
- mcp_agent/agents/workflow/router_agent.py +46 -21
- mcp_agent/cli/commands/go.py +25 -4
- mcp_agent/context.py +4 -0
- mcp_agent/core/__init__.py +26 -0
- mcp_agent/core/direct_decorators.py +117 -20
- mcp_agent/core/enhanced_prompt.py +4 -5
- mcp_agent/human_input/__init__.py +50 -0
- mcp_agent/human_input/elicitation_form.py +16 -13
- mcp_agent/human_input/form_fields.py +252 -0
- mcp_agent/human_input/simple_form.py +111 -0
- mcp_agent/llm/augmented_llm.py +12 -4
- mcp_agent/llm/augmented_llm_passthrough.py +0 -11
- mcp_agent/llm/augmented_llm_playback.py +5 -3
- mcp_agent/llm/providers/augmented_llm_anthropic.py +254 -95
- mcp_agent/mcp/__init__.py +50 -0
- mcp_agent/mcp/helpers/__init__.py +23 -1
- mcp_agent/mcp/interfaces.py +13 -2
- mcp_agent/py.typed +0 -0
- mcp_agent/resources/examples/workflows/evaluator.py +2 -2
- mcp_agent/resources/examples/workflows/router.py +1 -1
- mcp_agent/ui/console_display.py +19 -9
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.46.dist-info}/WHEEL +0 -0
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.46.dist-info}/entry_points.txt +0 -0
- {fast_agent_mcp-0.2.44.dist-info → fast_agent_mcp-0.2.46.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ for creating agents in the DirectFastAgent framework.
|
|
|
6
6
|
|
|
7
7
|
import inspect
|
|
8
8
|
from functools import wraps
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import (
|
|
10
11
|
Awaitable,
|
|
11
12
|
Callable,
|
|
@@ -21,8 +22,12 @@ from typing import (
|
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
from mcp.client.session import ElicitationFnT
|
|
25
|
+
from pydantic import AnyUrl
|
|
24
26
|
|
|
25
27
|
from mcp_agent.agents.agent import AgentConfig
|
|
28
|
+
from mcp_agent.agents.workflow.router_agent import (
|
|
29
|
+
ROUTING_SYSTEM_INSTRUCTION,
|
|
30
|
+
)
|
|
26
31
|
from mcp_agent.core.agent_types import AgentType
|
|
27
32
|
from mcp_agent.core.request_params import RequestParams
|
|
28
33
|
|
|
@@ -82,6 +87,91 @@ class DecoratedEvaluatorOptimizerProtocol(DecoratedAgentProtocol[P, R], Protocol
|
|
|
82
87
|
_evaluator: str
|
|
83
88
|
|
|
84
89
|
|
|
90
|
+
def _fetch_url_content(url: str) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Fetch content from a URL.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
url: The URL to fetch content from
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The text content from the URL
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
requests.RequestException: If the URL cannot be fetched
|
|
102
|
+
UnicodeDecodeError: If the content cannot be decoded as UTF-8
|
|
103
|
+
"""
|
|
104
|
+
import requests
|
|
105
|
+
|
|
106
|
+
response = requests.get(url, timeout=10)
|
|
107
|
+
response.raise_for_status() # Raise exception for HTTP errors
|
|
108
|
+
return response.text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _apply_templates(text: str) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Apply template substitutions to instruction text.
|
|
114
|
+
|
|
115
|
+
Supported templates:
|
|
116
|
+
{{currentDate}} - Current date in format "24 July 2025"
|
|
117
|
+
{{url:https://...}} - Content fetched from the specified URL
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: The text to process
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Text with template substitutions applied
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
requests.RequestException: If a URL in {{url:...}} cannot be fetched
|
|
127
|
+
UnicodeDecodeError: If URL content cannot be decoded as UTF-8
|
|
128
|
+
"""
|
|
129
|
+
import re
|
|
130
|
+
from datetime import datetime
|
|
131
|
+
|
|
132
|
+
# Apply {{currentDate}} template
|
|
133
|
+
current_date = datetime.now().strftime("%d %B %Y")
|
|
134
|
+
text = text.replace("{{currentDate}}", current_date)
|
|
135
|
+
|
|
136
|
+
# Apply {{url:...}} templates
|
|
137
|
+
url_pattern = re.compile(r"\{\{url:(https?://[^}]+)\}\}")
|
|
138
|
+
|
|
139
|
+
def replace_url(match):
|
|
140
|
+
url = match.group(1)
|
|
141
|
+
return _fetch_url_content(url)
|
|
142
|
+
|
|
143
|
+
text = url_pattern.sub(replace_url, text)
|
|
144
|
+
|
|
145
|
+
return text
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _resolve_instruction(instruction: str | Path | AnyUrl) -> str:
|
|
149
|
+
"""
|
|
150
|
+
Resolve instruction from either a string, Path, or URL with template support.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
instruction: Either a string instruction, Path to a file, or URL containing the instruction
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
The resolved instruction string with templates applied
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
FileNotFoundError: If the Path doesn't exist
|
|
160
|
+
PermissionError: If the Path can't be read
|
|
161
|
+
UnicodeDecodeError: If the file/URL content can't be decoded as UTF-8
|
|
162
|
+
requests.RequestException: If the URL cannot be fetched
|
|
163
|
+
"""
|
|
164
|
+
if isinstance(instruction, Path):
|
|
165
|
+
text = instruction.read_text(encoding="utf-8")
|
|
166
|
+
elif isinstance(instruction, AnyUrl):
|
|
167
|
+
text = _fetch_url_content(str(instruction))
|
|
168
|
+
else:
|
|
169
|
+
text = instruction
|
|
170
|
+
|
|
171
|
+
# Apply template substitutions
|
|
172
|
+
return _apply_templates(text)
|
|
173
|
+
|
|
174
|
+
|
|
85
175
|
def _decorator_impl(
|
|
86
176
|
self,
|
|
87
177
|
agent_type: AgentType,
|
|
@@ -180,9 +270,9 @@ def _decorator_impl(
|
|
|
180
270
|
def agent(
|
|
181
271
|
self,
|
|
182
272
|
name: str = "default",
|
|
183
|
-
instruction_or_kwarg: Optional[str] = None,
|
|
273
|
+
instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
|
|
184
274
|
*,
|
|
185
|
-
instruction: str = "You are a helpful agent.",
|
|
275
|
+
instruction: str | Path | AnyUrl = "You are a helpful agent.",
|
|
186
276
|
servers: List[str] = [],
|
|
187
277
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
188
278
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
@@ -217,7 +307,10 @@ def agent(
|
|
|
217
307
|
Returns:
|
|
218
308
|
A decorator that registers the agent with proper type annotations
|
|
219
309
|
"""
|
|
220
|
-
|
|
310
|
+
final_instruction_raw = (
|
|
311
|
+
instruction_or_kwarg if instruction_or_kwarg is not None else instruction
|
|
312
|
+
)
|
|
313
|
+
final_instruction = _resolve_instruction(final_instruction_raw)
|
|
221
314
|
|
|
222
315
|
return _decorator_impl(
|
|
223
316
|
self,
|
|
@@ -242,9 +335,9 @@ def custom(
|
|
|
242
335
|
self,
|
|
243
336
|
cls,
|
|
244
337
|
name: str = "default",
|
|
245
|
-
instruction_or_kwarg: Optional[str] = None,
|
|
338
|
+
instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
|
|
246
339
|
*,
|
|
247
|
-
instruction: str = "You are a helpful agent.",
|
|
340
|
+
instruction: str | Path | AnyUrl = "You are a helpful agent.",
|
|
248
341
|
servers: List[str] = [],
|
|
249
342
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
250
343
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
@@ -274,7 +367,10 @@ def custom(
|
|
|
274
367
|
Returns:
|
|
275
368
|
A decorator that registers the agent with proper type annotations
|
|
276
369
|
"""
|
|
277
|
-
|
|
370
|
+
final_instruction_raw = (
|
|
371
|
+
instruction_or_kwarg if instruction_or_kwarg is not None else instruction
|
|
372
|
+
)
|
|
373
|
+
final_instruction = _resolve_instruction(final_instruction_raw)
|
|
278
374
|
|
|
279
375
|
return _decorator_impl(
|
|
280
376
|
self,
|
|
@@ -308,7 +404,7 @@ def orchestrator(
|
|
|
308
404
|
name: str,
|
|
309
405
|
*,
|
|
310
406
|
agents: List[str],
|
|
311
|
-
instruction: str = DEFAULT_INSTRUCTION_ORCHESTRATOR,
|
|
407
|
+
instruction: str | Path | AnyUrl = DEFAULT_INSTRUCTION_ORCHESTRATOR,
|
|
312
408
|
model: Optional[str] = None,
|
|
313
409
|
request_params: RequestParams | None = None,
|
|
314
410
|
use_history: bool = False,
|
|
@@ -338,6 +434,7 @@ def orchestrator(
|
|
|
338
434
|
"""
|
|
339
435
|
|
|
340
436
|
# Create final request params with plan_iterations
|
|
437
|
+
resolved_instruction = _resolve_instruction(instruction)
|
|
341
438
|
|
|
342
439
|
return cast(
|
|
343
440
|
"Callable[[AgentCallable[P, R]], DecoratedOrchestratorProtocol[P, R]]",
|
|
@@ -345,7 +442,7 @@ def orchestrator(
|
|
|
345
442
|
self,
|
|
346
443
|
AgentType.ORCHESTRATOR,
|
|
347
444
|
name=name,
|
|
348
|
-
instruction=
|
|
445
|
+
instruction=resolved_instruction,
|
|
349
446
|
servers=[], # Orchestrators don't connect to servers directly
|
|
350
447
|
model=model,
|
|
351
448
|
use_history=use_history,
|
|
@@ -365,7 +462,7 @@ def router(
|
|
|
365
462
|
name: str,
|
|
366
463
|
*,
|
|
367
464
|
agents: List[str],
|
|
368
|
-
instruction: Optional[str] = None,
|
|
465
|
+
instruction: Optional[str | Path | AnyUrl] = None,
|
|
369
466
|
servers: List[str] = [],
|
|
370
467
|
tools: Optional[Dict[str, List[str]]] = None,
|
|
371
468
|
resources: Optional[Dict[str, List[str]]] = None,
|
|
@@ -397,10 +494,7 @@ def router(
|
|
|
397
494
|
Returns:
|
|
398
495
|
A decorator that registers the router with proper type annotations
|
|
399
496
|
"""
|
|
400
|
-
|
|
401
|
-
You are a router that determines which specialized agent should handle a given query.
|
|
402
|
-
Analyze the query and select the most appropriate agent to handle it.
|
|
403
|
-
"""
|
|
497
|
+
resolved_instruction = _resolve_instruction(instruction or ROUTING_SYSTEM_INSTRUCTION)
|
|
404
498
|
|
|
405
499
|
return cast(
|
|
406
500
|
"Callable[[AgentCallable[P, R]], DecoratedRouterProtocol[P, R]]",
|
|
@@ -408,7 +502,7 @@ def router(
|
|
|
408
502
|
self,
|
|
409
503
|
AgentType.ROUTER,
|
|
410
504
|
name=name,
|
|
411
|
-
instruction=
|
|
505
|
+
instruction=resolved_instruction,
|
|
412
506
|
servers=servers,
|
|
413
507
|
model=model,
|
|
414
508
|
use_history=use_history,
|
|
@@ -430,7 +524,7 @@ def chain(
|
|
|
430
524
|
name: str,
|
|
431
525
|
*,
|
|
432
526
|
sequence: List[str],
|
|
433
|
-
instruction: Optional[str] = None,
|
|
527
|
+
instruction: Optional[str | Path | AnyUrl] = None,
|
|
434
528
|
cumulative: bool = False,
|
|
435
529
|
default: bool = False,
|
|
436
530
|
) -> Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]:
|
|
@@ -457,6 +551,7 @@ def chain(
|
|
|
457
551
|
You are a chain that processes requests through a series of specialized agents in sequence.
|
|
458
552
|
Pass the output of each agent to the next agent in the chain.
|
|
459
553
|
"""
|
|
554
|
+
resolved_instruction = _resolve_instruction(instruction or default_instruction)
|
|
460
555
|
|
|
461
556
|
return cast(
|
|
462
557
|
"Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]",
|
|
@@ -464,7 +559,7 @@ def chain(
|
|
|
464
559
|
self,
|
|
465
560
|
AgentType.CHAIN,
|
|
466
561
|
name=name,
|
|
467
|
-
instruction=
|
|
562
|
+
instruction=resolved_instruction,
|
|
468
563
|
sequence=sequence,
|
|
469
564
|
cumulative=cumulative,
|
|
470
565
|
default=default,
|
|
@@ -478,7 +573,7 @@ def parallel(
|
|
|
478
573
|
*,
|
|
479
574
|
fan_out: List[str],
|
|
480
575
|
fan_in: str | None = None,
|
|
481
|
-
instruction: Optional[str] = None,
|
|
576
|
+
instruction: Optional[str | Path | AnyUrl] = None,
|
|
482
577
|
include_request: bool = True,
|
|
483
578
|
default: bool = False,
|
|
484
579
|
) -> Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]:
|
|
@@ -500,6 +595,7 @@ def parallel(
|
|
|
500
595
|
You are a parallel processor that executes multiple agents simultaneously
|
|
501
596
|
and aggregates their results.
|
|
502
597
|
"""
|
|
598
|
+
resolved_instruction = _resolve_instruction(instruction or default_instruction)
|
|
503
599
|
|
|
504
600
|
return cast(
|
|
505
601
|
"Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]",
|
|
@@ -507,7 +603,7 @@ def parallel(
|
|
|
507
603
|
self,
|
|
508
604
|
AgentType.PARALLEL,
|
|
509
605
|
name=name,
|
|
510
|
-
instruction=
|
|
606
|
+
instruction=resolved_instruction,
|
|
511
607
|
servers=[], # Parallel agents don't connect to servers directly
|
|
512
608
|
fan_in=fan_in,
|
|
513
609
|
fan_out=fan_out,
|
|
@@ -523,7 +619,7 @@ def evaluator_optimizer(
|
|
|
523
619
|
*,
|
|
524
620
|
generator: str,
|
|
525
621
|
evaluator: str,
|
|
526
|
-
instruction: Optional[str] = None,
|
|
622
|
+
instruction: Optional[str | Path | AnyUrl] = None,
|
|
527
623
|
min_rating: str = "GOOD",
|
|
528
624
|
max_refinements: int = 3,
|
|
529
625
|
default: bool = False,
|
|
@@ -548,6 +644,7 @@ def evaluator_optimizer(
|
|
|
548
644
|
evaluated for quality, and then refined based on specific feedback until
|
|
549
645
|
it reaches an acceptable quality standard.
|
|
550
646
|
"""
|
|
647
|
+
resolved_instruction = _resolve_instruction(instruction or default_instruction)
|
|
551
648
|
|
|
552
649
|
return cast(
|
|
553
650
|
"Callable[[AgentCallable[P, R]], DecoratedEvaluatorOptimizerProtocol[P, R]]",
|
|
@@ -555,7 +652,7 @@ def evaluator_optimizer(
|
|
|
555
652
|
self,
|
|
556
653
|
AgentType.EVALUATOR_OPTIMIZER,
|
|
557
654
|
name=name,
|
|
558
|
-
instruction=
|
|
655
|
+
instruction=resolved_instruction,
|
|
559
656
|
servers=[], # Evaluator-optimizer doesn't connect to servers directly
|
|
560
657
|
generator=generator,
|
|
561
658
|
evaluator=evaluator,
|
|
@@ -511,13 +511,12 @@ def create_keybindings(on_toggle_multiline=None, app=None, agent_provider=None,
|
|
|
511
511
|
rich_print("\n[green]✓ Copied to clipboard[/green]")
|
|
512
512
|
return
|
|
513
513
|
|
|
514
|
-
rich_print("\n[yellow]No assistant messages found[/yellow]")
|
|
515
514
|
else:
|
|
516
|
-
|
|
517
|
-
except Exception
|
|
518
|
-
|
|
515
|
+
pass
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
519
518
|
else:
|
|
520
|
-
|
|
519
|
+
pass
|
|
521
520
|
|
|
522
521
|
return kb
|
|
523
522
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Human input modules for forms and elicitation."""
|
|
2
|
+
|
|
3
|
+
# Export the simple form API
|
|
4
|
+
# Export field types and schema builder
|
|
5
|
+
from mcp_agent.human_input.form_fields import (
|
|
6
|
+
BooleanField,
|
|
7
|
+
EnumField,
|
|
8
|
+
FormSchema,
|
|
9
|
+
IntegerField,
|
|
10
|
+
NumberField,
|
|
11
|
+
# Field classes
|
|
12
|
+
StringField,
|
|
13
|
+
boolean,
|
|
14
|
+
choice,
|
|
15
|
+
date,
|
|
16
|
+
datetime,
|
|
17
|
+
email,
|
|
18
|
+
integer,
|
|
19
|
+
number,
|
|
20
|
+
# Convenience functions
|
|
21
|
+
string,
|
|
22
|
+
url,
|
|
23
|
+
)
|
|
24
|
+
from mcp_agent.human_input.simple_form import ask, ask_sync, form, form_sync
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Form functions
|
|
28
|
+
"form",
|
|
29
|
+
"form_sync",
|
|
30
|
+
"ask",
|
|
31
|
+
"ask_sync",
|
|
32
|
+
# Schema builder
|
|
33
|
+
"FormSchema",
|
|
34
|
+
# Field classes
|
|
35
|
+
"StringField",
|
|
36
|
+
"IntegerField",
|
|
37
|
+
"NumberField",
|
|
38
|
+
"BooleanField",
|
|
39
|
+
"EnumField",
|
|
40
|
+
# Field convenience functions
|
|
41
|
+
"string",
|
|
42
|
+
"email",
|
|
43
|
+
"url",
|
|
44
|
+
"date",
|
|
45
|
+
"datetime",
|
|
46
|
+
"integer",
|
|
47
|
+
"number",
|
|
48
|
+
"boolean",
|
|
49
|
+
"choice",
|
|
50
|
+
]
|
|
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
from mcp.types import ElicitRequestedSchema
|
|
7
7
|
from prompt_toolkit import Application
|
|
8
|
+
from prompt_toolkit.application.current import get_app
|
|
8
9
|
from prompt_toolkit.buffer import Buffer
|
|
9
10
|
from prompt_toolkit.filters import Condition
|
|
10
11
|
from prompt_toolkit.formatted_text import FormattedText
|
|
@@ -272,29 +273,32 @@ class ElicitationForm:
|
|
|
272
273
|
keep_focused_window_visible=True,
|
|
273
274
|
)
|
|
274
275
|
|
|
275
|
-
#
|
|
276
|
-
title_bar = Window(
|
|
277
|
-
FormattedTextControl(FormattedText([("class:title", "Elicitation Request")])),
|
|
278
|
-
height=1,
|
|
279
|
-
style="class:dialog.title",
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
# Combine title, sticky headers, and scrollable content
|
|
276
|
+
# Combine sticky headers and scrollable content (no separate title bar needed)
|
|
283
277
|
full_content = HSplit(
|
|
284
278
|
[
|
|
285
|
-
|
|
286
|
-
Window(height=1), # Spacing after title
|
|
279
|
+
Window(height=1), # Top spacing
|
|
287
280
|
sticky_headers, # Headers stay fixed at top
|
|
288
281
|
scrollable_content, # Form fields can scroll
|
|
289
282
|
]
|
|
290
283
|
)
|
|
291
284
|
|
|
292
|
-
# Create dialog frame
|
|
285
|
+
# Create dialog frame with title
|
|
293
286
|
dialog = Frame(
|
|
294
287
|
body=full_content,
|
|
288
|
+
title="Elicitation Request",
|
|
295
289
|
style="class:dialog",
|
|
296
290
|
)
|
|
297
291
|
|
|
292
|
+
# Apply width constraints by putting Frame in VSplit with flexible spacers
|
|
293
|
+
# This prevents console display interference and constrains the Frame border
|
|
294
|
+
constrained_dialog = VSplit(
|
|
295
|
+
[
|
|
296
|
+
Window(width=10), # Smaller left spacer
|
|
297
|
+
dialog,
|
|
298
|
+
Window(width=10), # Smaller right spacer
|
|
299
|
+
]
|
|
300
|
+
)
|
|
301
|
+
|
|
298
302
|
# Key bindings
|
|
299
303
|
kb = KeyBindings()
|
|
300
304
|
|
|
@@ -370,7 +374,7 @@ class ElicitationForm:
|
|
|
370
374
|
# Add toolbar to the layout
|
|
371
375
|
root_layout = HSplit(
|
|
372
376
|
[
|
|
373
|
-
|
|
377
|
+
constrained_dialog, # The width-constrained dialog
|
|
374
378
|
self._toolbar_window,
|
|
375
379
|
]
|
|
376
380
|
)
|
|
@@ -588,7 +592,6 @@ class ElicitationForm:
|
|
|
588
592
|
|
|
589
593
|
def _is_in_multiline_field(self) -> bool:
|
|
590
594
|
"""Check if currently focused field is a multiline field."""
|
|
591
|
-
from prompt_toolkit.application.current import get_app
|
|
592
595
|
|
|
593
596
|
focused = get_app().layout.current_control
|
|
594
597
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""High-level field types for elicitation forms with default support."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class StringField:
|
|
9
|
+
"""String field with validation and default support."""
|
|
10
|
+
|
|
11
|
+
title: Optional[str] = None
|
|
12
|
+
description: Optional[str] = None
|
|
13
|
+
default: Optional[str] = None
|
|
14
|
+
min_length: Optional[int] = None
|
|
15
|
+
max_length: Optional[int] = None
|
|
16
|
+
format: Optional[str] = None # email, uri, date, date-time
|
|
17
|
+
|
|
18
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
19
|
+
"""Convert to MCP elicitation schema format."""
|
|
20
|
+
schema: Dict[str, Any] = {"type": "string"}
|
|
21
|
+
|
|
22
|
+
if self.title:
|
|
23
|
+
schema["title"] = self.title
|
|
24
|
+
if self.description:
|
|
25
|
+
schema["description"] = self.description
|
|
26
|
+
if self.default is not None:
|
|
27
|
+
schema["default"] = self.default
|
|
28
|
+
if self.min_length is not None:
|
|
29
|
+
schema["minLength"] = self.min_length
|
|
30
|
+
if self.max_length is not None:
|
|
31
|
+
schema["maxLength"] = self.max_length
|
|
32
|
+
if self.format:
|
|
33
|
+
schema["format"] = self.format
|
|
34
|
+
|
|
35
|
+
return schema
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class IntegerField:
|
|
40
|
+
"""Integer field with validation and default support."""
|
|
41
|
+
|
|
42
|
+
title: Optional[str] = None
|
|
43
|
+
description: Optional[str] = None
|
|
44
|
+
default: Optional[int] = None
|
|
45
|
+
minimum: Optional[int] = None
|
|
46
|
+
maximum: Optional[int] = None
|
|
47
|
+
|
|
48
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
49
|
+
"""Convert to MCP elicitation schema format."""
|
|
50
|
+
schema: Dict[str, Any] = {"type": "integer"}
|
|
51
|
+
|
|
52
|
+
if self.title:
|
|
53
|
+
schema["title"] = self.title
|
|
54
|
+
if self.description:
|
|
55
|
+
schema["description"] = self.description
|
|
56
|
+
if self.default is not None:
|
|
57
|
+
schema["default"] = self.default
|
|
58
|
+
if self.minimum is not None:
|
|
59
|
+
schema["minimum"] = self.minimum
|
|
60
|
+
if self.maximum is not None:
|
|
61
|
+
schema["maximum"] = self.maximum
|
|
62
|
+
|
|
63
|
+
return schema
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class NumberField:
|
|
68
|
+
"""Number (float) field with validation and default support."""
|
|
69
|
+
|
|
70
|
+
title: Optional[str] = None
|
|
71
|
+
description: Optional[str] = None
|
|
72
|
+
default: Optional[float] = None
|
|
73
|
+
minimum: Optional[float] = None
|
|
74
|
+
maximum: Optional[float] = None
|
|
75
|
+
|
|
76
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
77
|
+
"""Convert to MCP elicitation schema format."""
|
|
78
|
+
schema: Dict[str, Any] = {"type": "number"}
|
|
79
|
+
|
|
80
|
+
if self.title:
|
|
81
|
+
schema["title"] = self.title
|
|
82
|
+
if self.description:
|
|
83
|
+
schema["description"] = self.description
|
|
84
|
+
if self.default is not None:
|
|
85
|
+
schema["default"] = self.default
|
|
86
|
+
if self.minimum is not None:
|
|
87
|
+
schema["minimum"] = self.minimum
|
|
88
|
+
if self.maximum is not None:
|
|
89
|
+
schema["maximum"] = self.maximum
|
|
90
|
+
|
|
91
|
+
return schema
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class BooleanField:
|
|
96
|
+
"""Boolean field with default support."""
|
|
97
|
+
|
|
98
|
+
title: Optional[str] = None
|
|
99
|
+
description: Optional[str] = None
|
|
100
|
+
default: Optional[bool] = None
|
|
101
|
+
|
|
102
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
103
|
+
"""Convert to MCP elicitation schema format."""
|
|
104
|
+
schema: Dict[str, Any] = {"type": "boolean"}
|
|
105
|
+
|
|
106
|
+
if self.title:
|
|
107
|
+
schema["title"] = self.title
|
|
108
|
+
if self.description:
|
|
109
|
+
schema["description"] = self.description
|
|
110
|
+
if self.default is not None:
|
|
111
|
+
schema["default"] = self.default
|
|
112
|
+
|
|
113
|
+
return schema
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class EnumField:
|
|
118
|
+
"""Enum/choice field with default support."""
|
|
119
|
+
|
|
120
|
+
choices: List[str]
|
|
121
|
+
choice_names: Optional[List[str]] = None # Human-readable names
|
|
122
|
+
title: Optional[str] = None
|
|
123
|
+
description: Optional[str] = None
|
|
124
|
+
default: Optional[str] = None
|
|
125
|
+
|
|
126
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
127
|
+
"""Convert to MCP elicitation schema format."""
|
|
128
|
+
schema: Dict[str, Any] = {"type": "string", "enum": self.choices}
|
|
129
|
+
|
|
130
|
+
if self.title:
|
|
131
|
+
schema["title"] = self.title
|
|
132
|
+
if self.description:
|
|
133
|
+
schema["description"] = self.description
|
|
134
|
+
if self.default is not None:
|
|
135
|
+
schema["default"] = self.default
|
|
136
|
+
if self.choice_names:
|
|
137
|
+
schema["enumNames"] = self.choice_names
|
|
138
|
+
|
|
139
|
+
return schema
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Field type union
|
|
143
|
+
FieldType = Union[StringField, IntegerField, NumberField, BooleanField, EnumField]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class FormSchema:
|
|
147
|
+
"""High-level form schema builder."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, **fields: FieldType):
|
|
150
|
+
"""Create a form schema with named fields."""
|
|
151
|
+
self.fields = fields
|
|
152
|
+
self._required_fields: List[str] = []
|
|
153
|
+
|
|
154
|
+
def required(self, *field_names: str) -> "FormSchema":
|
|
155
|
+
"""Mark fields as required."""
|
|
156
|
+
self._required_fields.extend(field_names)
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def to_schema(self) -> Dict[str, Any]:
|
|
160
|
+
"""Convert to MCP ElicitRequestedSchema format."""
|
|
161
|
+
properties = {}
|
|
162
|
+
|
|
163
|
+
for field_name, field in self.fields.items():
|
|
164
|
+
properties[field_name] = field.to_schema()
|
|
165
|
+
|
|
166
|
+
schema: Dict[str, Any] = {"type": "object", "properties": properties}
|
|
167
|
+
|
|
168
|
+
if self._required_fields:
|
|
169
|
+
schema["required"] = self._required_fields
|
|
170
|
+
|
|
171
|
+
return schema
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Convenience functions for creating fields
|
|
175
|
+
def string(
|
|
176
|
+
title: Optional[str] = None,
|
|
177
|
+
description: Optional[str] = None,
|
|
178
|
+
default: Optional[str] = None,
|
|
179
|
+
min_length: Optional[int] = None,
|
|
180
|
+
max_length: Optional[int] = None,
|
|
181
|
+
format: Optional[str] = None,
|
|
182
|
+
) -> StringField:
|
|
183
|
+
"""Create a string field."""
|
|
184
|
+
return StringField(title, description, default, min_length, max_length, format)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def email(
|
|
188
|
+
title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
|
|
189
|
+
) -> StringField:
|
|
190
|
+
"""Create an email field."""
|
|
191
|
+
return StringField(title, description, default, format="email")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def url(
|
|
195
|
+
title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
|
|
196
|
+
) -> StringField:
|
|
197
|
+
"""Create a URL field."""
|
|
198
|
+
return StringField(title, description, default, format="uri")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def date(
|
|
202
|
+
title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
|
|
203
|
+
) -> StringField:
|
|
204
|
+
"""Create a date field."""
|
|
205
|
+
return StringField(title, description, default, format="date")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def datetime(
|
|
209
|
+
title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
|
|
210
|
+
) -> StringField:
|
|
211
|
+
"""Create a datetime field."""
|
|
212
|
+
return StringField(title, description, default, format="date-time")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def integer(
|
|
216
|
+
title: Optional[str] = None,
|
|
217
|
+
description: Optional[str] = None,
|
|
218
|
+
default: Optional[int] = None,
|
|
219
|
+
minimum: Optional[int] = None,
|
|
220
|
+
maximum: Optional[int] = None,
|
|
221
|
+
) -> IntegerField:
|
|
222
|
+
"""Create an integer field."""
|
|
223
|
+
return IntegerField(title, description, default, minimum, maximum)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def number(
|
|
227
|
+
title: Optional[str] = None,
|
|
228
|
+
description: Optional[str] = None,
|
|
229
|
+
default: Optional[float] = None,
|
|
230
|
+
minimum: Optional[float] = None,
|
|
231
|
+
maximum: Optional[float] = None,
|
|
232
|
+
) -> NumberField:
|
|
233
|
+
"""Create a number field."""
|
|
234
|
+
return NumberField(title, description, default, minimum, maximum)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def boolean(
|
|
238
|
+
title: Optional[str] = None, description: Optional[str] = None, default: Optional[bool] = None
|
|
239
|
+
) -> BooleanField:
|
|
240
|
+
"""Create a boolean field."""
|
|
241
|
+
return BooleanField(title, description, default)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def choice(
|
|
245
|
+
choices: List[str],
|
|
246
|
+
choice_names: Optional[List[str]] = None,
|
|
247
|
+
title: Optional[str] = None,
|
|
248
|
+
description: Optional[str] = None,
|
|
249
|
+
default: Optional[str] = None,
|
|
250
|
+
) -> EnumField:
|
|
251
|
+
"""Create a choice/enum field."""
|
|
252
|
+
return EnumField(choices, choice_names, title, description, default)
|