Skip to content

Commit e7c6727

Browse files
committed
refactor to support approach taken in RFC 371
1 parent 6244899 commit e7c6727

File tree

7 files changed

+130
-54
lines changed

7 files changed

+130
-54
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
from mcp.shared.context import LifespanContextT, RequestContext
5353
from mcp.types import (
5454
AnyFunction,
55-
DataContent,
5655
EmbeddedResource,
5756
GetPromptResult,
5857
ImageContent,
@@ -237,7 +236,7 @@ def run(
237236
def _setup_handlers(self) -> None:
238237
"""Set up core MCP protocol handlers."""
239238
self._mcp_server.list_tools()(self.list_tools)
240-
self._mcp_server.call_tool()(self.call_tool)
239+
self._mcp_server.call_tool()(self.call_tool, self._get_schema)
241240
self._mcp_server.list_resources()(self.list_resources)
242241
self._mcp_server.read_resource()(self.read_resource)
243242
self._mcp_server.list_prompts()(self.list_prompts)
@@ -271,13 +270,16 @@ def get_context(self) -> Context[ServerSession, object]:
271270

272271
async def call_tool(
273272
self, name: str, arguments: dict[str, Any]
274-
) -> Sequence[TextContent | DataContent | ImageContent | EmbeddedResource]:
273+
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
275274
"""Call a tool by name with arguments."""
276275
context = self.get_context()
277276
result = await self._tool_manager.call_tool(name, arguments, context=context)
278277
converted_result = _convert_to_content(result)
279278
return converted_result
280279

280+
def _get_schema(self, name: str) -> dict[str, Any] | None:
281+
return self._tool_manager.get_schema(name)
282+
281283
async def list_resources(self) -> list[MCPResource]:
282284
"""List all available resources."""
283285

@@ -871,12 +873,12 @@ async def get_prompt(
871873

872874
def _convert_to_content(
873875
result: Any,
874-
) -> Sequence[TextContent | ImageContent | EmbeddedResource | DataContent]:
876+
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
875877
"""Convert a result to a sequence of content objects."""
876878
if result is None:
877879
return []
878880

879-
if isinstance(result, TextContent | ImageContent | EmbeddedResource | DataContent):
881+
if isinstance(result, TextContent | ImageContent | EmbeddedResource):
880882
return [result]
881883

882884
if isinstance(result, Image):

src/mcp/server/fastmcp/tools/base.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from mcp.server.fastmcp.exceptions import ToolError
1010
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata
11-
from mcp.types import DataContent, ToolAnnotations
11+
from mcp.types import ToolAnnotations
1212

1313
if TYPE_CHECKING:
1414
from mcp.server.fastmcp.server import Context
@@ -102,12 +102,6 @@ async def run(
102102
if self.context_kwarg is not None
103103
else None,
104104
)
105-
if self.output and self.output.get("type") == "object":
106-
return DataContent(
107-
type="data",
108-
data=result,
109-
)
110-
else:
111-
return result
105+
return result
112106
except Exception as e:
113107
raise ToolError(f"Error executing tool {self.name}: {e}") from e

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,9 @@ async def call_tool(
6262
raise ToolError(f"Unknown tool: {name}")
6363

6464
return await tool.run(arguments, context=context)
65+
66+
def get_schema(self, name: str) -> dict[str, Any] | None:
67+
tool = self.get_tool(name)
68+
if not tool:
69+
raise ToolError(f"Unknown tool: {name}")
70+
return tool.output

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,9 @@ def func_metadata(
186186
# TODO this could be moved to a constant or passed in as param as per skip_names
187187
ignore = [inspect.Parameter.empty, None, types.Image]
188188
if sig.return_annotation not in ignore:
189-
output_schema = TypeAdapter(sig.return_annotation).json_schema()
189+
type_schema = TypeAdapter(sig.return_annotation).json_schema()
190+
if type_schema.get("type", None) == "object":
191+
output_schema = type_schema
190192

191193
return FuncMetadata(arg_model=arguments_model, output_schema=output_schema)
192194

src/mcp/server/lowlevel/server.py

Lines changed: 101 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ async def main():
7171
import warnings
7272
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
7373
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
74-
from typing import Any, Generic, TypeVar
74+
from typing import Any, Generic, TypeVar, cast
7575

7676
import anyio
7777
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
78-
from pydantic import AnyUrl
78+
from pydantic import AnyUrl, BaseModel
7979

8080
import mcp.types as types
8181
from mcp.server.lowlevel.helper_types import ReadResourceContents
@@ -399,30 +399,119 @@ def decorator(
399399
...,
400400
Awaitable[
401401
Iterable[
402-
types.TextContent
403-
| types.DataContent
404-
| types.ImageContent
405-
| types.EmbeddedResource
402+
types.TextContent | types.ImageContent | types.EmbeddedResource
406403
]
404+
| BaseModel
405+
| dict[str, Any]
407406
],
408407
],
408+
schema_func: Callable[..., dict[str, Any] | None] | None = None,
409409
):
410410
logger.debug("Registering handler for CallToolRequest")
411411

412-
async def handler(req: types.CallToolRequest):
413-
try:
414-
results = await func(req.params.name, (req.params.arguments or {}))
412+
def handle_result_without_schema(req: types.CallToolRequest, result: Any):
413+
if type(result) is dict or isinstance(result, BaseModel):
414+
error = f"""Tool {req.params.name} has no outputSchema and
415+
must return content"""
415416
return types.ServerResult(
416-
types.CallToolResult(content=list(results), isError=False)
417+
types.CallToolResult(
418+
content=[
419+
types.TextContent(
420+
type="text",
421+
text=error,
422+
)
423+
],
424+
structuredContent=None,
425+
isError=True,
426+
)
427+
)
428+
else:
429+
content_result = cast(
430+
Iterable[
431+
types.TextContent
432+
| types.ImageContent
433+
| types.EmbeddedResource
434+
],
435+
result,
436+
)
437+
return types.ServerResult(
438+
types.CallToolResult(
439+
content=list(content_result),
440+
structuredContent=None,
441+
isError=False,
442+
)
417443
)
418-
except Exception as e:
444+
445+
def handle_result_with_schema(
446+
req: types.CallToolRequest, result: Any, schema: dict[str, Any]
447+
):
448+
if isinstance(result, BaseModel):
449+
model_result = result.model_dump()
419450
return types.ServerResult(
420451
types.CallToolResult(
421-
content=[types.TextContent(type="text", text=str(e))],
452+
content=[],
453+
structuredContent=model_result,
454+
isError=False,
455+
)
456+
)
457+
elif type(result) is dict[str, Any]:
458+
return types.ServerResult(
459+
types.CallToolResult(
460+
content=[], structuredContent=result, isError=False
461+
)
462+
)
463+
else:
464+
error = f"""Tool {req.params.name} has outputSchema and "
465+
must return structured content"""
466+
return types.ServerResult(
467+
types.CallToolResult(
468+
content=[
469+
types.TextContent(
470+
type="text",
471+
text=error,
472+
)
473+
],
474+
structuredContent=None,
422475
isError=True,
423476
)
424477
)
425478

479+
def handle_error(e: Exception):
480+
return types.ServerResult(
481+
types.CallToolResult(
482+
content=[types.TextContent(type="text", text=str(e))],
483+
structuredContent=None,
484+
isError=True,
485+
)
486+
)
487+
488+
if schema_func is None:
489+
490+
async def handler(req: types.CallToolRequest):
491+
try:
492+
result = await func(
493+
req.params.name, (req.params.arguments or {})
494+
)
495+
return handle_result_without_schema(req, result)
496+
except Exception as e:
497+
return handle_error(e)
498+
499+
else:
500+
501+
async def handler(req: types.CallToolRequest):
502+
try:
503+
result = await func(
504+
req.params.name, (req.params.arguments or {})
505+
)
506+
schema = schema_func(req.params.name)
507+
508+
if schema:
509+
return handle_result_with_schema(req, result, schema)
510+
else:
511+
return handle_result_without_schema(req, result)
512+
except Exception as e:
513+
return handle_error(e)
514+
426515
self.request_handlers[types.CallToolRequest] = handler
427516
return func
428517

src/mcp/types.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -646,21 +646,6 @@ class ImageContent(BaseModel):
646646
model_config = ConfigDict(extra="allow")
647647

648648

649-
class DataContent(BaseModel):
650-
"""Data content for a message."""
651-
652-
type: Literal["data"]
653-
data: Any
654-
"""The JSON serializable object containing structured data."""
655-
schema_: str | Any | None = Field(serialization_alias="schema", default=None)
656-
657-
"""
658-
Optional reference to a JSON schema that describes the structure of the data.
659-
"""
660-
annotations: Annotations | None = None
661-
model_config = ConfigDict(extra="allow")
662-
663-
664649
class SamplingMessage(BaseModel):
665650
"""Describes a message issued to or received from an LLM API."""
666651

@@ -808,7 +793,8 @@ class CallToolRequest(Request[CallToolRequestParams, Literal["tools/call"]]):
808793
class CallToolResult(Result):
809794
"""The server's response to a tool call."""
810795

811-
content: list[TextContent | DataContent | ImageContent | EmbeddedResource]
796+
content: list[TextContent | ImageContent | EmbeddedResource]
797+
structuredContent: dict[str, Any] | None = None
812798
isError: bool = False
813799

814800

tests/server/fastmcp/test_func_metadata.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ def simple_str_fun() -> str:
9393
return "ok"
9494

9595

96+
def simple_list_str_fun() -> list[str]:
97+
return ["ok"]
98+
99+
96100
def simple_bool_fun() -> bool:
97101
return True
98102

@@ -444,18 +448,11 @@ def test_simple_function_output_schema():
444448
"""Test JSON schema generation for simple return types."""
445449

446450
assert func_metadata(simple_no_annotation_fun).output_schema is None
447-
assert func_metadata(simple_str_fun).output_schema == {
448-
"type": "string",
449-
}
450-
assert func_metadata(simple_bool_fun).output_schema == {
451-
"type": "boolean",
452-
}
453-
assert func_metadata(simple_int_fun).output_schema == {
454-
"type": "integer",
455-
}
456-
assert func_metadata(simple_float_fun).output_schema == {
457-
"type": "number",
458-
}
451+
assert func_metadata(simple_str_fun).output_schema is None
452+
assert func_metadata(simple_bool_fun).output_schema is None
453+
assert func_metadata(simple_int_fun).output_schema is None
454+
assert func_metadata(simple_float_fun).output_schema is None
455+
assert func_metadata(simple_list_str_fun).output_schema is None
459456

460457

461458
def test_complex_function_output_schema():

0 commit comments

Comments
 (0)