Skip to content

Commit 7553bba

Browse files
authored
Merge branch 'main' into fix-fastmcp-integration-tests
2 parents 551212e + 5c9cf29 commit 7553bba

File tree

6 files changed

+87
-8
lines changed

6 files changed

+87
-8
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,13 +1327,13 @@ The MCP protocol defines three core primitives that servers can implement:
13271327

13281328
MCP servers declare capabilities during initialization:
13291329

1330-
| Capability | Feature Flag | Description |
1331-
|-------------|------------------------------|------------------------------------|
1332-
| `prompts` | `listChanged` | Prompt template management |
1333-
| `resources` | `subscribe`<br/>`listChanged`| Resource exposure and updates |
1334-
| `tools` | `listChanged` | Tool discovery and execution |
1335-
| `logging` | - | Server logging configuration |
1336-
| `completion`| - | Argument completion suggestions |
1330+
| Capability | Feature Flag | Description |
1331+
|--------------|------------------------------|------------------------------------|
1332+
| `prompts` | `listChanged` | Prompt template management |
1333+
| `resources` | `subscribe`<br/>`listChanged`| Resource exposure and updates |
1334+
| `tools` | `listChanged` | Tool discovery and execution |
1335+
| `logging` | - | Server logging configuration |
1336+
| `completions`| - | Argument completion suggestions |
13371337

13381338
## Documentation
13391339

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def convert_result(self, result: Any) -> Any:
111111

112112
assert self.output_model is not None, "Output model must be set if output schema is defined"
113113
validated = self.output_model.model_validate(result)
114-
structured_content = validated.model_dump(mode="json")
114+
structured_content = validated.model_dump(mode="json", by_alias=True)
115115

116116
return (unstructured_content, structured_content)
117117

