Skip to content

SEP-1577: Sampling with tools#2551

Merged
jlowin merged 20 commits intomainfrom
sampling-tools-sep-1577
Dec 14, 2025
Merged

SEP-1577: Sampling with tools#2551
jlowin merged 20 commits intomainfrom
sampling-tools-sep-1577

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 4, 2025

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:

result = await ctx.sample("What is 2 + 2?")
print(result.text)  # "4"

Structured Output

Pass result_type to get a validated Pydantic model instead of raw text:

class Weather(BaseModel):
    temperature: float
    conditions: str

result = await ctx.sample(
    "What's the weather in NYC?",
    result_type=Weather,
)
print(result.result.temperature)  # 72.0

Sampling with Tools

Pass tools to 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:

def search_web(query: str) -> str:
    """Search the web."""
    return f"Results for: {query}"

result = await ctx.sample(
    "Find information about Python async",
    tools=[search_web],
)

Loop Control with sample_step()

For fine-grained control, use sample_step() which makes a single LLM call and returns a SampleStep. It's stateless—you control the conversation by passing history each iteration:

messages = [SamplingMessage(role="user", content=question)]

while True:
    step = await ctx.sample_step(messages, tools=[search])
    
    if step.is_text:
        return step.text
    
    # Log tool calls, add custom logic, etc.
    for call in step.tool_calls:
        print(f"Called: {call.name}")
    
    messages = step.history  # Continue with updated history

Set execute_tools=False to handle tool execution yourself for maximum control.

- 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
@marvin-context-protocol marvin-context-protocol Bot added feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality. client Related to the FastMCP client SDK or client-side functionality. documentation Updates to docs, examples, or guides. Primary change is documentation-related. labels Dec 4, 2025
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Dec 4, 2025

Test Failure Analysis

Summary: All CI jobs are failing during dependency installation because the project requires mcp>=1.23.3, but only mcp<=1.23.1 is currently available on PyPI.

Root Cause: This PR updates pyproject.toml to require mcp>=1.23.3 to support the new sampling with tools feature (SEP-1577). However, version 1.23.3 of the mcp package hasn't been released yet. The latest available version on PyPI is 1.23.1.

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:

  1. The upstream PR (feat: client-side support for SEP-1577 sampling with tools modelcontextprotocol/python-sdk#1722) is merged
  2. A new version of the mcp package (>=1.23.3) is released to PyPI containing the sampling with tools functionality

Once the dependency is available, the tests should pass without any code changes to this PR.

Detailed Analysis

Error Message from All Jobs

× No solution found when resolving dependencies:
╰─▶ Because only mcp<=1.23.1 is available and your project depends on
    mcp>=1.23.3, we can conclude that your project's requirements are
    unsatisfiable.

Affected Jobs

All 4 CI jobs failed at the uv sync step before any tests could run:

  • Run tests: Python 3.10 on ubuntu-latest
  • Run tests: Python 3.10 on windows-latest
  • Run integration tests
  • Run tests with lowest-direct dependencies

Dependency Chain

  1. This PR modifies pyproject.toml to require mcp>=1.23.3
  2. The 1.23.3 version will include support for SEP-1577 (sampling with tools)
  3. That functionality is being added via feat: client-side support for SEP-1577 sampling with tools modelcontextprotocol/python-sdk#1722
  4. Until that PR is merged and released, version 1.23.3 doesn't exist on PyPI
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)

@jlowin jlowin added this to the MCP 11/25/25 milestone Dec 4, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 4, 2025

Walkthrough

Adds end-to-end tool-enabled sampling across docs, examples, client, and server code. Client: optional sampling_capabilities in Client init and session kwargs; transports gain sampling_capabilities field. OpenAI handler: accepts tools and toolChoice, maps MCP tools to OpenAI functions, and returns tool-aware results types. Server: new SamplingResult, SampleStep, SamplingTool, sample_step runtime, execute_tools, handler-selection and call helpers, synthetic final-response tool for typed outputs, and re-exports sampling symbols. Several MCP* type aliases renamed to SDK* with updated return types.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title "SEP-1577: Sampling with tools" clearly summarizes the main feature being implemented—adding tool-enabled sampling functionality referenced by the SEP-1577 specification.
Description check ✅ Passed The description provides clear examples of basic sampling, structured output, tool-enabled sampling, and loop control via sample_step(), demonstrating the PR's core features. However, it does not fully follow the provided template by omitting the Contributors/Review Checklist sections.
Docstring Coverage ✅ Passed Docstring coverage is 84.75% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sampling-tools-sep-1577

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 evaluation

