Skip to content

Unify discovery API: deduplicate at protocol layer only#2919

Merged
jlowin merged 4 commits into
mainfrom
feature/phase-b-dedupe-to-protocol
Jan 19, 2026
Merged

Unify discovery API: deduplicate at protocol layer only#2919
jlowin merged 4 commits into
mainfrom
feature/phase-b-dedupe-to-protocol

Conversation

@jlowin
Copy link
Copy Markdown
Member

@jlowin jlowin commented Jan 19, 2026

Phase B of the provider refactor (follows #2913): consolidates the discovery API surface.

Before: Four methods per component type - list_tools() (raw), get_tools() (filtered + deduplicated), _list_tools() (override point), _get_tool() (override point).

After: list_tools() returns all versions with server-specific filtering (enabled, auth, middleware). Deduplication happens only in protocol handlers when serializing for MCP clients.

This enables version requests to flow through mounted server hierarchies - a parent can now request any version from a child, not just the deduplicated "latest" version.

# list_tools() returns ALL versions
tools = await mcp.list_tools()  # [add@1.0, add@2.0, multiply@1.0]

# get_tool() finds highest matching version
tool = await mcp.get_tool("add")  # add@2.0

# Protocol handler deduplicates for MCP wire format
# Client sees: [add (latest annotation shows 2.0), multiply]

The run_middleware: bool = True parameter prevents recursion when middleware wraps list calls.

…otocol layer

list_tools() now returns all versions with a run_middleware flag to avoid
recursion. Deduplication moves to protocol handlers only, enabling version
requests to flow through mounted server hierarchies.
@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. provider Related to the FastMCP Provider class labels Jan 19, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 19, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR replaces public get_* APIs with list_* equivalents (get_tools/get_resources/get_prompts/get_resource_templates → list_tools/list_resources/list_prompts/list_resource_templates), flips the default run_middleware to True and returns Sequence types, adds deduplication in MCP list handlers, and threads explicit VersionSpec/version through FastMCPProvider and server call paths (run/read/render/call). It also updates provider internals to call non-middleware list methods, adjusts utilities and example code to use the new list_* names, and adds a small signature/annotation change in an example function.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main architectural change: consolidating the discovery API by moving deduplication from application layer to protocol layer.
Description check ✅ Passed The PR description provides clear context about the refactor phase, before/after comparison, rationale (enabling version requests), and concrete examples of behavior changes.
Docstring Coverage ✅ Passed Docstring coverage is 94.44% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Caution

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

⚠️ Outside diff range comments (1)
src/fastmcp/server/providers/fastmcp_provider.py (1)

117-138: Forward versions for resources/prompts/templates too.

Tools now pass exact versions to the child server, but resource/prompt/template wrappers still call without version, so a version-specific request can resolve the wrong version in mounted hierarchies. Please propagate the wrapper’s version in those delegate calls as well.

✅ Suggested fix to keep all component types consistent
 class FastMCPProviderResource(Resource):
@@
     async def _read(
         self, task_meta: TaskMeta | None = None
     ) -> ResourceResult | mcp.types.CreateTaskResult:
@@
-        with delegate_span(
+        version = VersionSpec(eq=self.version) if self.version else None
+        with delegate_span(
             self._original_uri or "", "FastMCPProvider", self._original_uri or ""
         ):
             return await self._server.read_resource(
-                self._original_uri, task_meta=task_meta
+                self._original_uri, version=version, task_meta=task_meta
             )

 class FastMCPProviderPrompt(Prompt):
@@
     async def _render(
         self,
         arguments: dict[str, Any] | None = None,
         task_meta: TaskMeta | None = None,
     ) -> PromptResult | mcp.types.CreateTaskResult:
@@
-        with delegate_span(
+        version = VersionSpec(eq=self.version) if self.version else None
+        with delegate_span(
             self._original_name or "", "FastMCPProvider", self._original_name or ""
         ):
             return await self._server.render_prompt(
-                self._original_name, arguments, task_meta=task_meta
+                self._original_name, arguments, version=version, task_meta=task_meta
             )

     async def render(self, arguments: dict[str, Any] | None = None) -> PromptResult:
@@
-        result = await self._server.render_prompt(self._original_name, arguments)
+        version = VersionSpec(eq=self.version) if self.version else None
+        result = await self._server.render_prompt(
+            self._original_name, arguments, version=version
+        )

 class FastMCPProviderResourceTemplate(ResourceTemplate):
@@
     async def _read(
         self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
     ) -> ResourceResult | mcp.types.CreateTaskResult:
@@
-        with delegate_span(
+        version = VersionSpec(eq=self.version) if self.version else None
+        with delegate_span(
             original_uri, "FastMCPProvider", self._original_uri_template or ""
         ):
-            return await self._server.read_resource(original_uri, task_meta=task_meta)
+            return await self._server.read_resource(
+                original_uri, version=version, task_meta=task_meta
+            )

     async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
@@
-        result = await self._server.read_resource(original_uri)
+        version = VersionSpec(eq=self.version) if self.version else None
+        result = await self._server.read_resource(original_uri, version=version)
Based on learnings, maintain consistency across Tools, Resources, Resource Templates, and Prompts.
🧹 Nitpick comments (1)
src/fastmcp/server/server.py (1)

1008-1041: Silence unused middleware lambda arg to satisfy Ruff (ARG005).

Rename the unused lambda parameter to _context to keep lint clean without behavior changes.

♻️ Proposed cleanup
-                    call_next=lambda context: self.list_tools(run_middleware=False),
+                    call_next=lambda _context: self.list_tools(run_middleware=False),
@@
-                    call_next=lambda context: self.list_resources(run_middleware=False),
+                    call_next=lambda _context: self.list_resources(run_middleware=False),
@@
-                    call_next=lambda context: self.list_resource_templates(
+                    call_next=lambda _context: self.list_resource_templates(
                         run_middleware=False
                     ),
@@
-                    call_next=lambda context: self.list_prompts(run_middleware=False),
+                    call_next=lambda _context: self.list_prompts(run_middleware=False),

Also applies to: 1095-1130, 1183-1222, 1275-1308

Comment thread src/fastmcp/contrib/mcp_mixin/example.py Outdated
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

Here are some automated review suggestions for this pull request.

Reviewed commit: c307eb4f76

ℹ️ 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".

Comment thread src/fastmcp/server/providers/fastmcp_provider.py
Comment thread src/fastmcp/server/providers/fastmcp_provider.py
@jlowin jlowin merged commit c3111a8 into main Jan 19, 2026
11 checks passed
@jlowin jlowin deleted the feature/phase-b-dedupe-to-protocol branch January 19, 2026 02:01
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Jan 30, 2026
gfortaine pushed a commit to gfortaine/fastmcp that referenced this pull request Feb 4, 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. provider Related to the FastMCP Provider class 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