src/mcp/server/lowlevel/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def get_capabilities(
190190
resources_capability = None
191191
tools_capability = None
192192
logging_capability = None
193+
completions_capability = None
193194

194195
# Set prompt capabilities if handler exists
195196
if types.ListPromptsRequest in self.request_handlers:
@@ -209,12 +210,17 @@ def get_capabilities(
209210
if types.SetLevelRequest in self.request_handlers:
210211
logging_capability = types.LoggingCapability()
211212

213+
# Set completions capabilities if handler exists
214+
if types.CompleteRequest in self.request_handlers:
215+
completions_capability = types.CompletionsCapability()
216+
212217
return types.ServerCapabilities(
213218
prompts=prompts_capability,
214219
resources=resources_capability,
215220
tools=tools_capability,
216221
logging=logging_capability,
217222
experimental=experimental_capabilities,
223+
completions=completions_capability,
218224
)
219225

220226
@property

src/mcp/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ class LoggingCapability(BaseModel):
286286
model_config = ConfigDict(extra="allow")
287287

288288

289+
class CompletionsCapability(BaseModel):
290+
"""Capability for completions operations."""
291+
292+
model_config = ConfigDict(extra="allow")
293+
294+
289295
class ServerCapabilities(BaseModel):
290296
"""Capabilities that a server may support."""
291297

@@ -299,6 +305,8 @@ class ServerCapabilities(BaseModel):
299305
"""Present if the server offers any resources to read."""
300306
tools: ToolsCapability | None = None
301307
"""Present if the server offers any tools to call."""
308+
completions: CompletionsCapability | None = None
309+
"""Present if the server offers autocompletion suggestions for prompts and resources."""
302310
model_config = ConfigDict(extra="allow")
303311

304312

tests/server/fastmcp/test_func_metadata.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,3 +839,48 @@ def func_returning_namedtuple() -> Point:
839839
func_metadata(func_returning_namedtuple, structured_output=True)
840840
assert "is not serializable for structured output" in str(exc_info.value)
841841
assert "Point" in str(exc_info.value)
842+
843+
844+
def test_structured_output_aliases():
845+
"""Test that field aliases are consistent between schema and output"""
846+
847+
class ModelWithAliases(BaseModel):
848+
field_first: str | None = Field(default=None, alias="first", description="The first field.")
849+
field_second: str | None = Field(default=None, alias="second", description="The second field.")
850+
851+
def func_with_aliases() -> ModelWithAliases:
852+
# When aliases are defined, we must use the aliased names to set values
853+
return ModelWithAliases(**{"first": "hello", "second": "world"})
854+
855+
meta = func_metadata(func_with_aliases)
856+
857+
# Check that schema uses aliases
858+
assert meta.output_schema is not None
859+
assert "first" in meta.output_schema["properties"]
860+
assert "second" in meta.output_schema["properties"]
861+
assert "field_first" not in meta.output_schema["properties"]
862+
assert "field_second" not in meta.output_schema["properties"]
863+
864+
# Check that the actual output uses aliases too
865+
result = ModelWithAliases(**{"first": "hello", "second": "world"})
866+
unstructured_content, structured_content = meta.convert_result(result)
867+
868+
# The structured content should use aliases to match the schema
869+
assert "first" in structured_content
870+
assert "second" in structured_content
871+
assert "field_first" not in structured_content
872+
assert "field_second" not in structured_content
873+
assert structured_content["first"] == "hello"
874+
assert structured_content["second"] == "world"
875+
876+
# Also test the case where we have a model with defaults to ensure aliases work in all cases
877+
result_with_defaults = ModelWithAliases() # Uses default None values
878+
unstructured_content_defaults, structured_content_defaults = meta.convert_result(result_with_defaults)
879+
880+
# Even with defaults, should use aliases in output
881+
assert "first" in structured_content_defaults
882+
assert "second" in structured_content_defaults
883+
assert "field_first" not in structured_content_defaults
884+
assert "field_second" not in structured_content_defaults
885+
assert structured_content_defaults["first"] is None
886+
assert structured_content_defaults["second"] is None

tests/server/test_session.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
from mcp.shared.session import RequestResponder
1212
from mcp.types import (
1313
ClientNotification,
14+
Completion,
15+
CompletionArgument,
16+
CompletionsCapability,
1417
InitializedNotification,
18+
PromptReference,
1519
PromptsCapability,
20+
ResourceReference,
1621
ResourcesCapability,
1722
ServerCapabilities,
1823
)
@@ -80,6 +85,7 @@ async def test_server_capabilities():
8085
caps = server.get_capabilities(notification_options, experimental_capabilities)
8186
assert caps.prompts is None
8287
assert caps.resources is None
88+
assert caps.completions is None
8389

8490
# Add a prompts handler
8591
@server.list_prompts()
@@ -89,6 +95,7 @@ async def list_prompts():
8995
caps = server.get_capabilities(notification_options, experimental_capabilities)
9096
assert caps.prompts == PromptsCapability(listChanged=False)
9197
assert caps.resources is None
98+
assert caps.completions is None
9299

93100
# Add a resources handler
94101
@server.list_resources()
@@ -98,6 +105,19 @@ async def list_resources():
98105
caps = server.get_capabilities(notification_options, experimental_capabilities)
99106
assert caps.prompts == PromptsCapability(listChanged=False)
100107
assert caps.resources == ResourcesCapability(subscribe=False, listChanged=False)
108+
assert caps.completions is None
109+
110+
# Add a complete handler
111+
@server.completion()
112+
async def complete(ref: PromptReference | ResourceReference, argument: CompletionArgument):
113+
return Completion(
114+
values=["completion1", "completion2"],
115+
)
116+
117+
caps = server.get_capabilities(notification_options, experimental_capabilities)
118+
assert caps.prompts == PromptsCapability(listChanged=False)
119+
assert caps.resources == ResourcesCapability(subscribe=False, listChanged=False)
120+
assert caps.completions == CompletionsCapability()
101121

102122

103123
@pytest.mark.anyio

0 commit comments

Comments
 (0)