fast-agent-mcp 0.1.7__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 (56) hide show
  1. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/METADATA +37 -9
  2. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/RECORD +53 -31
  3. {fast_agent_mcp-0.1.7.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 +125 -44
  6. mcp_agent/core/decorators.py +3 -2
  7. mcp_agent/core/enhanced_prompt.py +106 -20
  8. mcp_agent/core/factory.py +28 -66
  9. mcp_agent/core/fastagent.py +13 -3
  10. mcp_agent/core/mcp_content.py +222 -0
  11. mcp_agent/core/prompt.py +132 -0
  12. mcp_agent/core/proxies.py +41 -36
  13. mcp_agent/human_input/handler.py +4 -1
  14. mcp_agent/logging/transport.py +30 -3
  15. mcp_agent/mcp/mcp_aggregator.py +27 -22
  16. mcp_agent/mcp/mime_utils.py +69 -0
  17. mcp_agent/mcp/prompt_message_multipart.py +64 -0
  18. mcp_agent/mcp/prompt_serialization.py +447 -0
  19. mcp_agent/mcp/prompts/__init__.py +0 -0
  20. mcp_agent/mcp/prompts/__main__.py +10 -0
  21. mcp_agent/mcp/prompts/prompt_server.py +508 -0
  22. mcp_agent/mcp/prompts/prompt_template.py +469 -0
  23. mcp_agent/mcp/resource_utils.py +203 -0
  24. mcp_agent/resources/examples/internal/agent.py +1 -1
  25. mcp_agent/resources/examples/internal/fastagent.config.yaml +2 -2
  26. mcp_agent/resources/examples/internal/sizer.py +0 -5
  27. mcp_agent/resources/examples/prompting/__init__.py +3 -0
  28. mcp_agent/resources/examples/prompting/agent.py +23 -0
  29. mcp_agent/resources/examples/prompting/fastagent.config.yaml +44 -0
  30. mcp_agent/resources/examples/prompting/image_server.py +56 -0
  31. mcp_agent/resources/examples/researcher/researcher-eval.py +1 -1
  32. mcp_agent/resources/examples/workflows/orchestrator.py +5 -4
  33. mcp_agent/resources/examples/workflows/router.py +0 -2
  34. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +57 -87
  35. mcp_agent/workflows/llm/anthropic_utils.py +101 -0
  36. mcp_agent/workflows/llm/augmented_llm.py +155 -141
  37. mcp_agent/workflows/llm/augmented_llm_anthropic.py +135 -281
  38. mcp_agent/workflows/llm/augmented_llm_openai.py +175 -337
  39. mcp_agent/workflows/llm/augmented_llm_passthrough.py +104 -0
  40. mcp_agent/workflows/llm/augmented_llm_playback.py +109 -0
  41. mcp_agent/workflows/llm/model_factory.py +25 -6
  42. mcp_agent/workflows/llm/openai_utils.py +65 -0
  43. mcp_agent/workflows/llm/providers/__init__.py +8 -0
  44. mcp_agent/workflows/llm/providers/multipart_converter_anthropic.py +348 -0
  45. mcp_agent/workflows/llm/providers/multipart_converter_openai.py +426 -0
  46. mcp_agent/workflows/llm/providers/openai_multipart.py +197 -0
  47. mcp_agent/workflows/llm/providers/sampling_converter_anthropic.py +258 -0
  48. mcp_agent/workflows/llm/providers/sampling_converter_openai.py +229 -0
  49. mcp_agent/workflows/llm/sampling_format_converter.py +39 -0
  50. mcp_agent/workflows/orchestrator/orchestrator.py +62 -153
  51. mcp_agent/workflows/router/router_llm.py +18 -24
  52. mcp_agent/core/server_validation.py +0 -44
  53. mcp_agent/core/simulator_registry.py +0 -22
  54. mcp_agent/workflows/llm/enhanced_passthrough.py +0 -70
  55. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/WHEEL +0 -0
  56. {fast_agent_mcp-0.1.7.dist-info → fast_agent_mcp-0.1.9.dist-info}/licenses/LICENSE +0 -0
@@ -1,48 +1,42 @@
1
- import json
2
1
  import os
3
- from typing import Iterable, List, Type
2
+ from typing import List, Type, TYPE_CHECKING
3
+
4
+ from mcp_agent.workflows.llm.providers.multipart_converter_anthropic import (
5
+ AnthropicConverter,
6
+ )
7
+ from mcp_agent.workflows.llm.providers.sampling_converter_anthropic import (
8
+ AnthropicSamplingConverter,
9
+ )
10
+
11
+ if TYPE_CHECKING:
12
+ from mcp_agent.mcp.prompt_message_multipart import PromptMessageMultipart
4
13
 
5
- from pydantic import BaseModel
6
14
 
7
- import instructor
8
15
  from anthropic import Anthropic, AuthenticationError
9
16
  from anthropic.types import (
10
- ContentBlock,
11
- DocumentBlockParam,
12
17
  Message,
13
18
  MessageParam,
14
- ImageBlockParam,
15
19
  TextBlock,
16
20
  TextBlockParam,
17
21
  ToolParam,
18
- ToolResultBlockParam,
19
22
  ToolUseBlockParam,
20
23
  )
21
24
  from mcp.types import (
22
25
  CallToolRequestParams,
23
26
  CallToolRequest,
24
- EmbeddedResource,
25
- ImageContent,
26
- StopReason,
27
- TextContent,
28
- TextResourceContents,
29
27
  )
28
+ from pydantic_core import from_json
30
29
 
31
- from mcp_agent.workflows.router.router_llm import StructuredResponse
32
30
  from mcp_agent.workflows.llm.augmented_llm import (
33
31
  AugmentedLLM,
34
32
  ModelT,
35
- MCPMessageParam,
36
- MCPMessageResult,
37
- ProviderToMCPConverter,
38
33
  RequestParams,
39
34
  )
40
35
  from mcp_agent.core.exceptions import ProviderKeyError
41
- from mcp_agent.logging.logger import get_logger
42
- from mcp.types import PromptMessage
43
36
  from rich.text import Text
44
37
 
45
- _logger = get_logger(__name__)
38
+ from mcp_agent.logging.logger import get_logger
39
+
46
40
  DEFAULT_ANTHROPIC_MODEL = "claude-3-7-sonnet-latest"
47
41
 
48
42
 
@@ -60,7 +54,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
60
54
  self.logger = get_logger(__name__)
61
55
 
62
56
  # Now call super().__init__
63
- super().__init__(*args, type_converter=AnthropicMCPTypeConverter, **kwargs)
57
+ super().__init__(*args, type_converter=AnthropicSamplingConverter, **kwargs)
64
58
 
65
59
  def _initialize_default_params(self, kwargs: dict) -> RequestParams:
66
60
  """Initialize Anthropic-specific default parameters"""
@@ -96,7 +90,7 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
96
90
  "Please check that your API key is valid and not expired.",
97
91
  ) from e
