Skip to content

Include context into completions #966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Prompts](#prompts)
- [Images](#images)
- [Context](#context)
- [Completions](#completions)
- [Running Your Server](#running-your-server)
- [Development Mode](#development-mode)
- [Claude Desktop Integration](#claude-desktop-integration)
Expand Down Expand Up @@ -310,6 +311,68 @@ async def long_task(files: list[str], ctx: Context) -> str:
return "Processing complete"
```

### Completions

MCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values:

Client usage:
```python
from mcp.client.session import ClientSession
from mcp.types import ResourceTemplateReference


async def use_completion(session: ClientSession):
# Complete without context
result = await session.complete(
ref=ResourceTemplateReference(
type="ref/resource", uri="github://repos/{owner}/{repo}"
),
argument={"name": "owner", "value": "model"},
)

# Complete with context - repo suggestions based on owner
result = await session.complete(
ref=ResourceTemplateReference(
type="ref/resource", uri="github://repos/{owner}/{repo}"
),
argument={"name": "repo", "value": "test"},
context_arguments={"owner": "modelcontextprotocol"},
)
```

Server implementation:
```python
from mcp.server import Server
from mcp.types import (
Completion,
CompletionArgument,
CompletionContext,
PromptReference,
ResourceTemplateReference,
)

server = Server("example-server")


@server.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
if isinstance(ref, ResourceTemplateReference):
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
# Use context to provide owner-specific repos
if context and context.arguments:
owner = context.arguments.get("owner")
if owner == "modelcontextprotocol":
repos = ["python-sdk", "typescript-sdk", "specification"]
# Filter based on partial input
filtered = [r for r in repos if r.startswith(argument.value)]
return Completion(values=filtered)
return None
```

### Authentication

Authentication can be used by servers that want to expose tools accessing protected resources.
Expand Down
6 changes: 6 additions & 0 deletions src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,15 +304,21 @@ async def complete(
self,
ref: types.ResourceTemplateReference | types.PromptReference,
argument: dict[str, str],
context_arguments: dict[str, str] | None = None,
) -> types.CompleteResult:
"""Send a completion/complete request."""
context = None
if context_arguments is not None:
context = types.CompletionContext(arguments=context_arguments)

return await self.send_request(
types.ClientRequest(
types.CompleteRequest(
method="completion/complete",
params=types.CompleteRequestParams(
ref=ref,
argument=types.CompletionArgument(**argument),
context=context,
),
)
),
Expand Down
18 changes: 18 additions & 0 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,24 @@ def decorator(fn: AnyFunction) -> AnyFunction:

return decorator

def completion(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it weird to route all completions to the same function in the high level API? Alternative would be to defer adding the fastmcp decorator for now and do something fancier that routes on URI, but agree that it's nice to bring fastmcp decorator coverage to parity with the lowlevel server.

Also a) maybe it's natural for "real" servers to route all completion requests through the same handler, and b) we could always add new decorators (resource_completion, prompt_completion) later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it is quite weird even on the lowlevel, TS has completions handling on a field that needs to be completed, which I found quite nice.

I'd love to make it nicer, but for now to have at least a way to handle completions on FastMCP level and not do to things like _server.

"""Decorator to register a completion handler.

The completion handler receives:
- ref: PromptReference or ResourceTemplateReference
- argument: CompletionArgument with name and partial value
- context: Optional CompletionContext with previously resolved arguments

Example:
@mcp.completion()
async def handle_completion(ref, argument, context):
if isinstance(ref, ResourceTemplateReference):
# Return completions based on ref, argument, and context
return Completion(values=["option1", "option2"])
return None
"""
return self._mcp_server.completion()

def add_resource(self, resource: Resource) -> None:
"""Add a resource to the server.

Expand Down
3 changes: 2 additions & 1 deletion src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,14 +433,15 @@ def decorator(
[
types.PromptReference | types.ResourceTemplateReference,
types.CompletionArgument,
types.CompletionContext | None,
],
Awaitable[types.Completion | None],
],
):
logger.debug("Registering handler for CompleteRequest")

async def handler(req: types.CompleteRequest):
completion = await func(req.params.ref, req.params.argument)
completion = await func(req.params.ref, req.params.argument, req.params.context)
return types.ServerResult(
types.CompleteResult(
completion=completion
Expand Down
10 changes: 10 additions & 0 deletions src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,11 +1028,21 @@ class CompletionArgument(BaseModel):
model_config = ConfigDict(extra="allow")


class CompletionContext(BaseModel):
"""Additional, optional context for completions."""

arguments: dict[str, str] | None = None
"""Previously-resolved variables in a URI template or prompt."""
model_config = ConfigDict(extra="allow")


class CompleteRequestParams(RequestParams):
"""Parameters for completion requests."""

ref: ResourceTemplateReference | PromptReference
argument: CompletionArgument
context: CompletionContext | None = None
"""Additional, optional context for completions"""
model_config = ConfigDict(extra="allow")


Expand Down
90 changes: 84 additions & 6 deletions tests/server/fastmcp/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,31 @@
from starlette.applications import Starlette
from starlette.requests import Request

import mcp.types as types
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client
from mcp.client.streamable_http import streamablehttp_client
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.resources import FunctionResource
from mcp.shared.context import RequestContext
from mcp.types import (
Completion,
CompletionArgument,
CompletionContext,
CreateMessageRequestParams,
CreateMessageResult,
GetPromptResult,
InitializeResult,
LoggingMessageNotification,
ProgressNotification,
PromptReference,
ReadResourceResult,
ResourceListChangedNotification,
ResourceTemplateReference,
SamplingMessage,
ServerNotification,
TextContent,
TextResourceContents,
ToolListChangedNotification,
)


Expand Down Expand Up @@ -191,6 +200,40 @@ def complex_prompt(user_query: str, context: str = "general") -> str:
# Since FastMCP doesn't support system messages in the same way
return f"Context: {context}. Query: {user_query}"

# Resource template with completion support
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo_resource(owner: str, repo: str) -> str:
return f"Repository: {owner}/{repo}"

# Add completion handler for the server
@mcp.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
# Handle GitHub repository completion
if isinstance(ref, ResourceTemplateReference):
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
# Return repos for modelcontextprotocol org
return Completion(values=["python-sdk", "typescript-sdk", "specification"], total=3, hasMore=False)
elif context and context.arguments and context.arguments.get("owner") == "test-org":
# Return repos for test-org
return Completion(values=["test-repo1", "test-repo2"], total=2, hasMore=False)

# Handle prompt completions
if isinstance(ref, PromptReference):
if ref.name == "complex_prompt" and argument.name == "context":
# Complete context values
contexts = ["general", "technical", "business", "academic"]
return Completion(
values=[c for c in contexts if c.startswith(argument.value)], total=None, hasMore=False
)

# Default: no completion available
return Completion(values=[], total=0, hasMore=False)

# Tool that echoes request headers from context
@mcp.tool(description="Echo request headers from context")
def echo_headers(ctx: Context[Any, Any, Request]) -> str:
Expand Down Expand Up @@ -597,15 +640,15 @@ async def handle_tool_list_changed(self, params) -> None:

async def handle_generic_notification(self, message) -> None:
# Check if this is a ServerNotification
if isinstance(message, types.ServerNotification):
if isinstance(message, ServerNotification):
# Check the specific notification type
if isinstance(message.root, types.ProgressNotification):
if isinstance(message.root, ProgressNotification):
await self.handle_progress(message.root.params)
elif isinstance(message.root, types.LoggingMessageNotification):
elif isinstance(message.root, LoggingMessageNotification):
await self.handle_log(message.root.params)
elif isinstance(message.root, types.ResourceListChangedNotification):
elif isinstance(message.root, ResourceListChangedNotification):
await self.handle_resource_list_changed(message.root.params)
elif isinstance(message.root, types.ToolListChangedNotification):
elif isinstance(message.root, ToolListChangedNotification):
await self.handle_tool_list_changed(message.root.params)


Expand Down Expand Up @@ -781,6 +824,41 @@ async def progress_callback(progress: float, total: float | None, message: str |
if context_data["method"]:
assert context_data["method"] == "POST"

# Test completion functionality
# 1. Test resource template completion with context
repo_result = await session.complete(
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
argument={"name": "repo", "value": ""},
context_arguments={"owner": "modelcontextprotocol"},
)
assert repo_result.completion.values == ["python-sdk", "typescript-sdk", "specification"]
assert repo_result.completion.total == 3
assert repo_result.completion.hasMore is False

# 2. Test with different context
repo_result2 = await session.complete(
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
argument={"name": "repo", "value": ""},
context_arguments={"owner": "test-org"},
)
assert repo_result2.completion.values == ["test-repo1", "test-repo2"]
assert repo_result2.completion.total == 2

# 3. Test prompt argument completion
context_result = await session.complete(
ref=PromptReference(type="ref/prompt", name="complex_prompt"),
argument={"name": "context", "value": "tech"},
)
assert "technical" in context_result.completion.values

# 4. Test completion without context (should return empty)
no_context_result = await session.complete(
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
argument={"name": "repo", "value": "test"},
)
assert no_context_result.completion.values == []
assert no_context_result.completion.total == 0


async def sampling_callback(
context: RequestContext[ClientSession, None],
Expand Down
Loading
Loading