Skip to content

Add type-prefixed keys for globally unique component identification#2704

Merged
jlowin merged 1 commit intomainfrom
type-prefixed-component-keys
Dec 24, 2025
Merged

Add type-prefixed keys for globally unique component identification#2704
jlowin merged 1 commit intomainfrom
type-prefixed-component-keys

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Dec 24, 2025

Component keys are now globally unique by including a type prefix. This eliminates any possibility of key collisions between different component types.

from fastmcp import FastMCP
from fastmcp.tools import Tool

mcp = FastMCP("Server")

@mcp.tool
def greet(name: str) -> str:
    return f"Hello, {name}!"

# Keys are now prefixed by type
tool = await mcp.get_tool("greet")
print(tool.key)  # "tool:greet"

# Direct lookup by prefixed key
component = await mcp.get_component("tool:greet")

# Class method to construct keys
Tool.make_key("greet")  # "tool:greet"

Key prefixes:

  • tool: for tools
  • prompt: for prompts
  • resource: for resources
  • template: for resource templates

LocalProvider now uses unified _components dict storage. get_tasks() returns a flat Sequence[FastMCPComponent] instead of the previous TaskComponents dataclass. Subclasses of FastMCPComponent that don't define KEY_PREFIX will emit a warning.

Closes #2703

Components now have globally unique keys with type prefixes:
- Tool "foo" → "tool:foo"
- Prompt "bar" → "prompt:bar"
- Resource "file://x" → "resource:file://x"
- ResourceTemplate "data://{id}" → "template:data://{id}"

Key changes:
- Add KEY_PREFIX class variable and make_key() classmethod to components
- Unify LocalProvider storage into single _components dict
- Add get_component(key) method to Provider and FastMCP for direct key lookup
- Simplify get_tasks() to return Sequence[FastMCPComponent]
- Add __init_subclass__ warning for missing KEY_PREFIX
@marvin-context-protocol marvin-context-protocol Bot added breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. enhancement Improvement to existing functionality. For issues and smaller PR improvements. server Related to FastMCP server implementation or server-side functionality. v3 Targeted for FastMCP 3 labels Dec 24, 2025
@jlowin jlowin removed enhancement Improvement to existing functionality. For issues and smaller PR improvements. breaking change Breaks backward compatibility. Requires minor version bump. Critical for maintainer attention. server Related to FastMCP server implementation or server-side functionality. labels Dec 24, 2025
@jlowin jlowin added this to the 3.0 milestone Dec 24, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 24, 2025

Walkthrough

This pull request implements universal component key prefixing to make keys unique across different component types. It introduces type-specific prefixes (tool, prompt, resource, template) prepended to component identifiers and shifts public lookup APIs from generic "key" parameters to semantic identifiers (name for tools/prompts, uri for resources/templates). Changes include: adding KEY_PREFIX class variables and a make_key() classmethod to component classes; refactoring providers to use unified _components storage instead of separate type-specific dictionaries; replacing the TaskComponents dataclass with a flat Sequence[FastMCPComponent] in provider get_tasks() methods; introducing a new get_component(key) method for prefixed-key lookups; and updating server registration and retrieval logic to use the new semantic-based identifier scheme.

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add type-prefixed keys for globally unique component identification' accurately describes the main change of adding type prefixes to component keys for uniqueness.
Description check ✅ Passed The PR description covers all required template sections with clear examples, implementation details (unified _components storage, get_tasks return type change), and issue closure reference.
Linked Issues check ✅ Passed The PR fully addresses #2703 requirements: prevents key collisions via type prefixes (tool:, prompt:, resource:, template:), removes per-type code paths through unified component handling and get_component lookup.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #2703 objectives: adding KEY_PREFIX constants, make_key methods, unified storage, get_tasks return type change, get_component method, and related parameter renames in public APIs.
Docstring Coverage ✅ Passed Docstring coverage is 97.06% 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 type-prefixed-component-keys

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

🧹 Nitpick comments (4)
docs/development/upgrade-guide.mdx (1)

11-33: Clear migration documentation, but consider mentioning get_component.