The temperature=params.temperature or NOT_GIVEN pattern on line 81 incorrectly treats 0.0 as falsy, sending NOT_GIVEN to the OpenAI API instead of the valid temperature value. Since temperature is typed as float | None, an explicit 0.0 is distinct from None and 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 None to NOT_GIVEN. (Note: stopSequences on 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}_mcp methods correctly map internal components to mcp.types SDK objects using the to_mcp_* helpers and the .key/uriTemplate conventions, which preserves prefixing and filtering semantics across mounted servers and middleware. The extra Context layer in both the *_mcp and *_middleware functions 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 example

The custom_agent example uses placeholders like some_condition and different_tool that 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: Ensure CallToolResult.data is populated for this example

The example prints result.data["summary"] / ["sources_used"] / ["confidence"]. This relies on Client.call_tool successfully parsing structured output into CallToolResult.data using the tool’s output_schema. Please double‑check that the research tool is registered with an appropriate output_schema (or auto‑derived one) so that data is not None; otherwise these subscripts will raise at runtime and you may need to read from result.structured_content instead.

examples/advanced_sampling/structured_output.py (1)

47-65: Confirm structured data reaches CallToolResult.data

As 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 via CallToolResult.data (through a proper output_schema), otherwise data may be None and these lines will fail. If needed, you could fall back to result.structured_content or 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 overrides

The updated set_sampling_callback correctly updates both the sampling callback and sampling_capabilities, defaulting to tools-enabled sampling when no explicit capabilities are provided, consistent with __init__. One thing to be aware of: calling set_sampling_callback without sampling_capabilities after 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 accepting sampling_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_openai maps MCP ToolChoice.mode values "auto", "required", and "none" directly to OpenAI’s accepted values, defaulting to "auto" for anything unknown. If MCP’s ToolChoice ever 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 with str.

The condition result_type is not str uses identity comparison, which works here because you're checking if the type itself is the str class. 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: Bare except Exception is 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 for response_type.

The checks at lines 1168-1184 use isinstance(response_type, dict) and isinstance(response_type, list), but response_type is declared as type[T] | list[str] | dict[str, dict[str, str]] | None. When it's a dict or list value (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

📥 Commits

Reviewing files that changed from the base of the PR and between acf4db0 and 0d8679c.

⛔ Files ignored due to path filters (7)
  • pyproject.toml is excluded by none and included by none
  • tests/client/test_sampling.py is excluded by none and included by none
  • tests/server/middleware/test_tool_injection.py is excluded by none and included by none
  • tests/server/sampling/__init__.py is excluded by none and included by none
  • tests/server/sampling/test_sampling_tool.py is excluded by none and included by none
  • tests/server/test_context.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and 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.mdx
  • docs/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 bare except - 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.py
  • src/fastmcp/server/sampling/handler.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/server/server.py
  • src/fastmcp/client/client.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/client/transports.py
  • src/fastmcp/server/context.py
  • src/fastmcp/server/sampling/__init__.py
  • src/fastmcp/experimental/sampling/handlers/openai.py
  • src/fastmcp/server/proxy.py
  • src/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.py
  • 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: Review and update related Manager classes (ToolManager, ResourceManager, PromptManager) when modifying MCP object definitions

Applied to files:

  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/server/server.py
  • src/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.text aligns 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 clean

Using SDKPrompt / SDKPromptArgument and updating the to_mcp_prompt signature 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 implementation

