Conversation
- Add tools and result_type parameters to ctx.sample() - Update OpenAI handler for tool content types - Client advertises sampling.tools capability by default - Collect tool results into single message with list content
Test Failure AnalysisSummary: All CI jobs are failing during dependency installation because the project requires Root Cause: This PR updates As noted in the PR description, this PR depends on modelcontextprotocol/python-sdk#1722 being merged and released first. That upstream PR is currently still open and unmerged. Suggested Solution: These test failures are expected and documented in the PR description. No action is needed at this time. The PR should remain in draft or marked as blocked until:
Once the dependency is available, the tests should pass without any code changes to this PR. Detailed AnalysisError Message from All JobsAffected JobsAll 4 CI jobs failed at the
Dependency Chain
Related Files
🤖 This analysis was automatically generated and updated. The failure is expected per the PR description. Edit: Updated analysis for workflow run 19951617050 (2025-12-05 03:24 UTC) |
WalkthroughAdds end-to-end tool-enabled sampling across docs, examples, client, and server code. Client: optional Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
51-86: Fix temperature=0.0 being dropped due to falsy evaluationThe
temperature=params.temperature or NOT_GIVENpattern on line 81 incorrectly treats0.0as falsy, sendingNOT_GIVENto the OpenAI API instead of the valid temperature value. Sincetemperatureis typed asfloat | None, an explicit0.0is distinct fromNoneand should be preserved.Change to:
- temperature=params.temperature or NOT_GIVEN, + temperature=( + params.temperature + if params.temperature is not None + else NOT_GIVEN + ),This preserves valid zero values while still defaulting
NonetoNOT_GIVEN. (Note:stopSequenceson line 83 has the same pattern and should also be reviewed.)
🧹 Nitpick comments (9)
src/fastmcp/server/server.py (1)
42-45: SDK-prefixed MCP types and list_ handlers look consistent*The new SDK* aliases and
_list_{tools,resources,resource_templates,prompts}_mcpmethods correctly map internal components tomcp.typesSDK objects using theto_mcp_*helpers and the.key/uriTemplateconventions, which preserves prefixing and filtering semantics across mounted servers and middleware. The extraContextlayer in both the*_mcpand*_middlewarefunctions is slightly redundant but not harmful; you could later simplify by having only the middleware layer own the request context, mirroring_call_tool_middleware, if you want to reduce nesting.Also applies to: 646-662, 730-746, 819-835, 913-928
docs/servers/sampling.mdx (1)
418-440: Clarify pseudo-code in the custom loop exampleThe
custom_agentexample uses placeholders likesome_conditionanddifferent_toolthat make the snippet non-runnable as-is. Consider adding a short comment (e.g.,# pseudo-code: define some_condition/different_tool as needed) so users don’t mistake it for a copy‑paste runnable example, given the repo’s usual “runnable examples” convention.examples/advanced_sampling/tool_use.py (1)
72-85: EnsureCallToolResult.datais populated for this exampleThe example prints
result.data["summary"]/["sources_used"]/["confidence"]. This relies onClient.call_toolsuccessfully parsing structured output intoCallToolResult.datausing the tool’soutput_schema. Please double‑check that theresearchtool is registered with an appropriateoutput_schema(or auto‑derived one) so thatdatais notNone; otherwise these subscripts will raise at runtime and you may need to read fromresult.structured_contentinstead.examples/advanced_sampling/structured_output.py (1)
47-65: Confirm structured data reachesCallToolResult.dataAs in the tool-use example, this script prints
result.data["sentiment"],["confidence"], and["keywords"]. Please verify that the tool’s structured output is being exposed viaCallToolResult.data(through a properoutput_schema), otherwisedatamay beNoneand these lines will fail. If needed, you could fall back toresult.structured_contentor wrap the output schema explicitly on the server side.src/fastmcp/client/client.py (1)
317-331: set_sampling_callback mirrors init behavior; watch for capability overridesThe updated
set_sampling_callbackcorrectly updates both the sampling callback andsampling_capabilities, defaulting to tools-enabled sampling when no explicit capabilities are provided, consistent with__init__. One thing to be aware of: callingset_sampling_callbackwithoutsampling_capabilitiesafter constructing a client with custom capabilities will overwrite them with the default tools-capability. If you expect users to swap handlers while preserving custom capabilities, consider documenting this or acceptingsampling_capabilities=...in those call sites.src/fastmcp/experimental/sampling/handlers/openai.py (1)
324-338: Tool choice mapping currently supports mode-only semantics
_convert_tool_choice_to_openaimaps MCPToolChoice.modevalues"auto","required", and"none"directly to OpenAI’s accepted values, defaulting to"auto"for anything unknown. If MCP’sToolChoiceever gains a “specific tool only” mode, this would need to be extended (e.g., to return{"type": "function", "function": {"name": ...}}). For now, with mode-only semantics, this is fine.src/fastmcp/server/context.py (3)
634-642: Clarify type identity check withstr.The condition
result_type is not struses identity comparison, which works here because you're checking if the type itself is thestrclass. However, this won't catch subclasses or type aliases. If that's intentional, consider adding a comment for clarity; otherwise, you might want a more robust check.
1003-1018: Bareexcept Exceptionis acceptable here but consider logging.The broad exception catches are justified since Pydantic validation and user-provided tool functions can raise various exception types. However, silently catching errors without logging may make debugging difficult.
Consider adding debug logging before returning error results:
except Exception as e: + logger.debug(f"Tool '{tool_use.name}' failed: {e}") tool_results.append( ToolResultContent(Also applies to: 1046-1054
1166-1184: Inconsistent type checks forresponse_type.The checks at lines 1168-1184 use
isinstance(response_type, dict)andisinstance(response_type, list), butresponse_typeis declared astype[T] | list[str] | dict[str, dict[str, str]] | None. When it's adictorlistvalue (not a type), these runtime checks make sense—but the logic is hard to follow and could be simplified.Consider extracting this into a helper or adding comments explaining the multi-select/single-select distinction.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (7)
pyproject.tomlis excluded by none and included by nonetests/client/test_sampling.pyis excluded by none and included by nonetests/server/middleware/test_tool_injection.pyis excluded by none and included by nonetests/server/sampling/__init__.pyis excluded by none and included by nonetests/server/sampling/test_sampling_tool.pyis excluded by none and included by nonetests/server/test_context.pyis excluded by none and included by noneuv.lockis excluded by!**/*.lockand included by none
📒 Files selected for processing (18)
docs/clients/sampling.mdx(2 hunks)docs/servers/sampling.mdx(11 hunks)examples/advanced_sampling/README.md(1 hunks)examples/advanced_sampling/structured_output.py(1 hunks)examples/advanced_sampling/tool_use.py(1 hunks)src/fastmcp/client/client.py(3 hunks)src/fastmcp/client/transports.py(1 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(5 hunks)src/fastmcp/prompts/prompt.py(2 hunks)src/fastmcp/resources/resource.py(2 hunks)src/fastmcp/resources/template.py(3 hunks)src/fastmcp/server/context.py(13 hunks)src/fastmcp/server/proxy.py(1 hunks)src/fastmcp/server/sampling/__init__.py(1 hunks)src/fastmcp/server/sampling/handler.py(2 hunks)src/fastmcp/server/sampling/sampling_tool.py(1 hunks)src/fastmcp/server/server.py(5 hunks)src/fastmcp/tools/tool.py(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/sampling.mdxdocs/clients/sampling.mdx
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bareexcept- be specific with exception types
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if shorter
Follow existing patterns and maintain consistency in code organization and style
Files:
src/fastmcp/resources/resource.pysrc/fastmcp/server/sampling/handler.pysrc/fastmcp/resources/template.pysrc/fastmcp/prompts/prompt.pysrc/fastmcp/server/server.pysrc/fastmcp/client/client.pysrc/fastmcp/tools/tool.pysrc/fastmcp/client/transports.pysrc/fastmcp/server/context.pysrc/fastmcp/server/sampling/__init__.pysrc/fastmcp/experimental/sampling/handlers/openai.pysrc/fastmcp/server/proxy.pysrc/fastmcp/server/sampling/sampling_tool.py
🧠 Learnings (3)
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Note that Resources and Resource Templates are distinct objects but both handled by ResourceManager, requiring coordinated updates when changes affect either object type
Applied to files:
src/fastmcp/resources/template.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: When implementing changes that affect MCP object types (Tools, Resources, Resource Templates, or Prompts), such as adding tags or importing, ensure changes are adopted, applied, and tested on all four object types
Applied to files:
src/fastmcp/resources/template.pysrc/fastmcp/server/server.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/prompts/prompt.pysrc/fastmcp/server/server.pysrc/fastmcp/tools/tool.py
🧬 Code graph analysis (6)
src/fastmcp/resources/template.py (1)
src/fastmcp/server/proxy.py (1)
from_mcp_template(366-382)
examples/advanced_sampling/tool_use.py (3)
src/fastmcp/server/context.py (2)
fastmcp(190-195)Context(138-1246)src/fastmcp/client/client.py (2)
Client(107-1023)call_tool(939-1015)src/fastmcp/experimental/sampling/handlers/openai.py (1)
OpenAISamplingHandler(46-395)
src/fastmcp/server/server.py (3)
src/fastmcp/prompts/prompt.py (1)
Prompt(64-153)src/fastmcp/resources/resource.py (1)
Resource(33-155)src/fastmcp/tools/tool.py (1)
Tool(116-258)
src/fastmcp/client/client.py (1)
src/fastmcp/client/sampling.py (1)
create_sampling_callback(31-56)
src/fastmcp/server/context.py (1)
src/fastmcp/server/sampling/sampling_tool.py (4)
SamplingTool(18-138)from_mcp_tool(79-102)_to_sdk_tool(66-76)run(49-64)
src/fastmcp/experimental/sampling/handlers/openai.py (2)
src/fastmcp/tools/tool.py (1)
Tool(116-258)src/fastmcp/server/server.py (4)
name(327-328)tool(1320-1334)tool(1337-1351)tool(1353-1488)
🪛 LanguageTool
docs/servers/sampling.mdx
[style] ~410-~410: Try using a synonym here to strengthen your writing.
Context: ...ling iteration and returns immediately, giving you access to result.history so you can c...
(GIVE_PROVIDE)
🪛 Ruff (0.14.7)
src/fastmcp/server/context.py
630-632: Avoid specifying long messages outside the exception class
(TRY003)
686-689: Avoid specifying long messages outside the exception class
(TRY003)
785-788: Avoid specifying long messages outside the exception class
(TRY003)
797-800: Avoid specifying long messages outside the exception class
(TRY003)
852-854: Avoid specifying long messages outside the exception class
(TRY003)
1003-1003: Do not catch blind exception: Exception
(BLE001)
1046-1046: Do not catch blind exception: Exception
(BLE001)
src/fastmcp/experimental/sampling/handlers/openai.py
270-270: Avoid specifying long messages outside the exception class
(TRY003)
345-345: Avoid specifying long messages outside the exception class
(TRY003)
388-388: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/sampling/sampling_tool.py
92-95: Avoid specifying long messages outside the exception class
(TRY003)
131-131: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (25)
src/fastmcp/server/proxy.py (1)
558-571: LGTM! Sampling handler updated to new SamplingResult API.The change from capturing raw content to using
result.textaligns with the new SamplingResult container introduced in this PR. The fallback to an empty string is appropriate for handling cases where text may be None.docs/clients/sampling.mdx (1)
178-211: Verify that code examples have been tested.The code examples demonstrating tool-enabled sampling are comprehensive and well-structured. Please confirm that these examples have been tested and are executable as written.
Based on coding guidelines, MDX documentation requires that "all code examples are syntactically correct and executable."
src/fastmcp/prompts/prompt.py (1)
12-13: Prompt → SDKPrompt migration is cleanUsing
SDKPrompt/SDKPromptArgumentand updating theto_mcp_promptsignature keeps behavior identical while aligning with the new SDK-prefixed type naming used by the server. No functional issues spotted.Also applies to: 87-103
docs/servers/sampling.mdx (1)
13-404: Sampling, tools, and structured-output docs align with the implementationThe page clearly explains
ctx.sample,SamplingResult,result_type, tool loops,tool_choice, and OpenAI handlers, and the examples are consistent with the new client/server APIs (includingsampling_handler_behaviorand tool-capability advertising). This should be very helpful for users wiring up agentic sampling flows.Also applies to: 451-568, 579-581
examples/advanced_sampling/tool_use.py (1)
25-69: Tool-using research workflow example is consistent with sampling APIsThe sampling tools,
ResearchReportmodel, andresearchtool correctly demonstratectx.samplewithtools=[...],result_type=ResearchReport, andmax_iterations, matching the documented behavior. Returningresult.result.model_dump()from the tool is a reasonable choice for exposing structured output to clients.src/fastmcp/resources/template.py (1)
12-13: ResourceTemplate ↔ SDKResourceTemplate conversions look correctSwitching to
SDKResourceTemplateand updatingto_mcp_template/from_mcp_templatekeeps the existing field mapping (name,uriTemplate,mimeType,icons,annotations,_meta) intact while aligning with the rest of the SDK-prefixed MCP types. No behavioral regression evident.Also applies to: 193-225
examples/advanced_sampling/structured_output.py (1)
24-45: Structured-output sentiment tool example matchesresult_typebehaviorThe
SentimentAnalysismodel andanalyze_sentimenttool nicely illustrate usingctx.sample(..., result_type=SentimentAnalysis)and returningresult.result.model_dump()so the client can consume a plain dict. This is consistent with the structured-output semantics described in the docs.src/fastmcp/client/client.py (1)
215-241: Client now cleanly advertises sampling capabilities when a handler is setAdding the
sampling_capabilitiesparameter and wiring_session_kwargs["sampling_capabilities"]whensampling_handleris provided ensures clients advertisesampling.toolsby default (viaSamplingCapability(tools=SamplingToolsCapability())), matching the docs about tool-enabled sampling. Passing an explicitsampling_capabilitieslets advanced users fine-tune this. The new argument is backward compatible and well-scoped.Also applies to: 278-289
src/fastmcp/experimental/sampling/handlers/openai.py (4)
21-36: OpenAI imports and type wiring for tools are correctThe additional imports for
CreateMessageResultWithTools,StopReason,Tool*content types, and OpenAI tool-related params (ChatCompletionToolParam,ChatCompletionMessageToolCallParam,ChatCompletionToolChoiceOptionParam,FunctionDefinition, etc.) match how they are used later in the handler. This keeps the module’s public surface aligned with MCP’s sampling/tool abstractions.
119-271: Conversion of SamplingMessages to OpenAI messages covers text, tool use, and tool resultsThe extended
_convert_to_openai_messageshandles:
- Raw string prompts,
- Text-only
SamplingMessageinstances,- Lists of
TextContent,ToolUseContent, andToolResultContent(from prior tool-enabled iterations),- Standalone
ToolUseContentandToolResultContent.It builds the right OpenAI message types (
user,assistantwithtool_calls, andtoolmessages) and raises a clear error for unsupported content types. This should allow multi-iteration tool loops to round-trip correctly between MCP and OpenAI.
302-322: _convert_tools_to_openai correctly maps MCP tools to OpenAI function toolsMapping each MCP
Tool’sname,description, andinputSchemato an OpenAIChatCompletionToolParamwith aFunctionDefinitionlooks correct, and the defensivetype="object"default for parameters is helpful. Just ensuretool.inputSchemais always a mapping (as produced by the SDK) sodict(tool.inputSchema)is safe.
340-395: CreateMessageResultWithTools construction mirrors OpenAI tool-call responses
_chat_completion_to_result_with_tools:
- Validates there is at least one choice,
- Maps OpenAI
finish_reasonto MCPStopReason(tool_calls→"toolUse",stop→"endTurn",length→"maxTokens"),- Builds a
contentlist containing aTextContent(if present) and oneToolUseContentper tool call (parsing JSON arguments),- Raises if there is no content at all.
This should interoperate cleanly with the MCP sampling/tool loop machinery.
src/fastmcp/server/context.py (7)
79-99: Well-designed generic result container.The
SamplingResult[ResultT]dataclass cleanly encapsulates text, typed result, and history. The docstring is clear and explains the purpose of each field.
512-559: Overloads correctly enable type inference.The two overloads properly distinguish between structured (
result_typespecified) and text-only sampling, allowing callers to benefit from static type checking on the return type.
1056-1063: Type ignore is acceptable for SDK compatibility.The
# type: ignore[arg-type]is necessary due to the SDK's type definitions. This is a pragmatic workaround for passing tool results back in the message flow.
1343-1358: Clean text extraction helper.The
_extract_text_from_contentfunction correctly handles both single blocks and lists, using duck typing to find text content. The early return on first text found is appropriate.
1318-1340: Well-structured synthetic tool creation.The
_create_final_response_toolfunction properly derives the JSON schema from the result type and creates a passthrough function for the tool. Schema compression withprune_titles=Trueis a good choice for reducing payload size.
678-689: Good capability validation before tool usage.Properly checks that the client advertises
sampling.toolscapability before attempting tool-enabled sampling. The error message clearly explains what's required.
274-288: Return type annotations updated for SDK consistency.The return types for
list_resourcesandlist_promptsnow useSDKResourceandSDKPromptaliases, aligning with the broader SDK type standardization in this PR.src/fastmcp/server/sampling/sampling_tool.py (6)
18-48: Well-designed SamplingTool class.The class structure is clean with proper Pydantic configuration for the callable field. The docstring with examples demonstrates the intended usage patterns effectively.
49-64: Correct handling of sync and async tool functions.The
run()method properly defaultsNonearguments to an empty dict and handles both synchronous and asynchronous functions with theisawaitablecheck.
66-76: Clean SDK conversion method.The
_to_sdk_tool()method provides a straightforward conversion to the MCP SDK's Tool type. The underscore prefix appropriately marks it as internal.
78-102: Proper validation when converting from FastMCP Tool.The
from_mcp_tool()method correctly validates that the source tool has anfnattribute before conversion. The error message clearly explains that only FunctionTools can be converted.
141-187: Flexible decorator with proper overloads.The
sampling_tooldecorator correctly supports both bare usage (@sampling_tool) and parameterized usage (@sampling_tool(name="...")). The overloads enable proper type inference in both cases.Note that the decorated function is replaced by a
SamplingToolinstance, which is the intended behavior for use withcontext.sample(tools=[...]).
133-138: No issue found. The code correctly usesparsed.fnrather than the originalfnparameter.ParsedFunction.from_function()intentionally normalizes the function to handle callable classes (extracting__call__) and staticmethods (extracting__func__). Using the modifiedparsed.fnis required for proper execution, as demonstrated by therun()method's direct invocation viaself.fn(**arguments). This pattern is consistently applied across the codebase (e.g.,FunctionTool.from_function()uses the same approach). The design is sound.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/fastmcp/experimental/sampling/handlers/openai.py (4)
154-216: Consider simplifying the list content handling logic.This block is quite complex with multiple nested conditionals and mixed responsibilities (collecting items, creating tool messages inline, then creating assistant messages). The logic:
- Iterates to collect tool_calls and text_parts
- Immediately creates tool messages for ToolResultContent during iteration
- After iteration, conditionally creates assistant/user messages
This mixed approach makes the flow hard to follow and maintain. Consider refactoring to separate concerns—perhaps first categorize all content items, then construct messages in a second pass.
Additionally, Line 192 concatenates text parts with newlines (
"\n".join(text_parts)). Verify this is the intended behavior, as it may relate to the past review comment about concatenation.
274-274: Make the error message more descriptive.The current error message
f"Unsupported content type: {type(content)}"is generic. Consider specifying which content types are supported (e.g., "Supported types: TextContent, ToolUseContent, ToolResultContent, or list").Note: Static analysis flagged this as a long message outside the exception class (TRY003). If following strict style guidelines, consider defining a custom exception or moving to a constant.
376-379: Consider more explicit JSON parsing error handling.When
json.loads(tool_call.function.arguments)fails, the code silently falls back to an empty dict. This could mask issues with malformed tool call arguments from the LLM. Consider logging the error or raising a more specific exception with context about which tool call failed to parse.Example:
try: arguments = json.loads(tool_call.function.arguments) -except json.JSONDecodeError: - arguments = {} +except json.JSONDecodeError as e: + raise ValueError( + f"Failed to parse arguments for tool '{tool_call.function.name}': {e}" + ) from e
349-349: Optional: Address static analysis hints about error messages.Static analysis (Ruff TRY003) suggests avoiding long messages directly in exception constructors. If following strict style guidelines, consider:
- Defining custom exception classes, or
- Moving messages to constants
This is a minor style concern and can be deferred.
Also applies to: 392-392
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/fastmcp/experimental/sampling/handlers/openai.py(5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bareexcept- be specific with exception types
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if shorter
Follow existing patterns and maintain consistency in code organization and style
Files:
src/fastmcp/experimental/sampling/handlers/openai.py
🧬 Code graph analysis (1)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
src/fastmcp/tools/tool.py (1)
Tool(116-258)
🪛 Ruff (0.14.7)
src/fastmcp/experimental/sampling/handlers/openai.py
274-274: Avoid specifying long messages outside the exception class
(TRY003)
349-349: Avoid specifying long messages outside the exception class
(TRY003)
392-392: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (4)
src/fastmcp/experimental/sampling/handlers/openai.py (4)
1-43: LGTM!The imports are well-organized and include all necessary types for tool-enabled sampling flows. The addition of
jsonfor argument parsing and the comprehensive OpenAI tool-related types are appropriate.
52-91: LGTM!The method signature correctly reflects the conditional return type based on tool usage. The tool and tool_choice conversion logic is properly guarded with presence checks before invoking the converters.
186-186: Potential type mismatch with empty string fallback.The code uses
content_parts if content_parts else "", which may not match the expected type forChatCompletionToolMessageParam. The OpenAI API expectscontentto be a string or a list of content parts. Verify that passing an empty string whencontent_partsis empty is the correct behavior, or consider using a more explicit empty list or a descriptive string like"(no content)".This same pattern appears on Line 251.
306-326: LGTM!The tool conversion logic is clean and handles the edge case of missing "type" fields in the input schema. The use of
tool.description or ""provides a sensible default.
| @staticmethod | ||
| def _convert_tool_choice_to_openai( | ||
| tool_choice: ToolChoice, | ||
| ) -> ChatCompletionToolChoiceOptionParam: | ||
| """Convert MCP tool_choice to OpenAI format.""" | ||
| if tool_choice.mode == "auto": | ||
| return "auto" | ||
| elif tool_choice.mode == "required": | ||
| return "required" | ||
| elif tool_choice.mode == "none": | ||
| return "none" | ||
| else: | ||
| # Unknown mode, default to auto | ||
| return "auto" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find ToolChoice definition
rg -nP 'class ToolChoice|ToolChoice\s*=' --type=py -A 10Repository: jlowin/fastmcp
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check imports in the openai.py file
head -50 src/fastmcp/experimental/sampling/handlers/openai.py | grep -E 'import|from'Repository: jlowin/fastmcp
Length of output: 686
🏁 Script executed:
#!/bin/bash
# Read the complete imports section more carefully
head -100 src/fastmcp/experimental/sampling/handlers/openai.pyRepository: jlowin/fastmcp
Length of output: 3654
🏁 Script executed:
#!/bin/bash
# Search for ToolChoice usage and definition in the codebase
rg 'ToolChoice' --type=py -B 2 -A 5 | head -100Repository: jlowin/fastmcp
Length of output: 6944
🏁 Script executed:
#!/bin/bash
# Look at the full _convert_tool_choice_to_openai method implementation
sed -n '328,341p' src/fastmcp/experimental/sampling/handlers/openai.pyRepository: jlowin/fastmcp
Length of output: 541
🏁 Script executed:
#!/bin/bash
# Search for ToolChoice type definition - it's from mcp.types
# Let's check if we can find documentation or see how it's used
rg 'ToolChoice\(' --type=py -B 2 -A 2 | grep -A 3 'name='Repository: jlowin/fastmcp
Length of output: 676
🏁 Script executed:
#!/bin/bash
# Check how ChatCompletionToolChoiceOptionParam is used elsewhere in the code
rg 'ChatCompletionToolChoiceOptionParam' --type=py -B 3 -A 3Repository: jlowin/fastmcp
Length of output: 2039
🌐 Web query:
OpenAI API ChatCompletionToolChoiceOptionParam tool_choice specific tool selection
💡 Result:
Summary — what tool_choice accepts and how to force a specific tool
-
Allowed values
- "none" — model will not call any tools. [1]
- "auto" — model may choose to call zero, one, or multiple tools (default when tools are provided). [1][2]
- "required" — model must call one or more tools. [1][2]
- Object forcing a specific function/tool — e.g. {"type":"function","function":{"name":"my_function"}} — forces the model to call that named tool. The forced tool must appear in the request's tools list. [1][2][3]
-
Notes
- If tools is empty, tool_choice defaults to "none"; if tools is non-empty it defaults to "auto". [1][2]
- The model can call multiple tools in one response unless you disable parallel_tool_calls (set parallel_tool_calls: false) to restrict to at most one. [2]
- Client libraries / wrappers commonly validate that a forced function name exists in the tools list before sending. [3]
Sources: OpenAI Cookbook examples and docs on function/tool calling, community documentation and library behavior. [1][4][2][3]
References
- OpenAI Cookbook — How to call functions with chat models. [2]
- OpenAI Cookbook example: Using tool_required for customer service. [3]
- LangChain / client validation examples showing forced-function must be in tools list. [8]
- Community summaries/docs describing tool_choice behavior (tool_choice options and parallel_tool_calls). [4]
🏁 Script executed:
#!/bin/bash
# Search for ToolChoice type definition or related documentation
rg -i 'toolchoice|tool.*choice' --type=py --type=md | grep -i 'mode\|enum\|literal' | head -20Repository: jlowin/fastmcp
Length of output: 936
🏁 Script executed:
#!/bin/bash
# Look for ToolChoice usage with name in test files or elsewhere
rg 'ToolChoice\(' --type=py -A 1 | grep -B 1 'name'Repository: jlowin/fastmcp
Length of output: 418
🏁 Script executed:
#!/bin/bash
# Check the full method context including type hints and surrounding code
sed -n '320,350p' src/fastmcp/experimental/sampling/handlers/openai.pyRepository: jlowin/fastmcp
Length of output: 1095
🏁 Script executed:
#!/bin/bash
# Verify there are no other places handling ToolChoice that might provide context
rg '_convert_tool_choice' --type=py -B 5 -A 15Repository: jlowin/fastmcp
Length of output: 3825
Handle specific tool selection and raise errors for invalid modes.
The method ignores the name field of ToolChoice. When a specific tool must be forced (e.g., ToolChoice(mode="required", name="final_response")), the conversion should return the OpenAI object format {"type": "function", "function": {"name": "..."}} instead of just the string. Additionally, replace the silent fallback to "auto" on line 340 with a ValueError to catch configuration errors explicitly rather than masking them.
🤖 Prompt for AI Agents
In src/fastmcp/experimental/sampling/handlers/openai.py around lines 328 to 341,
the converter currently ignores ToolChoice.name and silently falls back to
"auto" for unknown modes; change it so that when tool_choice.mode == "required"
and a name is provided you return the OpenAI object form {"type": "function",
"function": {"name": "<name>"}} (validate that name is non-empty and raise
ValueError if missing), keep "auto" and "none" string returns for the explicit
"auto" and "none" modes, and replace the silent fallback with raising a
ValueError for any unrecognized mode so misconfiguration is surfaced.
Functions passed to ctx.sample(tools=[...]) are now auto-converted via SamplingTool.from_function(). Users can still use that method directly for custom name/description overrides.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/fastmcp/server/context.py (2)
821-827: Type contract violation whenmax_iterationsexhausted (previously flagged).Returning
result=cast(ResultT, None)when iterations are exhausted without a final response violates the type contract. Callers expectingResultT(e.g., a Pydantic model) will receiveNone, causing downstreamAttributeErrors.The previous review suggested raising an exception, which seems appropriate here:
# Check if we've hit max iterations if iteration >= max_iterations: - # Return what we have with None text (tool use without completion) - return SamplingResult( - text=None, - result=cast(ResultT, None), - history=current_messages, - ) + raise RuntimeError( + f"Sampling loop exhausted after {max_iterations} iterations " + "without receiving a final response from the LLM." + )
916-923: SameNoneresult issue in_sample_with_client(previously flagged).This location has the identical type contract violation as
_sample_with_fallback_handler. Apply the same fix for consistency.
🧹 Nitpick comments (5)
examples/advanced_sampling/tool_use.py (1)
78-81: Minor output formatting inconsistency.Line 81 formats
confidencewith a%suffix, butResearchReport.confidenceis defined as a plainfloat(line 48). If it's meant to be 0.0-1.0, consider multiplying by 100 or clarifying the expected range in the model's docstring.- print(f" Confidence: {result.data['confidence']}%") + print(f" Confidence: {result.data['confidence'] * 100:.1f}%")docs/servers/sampling.mdx (2)
425-447: Placeholdersome_conditionin custom loop example.The
some_conditionplaceholder is undefined, which will cause aNameErrorif users copy-paste directly. Consider using a realistic condition or adding a comment clarifying it's illustrative.# Custom logic: check conditions, change tools, etc. - if some_condition: + if "weather" in str(result.history): # Example condition tools = [different_tool]Or add a clarifying comment:
# Custom logic: check conditions, change tools, etc. # Replace `some_condition` with your actual termination logic if some_condition:
432-446: Missing termination condition inwhile Trueloop example.The custom loop example lacks a break condition other than
result.text, which could lead to an infinite loop if the LLM keeps calling tools. Consider adding a max iteration guard or documenting this caveat.async def custom_agent(question: str, ctx: Context) -> str: """Agent with custom loop logic.""" history = [] tools = [search, get_time] + max_loops = 20 # Prevent infinite loops - while True: + for _ in range(max_loops): result = await ctx.sample( messages=history or question, tools=tools, max_iterations=1, ) history = result.history # Custom logic: check conditions, change tools, etc. if some_condition: tools = [different_tool] # When we get a text response, we're done if result.text: return result.text + + return "Max iterations reached without final response"src/fastmcp/server/sampling/sampling_tool.py (1)
52-67: Consider handling unexpected argument errors inrun().If the underlying function receives arguments that don't match its signature (e.g., extra keys from LLM hallucination), this will raise a
TypeError. While the caller (_execute_tool_calls) catches exceptions, providing a more descriptive error here could aid debugging.async def run(self, arguments: dict[str, Any] | None = None) -> Any: """Execute the tool with the given arguments.""" if arguments is None: arguments = {} - result = self.fn(**arguments) + try: + result = self.fn(**arguments) + except TypeError as e: + raise TypeError( + f"Failed to call tool '{self.name}' with arguments {arguments}: {e}" + ) from e if inspect.isawaitable(result): result = await result return resultsrc/fastmcp/server/context.py (1)
1345-1360:_extract_text_from_contentuses duck typing.Using
hasattr(block, "text")works but is less type-safe than checkingisinstance(block, TextContent). Consider the explicit check for clarity and IDE support.def _extract_text_from_content( content: SamplingMessageContentBlock | list[SamplingMessageContentBlock], ) -> str | None: if isinstance(content, list): for block in content: - if hasattr(block, "text"): + if isinstance(block, TextContent): return block.text return None - elif hasattr(content, "text"): + elif isinstance(content, TextContent): return content.text return None
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
tests/client/test_sampling.pyis excluded by none and included by nonetests/server/sampling/test_sampling_tool.pyis excluded by none and included by none
📒 Files selected for processing (5)
docs/servers/sampling.mdx(11 hunks)examples/advanced_sampling/tool_use.py(1 hunks)src/fastmcp/server/context.py(12 hunks)src/fastmcp/server/sampling/__init__.py(1 hunks)src/fastmcp/server/sampling/sampling_tool.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/fastmcp/server/sampling/init.py
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bareexcept- be specific with exception types
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if shorter
Follow existing patterns and maintain consistency in code organization and style
Files:
src/fastmcp/server/sampling/sampling_tool.pysrc/fastmcp/server/context.py
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/sampling.mdx
🧬 Code graph analysis (3)
src/fastmcp/server/sampling/sampling_tool.py (1)
src/fastmcp/tools/tool.py (2)
Tool(116-258)ParsedFunction(394-535)
src/fastmcp/server/context.py (1)
src/fastmcp/server/sampling/sampling_tool.py (5)
SamplingTool(18-141)from_mcp_tool(82-105)from_function(108-141)_to_sdk_tool(69-79)run(52-67)
examples/advanced_sampling/tool_use.py (3)
src/fastmcp/server/context.py (2)
fastmcp(190-195)Context(138-1248)src/fastmcp/client/client.py (2)
Client(107-1023)call_tool(939-1015)src/fastmcp/experimental/sampling/handlers/openai.py (1)
OpenAISamplingHandler(46-399)
🪛 LanguageTool
docs/servers/sampling.mdx
[style] ~417-~417: Try using a synonym here to strengthen your writing.
Context: ...ling iteration and returns immediately, giving you access to result.history so you can c...
(GIVE_PROVIDE)
🪛 Ruff (0.14.7)
src/fastmcp/server/sampling/sampling_tool.py
95-98: Avoid specifying long messages outside the exception class
(TRY003)
134-134: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/context.py
632-634: Avoid specifying long messages outside the exception class
(TRY003)
688-691: Avoid specifying long messages outside the exception class
(TRY003)
787-790: Avoid specifying long messages outside the exception class
(TRY003)
799-802: Avoid specifying long messages outside the exception class
(TRY003)
854-856: Avoid specifying long messages outside the exception class
(TRY003)
1005-1005: Do not catch blind exception: Exception
(BLE001)
1048-1048: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (8)
examples/advanced_sampling/tool_use.py (1)
1-85: Well-structured example demonstrating tool-enabled sampling.The example clearly shows:
- Defining sampling tools (
search_web,get_word_count)- Using
result_typefor structured output (ResearchReport)- Executing the agentic workflow via
ctx.sample()with toolsThe code follows the coding guidelines with proper type annotations and is runnable end-to-end.
docs/servers/sampling.mdx (1)
1-586: Comprehensive documentation for the new sampling features.The documentation effectively covers:
- Basic usage with clear examples
- Structured output with
result_type- Tool-enabled sampling with automatic loop handling
- Custom loop building for advanced use cases
- Fallback handler integration
Well-structured with progressive disclosure, proper component usage, and actionable code examples.
src/fastmcp/server/sampling/sampling_tool.py (1)
1-141: Clean implementation of SamplingTool.The class design is solid:
- Pydantic BaseModel with
arbitrary_types_allowedfor thefnfield- Proper async/sync handling in
run()- Multiple construction paths (
from_function,from_mcp_tool)- Lambda name validation prevents ambiguous tool names
src/fastmcp/server/context.py (5)
750-763:effective_tool_choicelogic handles last-iteration finalization well.The code correctly forces
final_responsewhenresult_typeis specified, or forcesmode="none"to get a text response on the last iteration. This prevents infinite loops and ensures a usable result.
1005-1020: BareExceptioncatch is acceptable here for LLM feedback loop.Catching
Exceptionwhen validatingfinal_responseinput allows the error to be sent back to the LLM for retry, which is the intended behavior for agentic workflows. The static analysis hint (BLE001) can be safely ignored in this context.
1038-1056: Tool execution error handling is robust.Catching exceptions during tool execution and returning them as
ToolResultContentwithisError=Trueallows the LLM to handle failures gracefully. This is appropriate for an agentic loop.
636-644: System prompt hint injection for structured output is clear.When
result_typeis specified, appending "Call final_response when you have completed the task." to the system prompt effectively guides the LLM. Consider making this hint configurable in future iterations if users need different phrasing.
79-98: Well-designedSamplingResultdataclass.The generic
SamplingResult[ResultT]cleanly separates:
.textfor string representation.resultfor typed result.historyfor conversation continuationThe docstring clearly explains each field's purpose.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/fastmcp/server/context.py (1)
836-842: Address the past review concern: Type-unsafeNonereturn.As flagged in the previous review, returning
cast(ResultT, None)whenmax_iterationsis exhausted violates the type contract whenresult_typeis a non-optional structured type. This creates a runtime hazard where callers expect a valid object but receiveNone.The previous review suggested raising an exception (e.g.,
RuntimeErroror a dedicatedMaxIterationsExceededError) instead of returningNone. This would make the failure explicit and type-safe.Apply a similar fix to both occurrences (here and at lines 931-938):
# Check if we've hit max iterations if iteration >= max_iterations: - # Return what we have with None text (tool use without completion) - return SamplingResult( - text=None, - result=cast(ResultT, None), - history=current_messages, - ) + raise RuntimeError( + f"Sampling exhausted {max_iterations} iterations without a final response. " + "Consider increasing max_iterations or simplifying the task." + )
🧹 Nitpick comments (1)
src/fastmcp/server/context.py (1)
647-649: Optional: Consider extracting error message to a constant.The error message is clear and functional. For strict adherence to Ruff's TRY003 guideline, you could extract it to a module-level constant, but this is a low-priority style improvement.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
pyproject.tomlis excluded by none and included by noneuv.lockis excluded by!**/*.lockand included by none
📒 Files selected for processing (7)
src/fastmcp/client/client.py(3 hunks)src/fastmcp/client/transports.py(1 hunks)src/fastmcp/prompts/prompt.py(2 hunks)src/fastmcp/resources/resource.py(2 hunks)src/fastmcp/resources/template.py(3 hunks)src/fastmcp/server/context.py(12 hunks)src/fastmcp/server/server.py(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/fastmcp/client/client.py
- src/fastmcp/resources/template.py
- src/fastmcp/client/transports.py
- src/fastmcp/server/server.py
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bareexcept- be specific with exception types
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if shorter
Follow existing patterns and maintain consistency in code organization and style
Files:
src/fastmcp/prompts/prompt.pysrc/fastmcp/server/context.pysrc/fastmcp/resources/resource.py
🧠 Learnings (2)
📓 Common learnings
Learnt from: jlowin
Repo: jlowin/fastmcp PR: 0
File: :0-0
Timestamp: 2025-12-01T15:48:05.095Z
Learning: PR #2505 in fastmcp adds NEW functionality to get_access_token(): it now first checks request.scope["user"] for the token (which never existed before), then falls back to _sdk_get_access_token() (the only thing the original code did). This is not a reversal of order but entirely new functionality to fix stale token issues.
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/prompts/prompt.py
🧬 Code graph analysis (1)
src/fastmcp/server/context.py (2)
src/fastmcp/tools/tool.py (6)
Tool(122-282)from_function(206-236)from_function(289-351)from_function(420-548)run(238-248)run(353-393)src/fastmcp/server/sampling/sampling_tool.py (5)
SamplingTool(18-141)from_mcp_tool(82-105)from_function(108-141)_to_sdk_tool(69-79)run(52-67)
🪛 Ruff (0.14.7)
src/fastmcp/server/context.py
647-649: Avoid specifying long messages outside the exception class
(TRY003)
703-706: Avoid specifying long messages outside the exception class
(TRY003)
802-805: Avoid specifying long messages outside the exception class
(TRY003)
814-817: Avoid specifying long messages outside the exception class
(TRY003)
869-871: Avoid specifying long messages outside the exception class
(TRY003)
1020-1020: Do not catch blind exception: Exception
(BLE001)
1063-1063: Do not catch blind exception: Exception
(BLE001)
🔇 Additional comments (9)
src/fastmcp/server/context.py (7)
71-99: LGTM: Well-designed type definitions and result container.The new
ResultTTypeVar withdefault=strmaintains backward compatibility, and theSamplingResultgeneric dataclass provides a clean interface for both text and structured responses with full history tracking.
527-617: LGTM: Well-structured overloads and comprehensive documentation.The overloads correctly distinguish between typed (
result_typespecified) and text-only responses, with clear parameter documentation covering the new tool-enabled sampling workflow.
1020-1035: Verify: Broad exception handling in tool execution is intentional.The code catches
Exceptionbroadly when executing tools (lines 1020, 1063). While Ruff flags this as BLE001, catching broad exceptions in tool execution contexts is often appropriate to:
- Prevent individual tool failures from crashing the entire sampling loop
- Return actionable error messages to the LLM for retry/recovery
Confirm this defensive pattern is intentional for robustness. If specific exception types are expected (e.g.,
ValidationError,ToolExecutionError), consider catching those explicitly first, with a final broad catch as a safety net.Also applies to: 1063-1071
1184-1201: LGTM: Proper handling of enum response types.The new logic correctly handles dict-based single-select and list-based multi-select enum responses with direct value extraction from
result.content, maintaining type safety with appropriate casts.
1335-1375: LGTM: Well-designed helper functions.Both
_create_final_response_tooland_extract_text_from_contentare cleanly implemented:
- The synthetic final_response tool properly uses the type adapter and compressed schema
- Text extraction defensively handles both single blocks and lists with duck typing
- Validation is correctly deferred to the caller for final_response
964-1082: LGTM: Robust tool execution flow.The
_execute_tool_callsmethod correctly:
- Handles both synthetic
final_responseand regular tools- Batches tool results into a single user message per MCP conventions
- Returns early with a structured result when
final_responsesucceeds- Converts tool errors into
ToolResultContentwithisError=Truefor LLM recovery- Continues the loop on validation failures, allowing the LLM to retry
765-779: Good: Last-iteration forcing ensures termination.The code correctly forces completion on the final iteration:
- Forces
final_responsetool whenresult_typeis specified- Forces text-only response (
tool_choice=none) when only tools are usedThis makes the max_iterations limit meaningful. However, this relies on fixing the type-unsafe
Nonereturn at lines 836-842 and 931-938 (flagged separately) for scenarios where forcing still doesn't produce a valid response.Also applies to: 894-908
src/fastmcp/prompts/prompt.py (1)
12-13: LGTM: Clean SDK type alias adoption.The rename from
MCP*toSDK*type aliases is straightforward and aligns with the repository-wide naming convention update. The changes maintain functional equivalence while improving naming consistency.Also applies to: 97-108
src/fastmcp/resources/resource.py (1)
11-11: LGTM: Clean SDK type alias adoption.The rename from
MCPResourcetoSDKResourceis straightforward and aligns with the repository-wide naming convention update. The docstring is correctly updated to reflect the new return type.Also applies to: 135-149
|
/marvin can you fix the merge conflicts please |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
i think this is currently a breaking change to |
|
Claude finished @strawgate's task in 6m 18s —— View job Fixing Merge Conflicts ✓Tasks
Successfully resolved all merge conflicts and pushed to the branch! Conflicts Resolvedsrc/fastmcp/server/context.py - 4 conflicts resolved:
uv.lock - Regenerated the lock file using Commit Details
The merge conflicts have been successfully resolved and the branch is now up to date with main! |
Resolved conflicts in src/fastmcp/server/context.py and uv.lock. Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
306-341:_convert_tool_choice_to_openaiignoresToolChoice.nameand masks invalid modes.This helper currently:
- Returns only string modes
"auto" | "required" | "none", ignoringtool_choice.name.- Silently falls back to
"auto"on any unexpectedmode.Because
Context.sample()usesToolChoice(mode="required", name="final_response")to try to force a specific tool on the last iteration, dropping thenamehere means OpenAI is never explicitly constrained to that tool. The silent"auto"fallback also hides misconfigurations instead of surfacing them early. This is the same concern raised in an earlier review.A more faithful conversion would:
- Return
"auto","none", or"required"for those explicit modes, when no name is set.- When
tool_choice.mode == "required"andtool_choice.nameis provided, emit the object form expected by OpenAI to force a tool:@staticmethod def _convert_tool_choice_to_openai( tool_choice: ToolChoice, ) -> ChatCompletionToolChoiceOptionParam: """Convert MCP tool_choice to OpenAI format.""" - if tool_choice.mode == "auto": - return "auto" - elif tool_choice.mode == "required": - return "required" - elif tool_choice.mode == "none": - return "none" - else: - # Unknown mode, default to auto - return "auto" + if tool_choice.mode == "auto": + return "auto" + if tool_choice.mode == "none": + return "none" + if tool_choice.mode == "required": + # If a specific tool name is provided, force that tool + if tool_choice.name: + return { + "type": "function", + "function": {"name": tool_choice.name}, + } + return "required" + # Unknown mode: fail fast so configuration errors are visible + raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode!r}")Please double‑check the latest OpenAI
tool_choicedocs and type stubs to ensure this object form and accepted string literals still match the current API:OpenAI Chat Completions tool_choice parameter: valid string values and object shape for forcing a specific function/tool.src/fastmcp/server/context.py (1)
780-909: Fixget_cached_typeadapterimport and reconsidermax_iterations+Noneresults in sampling loops.There are a few intertwined issues in the tool/structured-output sampling paths:
Missing
get_cached_typeadapterimport breaks structured outputs at runtime.
_execute_tool_calls()and_create_final_response_tool()both callget_cached_typeadapter(...), but this symbol isn’t imported in this module. That yields aNameErrorthe first time thefinal_responsetool is used, which then gets misclassified as a “Validation error” and prevents structured results from ever succeeding.Apply an import fix near the other utilities:
from fastmcp.server.sampling import SamplingTool from fastmcp.server.server import FastMCP from fastmcp.tools.tool import Tool as FastMCPTool from fastmcp.utilities.json_schema import compress_schema
- from fastmcp.utilities.logging import _clamp_logger, get_logger
- from fastmcp.utilities.logging import _clamp_logger, get_logger
- from fastmcp.utilities.types import get_cached_typeadapter
and keep the existing usages in `_execute_tool_calls()` and `_create_final_response_tool()`. 2. **`max_iterations` exhaustion returns `SamplingResult` with `result=None`.** In both `_sample_with_fallback_handler()` and `_sample_with_client()`, when `iteration >= max_iterations` and the LLM still returns `stopReason == "toolUse"`, you currently do: ```python return SamplingResult( text=None, result=cast(ResultT, None), history=current_messages, )That violates the
SamplingResult[ResultT]contract whenResultTis a non‑optional structured type, and was already called out in an earlier review. It also doesn’t match the PR description that the final iteration “forces the LLM to produce a result” and thatmax_iterations=1should disable that forced‑final behavior rather than short‑circuit tool execution and returnNone.A more robust approach would be to either:
- Raise a clear exception (e.g.
RuntimeErroror a dedicatedMaxIterationsExceededError) when the loop is exhausted without a final response, or- Change the API contract to return
SamplingResult[ResultT | None]and document thatresultcan beNonein this edge case.For example, raising makes the type contract honest while surfacing misbehaving models:
- # Check if we've hit max iterations - if iteration >= max_iterations: - # Return what we have with None text (tool use without completion) - return SamplingResult( - text=None, - result=cast(ResultT, None), - history=current_messages, - ) + # Check if we've hit max iterations + if iteration >= max_iterations: + raise RuntimeError( + f"Sampling loop exhausted after {max_iterations} iterations " + "without receiving a final response from the LLM." + )(and similarly in
_sample_with_client).This is the same concern previously flagged in this file.
Forced-final behavior also applies when
max_iterations == 1.
Both loops treatiteration == max_iterations - 1as the “last iteration” and then unconditionally:
- Force
ToolChoice(mode="required", name="final_response")when aresult_typeis set, or- Force
ToolChoice(mode="none")when tools are present but no structured output.When
max_iterations == 1, that condition is true on the first call, so you still enforce a forced final response instead of returning the “raw response” for a one‑shot call. Combined with the early‑exititeration >= max_iterationschecks, this meansmax_iterations=1can actually prevent any tool execution and just yield aSamplingResultwithNonefields.You may want to special‑case the “advanced loop control” behavior so that:
- Forced finalization (
final_response/none) only kicks in whenmax_iterations > 1, andmax_iterations=1truly behaves as “single LLM call + optional tool execution, no forced final result”.That will bring the implementation in line with the behavior described in the PR summary.
_execute_tool_callsoverall logic is sound once the adapter import is fixed.
Aside from the adapter import and broadExceptioncatches, the flow—adding the assistant message, running tools viaSamplingTool.run, appendingToolResultContent, and optionally short‑circuiting whenfinal_responsevalidates—is coherent. The deadshould_continue==Falsebranch in the callers could be simplified, but it’s harmless.Would you like to (a) switch to raising on max‑iteration exhaustion, or (b) relax the
SamplingResulttype to allowOptional[ResultT]and document that behavior? In either case, aligningmax_iterations=1semantics with the documented “advanced loop control” behavior is worth clarifying in docs/tests.Also applies to: 910-999, 1001-1119, 1313-1335
🧹 Nitpick comments (3)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
52-86: Message and tool/result conversion logic is reasonable, but be aware of text concatenation.The mapping from
SamplingMessagecontent (text/tool uses/results) into OpenAI chat params and back intoCreateMessageResultWithToolslooks coherent and should round‑trip tool calls correctly. Note that when a single message carries multipleTextContentblocks, they are concatenated with newlines into a single OpenAIcontentstring; that’s fine for plain text, but it does lose original segmentation if callers relied on it.Also applies to: 118-276, 344-399
src/fastmcp/server/context.py (2)
564-579:Context.sample()API shape and simple no-tools path look good.The overloads, parameterization with
ResultT, and basic non‑tool path (singlecreate_messagecall, text extraction via_extract_text_from_content, andSamplingResultconstruction) are consistent and type‑annotated as required. ConvertingtoolstoSamplingToolfromSamplingTool/FastMCPTool/callables and then toSDKToolis also a sensible layering.Also applies to: 581-612, 656-779
1338-1353:_extract_text_from_contenthelper is a reasonable convenience.The helper correctly pulls the first
.textfield from either a single content block or a list of blocks, and safely falls back toNonewhen no text is present. It keeps the sampling code cleaner where only the primary text matters.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
pyproject.tomlis excluded by none and included by noneuv.lockis excluded by!**/*.lockand included by none
📒 Files selected for processing (9)
src/fastmcp/client/client.py(3 hunks)src/fastmcp/client/transports.py(1 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(5 hunks)src/fastmcp/prompts/prompt.py(2 hunks)src/fastmcp/resources/resource.py(2 hunks)src/fastmcp/resources/template.py(3 hunks)src/fastmcp/server/context.py(11 hunks)src/fastmcp/server/proxy.py(1 hunks)src/fastmcp/server/server.py(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/fastmcp/prompts/prompt.py
- src/fastmcp/server/proxy.py
- src/fastmcp/client/client.py
- src/fastmcp/client/transports.py
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Python source code must use Python ≥3.10 with full type annotations
Never use bareexcept- be specific with exception types
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if shorter
Follow existing patterns and maintain consistency in code organization and style
Files:
src/fastmcp/resources/resource.pysrc/fastmcp/resources/template.pysrc/fastmcp/server/server.pysrc/fastmcp/server/context.pysrc/fastmcp/experimental/sampling/handlers/openai.py
🧠 Learnings (2)
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions
Applied to files:
src/fastmcp/server/server.py
📚 Learning: 2025-11-26T21:51:44.174Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: .cursor/rules/core-mcp-objects.mdc:0-0
Timestamp: 2025-11-26T21:51:44.174Z
Learning: When implementing changes that affect MCP object types (Tools, Resources, Resource Templates, or Prompts), such as adding tags or importing, ensure changes are adopted, applied, and tested on all four object types
Applied to files:
src/fastmcp/server/server.py
🧬 Code graph analysis (4)
src/fastmcp/resources/template.py (1)
src/fastmcp/server/proxy.py (1)
from_mcp_template(397-414)
src/fastmcp/server/server.py (4)
src/fastmcp/prompts/prompt.py (1)
Prompt(64-155)src/fastmcp/resources/resource.py (1)
Resource(33-156)src/fastmcp/resources/template.py (1)
ResourceTemplate(92-228)src/fastmcp/tools/tool.py (1)
Tool(123-272)
src/fastmcp/server/context.py (3)
src/fastmcp/tools/tool.py (6)
Tool(123-272)from_function(196-226)from_function(305-377)from_function(446-574)run(228-238)run(379-419)src/fastmcp/server/sampling/sampling_tool.py (5)
SamplingTool(18-141)from_mcp_tool(82-105)from_function(108-141)_to_sdk_tool(69-79)run(52-67)src/fastmcp/utilities/json_schema.py (1)
compress_schema(200-230)
src/fastmcp/experimental/sampling/handlers/openai.py (2)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/server.py (4)
name(370-371)tool(1899-1914)tool(1917-1932)tool(1934-2077)
🪛 Ruff (0.14.8)
src/fastmcp/server/context.py
684-686: Avoid specifying long messages outside the exception class
(TRY003)
740-743: Avoid specifying long messages outside the exception class
(TRY003)
839-842: Avoid specifying long messages outside the exception class
(TRY003)
851-854: Avoid specifying long messages outside the exception class
(TRY003)
906-908: Avoid specifying long messages outside the exception class
(TRY003)
1046-1046: Undefined name get_cached_typeadapter
(F821)
1057-1057: Do not catch blind exception: Exception
(BLE001)
1100-1100: Do not catch blind exception: Exception
(BLE001)
1319-1319: Undefined name get_cached_typeadapter
(F821)
src/fastmcp/experimental/sampling/handlers/openai.py
274-274: Avoid specifying long messages outside the exception class
(TRY003)
349-349: Avoid specifying long messages outside the exception class
(TRY003)
392-392: Avoid specifying long messages outside the exception class
(TRY003)
🔇 Additional comments (4)
src/fastmcp/resources/resource.py (1)
11-12: SDKResource alias andto_mcp_resourceconversion look consistent.The alias swap to
SDKResourceand the updated return annotation/docstring are self‑consistent and match howResource.to_mcp_resource()is used by the server and context.Also applies to: 124-143
src/fastmcp/resources/template.py (1)
12-13: SDKResourceTemplate alias and template conversions are correct and aligned.The switch to
SDKResourceTemplatein bothto_mcp_template()andfrom_mcp_template()cleanly matches the updated server/context APIs without altering behavior.Also applies to: 186-218
src/fastmcp/server/server.py (1)
39-52: SDK alias adoption in server list_ handlers is consistent across tools/resources/templates/prompts.**The new
SDKPrompt/SDKResource/SDKResourceTemplate/SDKToolaliases and updated_list_*_mcpreturn types line up with the corresponding converters (to_mcp_tool/resource/template/prompt) and withContext.list_resources/list_prompts, keeping all four MCP object types in sync.Also applies to: 655-661, 1156-1171, 1240-1255, 1329-1344, 1423-1438
src/fastmcp/server/context.py (1)
68-73: NewSamplingResultcontainer and SDK list APIs are well-structured.*
SamplingResult[ResultT]cleanly exposes.text,.result, and.history, andContext.list_resources()/list_prompts()now returninglist[SDKResource]/list[SDKPrompt]correctly reflect the server’s updated_list_*_mcpimplementations.Also applies to: 78-97, 286-300
Replace the mutable SampleRun/sample_iter() pattern with a simpler stateless sample_step() function. sample_step() makes a single LLM call and returns a SampleStep with the response and history. sample() now loops sample_step() internally. Key changes: - Add sample_step() for fine-grained control over the sampling loop - Remove SampleRun class and sample_iter() method - Structured output uses tool description only (no prompt modification) - execute_tools parameter controls automatic vs manual tool execution
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/fastmcp/server/context.py (1)
1059-1074: Type narrowing issue withhasattrchecks.As noted in a previous review, using
hasattr(block, "text")doesn't provide sufficient type narrowing for the type checker. The# type: ignore[return-value]comments mask the issue.Consider using
isinstancewithTextContent:def _extract_text_from_content( content: SamplingMessageContentBlock | list[SamplingMessageContentBlock], ) -> str | None: if isinstance(content, list): for block in content: - if hasattr(block, "text"): - return block.text # type: ignore[return-value] + if isinstance(block, TextContent): + return block.text return None - elif hasattr(content, "text"): - return content.text # type: ignore[return-value] + elif isinstance(content, TextContent): + return content.text return Nonesrc/fastmcp/experimental/sampling/handlers/openai.py (1)
326-339: Handle specific tool selection whennameis provided.This method ignores the
namefield ofToolChoice. When a specific tool must be forced (e.g.,ToolChoice(mode="required", name="my_tool")), OpenAI expects the object format{"type": "function", "function": {"name": "..."}}. The silent fallback to"auto"on line 339 also masks configuration errors.@staticmethod def _convert_tool_choice_to_openai( tool_choice: ToolChoice, ) -> ChatCompletionToolChoiceOptionParam: """Convert MCP tool_choice to OpenAI format.""" if tool_choice.mode == "auto": return "auto" elif tool_choice.mode == "required": + # If a specific tool name is provided, force that tool + if tool_choice.name: + return { + "type": "function", + "function": {"name": tool_choice.name}, + } return "required" elif tool_choice.mode == "none": return "none" else: - # Unknown mode, default to auto - return "auto" + raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode}")
🧹 Nitpick comments (4)
examples/advanced_sampling/tool_use.py (1)
45-48: Consider adding field constraints to the Pydantic model.The
confidencefield lacks constraints. For clarity and validation, consider adding bounds:class ResearchReport(BaseModel): summary: str sources_used: list[str] - confidence: float + confidence: float = Field(ge=0.0, le=1.0, description="Confidence score between 0 and 1")This would require importing
Fieldfrom pydantic.docs/servers/sampling.mdx (1)
156-156: Inconsistent import location for SamplingMessage.Line 156 imports
SamplingMessagefromfastmcp.client.sampling, but line 296 imports frommcp.types. For consistency and to use the canonical source, consider usingmcp.typesthroughout:-from fastmcp.client.sampling import SamplingMessage +from mcp.types import SamplingMessagesrc/fastmcp/server/context.py (1)
752-753: Hardcoded max_iterations limit.The
max_iterations = 50is hardcoded inside the method. Consider exposing this as a parameter for users who need more or fewer iterations:async def sample( self, messages: str | Sequence[str | SamplingMessage], *, system_prompt: str | None = None, ... + max_iterations: int = 50, ) -> SamplingResult[ResultT] | SamplingResult[str]:This would allow advanced users to tune the iteration limit without modifying the source.
src/fastmcp/server/sampling/run.py (1)
280-288: Consider catching a more specific exception type.The bare
except Exceptionis overly broad and may mask unexpected errors. If the intent is to catch tool execution failures, consider defining or using a more specific exception type, or at minimum logging the exception details for debugging.- except Exception as e: + except (TypeError, ValueError, RuntimeError) as e: + # Catch common tool execution errors; adjust based on actual tool behavior tool_results.append( ToolResultContent( type="tool_result", toolUseId=tool_use.id, content=[TextContent(type="text", text=f"Error: {e}")], isError=True, ) )Alternatively, if catching all exceptions is intentional for resilience, add a comment explaining the rationale.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
pyproject.tomlis excluded by none and included by nonetests/client/test_sampling.pyis excluded by none and included by nonetests/server/proxy/test_proxy_client.pyis excluded by none and included by noneuv.lockis excluded by!**/*.lockand included by none
📒 Files selected for processing (7)
docs/servers/sampling.mdx(1 hunks)examples/advanced_sampling/tool_use.py(1 hunks)src/fastmcp/client/client.py(3 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(5 hunks)src/fastmcp/server/context.py(7 hunks)src/fastmcp/server/sampling/__init__.py(1 hunks)src/fastmcp/server/sampling/run.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/client/client.pysrc/fastmcp/server/context.pyexamples/advanced_sampling/tool_use.pysrc/fastmcp/experimental/sampling/handlers/openai.pysrc/fastmcp/server/sampling/run.pysrc/fastmcp/server/sampling/__init__.py
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/sampling.mdx
🧬 Code graph analysis (4)
src/fastmcp/client/client.py (1)
src/fastmcp/client/sampling.py (1)
create_sampling_callback(31-56)
examples/advanced_sampling/tool_use.py (1)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
OpenAISamplingHandler(46-401)
src/fastmcp/experimental/sampling/handlers/openai.py (3)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/sampling/run.py (2)
tool_calls(86-93)text(73-83)src/fastmcp/server/server.py (4)
name(370-371)tool(1899-1914)tool(1917-1932)tool(1934-2077)
src/fastmcp/server/sampling/__init__.py (2)
src/fastmcp/server/sampling/sampling_tool.py (2)
run(45-60)SamplingTool(15-108)src/fastmcp/server/sampling/run.py (6)
SampleStep(51-93)SamplingResult(36-47)call_client(215-236)call_sampling_handler(171-212)determine_handler_mode(119-168)execute_tools(239-290)
🪛 Ruff (0.14.8)
src/fastmcp/server/context.py
802-802: Do not catch blind exception: Exception
(BLE001)
840-840: Avoid specifying long messages outside the exception class
(TRY003)
1110-1110: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/experimental/sampling/handlers/openai.py
272-272: Avoid specifying long messages outside the exception class
(TRY003)
347-347: Avoid specifying long messages outside the exception class
(TRY003)
394-394: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/sampling/run.py
108-108: Avoid specifying long messages outside the exception class
(TRY003)
111-113: Avoid specifying long messages outside the exception class
(TRY003)
147-149: Avoid specifying long messages outside the exception class
(TRY003)
156-159: Avoid specifying long messages outside the exception class
(TRY003)
160-160: Avoid specifying long messages outside the exception class
(TRY003)
163-166: Avoid specifying long messages outside the exception class
(TRY003)
280-280: Do not catch blind exception: Exception
(BLE001)
⏰ 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). (4)
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
- GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (28)
examples/advanced_sampling/tool_use.py (1)
55-65: LGTM!The tool correctly demonstrates sampling with tools and structured output. Passing plain functions to
tools=is the intended API, and FastMCP auto-converts them toSamplingToolobjects internally.src/fastmcp/client/client.py (2)
306-316: LGTM! Sensible default for tool-enabled sampling.The logic correctly defaults to tools-enabled capabilities when a sampling handler is provided but no explicit capabilities are specified. This ensures backward compatibility while enabling the new tool-based sampling features.
354-367: LGTM!The
set_sampling_callbackmethod correctly mirrors the defaulting behavior from__init__, ensuring consistent capability handling regardless of how the sampling callback is configured.docs/servers/sampling.mdx (4)
1-16: LGTM!The frontmatter and introduction are well-structured, clearly explaining the sampling feature's purpose and routing behavior.
17-92: LGTM!The method reference section provides clear documentation of both
ctx.sample()andctx.sample_step()with well-structured parameter and response descriptions.
212-248: LGTM!The "Sampling with Tools" section clearly explains the agentic workflow concept, tool definition patterns, and how to customize tools with
SamplingTool.from_function(). The examples are practical and demonstrate proper error handling behavior.
402-440: LGTM!The fallback handlers section clearly explains the OpenAI handler integration and the behavior modes (
fallbackvsalways). The note about client capability requirements is helpful for users integrating with external clients.src/fastmcp/server/context.py (5)
71-74: LGTM!The new type aliases
ResultTandToolChoiceOptionare well-defined and provide clear typing for the sampling API.
544-678: LGTM!The
sample_stepmethod is well-structured with clear separation of concerns: message preparation, tool conversion, handler selection, LLM call, and optional tool execution. The stateless design with explicit history management enables flexible loop control.
800-824: Validation retry appends to mutable history.When validation fails, the code appends to
step.history(line 804). Sincestep.historyis a list returned fromsample_step, mutating it here is safe for continuation. However, be aware this modifies the SampleStep's history in place.
1034-1056: LGTM!The
_create_final_response_toolfunction elegantly creates a synthetic tool for structured output validation. The schema compression and clear description help the LLM understand when to use this tool.
1096-1112: LGTM!The
_prepare_toolsfunction correctly handles bothSamplingToolinstances and plain callables, with a clear error message for invalid types.src/fastmcp/server/sampling/__init__.py (1)
1-23: LGTM!The module init file provides a clean public API surface for the sampling package. All necessary symbols are re-exported, and
__all__correctly declares the public interface.src/fastmcp/server/sampling/run.py (7)
1-32: LGTM!Imports are well-organized with proper TYPE_CHECKING guard for Context. The TypeVar with default is appropriate for the generic result type.
35-48: LGTM!The
SamplingResultdataclass is well-structured with clear documentation. The genericResultTprovides flexibility for both text and structured output scenarios.
50-93: LGTM!The
SampleStepclass with its helper properties (is_tool_use,is_text,text,tool_calls) provides a clean interface for inspecting sampling responses. The defensive handling of both list and single-item content is thorough.
96-113: LGTM!Clean input normalization with proper type handling for the three accepted formats. The validation for list elements is appropriate.
119-168: LGTM!The capability checking logic is thorough, properly handling the three behavior modes (
always,fallback, default) with clear error messages for misconfiguration.
171-212: LGTM!Good use of
inspect.isawaitableto support both sync and async handlers. The string-to-CreateMessageResult conversion provides a convenient API for simple handlers.
215-236: LGTM!Clean delegation to the session's
create_messagemethod with proper parameter passthrough.src/fastmcp/experimental/sampling/handlers/openai.py (8)
1-39: LGTM!The new imports for tool-related types from both
mcp.typesandopenai.types.chatare well-organized, with proper handling of the optionalopenaidependency.
51-91: LGTM!The
__call__method cleanly integrates tool support, conditionally converting tools and tool_choice when provided, and selecting the appropriate result type based on whether tools were used.
218-235: LGTM!Single
ToolUseContenthandling correctly wraps it in an assistant message with tool_calls array.
237-252: LGTM!
ToolResultContentis correctly converted to a tool message with the content extracted and joined.
254-272: LGTM!The
TextContenthandling properly respects the message role, and the finalraise ValueErrorensures exhaustive handling of content types.
304-324: LGTM!Clean conversion from MCP Tool format to OpenAI's ChatCompletionToolParam, with sensible defaults for missing type field.
341-401: LGTM!The conversion from OpenAI's ChatCompletion to
CreateMessageResultWithToolsis thorough:
- Proper mapping of finish_reason to MCP StopReason
- Defensive parsing of tool call arguments with fallback to empty dict
- Validation that content is not empty
151-216: No ordering issue exists —ToolUseContentandToolResultContentappear in separate messages in the history, never in the same content list. The assistant message with tool calls (if present) is always added before the user message with tool results, so the current code correctly handles them.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
src/fastmcp/server/sampling/run.py (2)
45-48: Consider makingSamplingResult.textnon-optional (or enforce the invariant).
SamplingResultrepresents a completed sampling run; havingtext: str | Noneinvites downstreamNonehandling despite “run to completion” semantics. IfNoneis truly possible, document when/why and consider a helper likerequire_text()that raises a clear error.
72-83:SampleStep.textshould avoidhasattr/type: ignoreand handle multi-block text predictably.Right now you return the first block with
.text, which can silently drop content and is structurally brittle. Prefer explicitisinstance(block, TextContent)and either concatenate text blocks or clearly define “first text block only”.docs/servers/sampling.mdx (1)
410-426: OpenAI handler section: add minimal prerequisites / failure mode guidance.Given the doc guidelines (“prerequisites” + “troubleshooting”), consider adding:
- required dependency install note for
openai- what happens if
OPENAI_API_KEYis unset / invalid (expected error)- whether tools require any extra OpenAI settings
Keeps the example closer to “runnable and verifiable.”
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
docs/servers/sampling.mdx(1 hunks)examples/advanced_sampling/tool_use.py(1 hunks)src/fastmcp/server/sampling/run.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- examples/advanced_sampling/tool_use.py
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/sampling.mdx
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/server/sampling/run.py
🧬 Code graph analysis (1)
src/fastmcp/server/sampling/run.py (2)
src/fastmcp/server/context.py (6)
fastmcp(168-173)Context(116-962)_parse_model_preferences(965-1003)session(411-421)request_context(211-237)request_id(346-356)src/fastmcp/server/sampling/sampling_tool.py (1)
SamplingTool(15-108)
🪛 Ruff (0.14.8)
src/fastmcp/server/sampling/run.py
108-108: Avoid specifying long messages outside the exception class
(TRY003)
111-113: Avoid specifying long messages outside the exception class
(TRY003)
147-149: Avoid specifying long messages outside the exception class
(TRY003)
156-159: Avoid specifying long messages outside the exception class
(TRY003)
160-160: Avoid specifying long messages outside the exception class
(TRY003)
163-166: Avoid specifying long messages outside the exception class
(TRY003)
280-280: Do not catch blind exception: Exception
(BLE001)
⏰ 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). (4)
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
| while True: | ||
| step = await ctx.sample_step( | ||
| messages=messages, | ||
| tools=[search, get_time], | ||
| ) | ||
|
|
||
| await server.run_http_async() | ||
| if step.is_tool_use: | ||
| # Tools already executed (execute_tools=True by default) | ||
| # Log what was called before continuing | ||
| for call in step.tool_calls: | ||
| print(f"Called tool: {call.name}") | ||
|
|
||
| if step.is_text: | ||
| return step.text | ||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(async_main()) | ||
| # Continue with updated history | ||
| messages = step.history | ||
| ``` |
There was a problem hiding this comment.
Add a termination guard to the sample_step() loop example.
while True + tool-use can spin indefinitely in user code. Even a small max_steps counter (or time budget) makes the example safer and more copy/paste friendly.
🤖 Prompt for AI Agents
In docs/servers/sampling.mdx around lines 303 to 320 the example uses an
infinite while True loop calling ctx.sample_step which can spin indefinitely
when tools are used; add a simple termination guard by introducing a max_steps
counter (or elapsed time budget) initialized before the loop, increment it
inside each iteration, and break/raise/return with a clear error or fallback
result when the counter exceeds the limit so the example is safe to copy/paste
and won’t run forever.
| if step.is_tool_use: | ||
| tool_results = [] | ||
| for call in step.tool_calls: | ||
| # Look up and execute the requested tool | ||
| fn = tools[call.name] | ||
| result = fn(**call.input) | ||
| tool_results.append( | ||
| ToolResultContent( | ||
| type="tool_result", | ||
| toolUseId=call.id, | ||
| content=[TextContent(type="text", text=result)], | ||
| ) | ||
| ) | ||
|
|
||
| messages = list(step.history) | ||
| messages.append(SamplingMessage(role="user", content=tool_results)) | ||
| ``` |
There was a problem hiding this comment.
Manual tool execution example should handle unknown tools and show isError=True in-line.
As written, fn = tools[call.name] can raise KeyError if the model requests a non-existent tool. Since the page already documents isError=True, it’d be better to demonstrate it here (unknown tool → tool_result error) rather than assuming perfect tool names.
🤖 Prompt for AI Agents
In docs/servers/sampling.mdx around lines 371–387 the code does a direct lookup
fn = tools[call.name] which can raise KeyError for unknown tools and currently
assumes successful execution; wrap the lookup and call in a try/except so that
unknown tool names and runtime exceptions are caught and converted into a
ToolResultContent entry with isError=True (include the error message and
toolUseId), and only append a successful ToolResultContent when the call
succeeds; ensure the flow continues to the next call after recording the error.
| for tool_use in tool_calls: | ||
| tool = tool_map.get(tool_use.name) | ||
| if tool is None: | ||
| tool_results.append( | ||
| ToolResultContent( | ||
| type="tool_result", | ||
| toolUseId=tool_use.id, | ||
| content=[ | ||
| TextContent( | ||
| type="text", | ||
| text=f"Error: Unknown tool '{tool_use.name}'", | ||
| ) | ||
| ], | ||
| isError=True, | ||
| ) | ||
| ) | ||
| else: | ||
| try: | ||
| result_value = await tool.run(tool_use.input) | ||
| tool_results.append( | ||
| ToolResultContent( | ||
| type="tool_result", | ||
| toolUseId=tool_use.id, | ||
| content=[TextContent(type="text", text=str(result_value))], | ||
| ) | ||
| ) | ||
| except Exception as e: | ||
| # Catch all exceptions to report errors to the LLM for recovery | ||
| tool_results.append( | ||
| ToolResultContent( | ||
| type="tool_result", | ||
| toolUseId=tool_use.id, | ||
| content=[TextContent(type="text", text=f"Error: {e}")], | ||
| isError=True, | ||
| ) | ||
| ) | ||
|
|
There was a problem hiding this comment.
Tool execution error reporting may leak secrets; also validate tool inputs before calling.
- Returning
f"Error: {e}"to the LLM can expose sensitive details (paths, tokens in exception messages, upstream HTTP errors). Prefer a generic error for the LLM and log the detailed exception server-side. tool_use.inputshould be validated as adict[str, Any] | Nonebefore passing toSamplingTool.run(...), otherwise a malformed payload can explode inside the tool.
Also, if you keep broad catching for agent resilience, at least scope it to “tool execution” and sanitize output.
async def execute_tools(
@@
for tool_use in tool_calls:
@@
else:
try:
- result_value = await tool.run(tool_use.input)
+ tool_args = tool_use.input
+ if tool_args is not None and not isinstance(tool_args, dict):
+ raise TypeError("Tool input must be an object/dict.")
+
+ result_value = await tool.run(tool_args)
tool_results.append(
ToolResultContent(
@@
)
)
- except Exception as e:
- # Catch all exceptions to report errors to the LLM for recovery
+ except (TypeError, ValueError) as e:
tool_results.append(
ToolResultContent(
@@
- content=[TextContent(type="text", text=f"Error: {e}")],
+ content=[TextContent(type="text", text=f"Error: {e}")],
isError=True,
)
)
+ except Exception:
+ # Avoid leaking sensitive exception details into the model context.
+ tool_results.append(
+ ToolResultContent(
+ type="tool_result",
+ toolUseId=tool_use.id,
+ content=[
+ TextContent(
+ type="text",
+ text="Error: tool execution failed.",
+ )
+ ],
+ isError=True,
+ )
+ )🧰 Tools
🪛 Ruff (0.14.8)
280-280: Do not catch blind exception: Exception
(BLE001)
Test Failure AnalysisSummary: The test in is timing out after 5 seconds due to DNS resolution failures when attempting to connect to . Root Cause: The test creates an with (a non-existent domain) and then actually calls tools via , which triggers real HTTP requests. The DNS lookup for hangs and eventually times out with This test expects to fail at the HTTP level with connection errors, but the DNS resolution is taking too long and triggering the pytest timeout first. Suggested Solution: The test needs to mock the HTTP transport to avoid actual network calls. Here are the options:
import respx
async def test_optional_parameter_handling(self, simple_spec):
with respx.mock:
respx.get("https://api.example.com/users/123").mock(
return_value=httpx.Response(500, json={"error": "mock error"})
)
async with httpx.AsyncClient(base_url="https://api.example.com") as client:
# ... rest of test
async with httpx.AsyncClient(
base_url="https://api.example.com",
timeout=0.1 # Fail fast
) as client:
# ... rest of test
@pytest.mark.integration
async def test_optional_parameter_handling(self, simple_spec):
# ... test codeRecommended approach: Option 1 (mocking) is best because it tests the parameter handling logic without depending on network behavior, making tests faster and more reliable. Detailed AnalysisError Log ExcerptCode Flow
Why This Wasn't Caught BeforeThe test file wasn't modified in this PR, but the test may have been running faster before due to changes in how the client processes requests. The DNS timeout behavior is non-deterministic and depends on network conditions. Related Files
|
Test Failure AnalysisSummary: Test Root Cause: The test creates a The timeout occurs at:
Suggested Solution: Option 1: Mock the DiskStore in tests (Recommended) Provide a mock in-memory client_storage for the WorkOS provider tests to avoid filesystem/SQLite operations entirely: from key_value.aio.stores.memory import MemoryStore
def test_init_defaults(self):
provider = WorkOSProvider(
client_id="test_client",
client_secret="test_secret",
authkit_domain="https://test.authkit.app",
jwt_signing_key="test-secret",
client_storage=MemoryStore(), # Use in-memory storage for tests
)
# ... rest of testOption 2: Increase timeout for Windows tests Add a platform-specific timeout marker: @pytest.mark.timeout(15 if sys.platform == 'win32' else 5)
def test_init_defaults(self):
# ... test codeHowever, this doesn't solve the underlying issue and tests will still be slow. Option 3: Skip on Windows @pytest.mark.skipif(sys.platform == 'win32', reason="SQLite timeout issues on Windows CI")
def test_init_defaults(self):
# ... test codeBut this reduces test coverage. Recommendation: Use Option 1 (mock with MemoryStore) as it makes tests faster, more reliable, and platform-independent while still testing the provider initialization logic. Detailed AnalysisStack TraceWhy This Happens on Windows
Test Passed on Other Platforms
Related Files
🤖 Updated analysis for workflow run 20211970342 (2025-12-14 18:05 UTC) |
- Fix temperature=0.0 being dropped due to falsy evaluation - Add ToolChoice.name support for forcing specific tools - Replace assert statements with explicit RuntimeError checks - Add mask_error_details parameter to sample()/sample_step() with ToolError escape hatch - Fix hasattr patterns with proper isinstance checks - Document mask_error_details and add OpenAI prerequisites to docs
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (5)
src/fastmcp/server/sampling/run.py (1)
149-172: Consider explicit validation forsampling_handler_behavior.The function handles
"always"and"fallback"explicitly, but other values (including typos like"fallbck") silently fall through. WhileNone(unconfigured) correctly falls through, an explicit invalid string should fail fast:elif needs_tools and not has_tools_capability: raise ValueError( "Client does not support sampling with tools. " "The client must advertise the sampling.tools capability." ) + elif fastmcp.sampling_handler_behavior is not None: + raise ValueError( + f"Invalid sampling_handler_behavior: {fastmcp.sampling_handler_behavior!r}. " + "Must be 'always', 'fallback', or None." + ) return Falsesrc/fastmcp/experimental/sampling/handlers/openai.py (1)
345-346: Silent fallback to"auto"for unknown modes masks configuration errors.The
elsebranch (line 346) silently returns"auto"for any unrecognizedtool_choice.modevalue. This could hide bugs where an invalid mode is passed.elif tool_choice.mode == "none": return "none" - else: - return "auto" + else: + raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode!r}")docs/servers/sampling.mdx (3)
48-50: Documentation understatesresult_typecapabilities.The parameter is described as "A Pydantic model" but the implementation (via
get_cached_typeadapter) also supports dataclasses and basic types. Consider updating to reflect the full capability:- <ResponseField name="result_type" type="type[T] | None" default="None"> - A Pydantic model for validated structured output. + <ResponseField name="result_type" type="type[T] | None" default="None"> + A type for validated structured output. Supports Pydantic models, dataclasses, and basic types. </ResponseField>
339-356: Add iteration limit tosample_step()loop example for safety.The
while Trueloop lacks a termination guard. Unlikesample()which has internalmax_iterations=50,sample_step()is stateless and relies on user code for loop control. Adding a counter makes the example safer to copy:messages = [SamplingMessage(role="user", content=question)] + max_steps = 20 - while True: + for _ in range(max_steps): step = await ctx.sample_step( messages=messages, tools=[search, get_time], ) ... if step.is_text: return step.text messages = step.history + + raise RuntimeError(f"Agent exceeded {max_steps} steps")
407-422: Manual tool execution example should handle unknown tools.The example does a direct dict lookup (
fn = tools[call.name]) which raisesKeyErrorfor unknown tools. Since the page documentsisError=True, demonstrate it here:if step.is_tool_use: tool_results = [] for call in step.tool_calls: - # Look up and execute the requested tool - fn = tools[call.name] - result = fn(**call.input) - tool_results.append( - ToolResultContent( - type="tool_result", - toolUseId=call.id, - content=[TextContent(type="text", text=result)], + fn = tools.get(call.name) + if fn is None: + tool_results.append( + ToolResultContent( + type="tool_result", + toolUseId=call.id, + content=[TextContent(type="text", text=f"Unknown tool: {call.name}")], + isError=True, + ) ) - ) + else: + result = fn(**call.input) + tool_results.append( + ToolResultContent( + type="tool_result", + toolUseId=call.id, + content=[TextContent(type="text", text=result)], + ) + )
🧹 Nitpick comments (3)
src/fastmcp/server/context.py (1)
821-843: Consider catching a more specific exception for validation errors.The bare
Exceptioncatch at line 821 is flagged by static analysis (BLE001). While Pydantic'sValidationErroris the primary expected exception, catching all exceptions here could mask unexpected bugs.+ from pydantic import ValidationError ... - except Exception as e: + except ValidationError as e: # Validation failed - add error as tool resultIf you need to handle other exceptions (e.g.,
TypeErrorfrom malformed input), consider catching them explicitly.src/fastmcp/experimental/sampling/handlers/openai.py (2)
153-218: Complex list content handling looks correct but could benefit from comments.The logic for handling mixed content (ToolUseContent, TextContent, ToolResultContent) is intricate. Consider adding a brief comment explaining the flow—especially that tool results are emitted immediately while tool_calls/text are accumulated for a single assistant message.
385-388: Consider logging a warning when tool arguments fail to parse.Defaulting to an empty dict on
JSONDecodeErroris a reasonable fallback, but silently swallowing parse errors could hide issues with malformed LLM responses.try: arguments = json.loads(func.arguments) # type: ignore[union-attr] except json.JSONDecodeError: + logger.warning( + f"Failed to parse tool arguments for {func.name}: {func.arguments!r}" + ) arguments = {}This requires adding
logger = get_logger(__name__)at module level.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
docs/servers/sampling.mdx(1 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(5 hunks)src/fastmcp/server/context.py(6 hunks)src/fastmcp/server/sampling/run.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/servers/sampling.mdx
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/experimental/sampling/handlers/openai.pysrc/fastmcp/server/context.pysrc/fastmcp/server/sampling/run.py
🧬 Code graph analysis (2)
src/fastmcp/experimental/sampling/handlers/openai.py (2)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/sampling/run.py (2)
tool_calls(90-97)text(77-87)
src/fastmcp/server/sampling/run.py (5)
src/fastmcp/server/context.py (5)
fastmcp(169-174)Context(117-981)session(412-422)request_context(212-238)request_id(347-357)src/fastmcp/exceptions.py (1)
ToolError(18-19)src/fastmcp/server/sampling/sampling_tool.py (1)
SamplingTool(15-108)src/fastmcp/utilities/logging.py (1)
get_logger(14-26)src/fastmcp/server/server.py (1)
name(370-371)
🪛 Ruff (0.14.8)
src/fastmcp/experimental/sampling/handlers/openai.py
274-274: Avoid specifying long messages outside the exception class
(TRY003)
354-354: Avoid specifying long messages outside the exception class
(TRY003)
401-401: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/context.py
821-821: Do not catch blind exception: Exception
(BLE001)
859-859: Avoid specifying long messages outside the exception class
(TRY003)
1129-1129: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/sampling/run.py
112-112: Avoid specifying long messages outside the exception class
(TRY003)
115-117: Avoid specifying long messages outside the exception class
(TRY003)
151-153: Avoid specifying long messages outside the exception class
(TRY003)
160-163: Avoid specifying long messages outside the exception class
(TRY003)
164-164: Avoid specifying long messages outside the exception class
(TRY003)
167-170: Avoid specifying long messages outside the exception class
(TRY003)
193-193: Avoid specifying long messages outside the exception class
(TRY003)
195-195: Avoid specifying long messages outside the exception class
(TRY003)
⏰ 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). (4)
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (9)
src/fastmcp/server/context.py (3)
1053-1075: LGTM -_create_final_response_toolcorrectly generates structured output tool.The implementation properly derives the JSON schema from the result type, compresses it, and creates a tool that returns kwargs as-is for validation in the calling loop.
1078-1093: LGTM - Previous type narrowing issue addressed.The
_extract_text_from_contentfunction now correctly usesisinstance(block, TextContent)checks instead ofhasattr, resolving the type narrowing issues flagged in past reviews.
1096-1131: LGTM - Message and tool preparation helpers are well-implemented.Both
_prepare_messagesand_prepare_toolscorrectly handle their respective input types and conversions. The TypeError in_prepare_tools(line 1129) provides a clear error message for invalid input.src/fastmcp/experimental/sampling/handlers/openai.py (1)
68-93: LGTM - Tool integration in__call__is well-structured.The method correctly:
- Converts MCP tools to OpenAI format only when tools are provided
- Passes tool_choice through the conversion
- Returns the appropriate result type based on whether tools were used
docs/servers/sampling.mdx (2)
101-157: LGTM - Basic sampling documentation is clear and complete.The examples demonstrate core functionality well: simple prompts, system prompts with temperature/max_tokens, and model preferences with clear explanations of when to use each feature.
438-486: LGTM - Fallback handlers documentation is comprehensive.Good coverage of the OpenAI handler setup, installation requirements, and behavior modes. The note about
sampling.toolscapability requirement is helpful.src/fastmcp/server/sampling/run.py (3)
192-195: LGTM - Runtime checks replace assertions.The explicit
RuntimeErrorchecks properly replace the previously flaggedassertstatements, ensuring errors are raised even with-Ooptimization.
250-322: LGTM - Tool execution with error masking is well-implemented.The function properly:
- Reports unknown tools as errors (lines 272-285)
- Uses
ToolErroras an escape hatch to always pass specific messages (lines 296-306)- Masks generic exception details when
mask_error_details=True(lines 307-321)- Logs full exceptions server-side regardless of masking
This addresses the previous concern about leaking sensitive details to the LLM.
39-97: LGTM -SamplingResultandSampleStepdataclasses are well-designed.The dataclasses provide clean abstractions:
SamplingResultbundles text, typed result, and historySampleStepoffers convenient properties (is_tool_use,is_text,text,tool_calls) for introspecting LLM responsesThe
textproperty correctly usesisinstancechecks for type narrowing.
- Catch ValidationError specifically instead of bare Exception - Update result_type docs to mention dataclasses and basic types - Raise ValueError for unknown tool_choice modes - Validate sampling_handler_behavior to catch typos - Remove ToolChoice.name handling (not part of MCP spec) - Validate tool_choice string in sample_step()
- Remove internal functions from sampling __init__.py exports - Remove fragile is_text property, use not is_tool_use instead - Inline call_client into context.py, remove from run.py - Fix SamplingMessage docs to use TextContent - Handle result.text being None in doc examples - Simplify client sampling docs to recommend OpenAISamplingHandler - Add sampling_capabilities override documentation - Raise iteration limit from 50 to 100 - Remove _parse_model_preferences duplication - Use AsyncOpenAI in OpenAISamplingHandler - Fix tool_choice docstring
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/clients/sampling.mdx (1)
18-39: Example return type is likely misleading now that tool-aware sampling exists.The handler is typed as
-> str, but tool-enabled flows require returning a tool-aware response model in some cases. Consider either:
- explicitly documenting “return
str(simple handlers) ORCreateMessageResult/CreateMessageResultWithTools(advanced)”, or- updating the signature in the snippet to the union return type.
♻️ Duplicate comments (5)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
345-357:ToolChoice.nameis still ignored, so you can’t force a specific tool on OpenAI.This still can’t express “required + specific function name” (OpenAI’s object form). That’s particularly useful for
final_response-style flows.src/fastmcp/server/sampling/run.py (1)
226-298: Validatetool_use.inputbefore calling tools; otherwise malformed payloads can explode inside tool code.Add a runtime guard (
Noneordict[str, Any]) beforeawait tool.run(...), and convert violations into aToolResultContent(isError=True).docs/servers/sampling.mdx (2)
345-362: Add a max-steps (or time budget) guard to thewhile Trueloop.Even with internal limits elsewhere, the example should be safe-by-default for copy/paste.
412-427: Manual tool execution example needs unknown-tool + exception handling and should demonstrateisError=True.Right now
tools[call.name]can raiseKeyError, and tool exceptions will crash the loop rather than producingToolResultContent(isError=True).src/fastmcp/server/context.py (1)
617-627: Disallowing non-modetool_choicestrings blocks forcing a specific tool name.If you intend to support “force tool X” (useful for structured output / OpenAI), consider accepting arbitrary strings as “required + name=...”, rather than raising.
🧹 Nitpick comments (1)
docs/clients/sampling.mdx (1)
186-208: OpenAI handler examples should be copy/paste runnable (async entrypoint + basic error handling).Per docs guidelines, consider wrapping in
async def main(): ...+asyncio.run(main()), and include a minimal “missing API key” error path so readers can verify setup quickly. As per coding guidelines.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
tests/client/test_sampling.pyis excluded by none and included by none
📒 Files selected for processing (6)
docs/clients/sampling.mdx(2 hunks)docs/servers/sampling.mdx(1 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(6 hunks)src/fastmcp/server/context.py(6 hunks)src/fastmcp/server/sampling/__init__.py(1 hunks)src/fastmcp/server/sampling/run.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/fastmcp/server/sampling/init.py
🧰 Additional context used
📓 Path-based instructions (2)
docs/**/*.mdx
📄 CodeRabbit inference engine (docs/.cursor/rules/mintlify.mdc)
docs/**/*.mdx: Use clear, direct language appropriate for technical audiences
Write in second person ('you') for instructions and procedures in MDX documentation
Use active voice over passive voice in MDX technical documentation
Employ present tense for current states and future tense for outcomes in MDX documentation
Maintain consistent terminology throughout all MDX documentation
Keep sentences concise while providing necessary context in MDX documentation
Use parallel structure in lists, headings, and procedures in MDX documentation
Lead with the most important information using inverted pyramid structure in MDX documentation
Use progressive disclosure in MDX documentation: present basic concepts before advanced ones
Break complex procedures into numbered steps in MDX documentation
Include prerequisites and context before instructions in MDX documentation
Provide expected outcomes for each major step in MDX documentation
End sections with next steps or related information in MDX documentation
Use descriptive, keyword-rich headings for navigation and SEO in MDX documentation
Focus on user goals and outcomes rather than system features in MDX documentation
Anticipate common questions and address them proactively in MDX documentation
Include troubleshooting for likely failure points in MDX documentation
Provide multiple pathways (beginner vs advanced) but offer an opinionated path to avoid overwhelming users in MDX documentation
Always include complete, runnable code examples that users can copy and execute in MDX documentation
Show proper error handling and edge case management in MDX code examples
Use realistic data instead of placeholder values in MDX code examples
Include expected outputs and results for verification in MDX code examples
Test all code examples thoroughly before publishing in MDX documentation
Specify language and include filename when relevant in MDX code examples
Add explanatory comments for complex logic in MDX code examples
Document all API...
Files:
docs/clients/sampling.mdxdocs/servers/sampling.mdx
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/experimental/sampling/handlers/openai.pysrc/fastmcp/server/sampling/run.pysrc/fastmcp/server/context.py
🧠 Learnings (1)
📚 Learning: 2025-11-26T21:52:08.947Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: docs/.cursor/rules/mintlify.mdc:0-0
Timestamp: 2025-11-26T21:52:08.947Z
Learning: Applies to docs/**/*.mdx : Always include realistic error handling in code examples in MDX documentation
Applied to files:
docs/servers/sampling.mdx
🧬 Code graph analysis (2)
src/fastmcp/experimental/sampling/handlers/openai.py (2)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/sampling/run.py (2)
tool_calls(85-92)text(72-82)
src/fastmcp/server/context.py (1)
src/fastmcp/server/sampling/run.py (8)
SampleStep(55-92)SamplingResult(40-51)call_sampling_handler(175-223)determine_handler_mode(118-172)execute_tools(226-299)is_tool_use(65-69)tool_calls(85-92)text(72-82)
🪛 Ruff (0.14.8)
src/fastmcp/experimental/sampling/handlers/openai.py
290-290: Avoid specifying long messages outside the exception class
(TRY003)
356-356: Avoid specifying long messages outside the exception class
(TRY003)
364-364: Avoid specifying long messages outside the exception class
(TRY003)
411-411: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/sampling/run.py
107-107: Avoid specifying long messages outside the exception class
(TRY003)
110-112: Avoid specifying long messages outside the exception class
(TRY003)
146-148: Avoid specifying long messages outside the exception class
(TRY003)
155-158: Avoid specifying long messages outside the exception class
(TRY003)
159-159: Avoid specifying long messages outside the exception class
(TRY003)
162-165: Avoid specifying long messages outside the exception class
(TRY003)
167-170: Avoid specifying long messages outside the exception class
(TRY003)
193-193: Avoid specifying long messages outside the exception class
(TRY003)
195-195: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/context.py
621-624: Avoid specifying long messages outside the exception class
(TRY003)
864-864: Avoid specifying long messages outside the exception class
(TRY003)
1093-1093: Avoid specifying long messages outside the exception class
(TRY003)
⏰ 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). (4)
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
| ## Sampling Capabilities | ||
|
|
||
| When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support (for simpler handlers that don't support tools), pass `sampling_capabilities` explicitly: | ||
|
|
||
| ```python | ||
| from mcp.types import SamplingCapability | ||
|
|
||
| client = Client( | ||
| "my_mcp_server.py", | ||
| sampling_handler=basic_handler, | ||
| sampling_capabilities=SamplingCapability(), # No tool support | ||
| ) | ||
| ``` |
There was a problem hiding this comment.
Clarify what “SamplingCapability()” disables (tools only vs sampling entirely).
The comment says “No tool support”, but SamplingCapability() reads like “no sampling features at all”. Consider showing SamplingCapability(tools=None) (or the exact “no-tools” shape) and stating what remains enabled.
🤖 Prompt for AI Agents
In docs/clients/sampling.mdx around lines 166 to 178, the example and text are
ambiguous about what SamplingCapability() disables; update the example to show
the explicit no-tools shape (e.g., SamplingCapability(tools=None) or the exact
parameter name used in the codebase) and change the surrounding copy to clearly
state that only tool support is being disabled while sampling itself (and other
capabilities) remains enabled; include the explicit shape and a short sentence
listing what still remains enabled.
| # Convert MCP tools to OpenAI format | ||
| openai_tools: list[ChatCompletionToolParam] | NotGiven = NOT_GIVEN | ||
| if params.tools: | ||
| openai_tools = self._convert_tools_to_openai(params.tools) | ||
|
|
||
| # Convert tool_choice to OpenAI format | ||
| openai_tool_choice: ChatCompletionToolChoiceOptionParam | NotGiven = NOT_GIVEN | ||
| if params.toolChoice: | ||
| openai_tool_choice = self._convert_tool_choice_to_openai(params.toolChoice) | ||
|
|
||
| response = await self.client.chat.completions.create( | ||
| model=model, | ||
| messages=openai_messages, | ||
| temperature=params.temperature or NOT_GIVEN, | ||
| temperature=( | ||
| params.temperature if params.temperature is not None else NOT_GIVEN | ||
| ), | ||
| max_tokens=params.maxTokens, | ||
| stop=params.stopSequences or NOT_GIVEN, | ||
| stop=params.stopSequences if params.stopSequences else NOT_GIVEN, | ||
| tools=openai_tools, | ||
| tool_choice=openai_tool_choice, | ||
| ) | ||
|
|
||
| # Return appropriate result type based on whether tools were provided | ||
| if params.tools: | ||
| return self._chat_completion_to_result_with_tools(response) | ||
| return self._chat_completion_to_create_message_result(response) | ||
|
|
There was a problem hiding this comment.
Return-type switch on params.tools is a breaking/footgun API shape.
If params.tools is an empty list vs None, the return type flips. It’s easy for callers to accidentally get CreateMessageResultWithTools sometimes and not others.
Suggestion: normalize with if params.tools is not None (or a dedicated “tool-aware” flag) and/or always return CreateMessageResultWithTools when the handler supports tools (with empty tool_calls).
🤖 Prompt for AI Agents
In src/fastmcp/experimental/sampling/handlers/openai.py around lines 84 to 110,
the handler currently switches return type based on the truthiness of
params.tools which causes an empty list vs None to flip the API result; change
the condition to check for presence (params.tools is not None) or always return
the tool-aware result when the handler supports tools: ensure openai_tools
conversion still runs when params.tools is not None, pass tools/tool_choice to
the client call as before, and choose the post-response conversion by using if
params.tools is not None then return the “with_tools” result (or always return
the “with_tools” shape with empty tool_calls) so callers get a stable return
type.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
src/fastmcp/experimental/sampling/handlers/openai.py (4)
72-97: Avoidparams.toolstruthiness checks (empty list vs None flips behavior/return type).Right now
[]behaves like “no tools”: you won’t send tools/tool_choice, and you returnCreateMessageResultinstead of the tool-aware result. Prefer a presence check (is not None) and keep result shape stable.- if params.tools: + if params.tools is not None: openai_tools = self._convert_tools_to_openai(params.tools) @@ - if params.tools: + if params.tools is not None: return self._chat_completion_to_result_with_tools(response)
157-222: Fix OpenAI chat ordering: tool result messages are emitted before the assistant tool_calls message.In the list-content path, you append
ChatCompletionToolMessageParam(...)while iterating (Line 188-194), but only append the assistant message withtool_callslater (Line 200-206). OpenAI expects the assistant tool_calls message first, followed by the tool messages replying to those calls.- for item in content: + tool_messages: list[ChatCompletionToolMessageParam] = [] + for item in content: @@ - elif isinstance(item, ToolResultContent): + elif isinstance(item, ToolResultContent): @@ - openai_messages.append( + tool_messages.append( ChatCompletionToolMessageParam( role="tool", tool_call_id=item.toolUseId, content=content_text, ) ) @@ if tool_calls: openai_messages.append( ChatCompletionAssistantMessageParam( role="assistant", content=msg_content, tool_calls=tool_calls, ) ) + openai_messages.extend(tool_messages)Also: if you ever see
ToolResultContentbut noToolUseContentin the same list, emitting tool messages is invalid; consider raising (or at least skipping with a loud error).
333-345: Support forcing a specific tool whenToolChoice.nameis set (OpenAI requires object form).
ToolChoice.nameis currently ignored. For OpenAI, selecting a specific tool is typically{"type":"function","function":{"name":"..."}}.def _convert_tool_choice_to_openai( tool_choice: ToolChoice, ) -> ChatCompletionToolChoiceOptionParam: """Convert MCP tool_choice to OpenAI format.""" if tool_choice.mode == "auto": return "auto" elif tool_choice.mode == "required": - return "required" + if getattr(tool_choice, "name", None): + name = tool_choice.name + if not name: + raise ValueError("tool_choice.name must be non-empty when provided") + return {"type": "function", "function": {"name": name}} + return "required" elif tool_choice.mode == "none": return "none" else: raise ValueError(f"Unsupported tool_choice mode: {tool_choice.mode!r}")
347-406: Don’t silently coerce invalid tool-arguments JSON to{}.This will execute tools with wrong args and hides model issues.
- except json.JSONDecodeError: - arguments = {} + except json.JSONDecodeError as e: + raise ValueError( + f"Invalid tool arguments JSON for tool '{func.name}': {func.arguments}" + ) from e
🧹 Nitpick comments (2)
src/fastmcp/client/client.py (2)
252-316: Prefer explicitis Nonedefaulting forsampling_capabilities(avoidoredge-cases, clearer intent).
sampling_capabilities or SamplingCapability(...)is less explicit than checkingis None. Also worth confirming this “default tools-enabled” behavior is what the MCP session negotiation expects when a callback exists.- self._session_kwargs["sampling_capabilities"] = ( - sampling_capabilities - or mcp.types.SamplingCapability( - tools=mcp.types.SamplingToolsCapability() - ) - ) + self._session_kwargs["sampling_capabilities"] = ( + sampling_capabilities + if sampling_capabilities is not None + else mcp.types.SamplingCapability(tools=mcp.types.SamplingToolsCapability()) + )
368-382: Keepset_sampling_callback()behavior consistent, but sameis Nonerefactor applies.Same readability/robustness point as in
__init__.- self._session_kwargs["sampling_capabilities"] = ( - sampling_capabilities - or mcp.types.SamplingCapability(tools=mcp.types.SamplingToolsCapability()) - ) + self._session_kwargs["sampling_capabilities"] = ( + sampling_capabilities + if sampling_capabilities is not None + else mcp.types.SamplingCapability(tools=mcp.types.SamplingToolsCapability()) + )
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
tests/experimental/sampling/test_openai_handler.pyis excluded by none and included by none
📒 Files selected for processing (2)
src/fastmcp/client/client.py(3 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/client/client.pysrc/fastmcp/experimental/sampling/handlers/openai.py
🧬 Code graph analysis (2)
src/fastmcp/client/client.py (1)
src/fastmcp/client/sampling.py (1)
create_sampling_callback(31-56)
src/fastmcp/experimental/sampling/handlers/openai.py (3)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/sampling/run.py (2)
tool_calls(85-92)text(72-82)src/fastmcp/server/server.py (4)
name(370-371)tool(1899-1914)tool(1917-1932)tool(1934-2077)
🪛 Ruff (0.14.8)
src/fastmcp/experimental/sampling/handlers/openai.py
278-278: Avoid specifying long messages outside the exception class
(TRY003)
344-344: Avoid specifying long messages outside the exception class
(TRY003)
352-352: Avoid specifying long messages outside the exception class
(TRY003)
399-399: Avoid specifying long messages outside the exception class
(TRY003)
⏰ 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). (1)
- GitHub Check: Run tests: Python 3.10 on windows-latest
- Fix message ordering in OpenAI handler: tool results now correctly
follow assistant message with tool_calls
- sample_step() now always includes assistant message in history
- Raise ValueError on JSON parse errors instead of silent {}
- Add has_sampling capability check when behavior is None
- Raise RuntimeError when structured output receives text response
- Wrap primitive result_type schemas in object wrapper
- Fix docs example using invalid SamplingMessage construction
- Add comprehensive client_sampling_test.py example
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/fastmcp/experimental/sampling/handlers/openai.py (1)
340-352: Silent fallback fixed, but specific tool selection still not supported.The previous concern about raising errors for invalid modes has been addressed (Line 352 now raises
ValueError).However, the
namefield ofToolChoiceis still ignored. When a specific tool must be forced (e.g.,ToolChoice(mode="required", name="final_response")), OpenAI expects the object format{"type": "function", "function": {"name": "..."}}rather than just the string"required".If specific tool forcing is needed for the structured output flow (forcing
final_responsetool), consider:@staticmethod def _convert_tool_choice_to_openai( tool_choice: ToolChoice, ) -> ChatCompletionToolChoiceOptionParam: """Convert MCP tool_choice to OpenAI format.""" + # Handle specific tool selection + if tool_choice.name: + return { + "type": "function", + "function": {"name": tool_choice.name}, + } if tool_choice.mode == "auto": return "auto"
🧹 Nitpick comments (3)
src/fastmcp/server/context.py (3)
687-693: Type annotation mismatch oncontentparameter.The
# type: ignore[arg-type]suggests a type incompatibility betweentool_results(list ofToolResultContent) and the expectedcontentparameter type. This is a known SDK limitation where the type union doesn't include all valid content types.Consider adding a brief comment explaining why this ignore is safe:
current_messages.append( SamplingMessage( role="user", - content=tool_results, # type: ignore[arg-type] + # type: ignore[arg-type] - SDK types don't include ToolResultContent in union + content=tool_results, ) )
775-776: Consider makingmax_iterationsconfigurable.The hardcoded limit of 100 is reasonable for safety but may be too high or low for different use cases. Consider exposing this as an optional parameter with a sensible default.
async def sample( self, messages: str | Sequence[str | SamplingMessage], *, system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None, tools: Sequence[SamplingTool | Callable[..., Any]] | None = None, result_type: type[ResultT] | None = None, mask_error_details: bool | None = None, + max_iterations: int = 100, ) -> SamplingResult[ResultT] | SamplingResult[str]:
857-857: Same type ignore pattern as insample_step.This
# type: ignore[arg-type]is for the same SDK type limitation. Consider adding a clarifying comment here as well for consistency.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
docs/servers/sampling.mdx(1 hunks)examples/advanced_sampling/README.md(1 hunks)examples/advanced_sampling/client_sampling_test.py(1 hunks)src/fastmcp/client/client.py(3 hunks)src/fastmcp/experimental/sampling/handlers/openai.py(6 hunks)src/fastmcp/server/context.py(6 hunks)src/fastmcp/server/sampling/run.py(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/servers/sampling.mdx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Write Python code with Python ≥3.10 and include full type annotations
Use specific exception types in error handling - never use bareexcept
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter
Files:
src/fastmcp/server/context.pyexamples/advanced_sampling/client_sampling_test.pysrc/fastmcp/experimental/sampling/handlers/openai.pysrc/fastmcp/client/client.pysrc/fastmcp/server/sampling/run.py
🧬 Code graph analysis (3)
examples/advanced_sampling/client_sampling_test.py (2)
src/fastmcp/server/context.py (6)
fastmcp(169-174)Context(117-1004)sample(698-710)sample(713-725)sample(727-882)sample_step(545-695)src/fastmcp/experimental/sampling/handlers/openai.py (1)
OpenAISamplingHandler(46-417)
src/fastmcp/experimental/sampling/handlers/openai.py (2)
src/fastmcp/tools/tool.py (1)
Tool(123-272)src/fastmcp/server/sampling/run.py (2)
tool_calls(85-92)text(72-82)
src/fastmcp/client/client.py (1)
src/fastmcp/client/sampling.py (1)
create_sampling_callback(31-56)
🪛 Ruff (0.14.8)
src/fastmcp/server/context.py
621-624: Avoid specifying long messages outside the exception class
(TRY003)
865-869: Avoid specifying long messages outside the exception class
(TRY003)
882-882: Avoid specifying long messages outside the exception class
(TRY003)
1119-1119: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/experimental/sampling/handlers/openai.py
286-286: Avoid specifying long messages outside the exception class
(TRY003)
352-352: Avoid specifying long messages outside the exception class
(TRY003)
360-360: Avoid specifying long messages outside the exception class
(TRY003)
394-397: Avoid specifying long messages outside the exception class
(TRY003)
410-410: Avoid specifying long messages outside the exception class
(TRY003)
src/fastmcp/server/sampling/run.py
107-107: Avoid specifying long messages outside the exception class
(TRY003)
110-112: Avoid specifying long messages outside the exception class
(TRY003)
146-148: Avoid specifying long messages outside the exception class
(TRY003)
155-158: Avoid specifying long messages outside the exception class
(TRY003)
159-159: Avoid specifying long messages outside the exception class
(TRY003)
162-165: Avoid specifying long messages outside the exception class
(TRY003)
167-167: Avoid specifying long messages outside the exception class
(TRY003)
169-172: Avoid specifying long messages outside the exception class
(TRY003)
195-195: Avoid specifying long messages outside the exception class
(TRY003)
197-197: Avoid specifying long messages outside the exception class
(TRY003)
⏰ 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). (4)
- GitHub Check: Run tests: Python 3.10 on windows-latest
- GitHub Check: Run tests: Python 3.13 on ubuntu-latest
- GitHub Check: Run tests: Python 3.10 on ubuntu-latest
- GitHub Check: Run tests with lowest-direct dependencies
🔇 Additional comments (23)
examples/advanced_sampling/README.md (1)
1-40: LGTM!Clear and well-structured documentation for the advanced sampling examples. Prerequisites and usage instructions are appropriately documented.
examples/advanced_sampling/client_sampling_test.py (1)
1-159: LGTM!Well-structured example that comprehensively demonstrates the new sampling features:
- Primitive
result_typewithintandlist[str]- Stepwise sampling with
sample_step()and history tracking- Structured output with Pydantic models
The code serves as good documentation for users learning the new API.
src/fastmcp/server/sampling/run.py (5)
39-93: LGTM!Well-designed dataclasses with proper generic typing. The property accessors correctly handle both list and single content cases for extracting text and tool calls from responses.
95-113: LGTM!The function correctly handles all expected input types with appropriate validation for the list case.
118-175: LGTM!Previous review concerns have been addressed:
- Invalid
sampling_handler_behaviorvalues now raiseValueError(Line 161-165)- Default path now fails fast if client lacks sampling capability (Line 166-167)
- Clear error messages for all failure cases
177-226: LGTM!Previous review concern addressed:
assertstatements replaced with explicitRuntimeErrorraises (Lines 194-197). The function properly handles awaitable results and converts string responses toCreateMessageResult.
228-300: Error masking for tool execution implemented well.The
mask_error_detailsparameter addresses the previous concern about leaking sensitive information. The distinction betweenToolError(intentional, passes through) and generic exceptions (can be masked) provides good flexibility.One note from the previous review: validating
tool_use.inputas adictbefore passing totool.run()was suggested but not implemented. SinceToolUseContent.inputis typed asdict[str, Any]in MCP types, the risk is low, but a defensive check could prevent confusing errors if a malformed payload arrives.src/fastmcp/client/client.py (2)
306-317: LGTM!The sampling capabilities handling is well-designed:
- Only sets capabilities when a sampling handler is provided
- Defaults to tools-enabled capabilities for the new tool-aware sampling features
- Allows explicit override via the
sampling_capabilitiesparameter
369-383: LGTM!The
set_sampling_callbackmethod maintains consistency with__init__by applying the same defaulting logic forsampling_capabilities.src/fastmcp/experimental/sampling/handlers/openai.py (5)
46-53: LGTM!The
-> Nonereturn type annotation has been added to__init__as requested in the previous review.
72-97: Tool conversion and API call implementation looks correct.Note on previous review concern (return type switching based on
params.tools): The current implementation uses a truthiness check, so bothNoneand[]returnCreateMessageResult. This is consistent behavior—if no tools are effectively provided, the simpler result type is returned.
157-230: Message ordering issue addressed.The previous review concern about tool results being emitted before the tool-call message has been fixed. The code now:
- Collects tool messages in a separate list (Line 165)
- Appends the assistant message with
tool_callsfirst (Lines 203-209)- Extends with tool messages after (Line 211)
This produces valid OpenAI chat sequences.
318-338: LGTM!The tool conversion correctly maps MCP
Tooldefinitions to OpenAIChatCompletionToolParamformat, with appropriate defaults for thetypefield and empty description.
354-417: LGTM!Previous review concern addressed: Invalid tool arguments JSON now raises
ValueErrorwith context (Lines 393-397) instead of silently coercing to{}.The stop reason mapping and content building logic are correct.
src/fastmcp/server/context.py (9)
72-75: LGTM: Type aliases are well-defined.The
ResultTTypeVar withdefault=strenables ergonomic generic usage, andToolChoiceOptionclearly constrains valid tool choice modes.
618-627: Good: Strict validation oftool_choiceparameter.The explicit validation and clear error message for invalid
tool_choicevalues is a good improvement. This addresses the previous concern about unsafe casting of arbitrary strings.
837-859: Good: Validation error recovery allows LLM to retry.Feeding validation errors back to the LLM with
isError=Trueis a solid pattern that gives the model a chance to correct its output format.
1035-1065: LGTM: Correct handling of primitive result types.The wrapper logic at lines 1046-1051 properly addresses the requirement that tool parameters must be object-shaped. Primitive types like
intorlist[str]are correctly wrapped in a{"value": <schema>}structure.
1068-1083: LGTM: Proper type narrowing withisinstancechecks.The use of
isinstance(block, TextContent)correctly narrows the type, addressing the previous static analysis errors about return type mismatches.
1086-1102: LGTM: Clean message normalization.The helper correctly handles all input variants (single string, list of strings, list of SamplingMessage) and produces a consistent output type.
1105-1121: LGTM: Flexible tool input handling.Accepting both
SamplingToolinstances and plain callables with automatic conversion viafrom_functionprovides good developer ergonomics.
1124-1133: LGTM: Robust tool call extraction.Handles both list content and single
ToolUseContentcases correctly.
267-281: LGTM: Return type alignment with SDK types.The updated return types
list[SDKResource]andlist[SDKPrompt]correctly reflect the SDK type renames and maintain consistency with the broader PR changes.
Sampling with tools (SEP-1577) allows servers to pass tools to
ctx.sample()for agentic workflows where the LLM can call helper functions during generation. Another method,ctx.sample_step(), allows complete control over the agentic loop.Basic Sampling
Request text generation from the client's LLM:
Structured Output
Pass
result_typeto get a validated Pydantic model instead of raw text:Sampling with Tools
Pass
toolsto let the LLM call functions during generation. The server automatically executes tool calls and feeds results back to the LLM until it produces a final response:Loop Control with sample_step()
For fine-grained control, use
sample_step()which makes a single LLM call and returns aSampleStep. It's stateless—you control the conversation by passing history each iteration:Set
execute_tools=Falseto handle tool execution yourself for maximum control.