The upgrade guide clearly documents the parameter name changes with helpful examples. However, consider adding a note about the new get_component(key) method that accepts prefixed keys (e.g., "tool:my_tool"), as this is part of the broader refactoring mentioned in the PR objectives.

📝 Suggested addition

After line 32, consider adding:

### New get_component() Method

A new `get_component(key)` method has been added that accepts fully-prefixed keys:

```python
# New unified lookup with prefixed keys
tool = await mcp.get_component("tool:my_tool")
resource = await mcp.get_component("resource:file://path")

This complements the semantic lookup methods and enables generic component handling.


</details>

</blockquote></details>
<details>
<summary>src/fastmcp/server/providers/transforming.py (1)</summary><blockquote>

`263-297`: **Consider handling unknown component types in transformation loop.**

The loop transforms known component types (Tool, ResourceTemplate, Resource, Prompt) but silently drops any other `FastMCPComponent` subclasses. While this may be intentional since only these four types exist currently, it could lead to subtle bugs if new component types are added in the future.


<details>
<summary>🔎 Optional: Add fallback or warning for unknown types</summary>

```diff
             elif isinstance(component, Prompt):
                 transformed.append(
                     component.model_copy(
                         update={"name": self._transform_prompt_name(component.name)}
                     )
                 )
+            else:
+                # Pass through unknown component types unchanged
+                transformed.append(component)

         return transformed
src/fastmcp/contrib/component_manager/component_service.py (1)

177-202: Minor: Consider renaming key parameter to name for consistency.

The prompt methods use key as the parameter name while tool methods use name. For consistency with the semantic identifier pattern (tools and prompts use names), consider renaming:

🔎 Optional: Rename for consistency
-    async def _enable_prompt(self, key: str) -> Prompt:
+    async def _enable_prompt(self, name: str) -> Prompt:
         """Handle 'enablePrompt' requests.

         Args:
-            key: The key of the prompt to enable
+            name: The name of the prompt to enable

         Returns:
             The prompt that was enabled
         """
-        logger.debug("Enabling prompt: %s", key)
+        logger.debug("Enabling prompt: %s", name)

         # 1. Check local prompts first. The server will have already applied its filter.
-        if Prompt.make_key(key) in self._server._local_provider._components:
-            prompt: Prompt = await self._server.get_prompt(key)
+        if Prompt.make_key(name) in self._server._local_provider._components:
+            prompt: Prompt = await self._server.get_prompt(name)
             prompt.enable()
             return prompt

         # 2. Check mounted servers via FastMCPProvider/TransformingProvider
         for provider in self._server._providers:
-            result = _get_mounted_server_and_key(provider, key, "prompt")
+            result = _get_mounted_server_and_key(provider, name, "prompt")
             if result is not None:
                 server, unprefixed = result
                 mounted_service = ComponentService(server)
                 prompt = await mounted_service._enable_prompt(unprefixed)
                 return prompt
-        raise NotFoundError(f"Unknown prompt: {key}")
+        raise NotFoundError(f"Unknown prompt: {name}")
src/fastmcp/server/server.py (1)

739-739: Consider extracting error messages to constants.

Static analysis flags these f-string error messages (TRY003). While this is a common pattern in the codebase, extracting messages to constants or factory methods could improve consistency and make them easier to test.

Example refactor pattern
# At module level or in exceptions module
def tool_not_found_error(name: str) -> NotFoundError:
    return NotFoundError(f"Unknown tool: {name}")

# Usage
raise tool_not_found_error(name)

Based on learnings, review and update related Manager classes when modifying MCP object definitions, though this file appears to be the server façade rather than a manager class.

Also applies to: 810-810, 849-849, 888-888, 915-915

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c49f7a and ea81576.

⛔ Files ignored due to path filters (4)
  • tests/server/providers/test_local_provider.py is excluded by none and included by none
  • tests/server/tasks/test_custom_subclass_tasks.py is excluded by none and included by none
  • tests/server/tasks/test_server_tasks_parameter.py is excluded by none and included by none
  • tests/utilities/test_components.py is excluded by none and included by none