The 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 (including sampling_handler_behavior and 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 APIs

The sampling tools, ResearchReport model, and research tool correctly demonstrate ctx.sample with tools=[...], result_type=ResearchReport, and max_iterations, matching the documented behavior. Returning result.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 correct

Switching to SDKResourceTemplate and updating to_mcp_template / from_mcp_template keeps 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 matches result_type behavior

The SentimentAnalysis model and analyze_sentiment tool nicely illustrate using ctx.sample(..., result_type=SentimentAnalysis) and returning result.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 set

Adding the sampling_capabilities parameter and wiring _session_kwargs["sampling_capabilities"] when sampling_handler is provided ensures clients advertise sampling.tools by default (via SamplingCapability(tools=SamplingToolsCapability())), matching the docs about tool-enabled sampling. Passing an explicit sampling_capabilities lets 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 correct

The 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 results

The extended _convert_to_openai_messages handles:

  • Raw string prompts,
  • Text-only SamplingMessage instances,
  • Lists of TextContent, ToolUseContent, and ToolResultContent (from prior tool-enabled iterations),
  • Standalone ToolUseContent and ToolResultContent.

It builds the right OpenAI message types (user, assistant with tool_calls, and tool messages) 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 tools

Mapping each MCP Tool’s name, description, and inputSchema to an OpenAI ChatCompletionToolParam with a FunctionDefinition looks correct, and the defensive type="object" default for parameters is helpful. Just ensure tool.inputSchema is always a mapping (as produced by the SDK) so dict(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_reason to MCP StopReason (tool_calls"toolUse", stop"endTurn", length"maxTokens"),
  • Builds a content list containing a TextContent (if present) and one ToolUseContent per 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_type specified) 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_content function 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_tool function properly derives the JSON schema from the result type and creates a passthrough function for the tool. Schema compression with prune_titles=True is a good choice for reducing payload size.


678-689: Good capability validation before tool usage.

Properly checks that the client advertises sampling.tools capability 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_resources and list_prompts now use SDKResource and SDKPrompt aliases, 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 defaults None arguments to an empty dict and handles both synchronous and asynchronous functions with the isawaitable check.


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 an fn attribute before conversion. The error message clearly explains that only FunctionTools can be converted.


141-187: Flexible decorator with proper overloads.

The sampling_tool decorator 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 SamplingTool instance, which is the intended behavior for use with context.sample(tools=[...]).


133-138: No issue found. The code correctly uses parsed.fn rather than the original fn parameter. ParsedFunction.from_function() intentionally normalizes the function to handle callable classes (extracting __call__) and staticmethods (extracting __func__). Using the modified parsed.fn is required for proper execution, as demonstrated by the run() method's direct invocation via self.fn(**arguments). This pattern is consistently applied across the codebase (e.g., FunctionTool.from_function() uses the same approach). The design is sound.

Comment thread src/fastmcp/server/context.py Outdated
Comment thread src/fastmcp/server/context.py Outdated
Comment thread src/fastmcp/experimental/sampling/handlers/openai.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 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:

  1. Iterates to collect tool_calls and text_parts
  2. Immediately creates tool messages for ToolResultContent during iteration
  3. 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:

  1. Defining custom exception classes, or
  2. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0d8679c and ead4730.

📒 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 bare except - 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 json for 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 for ChatCompletionToolMessageParam. The OpenAI API expects content to be a string or a list of content parts. Verify that passing an empty string when content_parts is 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.

Comment on lines +328 to +341
@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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find ToolChoice definition
rg -nP 'class ToolChoice|ToolChoice\s*=' --type=py -A 10

Repository: 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.py

Repository: 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 -100

Repository: 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.py

Repository: 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 3

Repository: 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

  1. OpenAI Cookbook — How to call functions with chat models. [2]
  2. OpenAI Cookbook example: Using tool_required for customer service. [3]
  3. LangChain / client validation examples showing forced-function must be in tools list. [8]
  4. 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 -20

Repository: 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.py

Repository: 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 15

Repository: 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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/fastmcp/server/context.py (2)

821-827: Type contract violation when max_iterations exhausted (previously flagged).

Returning result=cast(ResultT, None) when iterations are exhausted without a final response violates the type contract. Callers expecting ResultT (e.g., a Pydantic model) will receive None, causing downstream AttributeErrors.

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: Same None result 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 confidence with a % suffix, but ResearchReport.confidence is defined as a plain float (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: Placeholder some_condition in custom loop example.

The some_condition placeholder is undefined, which will cause a NameError if 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 in while True loop 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 in run().

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 result
src/fastmcp/server/context.py (1)

1345-1360: _extract_text_from_content uses duck typing.

Using hasattr(block, "text") works but is less type-safe than checking isinstance(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

📥 Commits

Reviewing files that changed from the base of the PR and between ead4730 and 5c0f055.

⛔ Files ignored due to path filters (2)
  • tests/client/test_sampling.py is excluded by none and included by none
  • tests/server/sampling/test_sampling_tool.py is 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 bare except - 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.py
  • src/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_type for structured output (ResearchReport)
  • Executing the agentic workflow via ctx.sample() with tools

The 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_allowed for the fn field
  • 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_choice logic handles last-iteration finalization well.

The code correctly forces final_response when result_type is specified, or forces mode="none" to get a text response on the last iteration. This prevents infinite loops and ensures a usable result.


1005-1020: Bare Exception catch is acceptable here for LLM feedback loop.

Catching Exception when validating final_response input 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 ToolResultContent with isError=True allows 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_type is 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-designed SamplingResult dataclass.

The generic SamplingResult[ResultT] cleanly separates:

  • .text for string representation
  • .result for typed result
  • .history for conversation continuation

The docstring clearly explains each field's purpose.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/fastmcp/server/context.py (1)

836-842: Address the past review concern: Type-unsafe None return.

As flagged in the previous review, returning cast(ResultT, None) when max_iterations is exhausted violates the type contract when result_type is a non-optional structured type. This creates a runtime hazard where callers expect a valid object but receive None.

The previous review suggested raising an exception (e.g., RuntimeError or a dedicated MaxIterationsExceededError) instead of returning None. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0f055 and b544cdd.

⛔ Files ignored due to path filters (2)
  • pyproject.toml is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and 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 bare except - 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.py
  • src/fastmcp/server/context.py
  • src/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 ResultT TypeVar with default=str maintains backward compatibility, and the SamplingResult generic 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_type specified) 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 Exception broadly when executing tools (lines 1020, 1063). While Ruff flags this as BLE001, catching broad exceptions in tool execution contexts is often appropriate to:

  1. Prevent individual tool failures from crashing the entire sampling loop
  2. 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_tool and _extract_text_from_content are 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_calls method correctly:

  • Handles both synthetic final_response and regular tools
  • Batches tool results into a single user message per MCP conventions
  • Returns early with a structured result when final_response succeeds
  • Converts tool errors into ToolResultContent with isError=True for 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_response tool when result_type is specified
  • Forces text-only response (tool_choice=none) when only tools are used

This makes the max_iterations limit meaningful. However, this relies on fixing the type-unsafe None return 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* to SDK* 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 MCPResource to SDKResource is 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

@strawgate
Copy link
Copy Markdown
Collaborator

/marvin can you fix the merge conflicts please

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Dec 10, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

@strawgate
Copy link
Copy Markdown
Collaborator

i think this is currently a breaking change to ctx.sample with the introduction of the SamplingResult response model?

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Dec 10, 2025

Claude finished @strawgate's task in 6m 18s —— View job


Fixing Merge Conflicts ✓

Tasks

  • Check git status and identify merge conflicts
  • Resolve merge conflicts in src/fastmcp/server/context.py
  • Resolve merge conflicts in uv.lock
  • Stage and commit changes
  • Push to remote branch

Successfully resolved all merge conflicts and pushed to the branch!

Conflicts Resolved

src/fastmcp/server/context.py - 4 conflicts resolved:

  1. Import statements (line 13) - Combined imports from both versions, keeping all necessary types (Generic, Literal, cast, get_origin) needed for the sampling tools feature
  2. Method addition (line 529) - Added the new close_sse_stream() method from main while preserving the @overload decorator that follows it
  3. Sampling handler call (line 725) - Kept the new _sample_with_fallback_handler implementation from this PR's branch
  4. Elicitation handling (line 1197) - Used the refactored handle_elicit_accept helper function from main instead of the inline logic

uv.lock - Regenerated the lock file using uv lock to incorporate the merged dependencies from both branches

Commit Details

  • Commit: 1cf947b6
  • Message: "Merge main into sampling-tools-sep-1577"
  • Pushed to: origin/sampling-tools-sep-1577

The merge conflicts have been successfully resolved and the branch is now up to date with main!

--- | Branch: sampling-tools-sep-1577

Resolved conflicts in src/fastmcp/server/context.py and uv.lock.

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Comment thread src/fastmcp/server/context.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/fastmcp/experimental/sampling/handlers/openai.py (1)

306-341: _convert_tool_choice_to_openai ignores ToolChoice.name and masks invalid modes.

This helper currently:

  • Returns only string modes "auto" | "required" | "none", ignoring tool_choice.name.
  • Silently falls back to "auto" on any unexpected mode.

Because Context.sample() uses ToolChoice(mode="required", name="final_response") to try to force a specific tool on the last iteration, dropping the name here 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" and tool_choice.name is 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_choice docs 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: Fix get_cached_typeadapter import and reconsider max_iterations + None results in sampling loops.

There are a few intertwined issues in the tool/structured-output sampling paths:

  1. Missing get_cached_typeadapter import breaks structured outputs at runtime.
    _execute_tool_calls() and _create_final_response_tool() both call get_cached_typeadapter(...), but this symbol isn’t imported in this module. That yields a NameError the first time the final_response tool 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 when ResultT is 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 that max_iterations=1 should disable that forced‑final behavior rather than short‑circuit tool execution and return None.

A more robust approach would be to either:

  • Raise a clear exception (e.g. RuntimeError or a dedicated MaxIterationsExceededError) when the loop is exhausted without a final response, or
  • Change the API contract to return SamplingResult[ResultT | None] and document that result can be None in 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.

  1. Forced-final behavior also applies when max_iterations == 1.
    Both loops treat iteration == max_iterations - 1 as the “last iteration” and then unconditionally:

    • Force ToolChoice(mode="required", name="final_response") when a result_type is 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‑exit iteration >= max_iterations checks, this means max_iterations=1 can actually prevent any tool execution and just yield a SamplingResult with None fields.

    You may want to special‑case the “advanced loop control” behavior so that:

    • Forced finalization (final_response/none) only kicks in when max_iterations > 1, and
    • max_iterations=1 truly 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.

  2. _execute_tool_calls overall logic is sound once the adapter import is fixed.
    Aside from the adapter import and broad Exception catches, the flow—adding the assistant message, running tools via SamplingTool.run, appending ToolResultContent, and optionally short‑circuiting when final_response validates—is coherent. The dead should_continue == False branch 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 SamplingResult type to allow Optional[ResultT] and document that behavior? In either case, aligning max_iterations=1 semantics 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 SamplingMessage content (text/tool uses/results) into OpenAI chat params and back into CreateMessageResultWithTools looks coherent and should round‑trip tool calls correctly. Note that when a single message carries multiple TextContent blocks, they are concatenated with newlines into a single OpenAI content string; 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 (single create_message call, text extraction via _extract_text_from_content, and SamplingResult construction) are consistent and type‑annotated as required. Converting tools to SamplingTool from SamplingTool/FastMCPTool/callables and then to SDKTool is also a sensible layering.

Also applies to: 581-612, 656-779


1338-1353: _extract_text_from_content helper is a reasonable convenience.

The helper correctly pulls the first .text field from either a single content block or a list of blocks, and safely falls back to None when 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

📥 Commits

Reviewing files that changed from the base of the PR and between b544cdd and 1cf947b.

⛔ Files ignored due to path filters (2)
  • pyproject.toml is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and 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 bare except - 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.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/context.py
  • src/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 and to_mcp_resource conversion look consistent.

The alias swap to SDKResource and the updated return annotation/docstring are self‑consistent and match how Resource.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 SDKResourceTemplate in both to_mcp_template() and from_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/SDKTool aliases and updated _list_*_mcp return types line up with the corresponding converters (to_mcp_tool/resource/template/prompt) and with Context.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: New SamplingResult container and SDK list APIs are well-structured.*

SamplingResult[ResultT] cleanly exposes .text, .result, and .history, and Context.list_resources()/list_prompts() now returning list[SDKResource]/list[SDKPrompt] correctly reflect the server’s updated _list_*_mcp implementations.

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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/fastmcp/server/context.py (1)

1059-1074: Type narrowing issue with hasattr checks.

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 isinstance with TextContent:

 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 None
src/fastmcp/experimental/sampling/handlers/openai.py (1)

326-339: Handle specific tool selection when name is provided.

This method ignores the name field of ToolChoice. 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 confidence field 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 Field from pydantic.

docs/servers/sampling.mdx (1)

156-156: Inconsistent import location for SamplingMessage.

Line 156 imports SamplingMessage from fastmcp.client.sampling, but line 296 imports from mcp.types. For consistency and to use the canonical source, consider using mcp.types throughout:

-from fastmcp.client.sampling import SamplingMessage
+from mcp.types import SamplingMessage
src/fastmcp/server/context.py (1)

752-753: Hardcoded max_iterations limit.

The max_iterations = 50 is 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 Exception is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5bfe6 and 2cdc9b6.

⛔ Files ignored due to path filters (4)
  • pyproject.toml is excluded by none and included by none
  • tests/client/test_sampling.py is excluded by none and included by none
  • tests/server/proxy/test_proxy_client.py is excluded by none and included by none
  • uv.lock is excluded by !**/*.lock and 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 bare except
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/client/client.py
  • src/fastmcp/server/context.py
  • examples/advanced_sampling/tool_use.py
  • src/fastmcp/experimental/sampling/handlers/openai.py
  • src/fastmcp/server/sampling/run.py
  • src/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 to SamplingTool objects 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_callback method 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() and ctx.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 (fallback vs always). 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 ResultT and ToolChoiceOption are well-defined and provide clear typing for the sampling API.


544-678: LGTM!

The sample_step method 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). Since step.history is a list returned from sample_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_tool function 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_tools function correctly handles both SamplingTool instances 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 SamplingResult dataclass is well-structured with clear documentation. The generic ResultT provides flexibility for both text and structured output scenarios.


50-93: LGTM!

The SampleStep class 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.isawaitable to 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_message method 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.types and openai.types.chat are well-organized, with proper handling of the optional openai dependency.


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 ToolUseContent handling correctly wraps it in an assistant message with tool_calls array.


237-252: LGTM!

ToolResultContent is correctly converted to a tool message with the content extracted and joined.


254-272: LGTM!

The TextContent handling properly respects the message role, and the final raise ValueError ensures 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 CreateMessageResultWithTools is 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 existsToolUseContent and ToolResultContent appear 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.

Comment thread examples/advanced_sampling/tool_use.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
src/fastmcp/server/sampling/run.py (2)

45-48: Consider making SamplingResult.text non-optional (or enforce the invariant).

SamplingResult represents a completed sampling run; having text: str | None invites downstream None handling despite “run to completion” semantics. If None is truly possible, document when/why and consider a helper like require_text() that raises a clear error.


72-83: SampleStep.text should avoid hasattr/type: ignore and handle multi-block text predictably.

Right now you return the first block with .text, which can silently drop content and is structurally brittle. Prefer explicit isinstance(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_KEY is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2cdc9b6 and 5e6c790.

📒 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 bare except
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

Comment thread docs/servers/sampling.mdx
Comment thread docs/servers/sampling.mdx
Comment on lines +303 to 320
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
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread docs/servers/sampling.mdx Outdated
Comment on lines +371 to +387
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))
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/fastmcp/server/sampling/run.py
Comment thread src/fastmcp/server/sampling/run.py Outdated
Comment on lines +254 to +290
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,
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.input should be validated as a dict[str, Any] | None before passing to SamplingTool.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)

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: 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 ConnectError: [Errno -2] Name or service not known. The pytest timeout (5s) kicks in before the DNS/connection timeout, failing the test.

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:

  1. Use httpx transport mocking - Use respx library (which FastMCP might already have as a test dependency) or httpx.MockTransport to intercept requests before they hit the network:
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
  1. Use a timeout on the httpx client - Set a very short timeout on the httpx.AsyncClient to fail fast:
async with httpx.AsyncClient(
    base_url="https://api.example.com",
    timeout=0.1  # Fail fast
) as client:
    # ... rest of test
  1. Mark the test appropriately - If this test is meant to test actual network behavior, it should be marked as an integration test:
@pytest.mark.integration
async def test_optional_parameter_handling(self, simple_spec):
    # ... test code

Recommended 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 Analysis

Error Log Excerpt

[31mFAILED[0m tests/server/openapi/test_end_to_end_compatibility.py::TestEndToEndCompatibility::test_optional_parameter_handling - Failed: Timeout (>5.0s) from pytest-timeout.

ConnectError: [Errno -2] Name or service not known

Code Flow

  1. Test creates httpx.AsyncClient(base_url="https://api.example.com")
  2. Creates FastMCPOpenAPI server instances with this client
  3. Calls await legacy_client.call_tool(tool_name, test_args_minimal)
  4. Tool execution reaches OpenAPITool.run() in src/fastmcp/server/openapi/components.py:111
  5. Executes response = await self._client.send(request)
  6. httpx attempts DNS resolution for api.example.com
  7. DNS lookup times out, pytest timeout (5s) triggers before the DNS timeout completes

Why This Wasn't Caught Before

The 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
  • tests/server/openapi/test_end_to_end_compatibility.py:274 - The failing test
  • src/fastmcp/server/openapi/components.py:111 - Where the HTTP request is sent
  • Similar tests in the same directory use api.example.com but don't actually call the tools, they only list them

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

marvin-context-protocol Bot commented Dec 14, 2025

Test Failure Analysis

Summary: Test test_init_defaults in test_workos.py is timing out on Windows after 5 seconds during OAuth provider initialization when creating a DiskCache for client storage.

Root Cause: The test creates a WorkOSProvider which initializes an OAuthProxyProvider with a default DiskStore for client storage (at line 821 in oauth_proxy.py). On Windows, the diskcache.Cache initialization hangs when executing SQLite PRAGMA commands (PRAGMA %s = %s). This appears to be a Windows-specific issue with SQLite operations timing out, possibly due to file locking or permissions issues in the GitHub Actions environment.

The timeout occurs at:

  • oauth_proxy.py:821 - Creating DiskStore(directory=settings.home / "oauth-proxy")
  • diskcache/core.py:478 - Cache.__init__ calling self.reset()
  • diskcache/core.py:2438 - Executing SQLite PRAGMA commands

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 test

Option 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 code

However, 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 code

But 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 Analysis

Stack Trace

File "test_workos.py", line 131, in test_init_defaults
    provider = WorkOSProvider(...)
File "workos.py", line 260, in __init__
    super().__init__(...)
File "oauth_proxy.py", line 821, in __init__
    key_value=DiskStore(directory=settings.home / "oauth-proxy"),
File "diskcache/core.py", line 478, in __init__
    self.reset(key, value, update=False)
File "diskcache/core.py", line 2438, in reset
    sql('PRAGMA %s = %s' % (pragma, value)).fetchall()
++++++++++++++ Timeout ++++++++++++++

Why This Happens on Windows

  • SQLite operations can be slower on Windows due to different file locking mechanisms
  • GitHub Actions Windows runners may have specific filesystem characteristics that exacerbate this
  • The diskcache library uses SQLite under the hood, which may have platform-specific timing issues
  • Tests only have a 5-second timeout, which isn't enough for SQLite initialization on Windows CI

Test Passed on Other Platforms

  • Ubuntu (Python 3.10): ✅ Passed
  • Ubuntu (Python 3.13): ✅ Passed
  • Windows (Python 3.10): ❌ Timeout
Related Files
  • tests/server/auth/providers/test_workos.py:131 - The failing test
  • src/fastmcp/server/auth/workos.py:260 - WorkOSProvider initialization
  • src/fastmcp/server/auth/oauth_proxy.py:821 - Default DiskStore creation
  • key_value.aio.stores.disk.store.DiskStore - Uses diskcache.Cache which hangs

🤖 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
src/fastmcp/server/sampling/run.py (1)

149-172: Consider explicit validation for sampling_handler_behavior.

The function handles "always" and "fallback" explicitly, but other values (including typos like "fallbck") silently fall through. While None (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 False
src/fastmcp/experimental/sampling/handlers/openai.py (1)

345-346: Silent fallback to "auto" for unknown modes masks configuration errors.

The else branch (line 346) silently returns "auto" for any unrecognized tool_choice.mode value. 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 understates result_type capabilities.

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 to sample_step() loop example for safety.

The while True loop lacks a termination guard. Unlike sample() which has internal max_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 raises KeyError for unknown tools. Since the page documents isError=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 Exception catch at line 821 is flagged by static analysis (BLE001). While Pydantic's ValidationError is 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 result

If you need to handle other exceptions (e.g., TypeError from 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 JSONDecodeError is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5e6c790 and 1ef5319.

📒 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 bare except
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/experimental/sampling/handlers/openai.py
  • src/fastmcp/server/context.py
  • src/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_tool correctly 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_content function now correctly uses isinstance(block, TextContent) checks instead of hasattr, resolving the type narrowing issues flagged in past reviews.


1096-1131: LGTM - Message and tool preparation helpers are well-implemented.

Both _prepare_messages and _prepare_tools correctly 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.tools capability requirement is helpful.

src/fastmcp/server/sampling/run.py (3)

192-195: LGTM - Runtime checks replace assertions.

The explicit RuntimeError checks properly replace the previously flagged assert statements, ensuring errors are raised even with -O optimization.


250-322: LGTM - Tool execution with error masking is well-implemented.

The function properly:

  • Reports unknown tools as errors (lines 272-285)
  • Uses ToolError as 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 - SamplingResult and SampleStep dataclasses are well-designed.

The dataclasses provide clean abstractions:

  • SamplingResult bundles text, typed result, and history
  • SampleStep offers convenient properties (is_tool_use, is_text, text, tool_calls) for introspecting LLM responses

The text property correctly uses isinstance checks for type narrowing.

Comment thread src/fastmcp/server/context.py
- 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) OR CreateMessageResult/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.name is 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: Validate tool_use.input before calling tools; otherwise malformed payloads can explode inside tool code.

Add a runtime guard (None or dict[str, Any]) before await tool.run(...), and convert violations into a ToolResultContent(isError=True).

docs/servers/sampling.mdx (2)

345-362: Add a max-steps (or time budget) guard to the while True loop.

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 demonstrate isError=True.

Right now tools[call.name] can raise KeyError, and tool exceptions will crash the loop rather than producing ToolResultContent(isError=True).

src/fastmcp/server/context.py (1)

617-627: Disallowing non-mode tool_choice strings 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

📥 Commits

Reviewing files that changed from the base of the PR and between b079fe5 and 07df7c6.

⛔ Files ignored due to path filters (1)
  • tests/client/test_sampling.py is 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.mdx
  • 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 bare except
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/experimental/sampling/handlers/openai.py
  • src/fastmcp/server/sampling/run.py
  • src/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

Comment thread docs/clients/sampling.mdx
Comment on lines +166 to +178
## 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
)
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread docs/servers/sampling.mdx
Comment on lines +84 to 110
# 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/fastmcp/experimental/sampling/handlers/openai.py
Comment thread src/fastmcp/experimental/sampling/handlers/openai.py
Comment thread src/fastmcp/server/context.py Outdated
Comment thread src/fastmcp/server/context.py
Comment thread src/fastmcp/server/context.py
Comment thread src/fastmcp/server/sampling/run.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
src/fastmcp/experimental/sampling/handlers/openai.py (4)

72-97: Avoid params.tools truthiness checks (empty list vs None flips behavior/return type).

Right now [] behaves like “no tools”: you won’t send tools/tool_choice, and you return CreateMessageResult instead 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 with tool_calls later (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 ToolResultContent but no ToolUseContent in 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 when ToolChoice.name is set (OpenAI requires object form).

ToolChoice.name is 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 explicit is None defaulting for sampling_capabilities (avoid or edge-cases, clearer intent).

sampling_capabilities or SamplingCapability(...) is less explicit than checking is 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: Keep set_sampling_callback() behavior consistent, but same is None refactor 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

📥 Commits

Reviewing files that changed from the base of the PR and between 07df7c6 and 6bc676e.

⛔ Files ignored due to path filters (1)
  • tests/experimental/sampling/test_openai_handler.py is 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 bare except
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/client/client.py
  • src/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

Comment thread src/fastmcp/experimental/sampling/handlers/openai.py
- 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
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 name field of ToolChoice is 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_response tool), 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 on content parameter.

The # type: ignore[arg-type] suggests a type incompatibility between tool_results (list of ToolResultContent) and the expected content parameter 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 making max_iterations configurable.

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 in sample_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

📥 Commits

Reviewing files that changed from the base of the PR and between 6bc676e and 2c63948.

📒 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 bare except
Prioritize readable, understandable code - clarity over cleverness; avoid obfuscated or confusing patterns even if they're shorter

Files:

  • src/fastmcp/server/context.py
  • examples/advanced_sampling/client_sampling_test.py
  • src/fastmcp/experimental/sampling/handlers/openai.py
  • src/fastmcp/client/client.py
  • src/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_type with int and list[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_behavior values now raise ValueError (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: assert statements replaced with explicit RuntimeError raises (Lines 194-197). The function properly handles awaitable results and converts string responses to CreateMessageResult.


228-300: Error masking for tool execution implemented well.

The mask_error_details parameter addresses the previous concern about leaking sensitive information. The distinction between ToolError (intentional, passes through) and generic exceptions (can be masked) provides good flexibility.

One note from the previous review: validating tool_use.input as a dict before passing to tool.run() was suggested but not implemented. Since ToolUseContent.input is typed as dict[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_capabilities parameter

369-383: LGTM!

The set_sampling_callback method maintains consistency with __init__ by applying the same defaulting logic for sampling_capabilities.

src/fastmcp/experimental/sampling/handlers/openai.py (5)

46-53: LGTM!

The -> None return 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 both None and [] return CreateMessageResult. 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:

  1. Collects tool messages in a separate list (Line 165)
  2. Appends the assistant message with tool_calls first (Lines 203-209)
  3. Extends with tool messages after (Line 211)

This produces valid OpenAI chat sequences.


318-338: LGTM!

The tool conversion correctly maps MCP Tool definitions to OpenAI ChatCompletionToolParam format, with appropriate defaults for the type field and empty description.


354-417: LGTM!

Previous review concern addressed: Invalid tool arguments JSON now raises ValueError with 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 ResultT TypeVar with default=str enables ergonomic generic usage, and ToolChoiceOption clearly constrains valid tool choice modes.


618-627: Good: Strict validation of tool_choice parameter.

The explicit validation and clear error message for invalid tool_choice values 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=True is 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 int or list[str] are correctly wrapped in a {"value": <schema>} structure.


1068-1083: LGTM: Proper type narrowing with isinstance checks.

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 SamplingTool instances and plain callables with automatic conversion via from_function provides good developer ergonomics.


1124-1133: LGTM: Robust tool call extraction.

Handles both list content and single ToolUseContent cases correctly.


267-281: LGTM: Return type alignment with SDK types.

The updated return types list[SDKResource] and list[SDKPrompt] correctly reflect the SDK type renames and maintain consistency with the broader PR changes.

@jlowin jlowin merged commit 41ec7ee into main Dec 14, 2025
14 checks passed
@jlowin jlowin deleted the sampling-tools-sep-1577 branch December 14, 2025 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Related to the FastMCP client SDK or client-side functionality. documentation Updates to docs, examples, or guides. Primary change is documentation-related. feature Major new functionality. Reserved for 2-4 significant PRs per release. Not for issues. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants