fast-agent-mcp 0.1.8__py3-none-any.whl → 0.1.9__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.
Files changed (46) hide show
  1. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +26 -4
  2. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +43 -22
  3. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/entry_points.txt +1 -0
  4. mcp_agent/agents/agent.py +5 -11
  5. mcp_agent/core/agent_app.py +89 -13
  6. mcp_agent/core/fastagent.py +13 -3
  7. mcp_agent/core/mcp_content.py +222 -0
  8. mcp_agent/core/prompt.py +132 -0
  9. mcp_agent/core/proxies.py +41 -36
  10. mcp_agent/logging/transport.py +30 -3
  11. mcp_agent/mcp/mcp_aggregator.py +11 -10
  12. mcp_agent/mcp/mime_utils.py +69 -0
  13. mcp_agent/mcp/prompt_message_multipart.py +64 -0
  14. mcp_agent/mcp/prompt_serialization.py +447 -0
  15. mcp_agent/mcp/prompts/__init__.py +0 -0
  16. mcp_agent/mcp/prompts/__main__.py +10 -0
  17. mcp_agent/mcp/prompts/prompt_server.py +508 -0
  18. mcp_agent/mcp/prompts/prompt_template.py +469 -0
  19. mcp_agent/mcp/resource_utils.py +203 -0
  20. mcp_agent/resources/examples/internal/agent.py +1 -1
  21. mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
  22. mcp_agent/resources/examples/internal/sizer.py +0 -5
  23. mcp_agent/resources/examples/prompting/__init__.py +3 -0
  24. mcp_agent/resources/examples/prompting/agent.py +23 -0
  25. mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
  26. mcp_agent/resources/examples/prompting/image_server.py +56 -0
  27. mcp_agent/workflows/llm/anthropic_utils.py +101 -0
  28. mcp_agent/workflows/llm/augmented_llm.py +139 -66
  29. mcp_agent/workflows/llm/augmented_llm_anthropic.py +127 -251
  30. mcp_agent/workflows/llm/augmented_llm_openai.py +149 -305
  31. mcp_agent/workflows/llm/augmented_llm_passthrough.py +43 -0
  32. mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
  33. mcp_agent/workflows/llm/model_factory.py +20 -3
  34. mcp_agent/workflows/llm/openai_utils.py +65 -0
  35. mcp_agent/workflows/llm/providers/__init__.py +8 -0
  36. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
  37. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
  38. mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
  39. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
  40. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
  41. mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
  42. mcp_agent/core/server_validation.py +0 -44
  43. mcp_agent/core/simulator_registry.py +0 -22
  44. mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
  45. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
  46. {fast_agent_mcp-0.1.8.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ FastMCP Prompt Server V2
3
+
4
+ A server that loads prompts from text files with simple delimiters and serves them via MCP.
5
+ Uses the prompt_template module for clean, testable handling of prompt templates.
6
+ """
7
+
8
+ import asyncio
9
+ import argparse
10
+ import base64
11
+ import logging
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import List, Dict, Optional, Callable, Awaitable, Literal, Any
15
+
16
+ from mcp_agent.mcp import mime_utils, resource_utils
17
+
18
+ from mcp.server.fastmcp import FastMCP
19
+ from mcp.server.fastmcp.prompts.base import (
20
+ UserMessage,
21
+ AssistantMessage,
22
+ Message,
23
+ )
24
+ from mcp.types import (
25
+ TextContent,
26
+ )
27
+
28
+ from mcp_agent.mcp.prompts.prompt_template import (
29
+ PromptTemplateLoader,
30
+ PromptMetadata,
31
+ PromptContent,
32
+ PromptTemplate,
33
+ )
34
+
35
+ # Configure logging
36
+ logging.basicConfig(level=logging.INFO)
37
+ logger = logging.getLogger("prompt_server")
38
+
39
+ # Create FastMCP server
40
+ mcp = FastMCP("Prompt Server")
41
+
42
+
43
+ class PromptConfig(PromptMetadata):
44
+ """Configuration for the prompt server"""
45
+
46
+ prompt_files: List[Path] = []
47
+ user_delimiter: str = "---USER"
48
+ assistant_delimiter: str = "---ASSISTANT"
49
+ resource_delimiter: str = "---RESOURCE"
50
+ http_timeout: float = 10.0
51
+ transport: str = "stdio"
52
+ port: int = 8000
53
+
54
+
55
+ # Will be initialized with command line args
56
+ config = None
57
+
58
+ # We'll maintain registries of all exposed resources and prompts
59
+ exposed_resources: Dict[str, Path] = {}
60
+ prompt_registry: Dict[str, PromptMetadata] = {}
61
+
62
+ # Define message role type
63
+ MessageRole = Literal["user", "assistant"]
64
+
65
+
66
+ def create_content_message(text: str, role: MessageRole) -> Message:
67
+ """Create a text content message with the specified role"""
68
+ message_class = UserMessage if role == "user" else AssistantMessage
69
+ return message_class(content=TextContent(type="text", text=text))
70
+
71
+
72
+ def create_resource_message(
73
+ resource_path: str, content: str, mime_type: str, is_binary: bool, role: MessageRole
74
+ ) -> Message:
75
+ """Create a resource message with the specified content and role"""
76
+ message_class = UserMessage if role == "user" else AssistantMessage
77
+
78
+ if mime_utils.is_image_mime_type(mime_type):
79
+ # For images, create an ImageContent
80
+ image_content = resource_utils.create_image_content(
81
+ data=content, mime_type=mime_type
82
+ )
83
+ return message_class(content=image_content)
84
+ else:
85
+ # For other resources, create an EmbeddedResource
86
+ embedded_resource = resource_utils.create_embedded_resource(
87
+ resource_path, content, mime_type, is_binary
88
+ )
89
+ return message_class(content=embedded_resource)
90
+
91
+
92
+ def create_messages_with_resources(
93
+ content_sections: List[PromptContent], prompt_files: List[Path]
94
+ ) -> List[Message]:
95
+ """
96
+ Create a list of messages from content sections, with resources properly handled.
97
+
98
+ This implementation produces one message for each content section's text,
99
+ followed by separate messages for each resource (with the same role type
100
+ as the section they belong to).
101
+
102
+ Args:
103
+ content_sections: List of PromptContent objects
104
+ prompt_files: List of prompt files (to help locate resource files)
105
+
106
+ Returns:
107
+ List of Message objects
108
+ """
109
+ messages = []
110
+
111
+ for section in content_sections:
112
+ # Convert to our literal type for role
113
+ role = cast_message_role(section.role)
114
+
115
+ # Add the text message
116
+ messages.append(create_content_message(section.text, role))
117
+
118
+ # Add resource messages with the same role type as the section
119
+ for resource_path in section.resources:
120
+ try:
121
+ # Load resource with information about its type
122
+ resource_content, mime_type, is_binary = (
123
+ resource_utils.load_resource_content(resource_path, prompt_files)
124
+ )
125
+
126
+ # Create and add the resource message
127
+ resource_message = create_resource_message(
128
+ resource_path, resource_content, mime_type, is_binary, role
129
+ )
130
+ messages.append(resource_message)
131
+ except Exception as e:
132
+ logger.error(f"Error loading resource {resource_path}: {e}")
133
+
134
+ return messages
135
+
136
+
137
+ def cast_message_role(role: str) -> MessageRole:
138
+ """Cast a string role to a MessageRole literal type"""
139
+ if role == "user" or role == "assistant":
140
+ return role # type: ignore
141
+ # Default to user if the role is invalid
142
+ logger.warning(f"Invalid message role: {role}, defaulting to 'user'")
143
+ return "user"
144
+
145
+
146
+ # Define a single type for prompt handlers to avoid mypy issues
147
+ PromptHandler = Callable[..., Awaitable[List[Message]]]
148
+
149
+
150
+ def create_prompt_handler(
151
+ template: "PromptTemplate", template_vars: List[str], prompt_files: List[Path]
152
+ ) -> PromptHandler:
153
+ """Create a prompt handler function for the given template"""
154
+ if template_vars:
155
+ # With template variables
156
+ docstring = f"Prompt with template variables: {', '.join(template_vars)}"
157
+
158
+ async def prompt_handler(**kwargs: Any) -> List[Message]:
159
+ # Build context from parameters
160
+ context = {
161
+ var: kwargs.get(var)
162
+ for var in template_vars
163
+ if var in kwargs and kwargs[var] is not None
164
+ }
165
+
166
+ # Apply substitutions to the template
167
+ content_sections = template.apply_substitutions(context)
168
+
169
+ # Convert to MCP Message objects, handling resources properly
170
+ return create_messages_with_resources(content_sections, prompt_files)
171
+ else:
172
+ # No template variables
173
+ docstring = "Get a prompt with no variable substitution"
174
+
175
+ async def prompt_handler(**kwargs: Any) -> List[Message]:
176
+ # Get the content sections
177
+ content_sections = template.content_sections
178
+
179
+ # Convert to MCP Message objects, handling resources properly
180
+ return create_messages_with_resources(content_sections, prompt_files)
181
+
182
+ # Set the docstring
183
+ prompt_handler.__doc__ = docstring
184
+ return prompt_handler
185
+
186
+
187
+ # Type for resource handler
188
+ ResourceHandler = Callable[[], Awaitable[str]]
189
+
190
+
191
+ def create_resource_handler(resource_path: Path, mime_type: str) -> ResourceHandler:
192
+ """Create a resource handler function for the given resource"""
193
+
194
+ async def get_resource() -> str:
195
+ is_binary = mime_utils.is_binary_content(mime_type)
196
+
197
+ if is_binary:
198
+ # For binary files, read in binary mode and base64 encode
199
+ with open(resource_path, "rb") as f:
200
+ return base64.b64encode(f.read()).decode("utf-8")
201
+ else:
202
+ # For text files, read as utf-8 text
203
+ with open(resource_path, "r", encoding="utf-8") as f:
204
+ return f.read()
205
+
206
+ return get_resource
207
+
208
+
209
+ # Default delimiter values
210
+ DEFAULT_USER_DELIMITER = "---USER"
211
+ DEFAULT_ASSISTANT_DELIMITER = "---ASSISTANT"
212
+ DEFAULT_RESOURCE_DELIMITER = "---RESOURCE"
213
+
214
+
215
+ def get_delimiter_config(file_path: Optional[Path] = None) -> Dict[str, Any]:
216
+ """Get delimiter configuration, falling back to defaults if config is None"""
217
+ # Set defaults
218
+ config_values = {
219
+ "user_delimiter": DEFAULT_USER_DELIMITER,
220
+ "assistant_delimiter": DEFAULT_ASSISTANT_DELIMITER,
221
+ "resource_delimiter": DEFAULT_RESOURCE_DELIMITER,
222
+ "prompt_files": [file_path] if file_path else [],
223
+ }
224
+
225
+ # Override with config values if available
226
+ if config is not None:
227
+ config_values["user_delimiter"] = config.user_delimiter
228
+ config_values["assistant_delimiter"] = config.assistant_delimiter
229
+ config_values["resource_delimiter"] = config.resource_delimiter
230
+ config_values["prompt_files"] = config.prompt_files
231
+
232
+ return config_values
233
+
234
+
235
+ def register_prompt(file_path: Path):
236
+ """Register a prompt file"""
237
+ try:
238
+ # Get delimiter configuration
239
+ config_values = get_delimiter_config(file_path)
240
+
241
+ # Use our prompt template loader to analyze the file
242
+ loader = PromptTemplateLoader(
243
+ {
244
+ config_values["user_delimiter"]: "user",
245
+ config_values["assistant_delimiter"]: "assistant",
246
+ config_values["resource_delimiter"]: "resource",
247
+ }
248
+ )
249
+
250
+ # Get metadata and load the template
251
+ metadata = loader.get_metadata(file_path)
252
+ template = loader.load_from_file(file_path)
253
+
254
+ # Ensure unique name
255
+ prompt_name = metadata.name
256
+ if prompt_name in prompt_registry:
257
+ base_name = prompt_name
258
+ suffix = 1
259
+ while prompt_name in prompt_registry:
260
+ prompt_name = f"{base_name}_{suffix}"
261
+ suffix += 1
262
+ metadata.name = prompt_name
263
+
264
+ prompt_registry[metadata.name] = metadata
265
+ logger.info(f"Registered prompt: {metadata.name} ({file_path})")
266
+
267
+ # Create and register prompt handler
268
+ template_vars = list(metadata.template_variables)
269
+ handler = create_prompt_handler(
270
+ template, template_vars, config_values["prompt_files"]
271
+ )
272
+ mcp.prompt(name=metadata.name, description=metadata.description)(handler)
273
+
274
+ # Register any referenced resources in the prompt
275
+ for resource_path in metadata.resource_paths:
276
+ if not resource_path.startswith(("http://", "https://")):
277
+ # It's a local resource
278
+ resource_file = file_path.parent / resource_path
279
+ if resource_file.exists():
280
+ resource_id = f"resource://fast-agent/{resource_file.name}"
281
+
282
+ # Register the resource if not already registered
283
+ if resource_id not in exposed_resources:
284
+ exposed_resources[resource_id] = resource_file
285
+ mime_type = mime_utils.guess_mime_type(str(resource_file))
286
+
287
+ # Register with the correct resource ID directly with MCP
288
+ resource_handler = create_resource_handler(
289
+ resource_file, mime_type
290
+ )
291
+ mcp.resource(
292
+ resource_id,
293
+ description=f"Resource from {file_path.name}",
294
+ mime_type=mime_type,
295
+ )(resource_handler)
296
+
297
+ logger.info(
298
+ f"Registered resource: {resource_id} ({resource_file})"
299
+ )
300
+ except Exception as e:
301
+ logger.error(f"Error registering prompt {file_path}: {e}", exc_info=True)
302
+
303
+
304
+ def parse_args():
305
+ """Parse command line arguments"""
306
+ parser = argparse.ArgumentParser(description="FastMCP Prompt Server")
307
+ parser.add_argument(
308
+ "prompt_files", nargs="+", type=str, help="Prompt files to serve"
309
+ )
310
+ parser.add_argument(
311
+ "--user-delimiter",
312
+ type=str,
313
+ default="---USER",
314
+ help="Delimiter for user messages (default: ---USER)",
315
+ )
316
+ parser.add_argument(
317
+ "--assistant-delimiter",
318
+ type=str,
319
+ default="---ASSISTANT",
320
+ help="Delimiter for assistant messages (default: ---ASSISTANT)",
321
+ )
322
+ parser.add_argument(
323
+ "--resource-delimiter",
324
+ type=str,
325
+ default="---RESOURCE",
326
+ help="Delimiter for resource references (default: ---RESOURCE)",
327
+ )
328
+ parser.add_argument(
329
+ "--http-timeout",
330
+ type=float,
331
+ default=10.0,
332
+ help="Timeout for HTTP requests in seconds (default: 10.0)",
333
+ )
334
+ parser.add_argument(
335
+ "--transport",
336
+ type=str,
337
+ choices=["stdio", "sse"],
338
+ default="stdio",
339
+ help="Transport to use (default: stdio)",
340
+ )
341
+ parser.add_argument(
342
+ "--port",
343
+ type=int,
344
+ default=8000,
345
+ help="Port to use for SSE transport (default: 8000)",
346
+ )
347
+ parser.add_argument(
348
+ "--test", type=str, help="Test a specific prompt without starting the server"
349
+ )
350
+
351
+ return parser.parse_args()
352
+
353
+
354
+ async def register_file_resource_handler():
355
+ """Register the general file resource handler"""
356
+
357
+ @mcp.resource("file://{path}")
358
+ async def get_file_resource(path: str):
359
+ """Read a file from the given path."""
360
+ try:
361
+ # Find the file, checking relative paths first
362
+ file_path = resource_utils.find_resource_file(path, config.prompt_files)
363
+ if file_path is None:
364
+ # If not found as relative path, try absolute path
365
+ file_path = Path(path)
366
+ if not file_path.exists():
367
+ raise FileNotFoundError(f"Resource file not found: {path}")
368
+
369
+ mime_type = mime_utils.guess_mime_type(str(file_path))
370
+ is_binary = mime_utils.is_binary_content(mime_type)
371
+
372
+ if is_binary:
373
+ # For binary files, read as binary and base64 encode
374
+ with open(file_path, "rb") as f:
375
+ return base64.b64encode(f.read()).decode("utf-8")
376
+ else:
377
+ # For text files, read as text with UTF-8 encoding
378
+ with open(file_path, "r", encoding="utf-8") as f:
379
+ return f.read()
380
+ except Exception as e:
381
+ # Log the error and re-raise
382
+ logger.error(f"Error accessing resource at '{path}': {e}")
383
+ raise
384
+
385
+
386
+ async def test_prompt(prompt_name: str) -> int:
387
+ """Test a prompt and print its details"""
388
+ if prompt_name not in prompt_registry:
389
+ logger.error(f"Test prompt not found: {prompt_name}")
390
+ return 1
391
+
392
+ # Get delimiter configuration with reasonable defaults
393
+ config_values = get_delimiter_config()
394
+
395
+ metadata = prompt_registry[prompt_name]
396
+ print(f"\nTesting prompt: {prompt_name}")
397
+ print(f"Description: {metadata.description}")
398
+ print(f"Template variables: {', '.join(metadata.template_variables)}")
399
+
400
+ # Load and print the template
401
+ loader = PromptTemplateLoader(
402
+ {
403
+ config_values["user_delimiter"]: "user",
404
+ config_values["assistant_delimiter"]: "assistant",
405
+ config_values["resource_delimiter"]: "resource",
406
+ }
407
+ )
408
+ template = loader.load_from_file(metadata.file_path)
409
+
410
+ # Print each content section
411
+ print("\nContent sections:")
412
+ for i, section in enumerate(template.content_sections):
413
+ print(f"\n[{i + 1}] Role: {section.role}")
414
+ print(f"Content: {section.text}")
415
+ if section.resources:
416
+ print(f"Resources: {', '.join(section.resources)}")
417
+
418
+ # If there are template variables, test with dummy values
419
+ if metadata.template_variables:
420
+ print("\nTemplate substitution test:")
421
+ test_context = {var: f"[TEST-{var}]" for var in metadata.template_variables}
422
+ applied = template.apply_substitutions(test_context)
423
+
424
+ for i, section in enumerate(applied):
425
+ print(f"\n[{i + 1}] Role: {section.role}")
426
+ print(f"Content with substitutions: {section.text}")
427
+ if section.resources:
428
+ print(f"Resources with substitutions: {', '.join(section.resources)}")
429
+
430
+ return 0
431
+
432
+
433
+ async def async_main():
434
+ """Run the FastMCP server (async version)"""
435
+ global config
436
+
437
+ # Parse command line arguments
438
+ args = parse_args()
439
+
440
+ # Resolve file paths
441
+ prompt_files = []
442
+ for file_path in args.prompt_files:
443
+ path = Path(file_path)
444
+ if not path.exists():
445
+ logger.warning(f"File not found: {path}")
446
+ continue
447
+ prompt_files.append(path.resolve())
448
+
449
+ if not prompt_files:
450
+ logger.error("No valid prompt files specified")
451
+ return 1
452
+
453
+ # Initialize configuration
454
+ config = PromptConfig(
455
+ name="prompt_server",
456
+ description="FastMCP Prompt Server",
457
+ template_variables=set(),
458
+ resource_paths=[],
459
+ file_path=Path(__file__),
460
+ prompt_files=prompt_files,
461
+ user_delimiter=args.user_delimiter,
462
+ assistant_delimiter=args.assistant_delimiter,
463
+ resource_delimiter=args.resource_delimiter,
464
+ http_timeout=args.http_timeout,
465
+ transport=args.transport,
466
+ port=args.port,
467
+ )
468
+
469
+ # Register resource handlers
470
+ await register_file_resource_handler()
471
+
472
+ # Register all prompts
473
+ for file_path in config.prompt_files:
474
+ register_prompt(file_path)
475
+
476
+ # Print startup info
477
+ logger.info("Starting prompt server")
478
+ logger.info(f"Registered {len(prompt_registry)} prompts")
479
+ logger.info(f"Registered {len(exposed_resources)} resources")
480
+ logger.info(
481
+ f"Using delimiters: {config.user_delimiter}, {config.assistant_delimiter}, {config.resource_delimiter}"
482
+ )
483
+
484
+ # If a test prompt was specified, print it and exit
485
+ if args.test:
486
+ return await test_prompt(args.test)
487
+
488
+ # Start the server with the specified transport
489
+ if config.transport == "stdio":
490
+ await mcp.run_stdio_async()
491
+ else: # sse
492
+ await mcp.run_sse_async(port=config.port)
493
+
494
+
495
+ def main() -> int:
496
+ """Run the FastMCP server"""
497
+ try:
498
+ return asyncio.run(async_main())
499
+ except KeyboardInterrupt:
500
+ logger.info("\nServer stopped by user")
501
+ except Exception as e:
502
+ logger.error(f"\nError: {e}", exc_info=True)
503
+ return 1
504
+ return 0
505
+
506
+
507
+ if __name__ == "__main__":
508
+ sys.exit(main())