📒 Files selected for processing (17)
  • docs/development/upgrade-guide.mdx
  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/resources/resource.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/server/providers/base.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/providers/local_provider.py
  • src/fastmcp/server/providers/openapi/provider.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/server/providers/transforming.py
  • src/fastmcp/server/server.py
  • src/fastmcp/server/tasks/requests.py
  • src/fastmcp/tools/tool.py
  • src/fastmcp/tools/tool_transform.py
  • src/fastmcp/utilities/components.py
  • src/fastmcp/utilities/inspect.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Use Python ≥ 3.10 with full type annotations
Never use bare except - be specific with exception types

Files:

  • src/fastmcp/tools/tool.py
  • src/fastmcp/utilities/inspect.py
  • src/fastmcp/tools/tool_transform.py
  • src/fastmcp/server/providers/fastmcp_provider.py
  • src/fastmcp/server/providers/openapi/provider.py
  • src/fastmcp/resources/template.py
  • src/fastmcp/server/tasks/requests.py
  • src/fastmcp/resources/resource.py
  • src/fastmcp/server/providers/proxy.py
  • src/fastmcp/prompts/prompt.py
  • src/fastmcp/utilities/components.py
  • src/fastmcp/server/providers/transforming.py
  • src/fastmcp/server/providers/base.py
  • src/fastmcp/contrib/component_manager/component_service.py
  • src/fastmcp/server/providers/local_provider.py
  • src/fastmcp/server/server.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/development/upgrade-guide.mdx
