mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 23:44:17 +02:00
Route app tool calls through provider chain, remove process-level registry (#3587)
Adds Provider.get_app_tool(app_name, tool_name) — a dedicated method for finding app-visible tools by their original name, bypassing transforms. AggregateProvider queries children, WrappedProvider delegates to inner, FastMCPProvider delegates to nested server. The default implementation checks _get_tool and matches meta.fastmcp.app. This replaces the process-level _APP_TOOLS registry. Tool routing now works through the provider tree, which exists in every process — no shared state needed for horizontal scaling.
This commit is contained in:
parent
52feff6878
commit
96497acd16
7 changed files with 164 additions and 127 deletions
|
|
@ -1,11 +1,11 @@
|
|||
"""FastMCPApp — a Provider that represents a composable MCP application.
|
||||
|
||||
FastMCPApp binds entry-point tools (model calls these) together with backend
|
||||
tools (the UI calls these via CallTool). Backend tools are registered in a
|
||||
process-level registry keyed by ``(app_name, tool_name)`` so that
|
||||
``CallTool("save_contact")`` reaches the right tool regardless of namespace
|
||||
transforms — the server looks up the tool directly when the request carries
|
||||
``_meta.fastmcp.app``.
|
||||
tools (the UI calls these via CallTool). Backend tools are tagged with
|
||||
``meta["fastmcp"]["app"]`` so they can be found through the provider chain
|
||||
even when transforms (namespace, visibility, etc.) have renamed or hidden
|
||||
them — the server sets a context var that tells ``Provider.get_tool`` to
|
||||
fall back to a direct lookup for app-visible tools.
|
||||
|
||||
Usage::
|
||||
|
||||
|
|
@ -44,20 +44,6 @@ logger = get_logger(__name__)
|
|||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Process-level registry
|
||||
# ---------------------------------------------------------------------------
|
||||
# (app_name, tool_name) → Tool object. Populated by @app.tool() at
|
||||
# decoration time. The server checks this registry (via get_app_tool)
|
||||
# when a call_tool request carries _meta.fastmcp.app, bypassing the
|
||||
# normal transform chain entirely.
|
||||
_APP_TOOLS: dict[tuple[str, str], Tool] = {}
|
||||
|
||||
|
||||
def get_app_tool(app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Look up an app tool by (app_name, tool_name), or return None."""
|
||||
return _APP_TOOLS.get((app_name, tool_name))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CallTool resolver
|
||||
|
|
@ -138,9 +124,8 @@ class FastMCPApp(Provider):
|
|||
|
||||
Binds together entry-point tools (``@app.ui``), backend tools
|
||||
(``@app.tool``), and the Prefab renderer resource. Backend tools
|
||||
are registered in a process-level registry keyed by
|
||||
``(app_name, tool_name)`` so the server can route app-originated
|
||||
calls directly, bypassing transforms.
|
||||
are tagged with ``meta["fastmcp"]["app"]`` so ``Provider.get_tool``
|
||||
can find them by original name even when transforms have been applied.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
|
|
@ -217,7 +202,10 @@ class FastMCPApp(Provider):
|
|||
from fastmcp.server.apps import AppConfig, app_config_to_meta_dict
|
||||
|
||||
app_config = AppConfig(visibility=visibility)
|
||||
meta: dict[str, Any] = {"ui": app_config_to_meta_dict(app_config)}
|
||||
meta: dict[str, Any] = {
|
||||
"ui": app_config_to_meta_dict(app_config),
|
||||
"fastmcp": {"app": self.name},
|
||||
}
|
||||
|
||||
tool_obj = Tool.from_function(
|
||||
fn,
|
||||
|
|
@ -228,7 +216,6 @@ class FastMCPApp(Provider):
|
|||
auth=auth,
|
||||
)
|
||||
self._local._add_component(tool_obj)
|
||||
_APP_TOOLS[(self.name, resolved_name)] = tool_obj
|
||||
return fn
|
||||
|
||||
return _dispatch_decorator(name_or_fn, name, _register, "tool")
|
||||
|
|
@ -283,9 +270,8 @@ class FastMCPApp(Provider):
|
|||
"""Register a UI entry-point tool that the model calls.
|
||||
|
||||
Entry-point tools default to ``visibility=["model"]`` and auto-wire
|
||||
the Prefab renderer resource and CSP. They do NOT get registered in
|
||||
the app tool registry — the model resolves them through the normal
|
||||
transform chain.
|
||||
the Prefab renderer resource and CSP. They are tagged with the app
|
||||
name so structured content includes ``_meta.fastmcp.app``.
|
||||
|
||||
Supports multiple calling patterns::
|
||||
|
||||
|
|
@ -363,13 +349,17 @@ class FastMCPApp(Provider):
|
|||
) -> Tool:
|
||||
"""Add a tool to this app programmatically.
|
||||
|
||||
The tool is registered as a backend tool in the app's registry.
|
||||
The tool is tagged with this app's name for routing.
|
||||
"""
|
||||
if not isinstance(tool, Tool):
|
||||
tool = Tool._ensure_tool(tool)
|
||||
|
||||
# Tag with app name for routing
|
||||
meta = dict(tool.meta) if tool.meta else {}
|
||||
meta.setdefault("fastmcp", {})["app"] = self.name
|
||||
tool.meta = meta
|
||||
|
||||
self._local._add_component(tool)
|
||||
_APP_TOOLS[(self.name, tool.name)] = tool
|
||||
return tool
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -168,6 +168,19 @@ class AggregateProvider(Provider):
|
|||
)
|
||||
return self._get_highest_version_result(results, f"get_tool({name!r})") # type: ignore[return-value]
|
||||
|
||||
async def get_app_tool(self, app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Query all child providers for an app tool."""
|
||||
results = await gather(
|
||||
*[p.get_app_tool(app_name, tool_name) for p in self.providers],
|
||||
return_exceptions=True,
|
||||
)
|
||||
for r in results:
|
||||
if isinstance(r, BaseException):
|
||||
continue
|
||||
if r is not None:
|
||||
return r
|
||||
return None
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Resources
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -158,11 +158,6 @@ class Provider:
|
|||
(FastMCP) performs enabled filtering after all transforms complete,
|
||||
allowing session-level transforms to override provider-level disables.
|
||||
|
||||
If the transform chain returns None, a fallback checks for
|
||||
app-visible tools by their original (untransformed) name. This
|
||||
lets ``CallTool("save_contact")`` reach backend tools even when
|
||||
namespace or other transforms have been applied to the provider.
|
||||
|
||||
Args:
|
||||
name: The transformed tool name to look up.
|
||||
version: Optional version filter. If None, returns highest version.
|
||||
|
|
@ -180,6 +175,29 @@ class Provider:
|
|||
|
||||
return await chain(name, version=version)
|
||||
|
||||
async def get_app_tool(self, app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Look up an app-visible tool by original name, bypassing transforms.
|
||||
|
||||
This is the routing path for tool calls from app UIs (identified by
|
||||
``_meta.fastmcp.app`` on the request). It skips the transform chain
|
||||
entirely — the tool is found by its registered name and matched
|
||||
against the app identity in its metadata.
|
||||
|
||||
The default implementation checks this provider's own storage via
|
||||
``_get_tool``. Aggregate and wrapped providers override to
|
||||
delegate to children.
|
||||
|
||||
Returns:
|
||||
The tool if found and tagged with the given app name, else None.
|
||||
"""
|
||||
tool = await self._get_tool(tool_name)
|
||||
if tool is not None:
|
||||
meta = tool.meta or {}
|
||||
fastmcp_meta = meta.get("fastmcp")
|
||||
if isinstance(fastmcp_meta, dict) and fastmcp_meta.get("app") == app_name:
|
||||
return tool
|
||||
return None
|
||||
|
||||
async def list_resources(self) -> Sequence[Resource]:
|
||||
"""List resources with all transforms applied.
|
||||
|
||||
|
|
|
|||
|
|
@ -562,6 +562,10 @@ class FastMCPProvider(Provider):
|
|||
return None
|
||||
return FastMCPProviderTool.wrap(self.server, raw_tool)
|
||||
|
||||
async def get_app_tool(self, app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Delegate to nested server's get_app_tool, bypassing transforms."""
|
||||
return await self.server.get_app_tool(app_name, tool_name)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Resource methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ class _WrappedProvider(Provider):
|
|||
"""Delegate to inner's get_tool (includes inner's transforms)."""
|
||||
return await self._inner.get_tool(name, version)
|
||||
|
||||
async def get_app_tool(self, app_name: str, tool_name: str) -> Tool | None:
|
||||
"""Delegate to inner, bypassing this wrapper's transforms."""
|
||||
return await self._inner.get_app_tool(app_name, tool_name)
|
||||
|
||||
async def _list_resources(self) -> Sequence[Resource]:
|
||||
"""Delegate to inner's list_resources (includes inner's transforms)."""
|
||||
return await self._inner.list_resources()
|
||||
|
|
|
|||
|
|
@ -1100,14 +1100,13 @@ class FastMCP(
|
|||
with server_span(
|
||||
f"tools/call {name}", "tools/call", self.name, "tool", name
|
||||
) as span:
|
||||
# If the call came from an app UI (_meta.fastmcp.app is set),
|
||||
# look up the tool directly in the app registry, bypassing
|
||||
# transforms. Otherwise use normal provider resolution.
|
||||
# If the call came from an app UI (_meta.fastmcp.app),
|
||||
# look up the tool via get_app_tool which walks the
|
||||
# provider tree bypassing transforms. Otherwise use
|
||||
# normal provider resolution.
|
||||
tool: Tool | None = None
|
||||
if app_name is not None:
|
||||
from fastmcp.server.app import get_app_tool
|
||||
|
||||
tool = get_app_tool(app_name, name)
|
||||
tool = await self.get_app_tool(app_name, name)
|
||||
if tool is not None:
|
||||
# Auth still applies to app tools
|
||||
skip_auth, token = _get_auth_context()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Covers:
|
||||
- @app.tool() decorator (visibility, calling patterns)
|
||||
- @app.ui() decorator (model visibility, CSP auto-wiring)
|
||||
- App tool registry and call_tool routing
|
||||
- get_app_tool routing through provider chain
|
||||
- Callable resolver (_resolve_tool_ref)
|
||||
- Composition with namespaced servers
|
||||
- Provider interface delegation
|
||||
|
|
@ -18,32 +18,17 @@ from prefab_ui.app import ResolvedTool
|
|||
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.server.app import (
|
||||
_APP_TOOLS,
|
||||
FastMCPApp,
|
||||
_resolve_tool_ref,
|
||||
get_app_tool,
|
||||
)
|
||||
from fastmcp.tools.base import Tool
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures / helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _clear_registries() -> None:
|
||||
"""Clear process-level registries between tests."""
|
||||
_APP_TOOLS.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# @app.tool() decorator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAppTool:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
def test_tool_bare_decorator(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -111,24 +96,17 @@ class TestAppTool:
|
|||
assert len(tools) == 1
|
||||
assert tools[0].name == "custom_save"
|
||||
|
||||
def test_tool_registered_in_app_tools(self):
|
||||
app = FastMCPApp("test")
|
||||
async def test_tool_has_app_name_in_meta(self):
|
||||
app = FastMCPApp("contacts")
|
||||
|
||||
@app.tool()
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
assert ("test", "save") in _APP_TOOLS
|
||||
assert _APP_TOOLS[("test", "save")].name == "save"
|
||||
|
||||
def test_tool_custom_name_registered_correctly(self):
|
||||
app = FastMCPApp("myapp")
|
||||
|
||||
@app.tool("custom_save")
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
assert ("myapp", "custom_save") in _APP_TOOLS
|
||||
tools = await app._list_tools()
|
||||
meta = tools[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["app"] == "contacts"
|
||||
|
||||
async def test_tool_default_visibility_app_only(self):
|
||||
app = FastMCPApp("test")
|
||||
|
|
@ -154,19 +132,6 @@ class TestAppTool:
|
|||
assert meta is not None
|
||||
assert meta["ui"]["visibility"] == ["app", "model"]
|
||||
|
||||
async def test_tool_no_global_key_in_meta(self):
|
||||
"""Tools should NOT have globalKey in meta (removed in refactor)."""
|
||||
app = FastMCPApp("test")
|
||||
|
||||
@app.tool()
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
tools = await app._list_tools()
|
||||
meta = tools[0].meta
|
||||
assert meta is not None
|
||||
assert "globalKey" not in meta.get("ui", {})
|
||||
|
||||
def test_tool_with_description(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -196,9 +161,6 @@ class TestAppTool:
|
|||
|
||||
|
||||
class TestAppUI:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
def test_ui_bare_decorator(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -249,15 +211,17 @@ class TestAppUI:
|
|||
assert meta is not None
|
||||
assert meta["ui"]["visibility"] == ["model"]
|
||||
|
||||
def test_ui_not_in_app_tools(self):
|
||||
"""UI entry points should NOT be in the app tool registry."""
|
||||
async def test_ui_has_app_name_in_meta(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
@app.ui()
|
||||
def dashboard() -> str:
|
||||
return "dashboard"
|
||||
|
||||
assert ("test", "dashboard") not in _APP_TOOLS
|
||||
tools = await app._list_tools()
|
||||
meta = tools[0].meta
|
||||
assert meta is not None
|
||||
assert meta["fastmcp"]["app"] == "test"
|
||||
|
||||
async def test_ui_has_resource_uri(self):
|
||||
app = FastMCPApp("test")
|
||||
|
|
@ -312,9 +276,6 @@ class TestAppUI:
|
|||
|
||||
|
||||
class TestResolveToolRef:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
def test_resolve_string_passes_through(self):
|
||||
"""Strings pass through as-is — server resolves at call time."""
|
||||
result = _resolve_tool_ref("save_contact")
|
||||
|
|
@ -347,44 +308,43 @@ class TestResolveToolRef:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_app_tool registry
|
||||
# get_app_tool — provider chain routing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetAppTool:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
def test_lookup_by_app_and_tool_name(self):
|
||||
async def test_direct_lookup(self):
|
||||
"""FastMCPApp.get_app_tool finds tools by app name + tool name."""
|
||||
app = FastMCPApp("contacts")
|
||||
|
||||
@app.tool()
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
tool = get_app_tool("contacts", "save")
|
||||
tool = await app.get_app_tool("contacts", "save")
|
||||
assert tool is not None
|
||||
assert tool.name == "save"
|
||||
|
||||
def test_lookup_wrong_app_returns_none(self):
|
||||
async def test_wrong_app_returns_none(self):
|
||||
app = FastMCPApp("contacts")
|
||||
|
||||
@app.tool()
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
assert get_app_tool("billing", "save") is None
|
||||
assert await app.get_app_tool("billing", "save") is None
|
||||
|
||||
def test_lookup_wrong_tool_returns_none(self):
|
||||
async def test_wrong_tool_returns_none(self):
|
||||
app = FastMCPApp("contacts")
|
||||
|
||||
@app.tool()
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
assert get_app_tool("contacts", "missing") is None
|
||||
assert await app.get_app_tool("contacts", "missing") is None
|
||||
|
||||
def test_two_apps_same_tool_name_no_collision(self):
|
||||
async def test_two_apps_no_collision(self):
|
||||
"""Two apps with the same tool name are disambiguated."""
|
||||
app1 = FastMCPApp("contacts")
|
||||
app2 = FastMCPApp("billing")
|
||||
|
||||
|
|
@ -396,12 +356,47 @@ class TestGetAppTool:
|
|||
def save_billing(amount: int) -> str:
|
||||
return f"invoice: {amount}"
|
||||
|
||||
t1 = get_app_tool("contacts", "save")
|
||||
t2 = get_app_tool("billing", "save")
|
||||
server = FastMCP("Platform")
|
||||
server.add_provider(app1)
|
||||
server.add_provider(app2)
|
||||
|
||||
t1 = await server.get_app_tool("contacts", "save")
|
||||
t2 = await server.get_app_tool("billing", "save")
|
||||
assert t1 is not None
|
||||
assert t2 is not None
|
||||
assert t1 is not t2
|
||||
|
||||
async def test_survives_namespace_transform(self):
|
||||
"""get_app_tool bypasses namespace transforms."""
|
||||
app = FastMCPApp("crm")
|
||||
|
||||
@app.tool()
|
||||
def save_contact(name: str) -> str:
|
||||
return name
|
||||
|
||||
server = FastMCP("Platform")
|
||||
server.add_provider(app, namespace="crm")
|
||||
|
||||
# Normal get_tool with untransformed name fails
|
||||
tool = await server.get_tool("save_contact")
|
||||
assert tool is None
|
||||
|
||||
# get_app_tool bypasses transforms
|
||||
tool = await server.get_app_tool("crm", "save_contact")
|
||||
assert tool is not None
|
||||
assert tool.name == "save_contact"
|
||||
|
||||
async def test_ui_tool_findable_by_app_name(self):
|
||||
"""@app.ui() tools are also tagged with app name."""
|
||||
app = FastMCPApp("dashboard")
|
||||
|
||||
@app.ui()
|
||||
def show() -> str:
|
||||
return "ui"
|
||||
|
||||
tool = await app.get_app_tool("dashboard", "show")
|
||||
assert tool is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider interface
|
||||
|
|
@ -409,9 +404,6 @@ class TestGetAppTool:
|
|||
|
||||
|
||||
class TestProviderInterface:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
async def test_list_tools_empty(self):
|
||||
app = FastMCPApp("test")
|
||||
assert await app._list_tools() == []
|
||||
|
|
@ -442,11 +434,8 @@ class TestProviderInterface:
|
|||
|
||||
|
||||
class TestCallToolAppRouting:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
async def test_call_tool_with_app_name(self):
|
||||
"""Server.call_tool routes directly when app_name is provided."""
|
||||
"""Server.call_tool routes via get_app_tool when app_name is set."""
|
||||
app = FastMCPApp("contacts")
|
||||
|
||||
@app.tool()
|
||||
|
|
@ -459,7 +448,7 @@ class TestCallToolAppRouting:
|
|||
result = await server.call_tool("save", {"name": "alice"}, app_name="contacts")
|
||||
assert result.content[0].text == "saved alice" # type: ignore[union-attr]
|
||||
|
||||
async def test_call_tool_by_name_without_app_name(self):
|
||||
async def test_call_tool_without_app_name(self):
|
||||
"""Regular name-based resolution still works."""
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -474,7 +463,7 @@ class TestCallToolAppRouting:
|
|||
assert result.content[0].text == "saved bob" # type: ignore[union-attr]
|
||||
|
||||
async def test_app_name_survives_namespace(self):
|
||||
"""app_name routing works even when the app is namespaced."""
|
||||
"""app_name routing bypasses namespace transforms."""
|
||||
app = FastMCPApp("crm")
|
||||
|
||||
@app.tool()
|
||||
|
|
@ -484,7 +473,6 @@ class TestCallToolAppRouting:
|
|||
server = FastMCP("Platform")
|
||||
server.add_provider(app, namespace="crm")
|
||||
|
||||
# app_name routes directly, bypassing namespace
|
||||
result = await server.call_tool(
|
||||
"save_contact", {"name": "alice"}, app_name="crm"
|
||||
)
|
||||
|
|
@ -527,7 +515,7 @@ class TestCallToolAppRouting:
|
|||
_current_transport.reset(token)
|
||||
|
||||
async def test_two_apps_same_tool_name_routed_correctly(self):
|
||||
"""Two apps with same tool name are disambiguated by app_name."""
|
||||
"""Two apps with same tool name disambiguated by app_name."""
|
||||
contacts = FastMCPApp("contacts")
|
||||
billing = FastMCPApp("billing")
|
||||
|
||||
|
|
@ -549,6 +537,25 @@ class TestCallToolAppRouting:
|
|||
assert r1.content[0].text == "contact: alice" # type: ignore[union-attr]
|
||||
assert r2.content[0].text == "invoice: 100" # type: ignore[union-attr]
|
||||
|
||||
async def test_deeply_nested_app(self):
|
||||
"""App tool is found even through multiple levels of nesting."""
|
||||
app = FastMCPApp("deep")
|
||||
|
||||
@app.tool()
|
||||
def hidden(x: str) -> str:
|
||||
return x
|
||||
|
||||
inner = FastMCP("Inner")
|
||||
inner.add_provider(app, namespace="app")
|
||||
|
||||
outer = FastMCP("Outer")
|
||||
outer.mount(inner, namespace="inner")
|
||||
|
||||
# Normal resolution: would need "inner_app_hidden"
|
||||
# App routing: bypasses all transforms
|
||||
result = await outer.call_tool("hidden", {"x": "found"}, app_name="deep")
|
||||
assert result.content[0].text == "found" # type: ignore[union-attr]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end via Client
|
||||
|
|
@ -556,9 +563,6 @@ class TestCallToolAppRouting:
|
|||
|
||||
|
||||
class TestEndToEnd:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
async def test_ui_tool_visible_to_client(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -607,9 +611,6 @@ class TestRun:
|
|||
|
||||
|
||||
class TestAddTool:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
async def test_add_tool_from_function(self):
|
||||
app = FastMCPApp("test")
|
||||
|
||||
|
|
@ -622,14 +623,25 @@ class TestAddTool:
|
|||
tools = await app._list_tools()
|
||||
assert len(tools) == 1
|
||||
|
||||
async def test_add_tool_registered_in_app_tools(self):
|
||||
app = FastMCPApp("test")
|
||||
async def test_add_tool_tagged_with_app_name(self):
|
||||
app = FastMCPApp("myapp")
|
||||
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
tool = app.add_tool(save)
|
||||
assert tool.meta is not None
|
||||
assert tool.meta["fastmcp"]["app"] == "myapp"
|
||||
|
||||
async def test_add_tool_findable_via_get_app_tool(self):
|
||||
app = FastMCPApp("myapp")
|
||||
|
||||
def save(name: str) -> str:
|
||||
return name
|
||||
|
||||
app.add_tool(save)
|
||||
assert ("test", "save") in _APP_TOOLS
|
||||
tool = await app.get_app_tool("myapp", "save")
|
||||
assert tool is not None
|
||||
|
||||
async def test_add_tool_object(self):
|
||||
app = FastMCPApp("test")
|
||||
|
|
@ -647,9 +659,6 @@ class TestAddTool:
|
|||
|
||||
|
||||
class TestComposition:
|
||||
def setup_method(self) -> None:
|
||||
_clear_registries()
|
||||
|
||||
async def test_multiple_apps_on_one_server(self):
|
||||
crm = FastMCPApp("CRM")
|
||||
billing = FastMCPApp("Billing")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue