Skip to content

Add timeout parameter for tool foreground execution#2872

Merged
jlowin merged 5 commits intomainfrom
tool-timeouts
Jan 14, 2026
Merged

Add timeout parameter for tool foreground execution#2872
jlowin merged 5 commits intomainfrom
tool-timeouts

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 14, 2026

Tools can now limit execution time with a timeout parameter:

@mcp.tool(timeout=30.0)
async def fetch_data(url: str) -> dict:
    """Fetch with 30-second timeout."""
    ...

When exceeded, clients receive MCP error code -32000. Both sync and async tools are supported—sync functions run in thread pools so the timeout applies regardless of execution model.

Important: This timeout applies to foreground execution only. Background tasks (task=True) execute in Docket workers where this timeout isn't enforced. For task timeouts, use Docket's Timeout dependency directly in the function signature.

Tools can now specify a timeout to limit execution time:

    @mcp.tool(timeout=30.0)
    async def fetch_data(url: str) -> dict:
        ...

When exceeded, returns MCP error -32000. Applies to foreground
execution only; background tasks should use Docket's Timeout
dependency for task-level timeouts.
@marvin-context-protocol marvin-context-protocol Bot added enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. labels Jan 14, 2026
@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: Static analysis failed due to a linting error (B904) and type check warnings in the new timeout test file.

Root Cause:

  1. Ruff B904 violation in - When catching TimeoutError and raising McpError, the code doesn't use proper exception chaining
  2. Type check warnings in tests/tools/test_tool_timeout.py - Accessing .text attribute without type narrowing on result.content[0] which could be various content types

Suggested Solution:

Fix 1: Exception Chaining (src/fastmcp/tools/function_tool.py:271)

Change line 271 from:

raise McpError(
    ErrorData(
        code=-32000,
        message=f"Tool '{self.name}' execution timed out after {self.timeout}s",
    )
)

To:

raise McpError(
    ErrorData(
        code=-32000,
        message=f"Tool '{self.name}' execution timed out after {self.timeout}s",
    )
) from None

The from None suppresses the original TimeoutError context since the timeout is an expected control flow (not an error during exception handling).

Fix 2: Type Check Warnings (tests/tools/test_tool_timeout.py)

For each assertion like result.content[0].text, either:

  • Add type narrowing: assert isinstance(result.content[0], TextContent) before accessing .text
  • Use type assertion: result.content[0].text # type: ignore[union-attr]

Since this is test code and we know the content will be text, the cleanest approach is to add proper assertions.

Detailed Analysis

Ruff Error:

B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
   --> src/fastmcp/tools/function_tool.py:271:17

Type Check Warnings (7 occurrences):

warning[possibly-missing-attribute]: Attribute `text` may be missing on object of type `TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource`
  --> tests/tools/test_tool_timeout.py:26:16
  --> tests/tools/test_tool_timeout.py:38:16
  --> tests/tools/test_tool_timeout.py:50:16
  --> tests/tools/test_tool_timeout.py:62:16
  --> tests/tools/test_tool_timeout.py:169:16
  --> tests/tools/test_tool_timeout.py:170:16
  --> tests/tools/test_tool_timeout.py:171:16
Related Files
  • src/fastmcp/tools/function_tool.py:271 - Exception handling in timeout logic
  • tests/tools/test_tool_timeout.py - Test file with type narrowing issues (lines 26, 38, 50, 62, 169, 170, 171)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

https://github.com/jlowin/fastmcp/blob/ab50336e807b44b44a7959722f4639ad94640959/src/fastmcp/tools/function_tool.py#L406-L410
P2 Badge Propagate timeout in object decorator mode

When fastmcp.settings.decorator_mode == "object", @tool(timeout=...) is handled via create_tool(), but the ToolMeta constructed there omits timeout, so the resulting FunctionTool gets timeout=None and timeouts are silently ignored. This only affects the deprecated object decorator mode, but existing users on that path will see the new timeout option not enforced despite being set.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 14, 2026

Warning

Rate limit exceeded

@jlowin has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4be7e8d and 1293ddd.

⛔ Files ignored due to path filters (1)
  • tests/tools/test_tool_timeout.py is excluded by none and included by none
📒 Files selected for processing (1)
  • docs/servers/tools.mdx

Walkthrough

Adds a per-tool execution timeout feature: a new timeout: float | None parameter is added to the tool decorator and registration APIs (FastMCP.tool, LocalProvider.tool), propagated into Tool, ToolMeta, FunctionTool.from_function, and used by FunctionTool.run. When set, execution is bounded with anyio.fail_after (sync functions run in a threadpool), timeouts log a warning and raise an McpError with ErrorData. Documentation (docs/servers/tools.mdx) is extended with timeout metadata, behavior, and examples.