🧠 Learnings (3)
📚 Learning: 2025-12-21T21:37:55.031Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.031Z
Learning: Applies to src/fastmcp/**/__init__.py : Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`), while specialized features can live in submodules

Applied to files:

  • src/fastmcp/server/providers/transforming.py
📚 Learning: 2025-12-21T21:37:55.031Z
Learnt from: CR
Repo: jlowin/fastmcp PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-21T21:37:55.031Z
Learning: Applies to src/fastmcp/__init__.py : All module exports should be intentional - only re-export to `fastmcp.*` for fundamental types like `FastMCP` and `Client`, prefer users importing from specific submodules for specialized features

Applied to files:

  • src/fastmcp/server/providers/transforming.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/contrib/component_manager/component_service.py
  • src/fastmcp/server/server.py
🧬 Code graph analysis (11)
src/fastmcp/utilities/inspect.py (2)
src/fastmcp/contrib/mcp_mixin/mcp_mixin.py (1)
  • mcp_tool (25-52)
src/fastmcp/tools/tool.py (2)
  • to_mcp_tool (173-199)
  • to_mcp_tool (378-398)
src/fastmcp/tools/tool_transform.py (4)
src/fastmcp/tools/tool.py (1)
  • Tool (127-372)
src/fastmcp/resources/resource.py (1)
  • key (304-306)
src/fastmcp/resources/template.py (1)
  • key (269-271)
src/fastmcp/utilities/components.py (1)
  • key (99-107)
src/fastmcp/server/providers/fastmcp_provider.py (3)
src/fastmcp/server/providers/base.py (2)
  • Provider (42-323)
  • get_tasks (258-293)
src/fastmcp/utilities/components.py (2)
  • FastMCPComponent (34-183)
  • key (99-107)
src/fastmcp/resources/resource.py (1)
  • key (304-306)
src/fastmcp/server/providers/openapi/provider.py (5)
src/fastmcp/server/providers/base.py (2)
  • Provider (42-323)
  • get_tasks (258-293)
src/fastmcp/utilities/components.py (1)
  • FastMCPComponent (34-183)
src/fastmcp/server/providers/fastmcp_provider.py (1)
  • get_tasks (560-573)
src/fastmcp/server/providers/local_provider.py (1)
  • get_tasks (312-319)
src/fastmcp/server/providers/proxy.py (1)
  • get_tasks (544-551)
src/fastmcp/resources/template.py (1)
src/fastmcp/utilities/components.py (1)
  • make_key (85-96)
src/fastmcp/server/tasks/requests.py (6)
src/fastmcp/server/tasks/keys.py (1)
  • parse_task_key (47-77)
src/fastmcp/resources/resource.py (2)
  • Resource (137-333)
  • convert_result (236-241)
src/fastmcp/resources/template.py (2)
  • ResourceTemplate (97-300)
  • convert_result (182-187)
src/fastmcp/tools/tool.py (2)
  • Tool (127-372)
  • convert_result (246-284)
src/fastmcp/server/providers/base.py (1)
  • get_component (228-252)
src/fastmcp/server/dependencies.py (1)
  • message (426-427)
src/fastmcp/resources/resource.py (1)
src/fastmcp/utilities/components.py (1)
  • make_key (85-96)
src/fastmcp/utilities/components.py (2)
src/fastmcp/resources/resource.py (1)
  • key (304-306)
src/fastmcp/resources/template.py (1)
  • key (269-271)
src/fastmcp/server/providers/transforming.py (8)
src/fastmcp/server/providers/base.py (2)
  • Provider (42-323)
  • get_tasks (258-293)
src/fastmcp/tools/tool.py (1)
  • Tool (127-372)
src/fastmcp/utilities/components.py (1)
  • FastMCPComponent (34-183)
src/fastmcp/server/providers/openapi/provider.py (1)
  • get_tasks (382-384)
src/fastmcp/server/providers/proxy.py (1)
  • get_tasks (544-551)
src/fastmcp/resources/template.py (1)
  • ResourceTemplate (97-300)
src/fastmcp/resources/resource.py (1)
  • Resource (137-333)
src/fastmcp/prompts/prompt.py (1)
  • Prompt (118-324)
src/fastmcp/server/providers/base.py (10)
src/fastmcp/utilities/components.py (2)
  • FastMCPComponent (34-183)
  • key (99-107)
src/fastmcp/server/providers/local_provider.py (13)
  • get_component (299-306)
  • tool (326-342)
  • tool (345-361)
  • tool (363-499)
  • list_tools (253-260)
  • resource (501-624)
  • list_resources (267-269)
  • list_resource_templates (276-278)
  • prompt (627-639)
  • prompt (642-654)
  • prompt (656-773)
  • list_prompts (290-292)
  • get_tasks (312-319)
src/fastmcp/server/server.py (8)
  • get_component (890-915)
  • tool (1745-1760)
  • tool (1763-1778)
  • tool (1780-1866)
  • resource (1890-1975)
  • prompt (1989-2001)
  • prompt (2004-2016)
  • prompt (2018-2115)
src/fastmcp/resources/resource.py (2)
  • key (304-306)
  • Resource (137-333)
src/fastmcp/resources/template.py (2)
  • key (269-271)
  • ResourceTemplate (97-300)
src/fastmcp/prompts/prompt.py (2)
  • Prompt (118-324)
  • FunctionPrompt (327-551)
src/fastmcp/server/providers/fastmcp_provider.py (5)
  • list_tools (481-489)
  • list_resources (500-508)
  • list_resource_templates (519-528)
  • list_prompts (542-549)
  • get_tasks (560-573)
src/fastmcp/server/providers/openapi/provider.py (5)
  • list_tools (351-353)
  • list_resources (359-361)
  • list_resource_templates (367-369)
  • list_prompts (378-380)
  • get_tasks (382-384)
src/fastmcp/server/providers/transforming.py (5)
  • list_tools (169-175)
  • list_resources (191-197)
  • list_resource_templates (213-221)
  • list_prompts (241-247)
  • get_tasks (263-297)
src/fastmcp/server/tasks/config.py (1)
  • supports_tasks (69-75)
src/fastmcp/server/server.py (6)
src/fastmcp/server/providers/base.py (1)
  • get_tasks (258-293)
src/fastmcp/prompts/prompt.py (3)
  • register_with_docket (297-301)
  • register_with_docket (518-526)
  • Prompt (118-324)
src/fastmcp/resources/resource.py (4)
  • register_with_docket (308-312)
  • register_with_docket (420-428)
  • Resource (137-333)
  • key (304-306)
src/fastmcp/resources/template.py (4)
  • register_with_docket (273-277)
  • register_with_docket (383-391)
  • ResourceTemplate (97-300)
  • key (269-271)
src/fastmcp/tools/tool.py (3)
  • register_with_docket (311-315)
  • register_with_docket (483-491)
  • Tool (127-372)
src/fastmcp/utilities/components.py (3)
  • register_with_docket (155-161)
  • key (99-107)
  • make_key (85-96)
🪛 Ruff (0.14.10)
src/fastmcp/contrib/component_manager/component_service.py

86-86: Avoid specifying long messages outside the exception class

(TRY003)


113-113: Avoid specifying long messages outside the exception class

(TRY003)


144-144: Avoid specifying long messages outside the exception class

(TRY003)


175-175: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/providers/local_provider.py

129-129: Avoid specifying long messages outside the exception class

(TRY003)


159-159: Avoid specifying long messages outside the exception class

(TRY003)

src/fastmcp/server/server.py

739-739: Avoid specifying long messages outside the exception class

(TRY003)


810-810: Avoid specifying long messages outside the exception class

(TRY003)


849-849: Avoid specifying long messages outside the exception class

(TRY003)


888-888: Avoid specifying long messages outside the exception class

(TRY003)


915-915: 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 ubuntu-latest
  • GitHub Check: Run tests with lowest-direct dependencies
  • GitHub Check: Run tests: Python 3.13 on ubuntu-latest
  • GitHub Check: Run tests: Python 3.10 on windows-latest
🔇 Additional comments (35)
src/fastmcp/utilities/inspect.py (3)

122-122: LGTM! Correctly uses semantic identifier.

The change to use tool.name instead of tool.key is correct. With the new prefixed-key system, tool.key returns a prefixed value like "tool:greet", but the MCP protocol expects the unprefixed name.


168-168: LGTM! Correctly uses URI instead of prefixed key.

Using str(resource.uri) instead of resource.key is correct. The URI is the semantic identifier for resources, while the key is now prefixed (e.g., "resource:file://x").


191-191: LGTM! Correctly uses uri_template instead of prefixed key.

Consistent with the other changes, using template.uri_template directly is correct since template.key now returns a prefixed value.

src/fastmcp/prompts/prompt.py (1)

9-9: LGTM! KEY_PREFIX added correctly.

The addition of KEY_PREFIX: ClassVar[str] = "prompt" aligns with the pattern used across other component types (Tool, Resource, ResourceTemplate) and enables the unified keying system introduced in this PR.

Also applies to: 121-122

src/fastmcp/tools/tool.py (1)

11-11: LGTM! KEY_PREFIX added correctly.

The addition of KEY_PREFIX: ClassVar[str] = "tool" follows the established pattern for component type prefixing.

Also applies to: 130-131

src/fastmcp/resources/resource.py (2)

9-9: LGTM! KEY_PREFIX added correctly.

The addition of KEY_PREFIX: ClassVar[str] = "resource" follows the established pattern.

Also applies to: 140-141


305-306: LGTM! Key property correctly uses make_key.

The key property now returns a prefixed key via self.make_key(str(self.uri)), which generates keys like "resource:file://x". The updated docstring accurately describes it as a "globally unique lookup key."

src/fastmcp/resources/template.py (2)

8-8: LGTM! KEY_PREFIX added correctly.

The addition of KEY_PREFIX: ClassVar[str] = "template" follows the established pattern.

Also applies to: 100-101


270-271: LGTM! Key property correctly uses make_key.

The key property now returns a prefixed key via self.make_key(self.uri_template), generating keys like "template:weather://{city}". The updated docstring accurately describes it as a "globally unique lookup key."

src/fastmcp/utilities/components.py (4)

4-4: LGTM! ClassVar and KEY_PREFIX added correctly.

The addition of ClassVar import and the KEY_PREFIX: ClassVar[str] = "" default value on the base class enables the type-prefixed key system.

Also applies to: 37-37


39-52: LGTM! init_subclass provides helpful guidance.

The __init_subclass__ hook issues a clear warning when subclasses don't define KEY_PREFIX, helping developers adopt the new keying system. The exclusion of MirroredComponent is appropriate given it's mentioned as a mixin.


84-97: LGTM! make_key implementation is correct.

The make_key classmethod correctly constructs prefixed keys (e.g., "tool:name", "resource:uri") when KEY_PREFIX is defined, and returns the identifier unchanged otherwise for backward compatibility.


99-107: LGTM! Key property correctly uses make_key.

The updated key property now returns a prefixed key via self.make_key(self.name). The docstring clearly explains the format and notes that subclasses should override to use their specific identifier (as Resource and ResourceTemplate do).

src/fastmcp/tools/tool_transform.py (1)

930-945: LGTM! Transformation logic correctly handles prefixed keys.

The updated implementation correctly separates internal prefixed keys (used in the tools dict) from user-facing tool names (used in transformations). The logic:

  1. Looks up transformations by tool.name (unprefixed)
  2. Stores transformed tools using transformed.key (prefixed)
  3. Preserves original prefixed keys for non-transformed tools

The updated docstring clearly explains this distinction.

src/fastmcp/server/tasks/requests.py (1)

298-349: LGTM on the component type branching logic.

The isinstance checks correctly handle all four component types with appropriate ordering (ResourceTemplate before Resource to avoid incorrect matching), and the else clause provides a clear error for unexpected types.

src/fastmcp/server/providers/openapi/provider.py (1)

382-384: LGTM!

The get_tasks() method correctly returns an empty sequence since OpenAPI components don't support background tasks. The return type update to Sequence[FastMCPComponent] aligns with the unified component model.

src/fastmcp/server/providers/proxy.py (1)

544-551: LGTM!

The get_tasks() implementation correctly returns an empty list since all proxy components have task_config.mode="forbidden". The docstring appropriately explains the design decision to avoid premature client initialization during server lifespan startup.

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

560-573: LGTM!

The get_tasks() method correctly aggregates task-eligible components from all wrapped server providers into a flat Sequence[FastMCPComponent]. The iteration pattern properly handles nested provider hierarchies.


354-357: The docket key format change is correct and properly aligned with Docket registration.

The code correctly sets _docket_fn_key to self.key (e.g., "template:item://{id}"), which matches how components are registered with Docket and how they're looked up. Docket uses the prefixed key format for both docket.register(self.read, names=[self.key]) and docket.add(lookup_key) operations, so the change maintains consistency across all three operations: registration, contextvar setting, and lookup.

However, the docstring at line 335 is outdated—it says "Sets _docket_fn_key to self.uri_template" but the code actually sets it to self.key. Update the docstring to reflect the actual implementation.

Likely an incorrect or invalid review comment.

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

228-252: LGTM on get_component default implementation.

The default implementation iterates through all component types to find a match by prefixed key. While O(n) per component type, this is appropriate as a fallback since:

  1. Providers like LocalProvider override with O(1) lookup
  2. The docstring correctly indicates providers should override for efficiency
  3. It handles all component types consistently

258-293: LGTM on get_tasks implementation.

The method correctly filters for function-based components (FunctionTool, FunctionResource, FunctionResourceTemplate, FunctionPrompt) that have task_config.supports_tasks() == True. The return type change to Sequence[FastMCPComponent] aligns with the unified component model.

src/fastmcp/contrib/component_manager/component_service.py (2)

61-86: LGTM on tool enable/disable with prefixed key lookup.

The implementation correctly uses Tool.make_key(name) to construct the prefixed key for component lookup in the unified _components store, then delegates to mounted servers if not found locally.


115-144: LGTM on resource enable with fallback to template.

The logic correctly tries Resource.make_key(uri) first, then falls back to ResourceTemplate.make_key(uri), handling both resource types with a single URI parameter.

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

109-110: LGTM on unified component storage.

The single _components dictionary keyed by prefixed keys (e.g., "tool:greet", "resource:file://x") is a clean design that prevents collisions between component types while enabling O(1) lookup.


117-146: LGTM on _add_component implementation.

The generic method correctly:

  1. Handles all duplicate behaviors (error, warn, replace, ignore)
  2. Uses component.key for storage (prefixed key)
  3. Sends appropriate notifications based on component type
  4. Returns the correct type via TypeVar _C

312-319: LGTM on get_tasks implementation.

The implementation is concise and correct—iterating through the unified _components and filtering by task_config.supports_tasks(). This correctly includes all component types eligible for background tasks.


253-260: Implementation already handles key format correctly—no action needed.

The apply_transformations_to_tools function is designed to accept tools keyed by prefixed key (e.g., "tool:greet") and transformations keyed by tool name. It looks up transformations using tool.name (not the prefixed key), so the new key format is compatible and transformations match as expected.

src/fastmcp/server/server.py (8)

464-465: LGTM! Task registration now uses unified component model.

The refactored code cleanly iterates over a flat sequence of components and delegates registration to each component's register_with_docket method. This is more maintainable than the previous per-type iteration approach.


702-739: Public API correctly shifts to name-based tool retrieval.

The methods now use tool.name for indexing and accept a name parameter instead of key. This aligns with the PR's goal of using semantic identifiers in public APIs while keeping prefixed keys internal.


772-810: LGTM! Resource retrieval now uses URI-based indexing.

The shift from key-based to URI-based retrieval is consistent with the semantic identifier approach. Using str(resource.uri) as the dictionary key is appropriate for URI-based indexing.


812-849: Template retrieval correctly implements URI matching.

The method accepts an actual URI and finds a matching template by iterating through providers. The dictionary is indexed by template.uri_template for deduplication, while get_resource_template(uri) performs pattern matching.


851-888: LGTM! Prompt retrieval now uses name-based indexing.

Consistent with tool retrieval, prompts are now indexed by name in the public API. The implementation correctly uses prompt.name as the dictionary key.


890-915: New get_component() method enables prefixed-key lookups.

This method provides a unified entry point for retrieving any component type using its prefixed key (e.g., "tool:name", "resource:uri"). It follows the same provider iteration pattern as the semantic-based retrieval methods.


988-992: MCP list handlers correctly pass semantic identifiers.

The handlers now explicitly pass semantic identifiers (name for tools/prompts, uri/uriTemplate for resources/templates) to the MCP representation methods. This ensures the MCP protocol uses the appropriate identifiers.

Also applies to: 1053-1057, 1118-1122, 1184-1188


1281-1281: Handlers correctly construct prefixed keys for docket.

The handlers use Tool.make_key(), Resource.make_key(), and Prompt.make_key() to convert semantic identifiers (name/URI) to prefixed keys before setting them in context variables. This ensures docket task tracking uses the globally unique prefixed keys.

Also applies to: 1325-1325, 1377-1377

Comment on lines +279 to +289
# Parse task key to get component key
key_parts = parse_task_key(task_key)
task_type = key_parts["task_type"]
component_id = key_parts["component_identifier"]
component_key = key_parts["component_identifier"]

# Look up component by its prefixed key
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools.tool import Tool

component = await server.get_component(component_key)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Dec 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Component lookup uses raw identifier instead of prefixed key.

The code extracts component_identifier from the parsed task key (e.g., "my_tool" or "file://data.txt"), but server.get_component() expects a prefixed key (e.g., "tool:my_tool" or "resource:file://data.txt").

You need to reconstruct the prefixed key using the task_type from the parsed key:

🔎 Proposed fix
         # Parse task key to get component key
         key_parts = parse_task_key(task_key)
-        component_key = key_parts["component_identifier"]
+        task_type = key_parts["task_type"]
+        component_identifier = key_parts["component_identifier"]
+        component_key = f"{task_type}:{component_identifier}"

         # Look up component by its prefixed key
         from fastmcp.prompts.prompt import Prompt
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Parse task key to get component key
key_parts = parse_task_key(task_key)
task_type = key_parts["task_type"]
component_id = key_parts["component_identifier"]
component_key = key_parts["component_identifier"]
# Look up component by its prefixed key
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools.tool import Tool
component = await server.get_component(component_key)
# Parse task key to get component key
key_parts = parse_task_key(task_key)
task_type = key_parts["task_type"]
component_identifier = key_parts["component_identifier"]
component_key = f"{task_type}:{component_identifier}"
# Look up component by its prefixed key
from fastmcp.prompts.prompt import Prompt
from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ResourceTemplate
from fastmcp.tools.tool import Tool
component = await server.get_component(component_key)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

no - the component identifier is the compoennt key. no issue here.

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.

@jlowin Thank you for the clarification! You're absolutely right—I misunderstood what parse_task_key() returns. The component_identifier is already the full prefixed key (e.g., "tool:greet"), not just the bare identifier. My suggested "fix" would have actually broken the code by double-prefixing. Apologies for the confusion!

@marvin-context-protocol
Copy link
Copy Markdown
Contributor

Test Failure Analysis

Summary: The test is failing on Python 3.13 (ubuntu-latest) because the rate limiter is not triggering as expected - the test expects a ToolError to be raised but the request completes successfully.

Root Cause: This appears to be a race condition or timing issue specific to Python 3.13. The test sets up a rate limiter with:

  • max_requests_per_second=9.0
  • burst_capacity=5

The test then makes:

  1. One initialize request (during client connection)
  2. One list_tools request (automatic)
  3. Two call_tool requests explicitly

The test expects the 4th explicit operation (3rd call_tool) to be rate-limited, but on Python 3.13, it's completing successfully. This suggests that either:

  • The timing between requests is slightly different on Python 3.13, allowing token bucket refills
  • The initialization sequence is different on Python 3.13
  • There's a race condition in how requests are counted

Suggested Solution:

Make the test more deterministic by:

  1. Increase burst capacity accounting: The test needs to account for ALL requests including initialization. Update line 378:

    RateLimitingMiddleware(max_requests_per_second=9.0, burst_capacity=4)  # Reduced from 5
  2. Or use a more restrictive rate limit: Make the refill rate slower to ensure the test is deterministic:

    RateLimitingMiddleware(max_requests_per_second=1.0, burst_capacity=4)
  3. Or add explicit list_tools call before testing: Make the initialization predictable:

    async with Client(rate_limit_server) as client:
        # Explicit list_tools to consume tokens predictably
        await client.list_tools()
        
        # Mix different operations
        await client.call_tool("quick_action", {"message": "test"})
        await client.call_tool("heavy_computation")
        
        # Should be rate limited
        with pytest.raises(ToolError, match="Rate limit exceeded"):
            await client.call_tool("batch_process", {"items": ["a", "b", "c"]})

The most robust solution is option 1 (reduce burst_capacity to 4) as it's the minimal change and makes the test expectations match reality.

Detailed Analysis

Failure Log:

FAILED tests/server/middleware/test_rate_limiting.py::TestRateLimitingMiddlewareIntegration::test_rate_limiting_with_different_operations - Failed: DID NOT RAISE <class 'fastmcp.exceptions.ToolError'>

Test Code (lines 375-388):

async def test_rate_limiting_with_different_operations(self, rate_limit_server):
    """Test that rate limiting applies to all types of operations."""
    rate_limit_server.add_middleware(
        RateLimitingMiddleware(max_requests_per_second=9.0, burst_capacity=5)
    )

    async with Client(rate_limit_server) as client:
        # Mix different operations
        await client.call_tool("quick_action", {"message": "test"})
        await client.call_tool("heavy_computation")

        # Should be rate limited regardless of operation type
        with pytest.raises(ToolError, match="Rate limit exceeded"):
            await client.call_tool("batch_process", {"items": ["a", "b", "c"]})

Why It's Failing: The rate limiter uses a token bucket algorithm. With burst_capacity=5, the test assumes exactly 5 tokens will be consumed before the rate limit kicks in. However, the actual request sequence includes:

  • 1 initialize (during async with Client(...))
  • 1 list_tools (automatic on connection)
  • 2 call_tool (explicit)
    = 4 total requests before the expected failure

The 5th request (the one expected to fail) is actually within the burst capacity, so it succeeds on Python 3.13. This might work on other Python versions due to timing variations or implementation differences in asyncio.

Related Files
  • tests/server/middleware/test_rate_limiting.py:375-388 - Failing test that needs burst_capacity adjustment
  • src/fastmcp/server/middleware/rate_limiting.py - Rate limiting middleware implementation using token bucket algorithm

@jlowin jlowin merged commit aef1ccc into main Dec 24, 2025
28 of 31 checks passed
@jlowin jlowin deleted the type-prefixed-component-keys branch December 24, 2025 13:39
@jlowin jlowin added the enhancement Improvement to existing functionality. For issues and smaller PR improvements. label Jan 20, 2026
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. v3 Targeted for FastMCP 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ensure component keys are universal

1 participant