Merge branch 'main' into claude/issue-3049-20260131-2232

This commit is contained in:
Bill Easton 2026-02-01 20:49:43 -06:00 committed by GitHub
commit b3b26e602c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 253 additions and 12 deletions

View file

@ -19,6 +19,7 @@ from typing import (
import pydantic_core
from mcp.types import Icon
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import resolve_task_config
@ -73,7 +74,7 @@ class PromptMeta:
class FunctionPrompt(Prompt):
"""A prompt that is a function."""
fn: Callable[..., Any]
fn: SkipJsonSchema[Callable[..., Any]]
@classmethod
def from_function(

View file

@ -25,6 +25,7 @@ from mcp.types import (
from mcp.types import Prompt as SDKPrompt
from mcp.types import PromptArgument as SDKPromptArgument
from pydantic import Field
from pydantic.json_schema import SkipJsonSchema
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.tools.tool import AuthCheckCallable
@ -194,7 +195,7 @@ class Prompt(FastMCPComponent):
arguments: list[PromptArgument] | None = Field(
default=None, description="Arguments that can be passed to the prompt"
)
auth: AuthCheckCallable | list[AuthCheckCallable] | None = Field(
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
default=None, description="Authorization checks for this prompt", exclude=True
)

View file

@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, runtime_check
from mcp.types import Annotations, Icon
from pydantic import AnyUrl
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import resolve_task_config
@ -73,7 +74,7 @@ class FunctionResource(Resource):
- other types will be converted to JSON
"""
fn: Callable[..., Any]
fn: SkipJsonSchema[Callable[..., Any]]
@classmethod
def from_function(

View file

@ -26,6 +26,7 @@ from pydantic import (
field_validator,
model_validator,
)
from pydantic.json_schema import SkipJsonSchema
from typing_extensions import Self
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
@ -226,7 +227,7 @@ class Resource(FastMCPComponent):
Field(description="Optional annotations about the resource's behavior"),
] = None
auth: Annotated[
AuthCheckCallable | list[AuthCheckCallable] | None,
SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None],
Field(description="Authorization checks for this resource", exclude=True),
] = None

View file

@ -10,6 +10,7 @@ from urllib.parse import parse_qs, unquote
import mcp.types
from mcp.types import Annotations, Icon
from pydantic.json_schema import SkipJsonSchema
if TYPE_CHECKING:
from docket import Docket
@ -116,7 +117,7 @@ class ResourceTemplate(FastMCPComponent):
annotations: Annotations | None = Field(
default=None, description="Optional annotations about the resource's behavior"
)
auth: AuthCheckCallable | list[AuthCheckCallable] | None = Field(
auth: SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None] = Field(
default=None,
description="Authorization checks for this resource template",
exclude=True,
@ -327,7 +328,7 @@ class ResourceTemplate(FastMCPComponent):
class FunctionResourceTemplate(ResourceTemplate):
"""A template for dynamically creating resources."""
fn: Callable[..., Any]
fn: SkipJsonSchema[Callable[..., Any]]
@overload
async def _read(

View file

@ -20,6 +20,7 @@ import anyio
import mcp.types
from mcp.shared.exceptions import McpError
from mcp.types import ErrorData, Icon, ToolAnnotations, ToolExecution
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.decorators import resolve_task_config
@ -81,7 +82,7 @@ class ToolMeta:
class FunctionTool(Tool):
fn: Callable[..., Any]
fn: SkipJsonSchema[Callable[..., Any]]
def to_mcp_tool(
self,

View file

@ -24,6 +24,7 @@ from mcp.types import (
)
from mcp.types import Tool as MCPTool
from pydantic import BaseModel, Field, model_validator
from pydantic.json_schema import SkipJsonSchema
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.components import FastMCPComponent
@ -140,13 +141,13 @@ class Tool(FastMCPComponent):
Field(description="Task execution configuration (SEP-1686)"),
] = None
serializer: Annotated[
ToolResultSerializerType | None,
SkipJsonSchema[ToolResultSerializerType | None],
Field(
description="Deprecated. Return ToolResult from your tools for full control over serialization."
),
] = None
auth: Annotated[
AuthCheckCallable | list[AuthCheckCallable] | None,
SkipJsonSchema[AuthCheckCallable | list[AuthCheckCallable] | None],
Field(description="Authorization checks for this tool", exclude=True),
] = None
timeout: Annotated[

View file

@ -13,6 +13,7 @@ from mcp.types import ToolAnnotations
from pydantic import ConfigDict
from pydantic.fields import Field
from pydantic.functional_validators import BeforeValidator
from pydantic.json_schema import SkipJsonSchema
import fastmcp
from fastmcp.tools.function_parsing import ParsedFunction
@ -253,9 +254,11 @@ class TransformedTool(Tool):
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
parent_tool: Tool
fn: Callable[..., Any]
forwarding_fn: Callable[..., Any] # Always present, handles arg transformation
parent_tool: SkipJsonSchema[Tool]
fn: SkipJsonSchema[Callable[..., Any]]
forwarding_fn: SkipJsonSchema[
Callable[..., Any]
] # Always present, handles arg transformation
transform_args: dict[str, ArgTransform]
async def run(self, arguments: dict[str, Any]) -> ToolResult:

View file

@ -0,0 +1,231 @@
"""Tests for JSON schema generation from FastMCP BaseModel classes.
Validates that callable fields are properly excluded from generated schemas
using SkipJsonSchema annotations.
"""
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.resources.template import FunctionResourceTemplate
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool import Tool
from fastmcp.tools.tool_transform import TransformedTool
class TestToolJsonSchema:
"""Test JSON schema generation for Tool classes."""
def test_tool_json_schema_generation(self):
"""Verify Tool.model_json_schema() works without errors."""
# This should not raise an error
schema = Tool.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable fields are excluded from schema
assert "serializer" not in schema["properties"]
# auth already uses exclude=True, so it shouldn't be in schema
assert "auth" not in schema["properties"]
def test_function_tool_json_schema_generation(self):
"""Verify FunctionTool.model_json_schema() works without errors."""
def sample_tool(x: int, y: int) -> int:
"""Add two numbers."""
return x + y
tool = FunctionTool.from_function(sample_tool)
# This should not raise an error
schema = tool.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable field 'fn' is excluded from schema
assert "fn" not in schema["properties"]
def test_transformed_tool_json_schema_generation(self):
"""Verify TransformedTool.model_json_schema() works without errors."""
def parent_fn(x: int) -> int:
return x * 2
parent_tool = FunctionTool.from_function(parent_fn)
transformed_tool = TransformedTool.from_tool(parent_tool, name="doubled")
# This should not raise an error
schema = transformed_tool.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable fields are excluded from schema
assert "fn" not in schema["properties"]
assert "forwarding_fn" not in schema["properties"]
assert "parent_tool" not in schema["properties"]
class TestResourceJsonSchema:
"""Test JSON schema generation for Resource classes."""
def test_function_resource_json_schema_generation(self):
"""Verify FunctionResource.model_json_schema() works without errors."""
def sample_resource() -> str:
"""Return sample data."""
return "Hello, world!"
resource = FunctionResource.from_function(
sample_resource, uri="test://resource"
)
# This should not raise an error
schema = resource.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable field 'fn' is excluded from schema
assert "fn" not in schema["properties"]
# auth already uses exclude=True
assert "auth" not in schema["properties"]
def test_function_resource_template_json_schema_generation(self):
"""Verify FunctionResourceTemplate.model_json_schema() works without errors."""
def sample_template(name: str) -> str:
"""Return greeting for name."""
return f"Hello, {name}!"
template = FunctionResourceTemplate.from_function(
sample_template, uri_template="greeting://{name}"
)
# This should not raise an error
schema = template.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable field 'fn' is excluded from schema
assert "fn" not in schema["properties"]
class TestPromptJsonSchema:
"""Test JSON schema generation for Prompt classes."""
def test_function_prompt_json_schema_generation(self):
"""Verify FunctionPrompt.model_json_schema() works without errors."""
def sample_prompt(topic: str) -> str:
"""Generate prompt about topic."""
return f"Tell me about {topic}"
prompt = FunctionPrompt.from_function(sample_prompt)
# This should not raise an error
schema = prompt.model_json_schema()
# Verify schema is valid
assert schema["type"] == "object"
assert "properties" in schema
# Verify callable field 'fn' is excluded from schema
assert "fn" not in schema["properties"]
# auth already uses exclude=True
assert "auth" not in schema["properties"]
class TestJsonSchemaIntegration:
"""Integration tests for JSON schema generation across all classes."""
def test_all_classes_generate_valid_schemas(self):
"""Verify all affected classes can generate valid JSON schemas."""
# Create instances of all affected classes
def tool_fn(x: int) -> int:
return x
def resource_fn() -> str:
return "data"
def template_fn(id: str) -> str:
return f"data-{id}"
def prompt_fn(input: str) -> str:
return f"Prompt: {input}"
tool = FunctionTool.from_function(tool_fn)
transformed_tool = TransformedTool.from_tool(tool)
resource = FunctionResource.from_function(resource_fn, uri="test://resource")
template = FunctionResourceTemplate.from_function(
template_fn, uri_template="test://{id}"
)
prompt = FunctionPrompt.from_function(prompt_fn)
# All of these should succeed without errors
schemas = [
Tool.model_json_schema(),
tool.model_json_schema(),
transformed_tool.model_json_schema(),
resource.model_json_schema(),
template.model_json_schema(),
prompt.model_json_schema(),
]
# Verify all schemas are valid
for schema in schemas:
assert isinstance(schema, dict)
assert schema["type"] == "object"
assert "properties" in schema
def test_callable_fields_not_in_any_schema(self):
"""Verify no callable fields appear in any generated schema."""
# Define test functions
def tool_fn(x: int) -> int:
return x
def resource_fn() -> str:
return "data"
def template_fn(id: str) -> str:
return f"data-{id}"
def prompt_fn(input: str) -> str:
return f"Prompt: {input}"
# Create instances
tool = FunctionTool.from_function(tool_fn)
transformed_tool = TransformedTool.from_tool(tool)
resource = FunctionResource.from_function(resource_fn, uri="test://resource")
template = FunctionResourceTemplate.from_function(
template_fn, uri_template="test://{id}"
)
prompt = FunctionPrompt.from_function(prompt_fn)
# List of (instance, callable_field_names) tuples
test_cases = [
(tool, ["fn", "serializer"]),
(transformed_tool, ["fn", "forwarding_fn", "parent_tool", "serializer"]),
(resource, ["fn"]),
(template, ["fn"]),
(prompt, ["fn"]),
]
for instance, callable_fields in test_cases:
schema = instance.model_json_schema()
properties = schema.get("properties", {})
# Verify none of the callable fields are in the schema
for field in callable_fields:
assert field not in properties, (
f"Callable field '{field}' found in schema for {type(instance).__name__}"
)