Possibly related PRs

  • jlowin/fastmcp PR 2676: Modifies the same server.tool decorator public signature and the provider registration call path.
  • jlowin/fastmcp PR 2570: Changes the tool registration and metadata flows (FastMCP.tool, LocalProvider.tool, Tool/FunctionTool) and relates to per-tool configuration handling.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description explains the feature with code examples and clarifies important limitations (foreground only, background tasks use Docket's Timeout), but lacks required checklist items from the template. Add the Contributors and Review checklists from the template, including issue reference, testing confirmation, and self-review sign-off.
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a timeout parameter for tool foreground execution, matching the primary objective and file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/fastmcp/tools/function_tool.py (1)

396-413: Propagate timeout in decorator_mode='object' path (currently dropped).

attach_metadata() includes timeout=timeout, but create_tool() (used when fastmcp.settings.decorator_mode == "object") builds ToolMeta without the timeout, so timeouts won’t apply in that mode.

Proposed fix
         tool_meta = ToolMeta(
             name=tool_name,
             title=title,
             description=description,
             icons=icons,
             tags=tags,
             output_schema=output_schema,
             annotations=annotations,
             meta=meta,
             task=resolve_task_config(task),
             exclude_args=exclude_args,
             serializer=serializer,
+            timeout=timeout,
             auth=auth,
         )

Also applies to: 414-433

🧹 Nitpick comments (4)
src/fastmcp/tools/tool.py (1)

152-157: Add basic validation for timeout (reject <= 0) to fail fast.

Right now timeout=0 / negative values are accepted and will produce unclear runtime behavior depending on anyio. Consider a Pydantic constraint (e.g., gt=0) or a validator that raises ValueError.

docs/servers/tools.mdx (2)

118-120: Timeout parameter docs are clear; consider adding constraints (must be > 0).

Right now docs don’t say what happens for timeout=0 / negative values—worth clarifying once code validates it.


774-823: Align Timeouts section with doc style + avoid overstating cancellation behavior.

  • Prefer second-person voice (“You can set timeout…”, “If you exceed it, you get…”).
  • “the tool stops processing” may be inaccurate for sync tools if the worker thread continues after cancellation—suggest wording like “FastMCP stops waiting and returns an error” unless you can guarantee hard cancellation.
src/fastmcp/server/server.py (1)

1981-2106: Timeout forwarding through FastMCP.tool() looks correct.

Minor: consider adding timeout to the method docstring “Args:” section for completeness.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee3bec and ab50336.

⛔ Files ignored due to path filters (1)
  • tests/tools/test_tool_timeout.py is excluded by none and included by none
📒 Files selected for processing (5)
  • docs/servers/tools.mdx
  • src/fastmcp/server/providers/local_provider.py
  • src/fastmcp/server/server.py
  • src/fastmcp/tools/function_tool.py
  • src/fastmcp/tools/tool.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Python ≥3.10 with full type annotations required for all code
Never use bare except - be specific with exception types in Python code

Files:

  • src/fastmcp/server/server.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/tools/function_tool.py
  • src/fastmcp/server/providers/local_provider.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/tools.mdx
🧬 Code graph analysis (1)
src/fastmcp/tools/function_tool.py (3)
src/fastmcp/utilities/logging.py (1)
  • get_logger (14-26)
src/fastmcp/utilities/types.py (1)
  • get_cached_typeadapter (45-117)
src/fastmcp/utilities/async_utils.py (1)
  • call_sync_fn_in_threadpool (13-21)
🪛 GitHub Actions: Run static analysis
src/fastmcp/tools/function_tool.py

[error] 271-271: ruff-check: B904 Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling.

🪛 Ruff (0.14.11)
src/fastmcp/tools/function_tool.py

271-276: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

⏰ 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 with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.10 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (5)
src/fastmcp/tools/tool.py (1)

193-230: Timeout plumbing in Tool.from_function() looks correct.

Forwarding timeout=timeout into FunctionTool.from_function() is the right place to keep the API consistent.

src/fastmcp/tools/function_tool.py (2)

61-79: ToolMeta / from_function timeout wiring looks consistent.

timeout is treated as an “individual param” correctly and is stored in ToolMeta, then persisted onto the resulting FunctionTool.

Also applies to: 106-180, 228-243


245-287: Fix timeout exception handling (ruff B904) and add explicit exception chaining.

The code has a ruff B904 violation because it raises an exception without explicit exception chaining. Add from None to the raise McpError() statement to fix this.

However, the exception type should remain TimeoutError, not anyio.TimeoutError. According to anyio's documentation, anyio.fail_after() raises the built-in TimeoutError exception, so catching anyio.TimeoutError (which does not exist) would fail.

The concern about except TimeoutError potentially catching user-raised TimeoutError is valid, but a solution would need different approaches (e.g., catching at a more specific scope or using a different timeout mechanism).

For the sync threadpool tool, verify that call_sync_fn_in_threadpool() is configured to abandon work on cancellation; otherwise, fail_after() may not enforce the deadline.

             except TimeoutError:
                 logger.warning(
                     f"Tool '{self.name}' timed out after {self.timeout}s. "
                     f"Consider using task=True for long-running operations. "
                     f"See https://gofastmcp.com/servers/tasks"
                 )
-                raise McpError(
+                raise McpError(
                     ErrorData(
                         code=-32000,
                         message=f"Tool '{self.name}' execution timed out after {self.timeout}s",
                     )
-                )
+                ) from None

Likely an incorrect or invalid review comment.

src/fastmcp/server/providers/local_provider.py (2)

182-213: Good: add_tool() correctly carries meta.timeout into Tool.from_function().

This ensures standalone @tool(timeout=...) metadata isn’t lost when later registered via add_tool.


421-654: Good: timeout is preserved across decorator call patterns (including partial(...)).

The timeout threading into both Tool construction paths and the returned partial() avoids surprising drops.

@jlowin jlowin merged commit 2d83831 into main Jan 14, 2026
11 checks passed
@jlowin jlowin deleted the tool-timeouts branch January 14, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant