-
Notifications
You must be signed in to change notification settings - Fork 771
Make mcp-agent Context a derivative of FastMCP context #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
efbfbd6
e203904
8d6457a
f30337a
53ec2e7
458b76f
9999b17
c55e9df
83b348e
7b9d0af
1bd4b41
d8bbab1
08839a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,12 @@ | |
import os | ||
import sys | ||
import functools | ||
|
||
from types import MethodType | ||
from typing import ( | ||
Any, | ||
Dict, | ||
Iterable, | ||
Mapping, | ||
Optional, | ||
Type, | ||
TypeVar, | ||
|
@@ -20,6 +21,8 @@ | |
|
||
from mcp import ServerSession | ||
from mcp.server.fastmcp import FastMCP | ||
from mcp.types import ToolAnnotations, Icon | ||
|
||
from mcp_agent.core.context import Context, initialize_context, cleanup_context | ||
from mcp_agent.config import Settings, get_settings | ||
from mcp_agent.executor.signal_registry import SignalRegistry | ||
|
@@ -586,6 +589,7 @@ def _create_workflow_from_function( | |
async def _invoke_target(workflow_self, *args, **kwargs): | ||
# Inject app_ctx (AppContext) and shim ctx (FastMCP Context) if requested by the function | ||
import inspect as _inspect | ||
import typing as _typing | ||
|
||
call_kwargs = dict(kwargs) | ||
|
||
|
@@ -622,24 +626,51 @@ async def _invoke_target(workflow_self, *args, **kwargs): | |
except Exception: | ||
pass | ||
|
||
# If the function expects a FastMCP Context (ctx/context), ensure it's present (None inside workflow) | ||
# If the function expects a FastMCP Context (ctx/context), ensure it's present. | ||
try: | ||
from mcp.server.fastmcp import Context as _Ctx # type: ignore | ||
except Exception: | ||
_Ctx = None # type: ignore | ||
|
||
def _is_fast_ctx_annotation(annotation) -> bool: | ||
if _Ctx is None or annotation is _inspect._empty: | ||
return False | ||
if annotation is _Ctx: | ||
return True | ||
try: | ||
origin = _typing.get_origin(annotation) | ||
if origin is not None: | ||
return any( | ||
_is_fast_ctx_annotation(arg) | ||
for arg in _typing.get_args(annotation) | ||
) | ||
except Exception: | ||
pass | ||
try: | ||
return "fastmcp" in str(annotation) | ||
except Exception: | ||
return False | ||
|
||
try: | ||
sig = sig if "sig" in locals() else _inspect.signature(fn) | ||
for p in sig.parameters.values(): | ||
if ( | ||
p.annotation is not _inspect._empty | ||
and _Ctx is not None | ||
and p.annotation is _Ctx | ||
needs_fast_ctx = False | ||
if _is_fast_ctx_annotation(p.annotation): | ||
needs_fast_ctx = True | ||
elif p.annotation is _inspect._empty and p.name in ( | ||
"ctx", | ||
"context", | ||
): | ||
if p.name not in call_kwargs: | ||
call_kwargs[p.name] = None | ||
if p.name in ("ctx", "context") and p.name not in call_kwargs: | ||
call_kwargs[p.name] = None | ||
needs_fast_ctx = True | ||
if needs_fast_ctx and p.name not in call_kwargs: | ||
fast_ctx = getattr(workflow_self, "_mcp_request_context", None) | ||
if fast_ctx is None and app_context_param_name: | ||
fast_ctx = getattr( | ||
call_kwargs.get(app_context_param_name, None), | ||
"fastmcp", | ||
None, | ||
) | ||
call_kwargs[p.name] = fast_ctx | ||
except Exception: | ||
pass | ||
|
||
|
@@ -739,15 +770,23 @@ def tool( | |
self, | ||
name: str | None = None, | ||
*, | ||
title: str | None = None, | ||
description: str | None = None, | ||
annotations: ToolAnnotations | Mapping[str, Any] | None = None, | ||
icons: Iterable[Icon | Mapping[str, Any]] | None = None, | ||
meta: Mapping[str, Any] | None = None, | ||
structured_output: bool | None = None, | ||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ... | ||
|
||
def tool( | ||
self, | ||
name: str | None = None, | ||
*, | ||
title: str | None = None, | ||
description: str | None = None, | ||
annotations: ToolAnnotations | Mapping[str, Any] | None = None, | ||
icons: Iterable[Icon | Mapping[str, Any]] | None = None, | ||
meta: Mapping[str, Any] | None = None, | ||
structured_output: bool | None = None, | ||
): | ||
""" | ||
|
@@ -766,6 +805,28 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]: | |
|
||
validate_tool_schema(fn, tool_name) | ||
|
||
annotations_obj: ToolAnnotations | None = None | ||
if annotations is not None: | ||
if isinstance(annotations, ToolAnnotations): | ||
annotations_obj = annotations | ||
else: | ||
annotations_obj = ToolAnnotations(**dict(annotations)) | ||
|
||
icons_list: list[Icon] | None = None | ||
if icons is not None: | ||
icons_list = [] | ||
for icon in icons: | ||
if isinstance(icon, Icon): | ||
icons_list.append(icon) | ||
elif isinstance(icon, Mapping): | ||
icons_list.append(Icon(**icon)) | ||
else: | ||
raise TypeError("icons entries must be Icon or mapping") | ||
|
||
meta_payload: Dict[str, Any] | None = None | ||
if meta is not None: | ||
meta_payload = dict(meta) | ||
|
||
# Construct the workflow from function | ||
workflow_cls = self._create_workflow_from_function( | ||
fn, | ||
|
@@ -784,13 +845,25 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]: | |
"source_fn": fn, | ||
"structured_output": structured_output, | ||
"description": description or (fn.__doc__ or ""), | ||
"title": title, | ||
"annotations": annotations_obj, | ||
"icons": icons_list, | ||
"meta": meta_payload, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm I looked at the MCP SDK and it looks like the decorator has meta, not _meta. Where did you see _meta? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, Looking at the MCP docs it seems it may be abused for that reason -- https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#_meta -- since supposed to be reserved for MCP itself There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So However that change needs to land in pypi first so we can consume it |
||
} | ||
) | ||
|
||
return fn | ||
|
||
# Support bare usage: @app.tool without parentheses | ||
if callable(name) and description is None and structured_output is None: | ||
if ( | ||
callable(name) | ||
and title is None | ||
and description is None | ||
and annotations is None | ||
and icons is None | ||
and meta is None | ||
and structured_output is None | ||
): | ||
_fn = name # type: ignore[assignment] | ||
name = None | ||
return decorator(_fn) # type: ignore[arg-type] | ||
|
@@ -805,14 +878,24 @@ def async_tool( | |
self, | ||
name: str | None = None, | ||
*, | ||
title: str | None = None, | ||
description: str | None = None, | ||
annotations: ToolAnnotations | Mapping[str, Any] | None = None, | ||
icons: Iterable[Icon | Mapping[str, Any]] | None = None, | ||
meta: Mapping[str, Any] | None = None, | ||
structured_output: bool | None = None, | ||
) -> Callable[[Callable[P, R]], Callable[P, R]]: ... | ||
|
||
def async_tool( | ||
self, | ||
name: str | None = None, | ||
*, | ||
title: str | None = None, | ||
description: str | None = None, | ||
annotations: ToolAnnotations | Mapping[str, Any] | None = None, | ||
icons: Iterable[Icon | Mapping[str, Any]] | None = None, | ||
meta: Mapping[str, Any] | None = None, | ||
structured_output: bool | None = None, | ||
): | ||
""" | ||
Decorator to declare an asynchronous MCP tool. | ||
|
@@ -830,6 +913,28 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]: | |
|
||
validate_tool_schema(fn, workflow_name) | ||
|
||
annotations_obj: ToolAnnotations | None = None | ||
if annotations is not None: | ||
if isinstance(annotations, ToolAnnotations): | ||
annotations_obj = annotations | ||
else: | ||
annotations_obj = ToolAnnotations(**dict(annotations)) | ||
|
||
icons_list: list[Icon] | None = None | ||
if icons is not None: | ||
icons_list = [] | ||
for icon in icons: | ||
if isinstance(icon, Icon): | ||
icons_list.append(icon) | ||
elif isinstance(icon, Mapping): | ||
icons_list.append(Icon(**icon)) | ||
else: | ||
raise TypeError("icons entries must be Icon or mapping") | ||
|
||
meta_payload: Dict[str, Any] | None = None | ||
if meta is not None: | ||
meta_payload = dict(meta) | ||
|
||
workflow_cls = self._create_workflow_from_function( | ||
fn, | ||
workflow_name=workflow_name, | ||
|
@@ -844,14 +949,26 @@ def decorator(fn: Callable[P, R]) -> Callable[P, R]: | |
"workflow_name": workflow_name, | ||
"workflow_cls": workflow_cls, | ||
"source_fn": fn, | ||
"structured_output": None, | ||
"structured_output": structured_output, | ||
"description": description or (fn.__doc__ or ""), | ||
"title": title, | ||
"annotations": annotations_obj, | ||
"icons": icons_list, | ||
"meta": meta_payload, | ||
} | ||
) | ||
return fn | ||
|
||
# Support bare usage: @app.async_tool without parentheses | ||
if callable(name) and description is None: | ||
if ( | ||
callable(name) | ||
and title is None | ||
and description is None | ||
and annotations is None | ||
and icons is None | ||
and meta is None | ||
and structured_output is None | ||
): | ||
_fn = name # type: ignore[assignment] | ||
name = None | ||
return decorator(_fn) # type: ignore[arg-type] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,9 +24,7 @@ | |
from mcp_agent.cli.core.utils import run_async | ||
from mcp_agent.cli.exceptions import CLIError | ||
from mcp_agent.cli.mcp_app.api_client import MCPAppClient | ||
from mcp_agent.cli.secrets.processor import ( | ||
process_config_secrets, | ||
) | ||
from mcp_agent.cli.secrets import processor as secrets_processor | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainInconsistent with PR objectives: verify import path. This import refactor appears unrelated to the PR's stated goal of making Context a derivative of FastMCP context. The change from direct function import to module alias assumes that Run the following script to verify the import path is correct: 🏁 Script executed: #!/bin/bash
# Verify that mcp_agent.cli.secrets exports processor module
# Check if __init__.py exists and what it exports
fd -t f "__init__.py" src/mcp_agent/cli/secrets/ --exec cat {}
# Verify processor.py exists
fd -t f "processor.py" src/mcp_agent/cli/secrets/ Length of output: 339 Critical issue: Import bypasses package's public API. The import at line 27 attempts to import Update the import to use the explicit module path: 🤖 Prompt for AI Agents
|
||
from mcp_agent.cli.utils.retry import retry_async_with_exponential_backoff, RetryError | ||
from mcp_agent.cli.utils.ux import ( | ||
print_deployment_header, | ||
|
@@ -173,9 +171,7 @@ def deploy_config( | |
|
||
if app_name is None: | ||
if default_app_name: | ||
print_info( | ||
f"Using app name from config.yaml: '{default_app_name}'" | ||
) | ||
print_info(f"Using app name from config.yaml: '{default_app_name}'") | ||
app_name = default_app_name | ||
else: | ||
app_name = "default" | ||
|
@@ -205,7 +201,7 @@ def deploy_config( | |
" • Or use the --api-key flag with your key", | ||
retriable=False, | ||
) | ||
|
||
if settings.VERBOSE: | ||
print_info(f"Using API at {effective_api_url}") | ||
|
||
|
@@ -231,9 +227,7 @@ def deploy_config( | |
print_info(f"New app id: `{app_id}`") | ||
else: | ||
short_id = f"{app_id[:8]}…" | ||
print_success( | ||
f"Found existing app '{app_name}' (ID: `{short_id}`)" | ||
) | ||
print_success(f"Found existing app '{app_name}' (ID: `{short_id}`)") | ||
if not non_interactive: | ||
use_existing = typer.confirm( | ||
f"Deploy an update to '{app_name}' (ID: `{short_id}`)?", | ||
|
@@ -292,7 +286,7 @@ def deploy_config( | |
secrets_transformed_path = config_dir / MCP_DEPLOYED_SECRETS_FILENAME | ||
|
||
run_async( | ||
process_config_secrets( | ||
secrets_processor.process_config_secrets( | ||
input_path=secrets_file, | ||
output_path=secrets_transformed_path, | ||
api_url=effective_api_url, | ||
|
Uh oh!
There was an error while loading. Please reload this page.