98
92
 
99
- # Always include prompt messages, but only include conversation history
93
+ # Always include prompt messages, but only include conversation history
100
94
  # if use_history is True
101
95
  messages.extend(self.history.get(include_history=params.use_history))
102
96
 
@@ -273,21 +267,10 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
273
267
  self.show_tool_result(result)
274
268
 
275
269
  # Add each result to our collection
276
- tool_results.append(
277
- ToolResultBlockParam(
278
- type="tool_result",
279
- tool_use_id=tool_use_id,
280
- content=result.content,
281
- is_error=result.isError,
282
- )
283
- )
270
+ tool_results.append((tool_use_id, result))
284
271
 
285
- # Add all tool results in a single message
286
272
  messages.append(
287
- MessageParam(
288
- role="user",
289
- content=tool_results,
290
- )
273
+ AnthropicConverter.create_tool_results_message(tool_results)
291
274
  )
292
275
 
293
276
  # Only save the new conversation messages to history if use_history is true
@@ -295,10 +278,10 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
295
278
  if params.use_history:
296
279
  # Get current prompt messages
297
280
  prompt_messages = self.history.get(include_history=False)
298
-
281
+
299
282
  # Calculate new conversation messages (excluding prompts)
300
- new_messages = messages[len(prompt_messages):]
301
-
283
+ new_messages = messages[len(prompt_messages) :]
284
+
302
285
  # Update conversation history
303
286
  self.history.set(new_messages)
304
287
 
@@ -336,7 +319,15 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
336
319
  Process a query using an LLM and available tools.
337
320
  The default implementation uses Claude as the LLM.
338
321
  Override this method to use a different LLM.
