|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import functools |
5 | 6 | import inspect |
6 | 7 | import typing |
7 | 8 | from collections.abc import Callable |
8 | 9 | from typing import Any |
9 | 10 |
|
| 11 | +from pydantic import SkipValidation, validate_call |
| 12 | + |
10 | 13 | from mcp.server.mcpserver.context import Context |
| 14 | +from mcp.shared._callable_inspection import is_async_callable |
11 | 15 |
|
12 | 16 |
|
13 | 17 | def find_context_parameter(fn: Callable[..., Any]) -> str | None: |
@@ -66,3 +70,59 @@ def inject_context( |
66 | 70 | if context_kwarg is not None and context is not None: |
67 | 71 | return {**kwargs, context_kwarg: context} |
68 | 72 | return kwargs |
| 73 | + |
| 74 | + |
| 75 | +def validate_call_with_injected_context( |
| 76 | + fn: Callable[..., Any], |
| 77 | + context_kwarg: str | None, |
| 78 | +) -> Callable[..., Any]: |
| 79 | + """Validate a handler call without re-validating its injected context.""" |
| 80 | + if context_kwarg is None: |
| 81 | + return validate_call(fn) |
| 82 | + |
| 83 | + signature = inspect.signature(fn, eval_str=True) |
| 84 | + context_parameter = signature.parameters.get(context_kwarg) |
| 85 | + if context_parameter is None: |
| 86 | + return validate_call(fn) |
| 87 | + |
| 88 | + context_annotation = context_parameter.annotation |
| 89 | + if context_annotation is inspect.Parameter.empty: |
| 90 | + context_annotation = Any |
| 91 | + skipped_context_annotation = SkipValidation[context_annotation] |
| 92 | + validation_signature = signature.replace( |
| 93 | + parameters=[ |
| 94 | + parameter.replace(annotation=skipped_context_annotation) if parameter.name == context_kwarg else parameter |
| 95 | + for parameter in signature.parameters.values() |
| 96 | + ] |
| 97 | + ) |
| 98 | + validation_annotations = { |
| 99 | + parameter.name: parameter.annotation |
| 100 | + for parameter in validation_signature.parameters.values() |
| 101 | + if parameter.annotation is not inspect.Parameter.empty |
| 102 | + } |
| 103 | + if validation_signature.return_annotation is not inspect.Signature.empty: |
| 104 | + validation_annotations["return"] = validation_signature.return_annotation |
| 105 | + |
| 106 | + if is_async_callable(fn): |
| 107 | + |
| 108 | + @functools.wraps(fn) |
| 109 | + async def async_forwarding(*args: Any, **kwargs: Any) -> Any: |
| 110 | + return await fn(*args, **kwargs) |
| 111 | + |
| 112 | + forwarding: Callable[..., Any] = async_forwarding |
| 113 | + else: |
| 114 | + |
| 115 | + @functools.wraps(fn) |
| 116 | + def sync_forwarding(*args: Any, **kwargs: Any) -> Any: |
| 117 | + return fn(*args, **kwargs) |
| 118 | + |
| 119 | + forwarding = sync_forwarding |
| 120 | + |
| 121 | + # Pydantic builds its validator from these temporary annotations. Restore the |
| 122 | + # handler's public signature after validation is compiled. |
| 123 | + setattr(forwarding, "__signature__", validation_signature) |
| 124 | + forwarding.__annotations__ = validation_annotations |
| 125 | + validated = validate_call(forwarding) |
| 126 | + setattr(validated, "__signature__", signature) |
| 127 | + validated.__annotations__ = fn.__annotations__ |
| 128 | + return validated |
0 commit comments