-
Couldn't load subscription status.
- Fork 1.4k
Add meta support to ToolResult #2283
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
base: main
Are you sure you want to change the base?
Conversation
WalkthroughFastMCP now carries optional metadata on tool results: Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant FastMCP as FastMCP Server
participant Tool
participant Cache as Caching Middleware
participant CallResult as CallToolResult
Client->>FastMCP: call_tool(tool_name, args)
FastMCP->>Tool: invoke tool(...)
Tool->>Tool: produce ToolResult(content, structured_content, meta)
Tool->>FastMCP: ToolResult.to_mcp_result()
note right of FastMCP #efefef: when meta present → returns CallToolResult
FastMCP->>Cache: wrap CallToolResult (preserve meta)
Cache-->>FastMCP: cached CachableToolResult.unwrap() → CallToolResult (meta preserved)
FastMCP->>Client: return CallToolResult(content, structured_content, data, meta)
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/servers/tools.mdx (1)
621-631: Example not runnable: missing TextContent import.Add the import so users can copy/paste.
Apply:
from fastmcp.tools.tool import ToolResult +from mcp.types import TextContent
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
docs/servers/tools.mdx(2 hunks)examples/tool_result_echo.py(1 hunks)pyproject.toml(1 hunks)src/fastmcp/client/client.py(2 hunks)src/fastmcp/server/middleware/caching.py(1 hunks)src/fastmcp/tools/tool.py(3 hunks)tests/server/middleware/test_caching.py(2 hunks)tests/server/test_server_interactions.py(1 hunks)tests/tools/test_tool.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Use Python ≥ 3.10 and provide full type annotations for library code
Files:
src/fastmcp/client/client.pysrc/fastmcp/server/middleware/caching.pysrc/fastmcp/tools/tool.py
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Never use bare except; always catch specific exception types
Files:
src/fastmcp/client/client.pytests/server/middleware/test_caching.pysrc/fastmcp/server/middleware/caching.pytests/tools/test_tool.pysrc/fastmcp/tools/tool.pyexamples/tool_result_echo.pytests/server/test_server_interactions.py
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write instructions and procedures in second person ("you")
Use active voice over passive voice
Use present tense for current states and future tense for outcomes
Maintain consistent terminology across the documentation
Keep sentences concise while preserving necessary context
Use parallel structure in lists, headings, and procedures
Lead with the most important information (inverted pyramid)
Use progressive disclosure: basic concepts before advanced ones
Break complex procedures into numbered steps
Include prerequisites and context before instructions
Provide expected outcomes for each major step
End sections with next steps or related information
Use descriptive, keyword-rich headings for navigation and SEO
Focus on user goals and outcomes rather than system features
Anticipate common questions and address them proactively
Include troubleshooting for likely failure points
Offer multiple pathways when appropriate (beginner vs advanced) and provide an opinionated recommended path
Use for supplementary information that supports the main content
Use for expert advice, shortcuts, or best practices
Use for critical cautions, breaking changes, or destructive actions
Use for neutral background or contextual information
Use to confirm success or completion
Provide single code examples using fenced code blocks with language (and filename when relevant)
Use to present the same concept in multiple languages
For API docs, use to show requests
For API docs, use to show responses
Use and to document procedures and sequential instructions
Use and for platform-specific or alternative approaches
Use / for supplementary content that might interrupt flow
In API docs, use for parameters (path, body, query, header) with type and required/default as appropria...
Files:
docs/servers/tools.mdx
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
docs/**/*.{md,mdx}: Documentation code examples should be explained before the code and be fully runnable, including imports
Use clear headers with logical H2/H3 hierarchy to form navigation
Write user-focused content that motivates the why before the how
Prefer prose for important information over code comments in docs
Files:
docs/servers/tools.mdx
**/*.{md,mdx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{md,mdx}: Be brief and to the point in written materials; avoid regurgitating obvious code details
Avoid defensive constructions like "This isn't X" or "Not just X, but Y"; state what it is directly
Files:
docs/servers/tools.mdx
tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
tests/**/*.py: Tests must be atomic, self-contained, and cover a single functionality
Use pytest parameterization for multiple examples of the same functionality
Use separate tests for distinct pieces of functionality
Always put imports at the top of test files; do not import inside test bodies
Do not add @pytest.mark.asyncio; asyncio_mode = "auto" is set globally
Prefer in-memory transport for tests; use HTTP transport only when explicitly testing networking
For slow/long-running tests, mark them as integration or optimize (default timeout is 5s)
In tests, use # type: ignore[attr-defined] for MCP results instead of type assertions
Files:
tests/server/middleware/test_caching.pytests/tools/test_tool.pytests/server/test_server_interactions.py
🧬 Code graph analysis (7)
src/fastmcp/client/client.py (1)
src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py (2)
CallToolRequestResult(25-48)from_call_tool_result(37-48)
tests/server/middleware/test_caching.py (2)
src/fastmcp/server/middleware/caching.py (5)
CachableToolResult(63-81)wrap(52-53)wrap(69-74)unwrap(56-60)unwrap(76-81)src/fastmcp/tools/tool.py (1)
ToolResult(66-112)
src/fastmcp/server/middleware/caching.py (1)
src/fastmcp/tools/tool.py (1)
ToolResult(66-112)
tests/tools/test_tool.py (3)
src/fastmcp/client/client.py (1)
call_tool(865-934)src/fastmcp/tools/tool.py (1)
ToolResult(66-112)src/fastmcp/tools/tool_manager.py (1)
call_tool(153-172)
src/fastmcp/tools/tool.py (3)
src/fastmcp/client/client.py (1)
CallToolResult(946-951)src/fastmcp/contrib/mcp_mixin/mcp_mixin.py (1)
mcp_tool(25-52)src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py (1)
CallToolRequestResult(25-48)
examples/tool_result_echo.py (1)
src/fastmcp/tools/tool.py (1)
ToolResult(66-112)
tests/server/test_server_interactions.py (2)
tests/contrib/test_mcp_mixin.py (2)
test_tool_with_title_and_meta(257-279)MyToolWithMeta(263-269)tests/server/test_server.py (1)
test_tool_decorator_with_meta(434-448)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Run tests with lowest-direct dependencies
- GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (11)
src/fastmcp/server/middleware/caching.py (2)
66-75: Meta preserved in cache wrapper. LGTM.
78-81: Unwrap returns ToolResult with meta. LGTM.tests/server/middleware/test_caching.py (2)
26-30: New import for CachableToolResult — OK.
511-523: Round‑trip test for meta/content/structured_content — OK.tests/server/test_server_interactions.py (1)
1182-1183: Snapshot updated to include meta=None — OK.Keeps equality stable after dataclass change.
src/fastmcp/client/client.py (1)
931-931: Code change verified—no issues found.The script confirms that
result.metais available in the installed MCP SDK'sCallToolResultmodel. The code correctly accesses the field without errors. The original review's approval stands.pyproject.toml (1)
10-10: Constraint is correctly configured in pyproject.toml and uv.lock, but runtime mcp 1.16.0 violates the ≥1.19.0 floor.✓ What's correct:
- pyproject.toml pins
mcp>=1.19.0,<2.0.0(line 10)- uv.lock resolves to mcp 1.19.0
- CI matrix includes
run_tests_lowest_directjob that enforces floor version (Python 3.10 viauv sync --resolution lowest-direct)- Both ubuntu-latest and windows-latest tested
✗ Critical gap:
Runtime resolved to mcp 1.16.0, which violates the constraint and risksCallToolResult.metaattribute mismatches. Verify that:
- Deployment environments use
uv.lockor respectpyproject.tomlconstraints- No cached/stale environments are bypassing the floor version
- All dependency resolution (pip, poetry, uv) respects the ≥1.19.0 constraint
examples/tool_result_echo.py (1)
1-21: LGTM! Clear demonstration of the new meta field.This example effectively demonstrates the new
metasupport inToolResult. The usage of all three fields (content, structured_content, and meta) clearly illustrates the intended API for users.src/fastmcp/tools/tool.py (3)
19-19: LGTM! Necessary import for meta support.The
CallToolResultimport frommcp.typesis required for returning tool results with metadata, aligning with the MCP SDK v1.19.0 changes.
67-79: LGTM! Proper parameter addition and storage.The
metaparameter is correctly added with appropriate type annotations (dict[str, Any] | None) and default value (None), ensuring backwards compatibility. The storage onself.metais straightforward and correctly typed.
99-112: LGTM! Correct implementation of meta support.The
to_mcp_resultmethod correctly handles the newmetafield:
- When
metais present, returns aCallToolResultwith all three fields, using the correct MCP SDK conventions (structuredContentin camelCase,_metawith underscore prefix)- Falls back to existing behavior when
metaisNone- Return type annotation properly reflects the new
CallToolResultoptionThe logic correctly prioritizes meta handling and maintains backwards compatibility.
| - You control exactly what content, structured data, and metadata is sent | ||
| - Output schemas are optional - structured content can be provided without a schema | ||
| - Clients receive both traditional content blocks and structured data | ||
| - Clients receive traditional content blocks, structured data, and metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Clarify meta constraints and size guidance.
Add a brief warning: JSON-serializable only; keep small to avoid bloating responses/cache.
Apply:
- Clients receive traditional content blocks, structured data, and metadata
+ - Clients receive traditional content blocks, structured data, and metadata
+
+<Warning>
+Keep `meta` JSON‑serializable and small (a few KB). Large metadata increases latency and may be skipped by caches.
+</Warning>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - You control exactly what content, structured data, and metadata is sent | |
| - Output schemas are optional - structured content can be provided without a schema | |
| - Clients receive both traditional content blocks and structured data | |
| - Clients receive traditional content blocks, structured data, and metadata | |
| - You control exactly what content, structured data, and metadata is sent | |
| - Output schemas are optional - structured content can be provided without a schema | |
| - Clients receive traditional content blocks, structured data, and metadata | |
| <Warning> | |
| Keep `meta` JSON‑serializable and small (a few KB). Large metadata increases latency and may be skipped by caches. | |
| </Warning> |
🤖 Prompt for AI Agents
In docs/servers/tools.mdx around lines 635 to 637, add a concise warning
clarifying that metadata must be JSON-serializable and should be kept small to
avoid bloating responses and caches; update the three bullet points by appending
or inserting a short parenthetical note such as “(JSON-serializable only; keep
small to avoid bloating responses/cache)” so readers know the constraint and
size guidance.
| class CallToolResult: | ||
| content: list[mcp.types.ContentBlock] | ||
| structured_content: dict[str, Any] | None | ||
| meta: dict[str, Any] | None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Dataclass extended with meta — update downstream extenders.
Classes extending/deriving from CallToolResult should propagate structured_content and meta. Example fix for bulk caller:
class CallToolRequestResult(CallToolResult):
@@
def from_call_tool_result(
cls, result: CallToolResult, tool: str, arguments: dict[str, Any]
) -> "CallToolRequestResult":
return cls(
tool=tool,
arguments=arguments,
- isError=result.isError,
- content=result.content,
+ isError=result.is_error,
+ content=result.content,
+ structured_content=result.structured_content,
+ meta=result.meta,
)Search for similar helpers to update:
🏁 Script executed:
#!/bin/bash
rg -nP 'from_call_tool_result|class\s+CallToolResult|result\.(structuredContent|meta)\b' -C2Length of output: 5210
🏁 Script executed:
#!/bin/bash
# View the implementation of from_call_tool_result
cat src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py | head -60Length of output: 1693
🏁 Script executed:
#!/bin/bash
# View the CallToolResult dataclass definition
sed -n '945,960p' src/fastmcp/client/client.pyLength of output: 200
Update CallToolRequestResult.from_call_tool_result() to propagate all fields.
The method currently attempts to access result.isError (which doesn't exist) and fails to propagate structured_content and meta. Fix:
@classmethod
def from_call_tool_result(
cls, result: CallToolResult, tool: str, arguments: dict[str, Any]
) -> "CallToolRequestResult":
"""
Create a CallToolRequestResult from a CallToolResult.
"""
return cls(
tool=tool,
arguments=arguments,
- isError=result.isError,
+ is_error=result.is_error,
content=result.content,
+ structured_content=result.structured_content,
+ meta=result.meta,
)Location: src/fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py lines 37–48
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/fastmcp/client/client.py around line 949, update
CallToolRequestResult.from_call_tool_result to stop referencing the non-existent
result.isError and instead propagate the actual fields from the incoming result:
use result.error (or set is_error = bool(result.error)) for error state, and
copy result.structured_content and result.meta into the returned
CallToolRequestResult object so all fields are preserved.
| class TestToolResultCasting: | ||
| @pytest.fixture | ||
| async def client(self): | ||
| from fastmcp import FastMCP | ||
| from fastmcp.client import Client | ||
|
|
||
| mcp = FastMCP() | ||
|
|
||
| @mcp.tool | ||
| def test_tool( | ||
| unstructured: str | None = None, | ||
| structured: dict[str, Any] | None = None, | ||
| meta: dict[str, Any] | None = None, | ||
| ): | ||
| return ToolResult( | ||
| content=unstructured, | ||
| structured_content=structured, | ||
| meta=meta, | ||
| ) | ||
|
|
||
| async with Client(mcp) as client: | ||
| yield client | ||
|
|
||
| async def test_only_unstructured_content(self, client): | ||
| result = await client.call_tool("test_tool", {"unstructured": "test data"}) | ||
|
|
||
| assert result.content[0].type == "text" | ||
| assert result.content[0].text == "test data" | ||
| assert result.structured_content is None | ||
| assert result.meta is None | ||
|
|
||
| async def test_neither_unstructured_or_structured_content(self, client): | ||
| from fastmcp.exceptions import ToolError | ||
|
|
||
| with pytest.raises(ToolError): | ||
| await client.call_tool("test_tool", {}) | ||
|
|
||
| async def test_structured_and_unstructured_content(self, client): | ||
| result = await client.call_tool( | ||
| "test_tool", | ||
| {"unstructured": "test data", "structured": {"data_type": "test"}}, | ||
| ) | ||
|
|
||
| assert result.content[0].type == "text" | ||
| assert result.content[0].text == "test data" | ||
| assert result.structured_content == {"data_type": "test"} | ||
| assert result.meta is None | ||
|
|
||
| async def test_structured_unstructured_and_meta_content(self, client): | ||
| result = await client.call_tool( | ||
| "test_tool", | ||
| { | ||
| "unstructured": "test data", | ||
| "structured": {"data_type": "test"}, | ||
| "meta": {"some": "metadata"}, | ||
| }, | ||
| ) | ||
|
|
||
| assert result.content[0].type == "text" | ||
| assert result.content[0].text == "test data" | ||
| assert result.structured_content == {"data_type": "test"} | ||
| assert result.meta == {"some": "metadata"} | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Move fixture imports to top of file (tests guideline).
Avoid imports inside tests/fixtures.
Apply:
-from fastmcp import FastMCP
-from fastmcp.client import Client
+from fastmcp import FastMCP
+from fastmcp.client import Clientat the file top, then remove the same lines from the client fixture.
Ensure no duplicate TestToolResultCasting classes exist:
🏁 Script executed:
#!/bin/bash
rg -n "class\s+TestToolResultCasting\b" -C1 testsLength of output: 139
🏁 Script executed:
#!/bin/bash
# Check top 30 lines for imports
head -30 tests/tools/test_tool.py | grep -n "from fastmcp import\|from fastmcp.client import"Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check the fixture to confirm imports still exist inside it
sed -n '1409,1430p' tests/tools/test_tool.pyLength of output: 576
---
🏁 Script executed:
```shell
#!/bin/bash
# Check the actual imports at the top of the test file
head -50 tests/tools/test_tool.py
Length of output: 1545
Move fixture imports to file top per guideline.
The imports from fastmcp import FastMCP and from fastmcp.client import Client are inside the client fixture (lines 1410–1411) and should be at the file top. Add them to the import block at the beginning of tests/tools/test_tool.py, then remove them from the fixture.
🤖 Prompt for AI Agents
In tests/tools/test_tool.py around lines 1408 to 1470, the imports "from fastmcp
import FastMCP" and "from fastmcp.client import Client" are currently inside the
async client fixture; move those two import statements to the top import block
of the file (alongside other imports) and remove them from inside the fixture so
the fixture uses the top-level imports instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
examples/tool_result_echo.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Never use bare except; always catch specific exception types
Files:
examples/tool_result_echo.py
🧬 Code graph analysis (1)
examples/tool_result_echo.py (2)
src/fastmcp/server/server.py (1)
FastMCP(148-2693)src/fastmcp/tools/tool.py (1)
ToolResult(66-112)
🔇 Additional comments (1)
examples/tool_result_echo.py (1)
1-8: LGTM!The imports are appropriate for demonstrating the new
metafeature in ToolResult.
| @mcp.tool | ||
| def echo(text: str) -> ToolResult: | ||
| return ToolResult( | ||
| content=text, structured_content=EchoData(data=text), meta={"some": "metadata"} | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding documentation to enhance the example's educational value.
While the implementation correctly demonstrates the new meta feature, adding documentation would help users understand when and why to use it:
- Add a docstring to the
echofunction explaining what the tool does and the purpose of themetafield - Consider using a more realistic example for the
metacontent (e.g.,{"timestamp": "...", "source": "..."}) to illustrate typical use cases - Add a
if __name__ == "__main__":block showing how to run the server, if that's the convention for examples in this project
Example enhancement:
@mcp.tool
def echo(text: str) -> ToolResult:
+ """
+ Echo the input text back to the caller.
+
+ Demonstrates ToolResult with content, structured_content, and meta fields.
+ The meta field can carry additional metadata about the tool execution
+ (e.g., timestamps, tracing info, etc.) for Apps SDK integration.
+ """
return ToolResult(
- content=text, structured_content=EchoData(data=text), meta={"some": "metadata"}
+ content=text,
+ structured_content=EchoData(data=text),
+ meta={"source": "echo_tool", "version": "1.0"}
)
Description
As part of supporting OpenAI's Apps SDK, the underlying MCP python sdk added support for
metain tool call payloads in v1.19.0.This PR modifies the
ToolResultobject to additionally support themetafield. It makes the same change toCachedToolResultto support the caching middleware and the localCallToolResultdataclass in the client.It also adds some additional testing for
ToolResultandCachedToolResultContributors Checklist
Review Checklist
Summary by CodeRabbit
New Features
Documentation
Tests
Chores