322
+
323
+ Special commands:
324
+ - "***SAVE_HISTORY <filename.md>" - Saves the conversation history to the specified file
325
+ in MCP prompt format with user/assistant delimiters.
339
326
  """
327
+ # Check if this is a special command to save history
328
+ if isinstance(message, str) and message.startswith("***SAVE_HISTORY "):
329
+ return await self._save_history_to_file(message)
330
+
340
331
  responses: List[Message] = await self.generate(
341
332
  message=message,
342
333
  request_params=request_params,
@@ -361,16 +352,116 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
361
352
  # Join all collected text
362
353
  return "\n".join(final_text)
363
354
 
355
+ async def generate_prompt(
356
+ self, prompt: "PromptMessageMultipart", request_params: RequestParams | None
357
+ ) -> str:
358
+ return await self.generate_str(
359
+ AnthropicConverter.convert_to_anthropic(prompt), request_params
360
+ )
361
+
362
+ async def _apply_prompt_template_provider_specific(
363
+ self, multipart_messages: List["PromptMessageMultipart"]
364
+ ) -> str:
365
+ """
366
+ Anthropic-specific implementation of apply_prompt_template that handles
367
+ multimodal content natively.
368
+
369
+ Args:
370
+ multipart_messages: List of PromptMessageMultipart objects parsed from the prompt template
371
+
372
+ Returns:
373
+ String representation of the assistant's response if generated,
374
+ or the last assistant message in the prompt
375
+ """
376
+ # Check the last message role
377
+ last_message = multipart_messages[-1]
378
+
379
+ # Add all previous messages to history (or all messages if last is from assistant)
380
+ messages_to_add = (
381
+ multipart_messages[:-1]
382
+ if last_message.role == "user"
383
+ else multipart_messages
384
+ )
385
+ converted = []
386
+ for msg in messages_to_add:
387
+ converted.append(AnthropicConverter.convert_to_anthropic(msg))
388
+ self.history.extend(converted, is_prompt=True)
389
+
390
+ if last_message.role == "user":
391
+ # For user messages: Generate response to the last one
392
+ self.logger.debug(
393
+ "Last message in prompt is from user, generating assistant response"
394
+ )
395
+ message_param = AnthropicConverter.convert_to_anthropic(last_message)
396
+ return await self.generate_str(message_param)
397
+ else:
398
+ # For assistant messages: Return the last message content as text
399
+ self.logger.debug(
400
+ "Last message in prompt is from assistant, returning it directly"
401
+ )
402
+ return str(last_message)
403
+
404
+ async def _save_history_to_file(self, command: str) -> str:
405
+ """
406
+ Save the conversation history to a file in MCP prompt format.
407
+
408
+ Args:
409
+ command: The command string, expected format: "***SAVE_HISTORY <filename.md>"
410
+
411
+ Returns:
412
+ Success or error message
413
+ """
414
+ try:
415
+ # Extract the filename from the command
416
+ parts = command.split(" ", 1)
417
+ if len(parts) != 2 or not parts[1].strip():
418
+ return "Error: Invalid format. Expected '***SAVE_HISTORY <filename.md>'"
419
+
420
+ filename = parts[1].strip()
421
+
422
+ # Get all messages from history
423
+ messages = self.history.get(include_history=True)
424
+
425
+ # Import required utilities
426
+ from mcp_agent.workflows.llm.anthropic_utils import (
427
+ anthropic_message_param_to_prompt_message_multipart,
428
+ )
429
+ from mcp_agent.mcp.prompt_serialization import (
430
+ multipart_messages_to_delimited_format,
431
+ )
432
+
433
+ # Convert message params to PromptMessageMultipart objects
434
+ multipart_messages = []
435
+ for msg in messages:
436
+ multipart_messages.append(
437
+ anthropic_message_param_to_prompt_message_multipart(msg)
438
+ )
439
+
440
+ # Convert to delimited format
441
+ delimited_content = multipart_messages_to_delimited_format(
442
+ multipart_messages,
443
+ user_delimiter="---USER",
444
+ assistant_delimiter="---ASSISTANT",
445
+ )
446
+
447
+ # Write to file
448
+ with open(filename, "w", encoding="utf-8") as f:
449
+ f.write("\n\n".join(delimited_content))
450
+
451
+ self.logger.info(f"Saved conversation history to {filename}")
452
+ return f"Done. Saved conversation history to {filename}"
453
+
454
+ except Exception as e:
455
+ self.logger.error(f"Error saving history: {str(e)}")
456
+ return f"Error saving history: {str(e)}"
457
+
364
458
  async def generate_structured(
365
459
  self,
366
460
  message,
367
461
  response_model: Type[ModelT],
368
462
  request_params: RequestParams | None = None,
369
463
  ) -> ModelT:
370
- # First we invoke the LLM to generate a string response
371
- # We need to do this in a two-step process because Instructor doesn't
372
- # know how to invoke MCP tools via call_tool, so we'll handle all the
373
- # processing first and then pass the final response through Instructor
464
+ # TODO -- simiar to the OAI version, we should create a tool call for the expected schema
374
465
  response = await self.generate_str(
375
466
  message=message,
376
467
  request_params=request_params,
@@ -378,27 +469,9 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
378
469
  # Don't try to parse if we got no response
379
470
  if not response:
380
471
  self.logger.error("No response from generate_str")
381
- return StructuredResponse(categories=[])
382
-
383
- # Next we pass the text through instructor to extract structured data
384
- client = instructor.from_anthropic(
385
- Anthropic(api_key=self._api_key(self.context.config)),
386
- )
472
+ return None
387
473
 
388
- params = self.get_request_params(request_params)
389
- model = await self.select_model(params)
390
-
391
- # Extract structured data from natural language
392
- structured_response = client.chat.completions.create(
393
- model=model,
394
- response_model=response_model,
395
- messages=[{"role": "user", "content": response}],
396
- max_tokens=params.maxTokens,
397
- )
398
- await self.show_assistant_message(
399
- str(structured_response), title="ASSISTANT/STRUCTURED"
400
- )
401
- return structured_response
474
+ return response_model.model_validate(from_json(response, allow_partial=True))
402
475
 
403
476
  @classmethod
404
477
  def convert_message_to_message_param(
@@ -459,222 +532,3 @@ class AnthropicAugmentedLLM(AugmentedLLM[MessageParam, Message]):
459
532
  return str(content)
460
533
 
461
534
  return str(message)
462
-
463
-
464
- class AnthropicMCPTypeConverter(ProviderToMCPConverter[MessageParam, Message]):
465
- """
466
- Convert between Anthropic and MCP types.
467
- """
468
-
469
- @classmethod
470
- def from_mcp_message_result(cls, result: MCPMessageResult) -> Message:
471
- # MCPMessageResult -> Message
472
- if result.role != "assistant":
473
- raise ValueError(
474
- f"Expected role to be 'assistant' but got '{result.role}' instead."
475
- )
476
-
477
- return Message(
478
- role="assistant",
479
- type="message",
480
- content=[mcp_content_to_anthropic_content(result.content)],
481
- model=result.model,
482
- stop_reason=mcp_stop_reason_to_anthropic_stop_reason(result.stopReason),
483
- id=result.id or None,
484
- usage=result.usage or None,
485
- # TODO: should we push extras?
486
- )
487
-
488
- @classmethod
489
- def to_mcp_message_result(cls, result: Message) -> MCPMessageResult:
490
- # Message -> MCPMessageResult
491
-
492
- contents = anthropic_content_to_mcp_content(result.content)
493
- if len(contents) > 1:
494
- raise NotImplementedError(
495
- "Multiple content elements in a single message are not supported in MCP yet"
496
- )
497
- mcp_content = contents[0]
498
-
499
- return MCPMessageResult(
500
- role=result.role,
501
- content=mcp_content,
502
- model=result.model,
503
- stopReason=anthropic_stop_reason_to_mcp_stop_reason(result.stop_reason),
504
- # extras for Message fields
505
- **result.model_dump(exclude={"role", "content", "model", "stop_reason"}),
506
- )
507
-
508
- @classmethod
509
- def from_mcp_message_param(cls, param: MCPMessageParam) -> MessageParam:
510
- # MCPMessageParam -> MessageParam
511
- extras = param.model_dump(exclude={"role", "content"})
512
- return MessageParam(
513
- role=param.role,
514
- content=[mcp_content_to_anthropic_content(param.content)],
515
- **extras,
516
- )
517
-
518
- @classmethod
519
- def to_mcp_message_param(cls, param: MessageParam) -> MCPMessageParam:
520
- # Implement the conversion from ChatCompletionMessage to MCP message param
521
-
522
- contents = anthropic_content_to_mcp_content(param.content)
523
-
524
- # TODO: saqadri - the mcp_content can have multiple elements
525
- # while sampling message content has a single content element
526
- # Right now we error out if there are > 1 elements in mcp_content
527
- # We need to handle this case properly going forward
528
- if len(contents) > 1:
529
- raise NotImplementedError(
530
- "Multiple content elements in a single message are not supported"
531
- )
532
- mcp_content = contents[0]
533
-
534
- return MCPMessageParam(
535
- role=param.role,
536
- content=mcp_content,
537
- **typed_dict_extras(param, ["role", "content"]),
538
- )
539
-
540
- @classmethod
541
- def from_mcp_prompt_message(cls, message: PromptMessage) -> MessageParam:
542
- """Convert an MCP PromptMessage to an Anthropic MessageParam."""
543
-
544
- # Extract content text
545
- content_text = (
546
- message.content.text
547
- if hasattr(message.content, "text")
548
- else str(message.content)
549
- )
550
-
551
- # Extract extras for flexibility
552
- extras = message.model_dump(exclude={"role", "content"})
553
-
554
- # Handle based on role
555
- if message.role == "user":
556
- return {"role": "user", "content": content_text, **extras}
557
- elif message.role == "assistant":
558
- return {
559
- "role": "assistant",
560
- "content": [{"type": "text", "text": content_text}],
561
- **extras,
562
- }
563
- else:
564
- # Fall back to user for any unrecognized role, including "system"
565
- _logger.warning(
566
- f"Unsupported role '{message.role}' in PromptMessage. Falling back to 'user' role."
567
- )
568
- return {
569
- "role": "user",
570
- "content": f"[{message.role.upper()}] {content_text}",
571
- **extras,
572
- }
573
-
574
-
575
- def mcp_content_to_anthropic_content(
576
- content: TextContent | ImageContent | EmbeddedResource,
577
- ) -> ContentBlock:
578
- if isinstance(content, TextContent):
579
- return TextBlock(type=content.type, text=content.text)
580
- elif isinstance(content, ImageContent):
581
- # Best effort to convert an image to text (since there's no ImageBlock)
582
- return TextBlock(type="text", text=f"{content.mimeType}:{content.data}")
583
- elif isinstance(content, EmbeddedResource):
584
- if isinstance(content.resource, TextResourceContents):
585
- return TextBlock(type="text", text=content.resource.text)
586
- else: # BlobResourceContents
587
- return TextBlock(
588
- type="text", text=f"{content.resource.mimeType}:{content.resource.blob}"
589
- )
590
- else:
591
- # Last effort to convert the content to a string
592
- return TextBlock(type="text", text=str(content))
593
-
594
-
595
- def anthropic_content_to_mcp_content(
596
- content: str
597
- | Iterable[
598
- TextBlockParam
599
- | ImageBlockParam
600
- | ToolUseBlockParam
601
- | ToolResultBlockParam
602
- | DocumentBlockParam
603
- | ContentBlock
604
- ],
605
- ) -> List[TextContent | ImageContent | EmbeddedResource]:
606
- mcp_content = []
607
-
608
- if isinstance(content, str):
609
- mcp_content.append(TextContent(type="text", text=content))
610
- else:
611
- for block in content:
612
- if block.type == "text":
613
- mcp_content.append(TextContent(type="text", text=block.text))
614
- elif block.type == "image":
615
- raise NotImplementedError("Image content conversion not implemented")
616
- elif block.type == "tool_use":
617
- # Best effort to convert a tool use to text (since there's no ToolUseContent)
618
- mcp_content.append(
619
- TextContent(
620
- type="text",
621
- text=to_string(block),
622
- )
623
- )
624
- elif block.type == "tool_result":
625
- # Best effort to convert a tool result to text (since there's no ToolResultContent)
626
- mcp_content.append(
627
- TextContent(
628
- type="text",
629
- text=to_string(block),
630
- )
631
- )
632
- elif block.type == "document":
633
- raise NotImplementedError("Document content conversion not implemented")
634
- else:
635
- # Last effort to convert the content to a string
636
- mcp_content.append(TextContent(type="text", text=str(block)))
637
-
638
- return mcp_content
639
-
640
-
641
- def mcp_stop_reason_to_anthropic_stop_reason(stop_reason: StopReason):
642
- if not stop_reason:
643
- return None
644
- elif stop_reason == "endTurn":
645
- return "end_turn"
646
- elif stop_reason == "maxTokens":
647
- return "max_tokens"
648
- elif stop_reason == "stopSequence":
649
- return "stop_sequence"
650
- elif stop_reason == "toolUse":
651
- return "tool_use"
652
- else:
653
- return stop_reason
654
-
655
-
656
- def anthropic_stop_reason_to_mcp_stop_reason(stop_reason: str) -> StopReason:
657
- if not stop_reason:
658
- return None
659
- elif stop_reason == "end_turn":
660
- return "endTurn"
661
- elif stop_reason == "max_tokens":
662
- return "maxTokens"
663
- elif stop_reason == "stop_sequence":
664
- return "stopSequence"
665
- elif stop_reason == "tool_use":
666
- return "toolUse"
667
- else:
668
- return stop_reason
669
-
670
-
671
- def to_string(obj: BaseModel | dict) -> str:
672
- if isinstance(obj, BaseModel):
673
- return obj.model_dump_json()
674
- else:
675
- return json.dumps(obj)
676
-
677
-
678
- def typed_dict_extras(d: dict, exclude: List[str]):
679
- extras = {k: v for k, v in d.items() if k not in exclude}
680
- return extras