mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-09-01 12:03:19 +02:00
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com> Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com> Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com> Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com> Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Co-authored-by: nightcityblade <nightcityblade@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Bill Easton <strawgate@users.noreply.github.com> Co-authored-by: Sumanshu Nankana <sumanshunankana@gmail.com> Co-authored-by: Eric Robinson <ericrobinson@indeed.com> Co-authored-by: Martim Santos <martimfasantos@gmail.com> Co-authored-by: d 🔹 <liusway405@gmail.com> Co-authored-by: Matthieu B <66959271+mtthidoteu@users.noreply.github.com> Co-authored-by: Sascha Buehrle <47737812+saschabuehrle@users.noreply.github.com> Co-authored-by: Hakancan <142545736+hkc5@users.noreply.github.com> Co-authored-by: nightcityblade <jackchen@haloailabs.com> Co-authored-by: Matt Hallowell <17804673+mhallo@users.noreply.github.com> Co-authored-by: nate nowack <thrast36@gmail.com> Co-authored-by: Bill Easton <williamseaston@gmail.com> Co-authored-by: Marcus Shu <46469249+shulkx@users.noreply.github.com> Co-authored-by: Rushabh Doshi <radoshi@gmail.com> Co-authored-by: AIKAWA Shigechika <shige@aikawa.jp> Co-authored-by: Jeremy Simon <simonjer805@gmail.com> Co-authored-by: Miguel Miranda Dias <7780875+pandego@users.noreply.github.com> Co-authored-by: Anthony James Padavano <padavano.anthony@gmail.com> Co-authored-by: Mostafa Kamal <hiremostafa@gmail.com> Fix auto-close MRE script posting comment without closing (#3386) Fix WorkOS token scope verification bypass 🤖 Generated with Codex (#3407) Fix initialize McpError fallthrough 🤖 Generated with Codex (#3413) Fix transform arg collisions with passthrough params (#3431) Fix get_* returning None when latest version is disabled (#3439) Fix get_* returning None when latest version is disabled (#3421) Fix server lifespan overlap teardown (#3415) Fix $ref output schema object detection regression (#3420) resolved annotations (#3429) Fix async partial callables rejected by iscoroutinefunction (#3438) Fix async partial callables rejected by iscoroutinefunction (#3423) fix: add version to components (#3458) fix: use intent-based flag for OIDC scope patch in load_access_token (#3465) Fixes #3461 fix: normalize Google scope shorthands and surface valid_scopes (#3477) fix: resolve ty 0.0.23 type-checking errors and bump pin (#3481) fix: shield lifespan teardown from cancellation (#3480) fix: forward custom_route endpoints from mounted servers (#3462) fix updates _get_additional_http_routes() to traverse providers, Fixes #3457 fix: remove hardcoded version from CLI help text (#3456) fix: monty 0.0.8 compatibility, drop external_functions from constructor (#3468) fix: task test teardown hanging 5s per test (#3499) Closes #3498 fix: validate workspace path is a directory before cursor install (#3440) Fixes #3426 fix: handle re.error from malformed URI templates in build_regex (#3501) fix: reject empty/OIDC-only required_scopes in AzureProvider (#3503) fix: restrict $ref resolution to local refs only (SSRF/LFI) (#3502) fix warnings and timeouts (#3504) close upgrade check issue when build passes (#3505) Closes #3484 fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) (#3507) fix: prevent path traversal in skill download (#3493) fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy (#3492) fix: remove unrelated transform and http.py changes from PR scope fix: remove forced follow_redirects from httpx_client_factory calls (#3496) fix: stop passing follow_redirects to httpx_client_factory fix: restore follow_redirects=True for custom httpx client factories Closes #3509 fix: CSRF double-submit cookie check in consent flow (#3519) fix: validate server names in install commands (#3522) fix: use raw strings for regex in pytest.raises match (#3523) fix: reject refresh tokens used as Bearer access tokens (#3524) fix: route ResourcesAsTools/PromptsAsTools through server middleware (#3495) fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators (#3540) fix: filter warnings by message in KEY_PREFIX test (#3549) fix: suppress output schema for ToolResult subclass annotations (#3548) fix: increase sleep duration in proxy cache tests (#3567) fix: store absolute token expiry to prevent stale expires_in on reload (#3572) fix: preserve tool properties named 'title' during schema compression (#3582) Fix loopback redirect URI port matching per RFC 8252 §7.3 (#3589) Fix app tool routing: visibility check and middleware propagation (#3591) Fix query parameter serialization to respect OpenAPI explode/style settings (#3595) Fix dev apps form: union types, textarea support, JSON parsing (#3597) fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo (#3603) fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609) fix(docs): correct misleading stateless_http header (#3622) fix: filesystem provider import machinery (#3626) Closes #3625 (issues 2, 3, 6) fix: recover StdioTransport after subprocess exits (#3630) fix(server): preserve mounted tool task metadata (#3632) fix: scope deprecation warning filter to FastMCPDeprecationWarning (#3649) fix imports, add PrefabAppConfig (#3650) fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks (#3651) Fix blocking docs issues: chart imports, Select API, Rx consistency (#3652) closed by default (#3657) Fix prompt caching middleware missing wrap/unwrap round-trip (#3666) fix: serialize object query params per OpenAPI style/explode rules (#3662) Fixes #2857 fix: HTTP request headers not accessible in background task workers (#3631) fix: restore HTTP headers in worker execution path for background tasks (#3681) fix: strip discriminator after dereferencing schemas (#3682) fix: remove stale ty:ignore directives for ty 0.0.26 (#3684) Fix docs gaps in app provider pages (#3690) fix: dev apps log panel UX improvements (#3698) fix dev server empty string args (#3700)
551 lines
17 KiB
Python
551 lines
17 KiB
Python
"""Tests for the MCPMixin class."""
|
|
|
|
import inspect
|
|
|
|
import pytest
|
|
|
|
from fastmcp import FastMCP
|
|
from fastmcp.contrib.mcp_mixin import (
|
|
MCPMixin,
|
|
mcp_prompt,
|
|
mcp_resource,
|
|
mcp_tool,
|
|
)
|
|
from fastmcp.contrib.mcp_mixin.mcp_mixin import (
|
|
_DEFAULT_SEPARATOR_PROMPT,
|
|
_DEFAULT_SEPARATOR_RESOURCE,
|
|
_DEFAULT_SEPARATOR_TOOL,
|
|
_PROMPT_VALID_KWARGS,
|
|
_RESOURCE_VALID_KWARGS,
|
|
_TOOL_VALID_KWARGS,
|
|
)
|
|
|
|
|
|
class TestMCPMixin:
|
|
"""Test suite for MCPMixin functionality."""
|
|
|
|
def test_initialization(self):
|
|
"""Test that a class inheriting MCPMixin can be initialized."""
|
|
|
|
class MyMixin(MCPMixin):
|
|
pass
|
|
|
|
instance = MyMixin()
|
|
assert instance is not None
|
|
|
|
# --- Tool Registration Tests ---
|
|
@pytest.mark.parametrize(
|
|
"prefix, separator, expected_key, unexpected_key",
|
|
[
|
|
(
|
|
None,
|
|
_DEFAULT_SEPARATOR_TOOL,
|
|
"sample_tool",
|
|
f"None{_DEFAULT_SEPARATOR_TOOL}sample_tool",
|
|
),
|
|
(
|
|
"pref",
|
|
_DEFAULT_SEPARATOR_TOOL,
|
|
f"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool",
|
|
"sample_tool",
|
|
),
|
|
(
|
|
"pref",
|
|
"-",
|
|
"pref-sample_tool",
|
|
f"pref{_DEFAULT_SEPARATOR_TOOL}sample_tool",
|
|
),
|
|
],
|
|
ids=["No prefix", "Default separator", "Custom separator"],
|
|
)
|
|
async def test_tool_registration(
|
|
self, prefix, separator, expected_key, unexpected_key
|
|
):
|
|
"""Test tool registration with prefix and separator variations."""
|
|
mcp = FastMCP()
|
|
|
|
class MyToolMixin(MCPMixin):
|
|
@mcp_tool()
|
|
def sample_tool(self):
|
|
pass
|
|
|
|
instance = MyToolMixin()
|
|
instance.register_tools(mcp, prefix=prefix, separator=separator)
|
|
|
|
registered_tools = await mcp.list_tools()
|
|
assert any(t.name == expected_key for t in registered_tools)
|
|
assert not any(t.name == unexpected_key for t in registered_tools)
|
|
|
|
@pytest.mark.parametrize(
|
|
"prefix, separator, expected_uri_key, expected_name, unexpected_uri_key",
|
|
[
|
|
(
|
|
None,
|
|
_DEFAULT_SEPARATOR_RESOURCE,
|
|
"test://resource",
|
|
"sample_resource",
|
|
f"None{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
|
|
),
|
|
(
|
|
"pref",
|
|
_DEFAULT_SEPARATOR_RESOURCE,
|
|
f"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
|
|
f"pref{_DEFAULT_SEPARATOR_RESOURCE}sample_resource",
|
|
"test://resource",
|
|
),
|
|
(
|
|
"pref",
|
|
"fff",
|
|
"prefffftest://resource",
|
|
"preffffsample_resource",
|
|
f"pref{_DEFAULT_SEPARATOR_RESOURCE}test://resource",
|
|
),
|
|
],
|
|
ids=["No prefix", "Default separator", "Custom separator"],
|
|
)
|
|
async def test_resource_registration(
|
|
self, prefix, separator, expected_uri_key, expected_name, unexpected_uri_key
|
|
):
|
|
"""Test resource registration with prefix and separator variations."""
|
|
mcp = FastMCP()
|
|
|
|
class MyResourceMixin(MCPMixin):
|
|
@mcp_resource(uri="test://resource")
|
|
def sample_resource(self):
|
|
pass
|
|
|
|
instance = MyResourceMixin()
|
|
instance.register_resources(mcp, prefix=prefix, separator=separator)
|
|
|
|
registered_resources = await mcp.list_resources()
|
|
assert any(str(r.uri) == expected_uri_key for r in registered_resources)
|
|
resource = next(
|
|
r for r in registered_resources if str(r.uri) == expected_uri_key
|
|
)
|
|
assert resource.name == expected_name
|
|
assert not any(str(r.uri) == unexpected_uri_key for r in registered_resources)
|
|
|
|
@pytest.mark.parametrize(
|
|
"prefix, separator, expected_name, unexpected_name",
|
|
[
|
|
(
|
|
None,
|
|
_DEFAULT_SEPARATOR_PROMPT,
|
|
"sample_prompt",
|
|
f"None{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
|
|
),
|
|
(
|
|
"pref",
|
|
_DEFAULT_SEPARATOR_PROMPT,
|
|
f"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
|
|
"sample_prompt",
|
|
),
|
|
(
|
|
"pref",
|
|
":",
|
|
"pref:sample_prompt",
|
|
f"pref{_DEFAULT_SEPARATOR_PROMPT}sample_prompt",
|
|
),
|
|
],
|
|
ids=["No prefix", "Default separator", "Custom separator"],
|
|
)
|
|
async def test_prompt_registration(
|
|
self, prefix, separator, expected_name, unexpected_name
|
|
):
|
|
"""Test prompt registration with prefix and separator variations."""
|
|
mcp = FastMCP()
|
|
|
|
class MyPromptMixin(MCPMixin):
|
|
@mcp_prompt()
|
|
def sample_prompt(self):
|
|
pass
|
|
|
|
instance = MyPromptMixin()
|
|
instance.register_prompts(mcp, prefix=prefix, separator=separator)
|
|
|
|
prompts = await mcp.list_prompts()
|
|
assert any(p.name == expected_name for p in prompts)
|
|
assert not any(p.name == unexpected_name for p in prompts)
|
|
|
|
async def test_register_all_no_prefix(self):
|
|
"""Test register_all method registers all types without a prefix."""
|
|
mcp = FastMCP()
|
|
|
|
class MyFullMixin(MCPMixin):
|
|
@mcp_tool()
|
|
def tool_all(self):
|
|
pass
|
|
|
|
@mcp_resource(uri="res://all")
|
|
def resource_all(self):
|
|
pass
|
|
|
|
@mcp_prompt()
|
|
def prompt_all(self):
|
|
pass
|
|
|
|
instance = MyFullMixin()
|
|
instance.register_all(mcp)
|
|
|
|
tools = await mcp.list_tools()
|
|
resources = await mcp.list_resources()
|
|
prompts = await mcp.list_prompts()
|
|
|
|
assert any(t.name == "tool_all" for t in tools)
|
|
assert any(str(r.uri) == "res://all" for r in resources)
|
|
assert any(p.name == "prompt_all" for p in prompts)
|
|
|
|
async def test_register_all_with_prefix_default_separators(self):
|
|
"""Test register_all method registers all types with a prefix and default separators."""
|
|
mcp = FastMCP()
|
|
|
|
class MyFullMixinPrefixed(MCPMixin):
|
|
@mcp_tool()
|
|
def tool_all_p(self):
|
|
pass
|
|
|
|
@mcp_resource(uri="res://all_p")
|
|
def resource_all_p(self):
|
|
pass
|
|
|
|
@mcp_prompt()
|
|
def prompt_all_p(self):
|
|
pass
|
|
|
|
instance = MyFullMixinPrefixed()
|
|
instance.register_all(mcp, prefix="all")
|
|
|
|
tools = await mcp.list_tools()
|
|
resources = await mcp.list_resources()
|
|
prompts = await mcp.list_prompts()
|
|
|
|
assert any(t.name == f"all{_DEFAULT_SEPARATOR_TOOL}tool_all_p" for t in tools)
|
|
assert any(
|
|
str(r.uri) == f"all{_DEFAULT_SEPARATOR_RESOURCE}res://all_p"
|
|
for r in resources
|
|
)
|
|
assert any(
|
|
p.name == f"all{_DEFAULT_SEPARATOR_PROMPT}prompt_all_p" for p in prompts
|
|
)
|
|
|
|
async def test_register_all_with_prefix_custom_separators(self):
|
|
"""Test register_all method registers all types with a prefix and custom separators."""
|
|
mcp = FastMCP()
|
|
|
|
class MyFullMixinCustomSep(MCPMixin):
|
|
@mcp_tool()
|
|
def tool_cust(self):
|
|
pass
|
|
|
|
@mcp_resource(uri="res://cust")
|
|
def resource_cust(self):
|
|
pass
|
|
|
|
@mcp_prompt()
|
|
def prompt_cust(self):
|
|
pass
|
|
|
|
instance = MyFullMixinCustomSep()
|
|
instance.register_all(
|
|
mcp,
|
|
prefix="cust",
|
|
tool_separator="-",
|
|
resource_separator="::",
|
|
prompt_separator=".",
|
|
)
|
|
|
|
tools = await mcp.list_tools()
|
|
resources = await mcp.list_resources()
|
|
prompts = await mcp.list_prompts()
|
|
|
|
assert any(t.name == "cust-tool_cust" for t in tools)
|
|
assert any(str(r.uri) == "cust::res://cust" for r in resources)
|
|
assert any(p.name == "cust.prompt_cust" for p in prompts)
|
|
|
|
# Check default separators weren't used
|
|
assert not any(
|
|
t.name == f"cust{_DEFAULT_SEPARATOR_TOOL}tool_cust" for t in tools
|
|
)
|
|
assert not any(
|
|
str(r.uri) == f"cust{_DEFAULT_SEPARATOR_RESOURCE}res://cust"
|
|
for r in resources
|
|
)
|
|
assert not any(
|
|
p.name == f"cust{_DEFAULT_SEPARATOR_PROMPT}prompt_cust" for p in prompts
|
|
)
|
|
|
|
async def test_tool_with_title_and_meta(self):
|
|
"""Test that title (via annotations) and meta arguments are properly passed through."""
|
|
from mcp.types import ToolAnnotations
|
|
|
|
mcp = FastMCP()
|
|
|
|
class MyToolWithMeta(MCPMixin):
|
|
@mcp_tool(
|
|
annotations=ToolAnnotations(title="My Tool Title"),
|
|
meta={"version": "1.0", "author": "test"},
|
|
)
|
|
def sample_tool(self):
|
|
pass
|
|
|
|
instance = MyToolWithMeta()
|
|
instance.register_tools(mcp)
|
|
|
|
registered_tools = await mcp.list_tools()
|
|
tool = next(t for t in registered_tools if t.name == "sample_tool")
|
|
|
|
assert tool.annotations is not None
|
|
assert tool.annotations.title == "My Tool Title"
|
|
assert tool.meta == {"version": "1.0", "author": "test"}
|
|
|
|
async def test_resource_with_meta(self):
|
|
"""Test that meta argument is properly passed through for resources."""
|
|
mcp = FastMCP()
|
|
|
|
class MyResourceWithMeta(MCPMixin):
|
|
@mcp_resource(
|
|
uri="test://resource",
|
|
title="My Resource Title",
|
|
meta={"category": "data", "internal": True},
|
|
)
|
|
def sample_resource(self):
|
|
pass
|
|
|
|
instance = MyResourceWithMeta()
|
|
instance.register_resources(mcp)
|
|
|
|
registered_resources = await mcp.list_resources()
|
|
resource = next(
|
|
r for r in registered_resources if str(r.uri) == "test://resource"
|
|
)
|
|
|
|
assert resource.meta == {"category": "data", "internal": True}
|
|
assert resource.title == "My Resource Title"
|
|
|
|
async def test_prompt_with_title_and_meta(self):
|
|
"""Test that title and meta arguments are properly passed through for prompts."""
|
|
mcp = FastMCP()
|
|
|
|
class MyPromptWithMeta(MCPMixin):
|
|
@mcp_prompt(
|
|
title="My Prompt Title",
|
|
meta={"priority": "high", "category": "analysis"},
|
|
)
|
|
def sample_prompt(self):
|
|
pass
|
|
|
|
instance = MyPromptWithMeta()
|
|
instance.register_prompts(mcp)
|
|
|
|
prompts = await mcp.list_prompts()
|
|
prompt = next(p for p in prompts if p.name == "sample_prompt")
|
|
|
|
assert prompt.title == "My Prompt Title"
|
|
assert prompt.meta == {"priority": "high", "category": "analysis"}
|
|
|
|
|
|
class TestMCPMixinKwargsSync:
|
|
"""Verify that the valid-kwarg sets stay in sync with from_function signatures."""
|
|
|
|
def test_tool_valid_kwargs_match_from_function(self):
|
|
from fastmcp.tools.base import Tool
|
|
|
|
expected = frozenset(
|
|
p for p in inspect.signature(Tool.from_function).parameters if p != "fn"
|
|
)
|
|
assert _TOOL_VALID_KWARGS == expected
|
|
|
|
def test_resource_valid_kwargs_match_from_function(self):
|
|
from fastmcp.resources.base import Resource
|
|
|
|
expected = frozenset(
|
|
p
|
|
for p in inspect.signature(Resource.from_function).parameters
|
|
if p not in ("fn", "uri")
|
|
)
|
|
assert _RESOURCE_VALID_KWARGS == expected
|
|
|
|
def test_prompt_valid_kwargs_match_from_function(self):
|
|
from fastmcp.prompts.base import Prompt
|
|
|
|
expected = frozenset(
|
|
p for p in inspect.signature(Prompt.from_function).parameters if p != "fn"
|
|
)
|
|
assert _PROMPT_VALID_KWARGS == expected
|
|
|
|
|
|
class TestMCPMixinValidation:
|
|
"""Unknown kwargs raise TypeError at decoration time, not at registration."""
|
|
|
|
def test_mcp_tool_rejects_unknown_param(self):
|
|
with pytest.raises(TypeError, match="unexpected keyword argument"):
|
|
|
|
@mcp_tool(definitely_not_a_real_param="oops")
|
|
def my_tool(self):
|
|
pass
|
|
|
|
def test_mcp_resource_rejects_unknown_param(self):
|
|
with pytest.raises(TypeError, match="unexpected keyword argument"):
|
|
|
|
@mcp_resource(uri="test://x", definitely_not_a_real_param="oops")
|
|
def my_resource(self):
|
|
pass
|
|
|
|
def test_mcp_prompt_rejects_unknown_param(self):
|
|
with pytest.raises(TypeError, match="unexpected keyword argument"):
|
|
|
|
@mcp_prompt(definitely_not_a_real_param="oops")
|
|
def my_prompt(self):
|
|
pass
|
|
|
|
def test_error_raised_at_decoration_not_registration(self):
|
|
"""The TypeError must surface when the decorator is applied, not later."""
|
|
with pytest.raises(TypeError):
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(bad_kwarg=True)
|
|
def tool(self):
|
|
pass
|
|
|
|
|
|
class TestMCPMixinEnabled:
|
|
"""enabled=False suppresses registration; enabled=True (default) registers normally."""
|
|
|
|
async def test_tool_enabled_false_skips_registration(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(enabled=False)
|
|
def hidden_tool(self):
|
|
pass
|
|
|
|
@mcp_tool()
|
|
def visible_tool(self):
|
|
pass
|
|
|
|
MyMixin().register_tools(mcp)
|
|
tools = await mcp.list_tools()
|
|
names = {t.name for t in tools}
|
|
assert "visible_tool" in names
|
|
assert "hidden_tool" not in names
|
|
|
|
async def test_resource_enabled_false_skips_registration(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_resource(uri="test://hidden", enabled=False)
|
|
def hidden_resource(self):
|
|
pass
|
|
|
|
@mcp_resource(uri="test://visible")
|
|
def visible_resource(self):
|
|
pass
|
|
|
|
MyMixin().register_resources(mcp)
|
|
resources = await mcp.list_resources()
|
|
uris = {str(r.uri) for r in resources}
|
|
assert "test://visible" in uris
|
|
assert "test://hidden" not in uris
|
|
|
|
async def test_prompt_enabled_false_skips_registration(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_prompt(enabled=False)
|
|
def hidden_prompt(self):
|
|
pass
|
|
|
|
@mcp_prompt()
|
|
def visible_prompt(self):
|
|
pass
|
|
|
|
MyMixin().register_prompts(mcp)
|
|
prompts = await mcp.list_prompts()
|
|
names = {p.name for p in prompts}
|
|
assert "visible_prompt" in names
|
|
assert "hidden_prompt" not in names
|
|
|
|
async def test_tool_enabled_true_registers_normally(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(enabled=True)
|
|
def my_tool(self):
|
|
pass
|
|
|
|
MyMixin().register_tools(mcp)
|
|
tools = await mcp.list_tools()
|
|
assert any(t.name == "my_tool" for t in tools)
|
|
|
|
|
|
class TestMCPMixinNewParams:
|
|
"""Parameters that were previously missing now work end-to-end."""
|
|
|
|
async def test_tool_auth_param_forwarded(self):
|
|
from fastmcp.server.auth import require_scopes
|
|
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(auth=require_scopes("write"))
|
|
def secure_tool(self):
|
|
return "ok"
|
|
|
|
MyMixin().register_tools(mcp)
|
|
# list_tools() filters by auth context; check internal provider directly
|
|
tools = await mcp.local_provider.list_tools()
|
|
assert any(t.name == "secure_tool" for t in tools)
|
|
|
|
async def test_tool_timeout_param_forwarded(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(timeout=5.0)
|
|
def timed_tool(self):
|
|
return "ok"
|
|
|
|
MyMixin().register_tools(mcp)
|
|
tools = await mcp.list_tools()
|
|
assert any(t.name == "timed_tool" for t in tools)
|
|
|
|
async def test_tool_version_param_forwarded(self):
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_tool(version="2.0")
|
|
def versioned_tool(self):
|
|
return "ok"
|
|
|
|
MyMixin().register_tools(mcp)
|
|
tools = await mcp.list_tools()
|
|
assert any(t.name == "versioned_tool" for t in tools)
|
|
|
|
async def test_resource_auth_param_forwarded(self):
|
|
from fastmcp.server.auth import require_scopes
|
|
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_resource(uri="test://secure", auth=require_scopes("read"))
|
|
def secure_resource(self):
|
|
return "data"
|
|
|
|
MyMixin().register_resources(mcp)
|
|
# list_resources() filters by auth context; check internal provider directly
|
|
resources = await mcp.local_provider.list_resources()
|
|
assert any(str(r.uri) == "test://secure" for r in resources)
|
|
|
|
async def test_prompt_auth_param_forwarded(self):
|
|
from fastmcp.server.auth import require_scopes
|
|
|
|
mcp = FastMCP()
|
|
|
|
class MyMixin(MCPMixin):
|
|
@mcp_prompt(auth=require_scopes("read"))
|
|
def secure_prompt(self):
|
|
return "prompt text"
|
|
|
|
MyMixin().register_prompts(mcp)
|
|
# list_prompts() filters by auth context; check internal provider directly
|
|
prompts = await mcp.local_provider.list_prompts()
|
|
assert any(p.name == "secure_prompt" for p in prompts)
|