10000 Include context into completions (#966) · rsp2k/python-sdk@a3bcabd · GitHub
[go: up one dir, main page]

Skip to content

Commit a3bcabd

Browse files
authored
Include context into completions (modelcontextprotocol#966)
1 parent 7b42065 commit a3bcabd

File tree

7 files changed

+363
-7
lines changed

7 files changed

+363
-7
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- [Prompts](#prompts)
3131
- [Images](#images)
3232
- [Context](#context)
33+
- [Completions](#completions)
3334
- [Running Your Server](#running-your-server)
3435
- [Development Mode](#development-mode)
3536
- [Claude Desktop Integration](#claude-desktop-integration)
@@ -310,6 +311,68 @@ async def long_task(files: list[str], ctx: Context) -> str:
310311
return "Processing complete"
311312
```
312313

314+
### Completions
315+
316+
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:
317+
318+
Client usage:
319+
```python
320+
from mcp.client.session import ClientSession
321+
from mcp.types import ResourceTemplateReference
322+
323+
324+
async def use_completion(session: ClientSession):
325+
# Complete without context
326+
result = await session.complete(
327+
ref=ResourceTemplateReference(
328+
type="ref/resource", uri="github://repos/{owner}/{repo}"
329+
),
330+
argument={"name": "owner", "value": "model"},
331+
)
332+
333+
# Complete with context - repo suggestions based on owner
334+
result = await session.complete(
335+
ref=ResourceTemplateReference(
336+
type="ref/resource", uri="github://repos/{owner}/{repo}"
337+
),
338+
argument={"name": "repo", "value": "test"},
339+
context_arguments={"owner": "modelcontextprotocol"},
340+
)
341+
```
342+
343+
Server implementation:
344+
```python
345+
from mcp.server import Server
346+
from mcp.types import (
347+
Completion,
348+
CompletionArgument,
349+
CompletionContext,
350+
PromptReference,
351+
ResourceTemplateReference,
352+
)
353+
354+
server = Server("example-server")
355+
356+
357+
@server.completion()
358+
async def handle_completion(
359+
ref: PromptReference | ResourceTemplateReference,
360+
argument: CompletionArgument,
361+
context: CompletionContext | None,
362+
) -> Completion | None:
363+
if isinstance(ref, ResourceTemplateReference):
364+
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
365+
# Use context to provide owner-specific repos
366+
if context and context.arguments:
367+
owner = context.arguments.get("owner")
368+
if owner == "modelcontextprotocol":
369+
repos = ["python-sdk", "typescript-sdk", "specification"]
370+
# Filter based on partial input
371+
filtered = [r for r in repos if r.startswith(argument.value)]
372+
return Completion(values=filtered)
373+
return None
374+
```
375+
313376
### Authentication
314377

315378
Authentication can be used by servers that want to expose tools accessing protected resources.

src/mcp/client/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,15 +304,21 @@ async def complete(
304304
self,
305305
ref: types.ResourceTemplateReference | types.PromptReference,
306306
argument: dict[str, str],
307+
context_arguments: dict[str, str] | None = None,
307308
) -> types.CompleteResult:
308309
"""Send a completion/complete request."""
310+
context = None
311+
if context_arguments is not None:
312+
context = types.CompletionContext(arguments=context_arguments)
313+
309314
return await self.send_request(
310315
types.ClientRequest(
311316
types.CompleteRequest(
312317
method="completion/complete",
313318
params=types.CompleteRequestParams(
314319
ref=ref,
315320
argument=types.CompletionArgument(**argument),
321+
context=context,
316322
),
317323
)
318324
),

src/mcp/server/fastmcp/server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,24 @@ def decorator(fn: AnyFunction) -> AnyFunction:
364364

365365
return decorator
366366

367+
def completion(self):
368+
"""Decorator to register a completion handler.
369+
370+
The completion handler receives:
371+
- ref: PromptReference or ResourceTemplateReference
372+
- argument: CompletionArgument with name and partial value
373+
- context: Optional CompletionContext with previously resolved arguments
374+
375+
Example:
376+
@mcp.completion()
377+
async def handle_completion(ref, argument, context):
378+
if isinstance(ref, ResourceTemplateReference):
379+
# Return completions based on ref, argument, and context
380+
return Completion(values=["option1", "option2"])
381+
return None
382+
"""
383+
return self._mcp_server.completion()
384+
367385
def add_resource(self, resource: Resource) -> None:
368386
"""Add a resource to the server.
369387

src/mcp/server/lowlevel/server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,14 +433,15 @@ def decorator(
433433
[
434434
types.PromptReference | types.ResourceTemplateReference,
435435
types.CompletionArgument,
436+
types.CompletionContext | None,
436437
],
437438
Awaitable[types.Completion | None],
438439
],
439440
):
440441
logger.debug("Registering handler for CompleteRequest")
441442

442443
async def handler(req: types.CompleteRequest):
443-
completion = await func(req.params.ref, req.params.argument)
444+
completion = await func(req.params.ref, req.params.argument, req.params.context)
444445
return types.ServerResult(
445446
types.CompleteResult(
446447
completion=completion

src/mcp/types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,11 +1028,21 @@ class CompletionArgument(BaseModel):
10281028
model_config = ConfigDict(extra="allow")
10291029

10301030

1031+
class CompletionContext(BaseModel):
1032+
"""Additional, optional context for completions."""
1033+
1034+
arguments: dict[str, str] | None = None
1035+
"""Previously-resolved variables in a URI template or prompt."""
1036+
model_config = ConfigDict(extra="allow")
1037+
1038+
10311039
class CompleteRequestParams(RequestParams):
10321040
"""Parameters for completion requests."""
10331041

10341042
ref: ResourceTemplateReference | PromptReference
10351043
argument: CompletionArgument
1044+
context: CompletionContext | None = None
1045+
"""Additional, optional context for completions"""
10361046
model_config = ConfigDict(extra="allow")
10371047

10381048

tests/server/fastmcp/test_integration.py

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,31 @@
1818
from starlette.applications import Starlette
1919
from starlette.requests import Request
2020

21-
import mcp.types as types
2221
from mcp.client.session import ClientSession
2322
from mcp.client.sse import sse_client
2423
from mcp.client.streamable_http import streamablehttp_client
2524
from mcp.server.fastmcp import FastMCP
2625
from mcp.server.fastmcp.resources import FunctionResource
2726
from mcp.shared.context import RequestContext
2827
from mcp.types import (
28+
Completion,
29+
CompletionArgument,
30+
CompletionContext,
2931
CreateMessageRequestParams,
3032
CreateMessageResult,
3133
GetPromptResult,
3234
InitializeResult,
35+
LoggingMessageNotification,
36+
ProgressNotification,
37+
PromptReference,
3338
ReadResourceResult,
39+
ResourceListChangedNotification,
40+
ResourceTemplateReference,
3441
SamplingMessage,
42+
ServerNotification,
3543
TextContent,
3644
TextResourceContents,
45+
ToolListChangedNotification,
3746
)
3847

3948

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

203+
# Resource template with completion support
204+
@mcp.resource("github://repos/{owner}/{repo}")
205+
def github_repo_resource(owner: str, repo: str) -> str:
206+
return f"Repository: {owner}/{repo}"
207+
208+
# Add completion handler for the server
209+
@mcp.completion()
210+
async def handle_completion(
211+
ref: PromptReference | ResourceTemplateReference,
212+
argument: CompletionArgument,
213+
context: CompletionContext | None,
214+
) -> Completion | None:
215+
# Handle GitHub repository completion
216+
if isinstance(ref, ResourceTemplateReference):
217+
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
218+
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
219+
# Return repos for modelcontextprotocol org
220+
return Completion(values=["python-sdk", "typescript-sdk", "specification"], total=3, hasMore=False)
221+
elif context and context.arguments and context.arguments.get("owner") == "test-org":
222+
# Return repos for test-org
223+
return Completion(values=["test-repo1", "test-repo2"], total=2, hasMore=False)
224+
225+
# Handle prompt completions
226+
if isinstance(ref, PromptReference):
227+
if ref.name == "complex_prompt" and argument.name == "context":
228+
# Complete context values
229+
contexts = ["general", "technical", "business", "academic"]
230+
return Completion(
231+
values=[c for c in contexts if c.startswith(argument.value)], total=None, hasMore=False
232+
)
233+
234+
# Default: no completion available
235+
return Completion(values=[], total=0, hasMore=False)
236+
194237
# Tool that echoes request headers from context
195238
@mcp.tool(description="Echo request headers from context")
196239
def echo_headers(ctx: Context[Any, Any, Request]) -> str:
@@ -597,15 +640,15 @@ async def handle_tool_list_changed(self, params) -> None:
597640

598641
async def handle_generic_notification(self, message) -> None:
599642
# Check if this is a ServerNotification
600-
if isinstance(message, types.ServerNotification):
643+
if isinstance(message, ServerNotification):
601644
# Check the specific notification type
602-
if isinstance(message.root, types.ProgressNotification):
645+
if isinstance(message.root, ProgressNotification):
603646
await self.handle_progress(message.root.params)
604-
elif isinstance(message.root, types.LoggingMessageNotification):
647+
elif isinstance(message.root, LoggingMessageNotification):
605648
await self.handle_log(message.root.params)
606-
elif isinstance(message.root, types.ResourceListChangedNotification):
649+
elif isinstance(message.root, ResourceListChangedNotification):
607650
await self.handle_resource_list_changed(message.root.params)
608-
elif isinstance(message.root, types.ToolListChangedNotification):
651+
elif isinstance(message.root, ToolListChangedNotification):
609652
await self.handle_tool_list_changed(message.root.params)
610653

611654

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

827+
# Test completion functionality
828+
# 1. Test resource template completion with context
829+
repo_result = await session.complete(
830+
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
831+
argument={"name": "repo", "value": ""},
832+
context_arguments={"owner": "modelcontextprotocol"},
833+
)
834+
assert repo_result.completion.values == ["python-sdk", "typescript-sdk", "specification"]
835+
assert repo_result.completion.total == 3
836+
assert repo_result.completion.hasMore is False
837+
838+
# 2. Test with different context
839+
repo_result2 = await session.complete(
840+
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
841+
argument={"name": "repo", "value": ""},
842+
context_arguments={"owner": "test-org"},
843+
)
844+
assert repo_result2.completion.values == ["test-repo1", "test-repo2"]
845+
assert repo_result2.completion.total == 2
846+
847+
# 3. Test prompt argument completion
848+
context_result = await session.complete(
849+
ref=PromptReference(type="ref/prompt", name="complex_prompt"),
850+
argument={"name": "context", "value": "tech"},
851+
)
852+
assert "technical" in context_result.completion.values
853+
854+
# 4. Test completion without context (should return empty)
855+
no_context_result = await session.complete(
856+
ref=ResourceTemplateReference(type="ref/resource", uri="github://repos/{owner}/{repo}"),
857+
argument={"name": "repo", "value": "test"},
858+
)
859+
assert no_context_result.completion.values == []
860+
assert no_context_result.completion.total == 0
861+
784862

785863
async def sampling_callback(
786864
context: RequestContext[ClientSession, None],

0 commit comments

Comments
 (0)
0