Skip to content

Commit 7417e97

Browse files
authored
Let a server answer argument-completion requests (#4582)
* Add server-side argument completion (@mcp.completion) * Reference CompletionValues directly in cast so the import reads as used * Import completion types from mcp_types, not the fastmcp.types mirror * Fix test imports after dropping the fastmcp.types mirror * Fix change-register example import after dropping the types mirror * Enforce 100-value completion cap; make docs example runnable * Document completion authorization contract * Offload sync completion handlers to threadpool * Exclude bare str from completion return type * Pass Any-typed value in bare-string rejection test * Point completion authoring types to mcp_types in v4 notes
1 parent 611a358 commit 7417e97

7 files changed

Lines changed: 723 additions & 1 deletion

File tree

docs/development/v4-notes/change-register.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,34 @@ There is deliberately no compatibility alias for the old spelling.
202202

203203
*Verify:* `fastmcp_slim/fastmcp/server/middleware/caching.py`.
204204

205+
### Server-side argument completion — New (opt-in feature)
206+
207+
A FastMCP server can now answer `completion/complete` requests, suggesting values for prompt arguments and resource-template parameters as a user types. Previously a FastMCP *client* could call `complete()` but a FastMCP *server* had no way to respond — the method was unregistered, so it returned `-32601` (method-not-found) on both eras. The new `@mcp.completion` decorator registers a single server-level handler that receives the reference (a `PromptReference` or `ResourceTemplateReference`), the `CompletionArgument` being completed, and the optional `CompletionContext` of already-supplied argument values, and returns candidates — a list of strings, a `Completion` (to carry the `total`/`has_more` pagination hints), or `None`/empty for a reference it does not recognize (which yields an empty completion, not an error).
208+
209+
```python
210+
from fastmcp import FastMCP
211+
from mcp_types import PromptReference
212+
213+
mcp = FastMCP("Completion Server")
214+
215+
216+
@mcp.prompt
217+
def write_poem(theme: str) -> str:
218+
return f"Write a poem about {theme}"
219+
220+
221+
@mcp.completion
222+
def complete(ref, argument, context):
223+
if isinstance(ref, PromptReference) and argument.name == "theme":
224+
options = ["nature", "love", "adventure"]
225+
return [o for o in options if o.startswith(argument.value)]
226+
return None
227+
```
228+
229+
The completions capability is declared exactly when a handler exists: `add_completion_handler` registers the low-level `completion/complete` handler, and the SDK derives the capability from that handler's presence — a server with no completion handler does not advertise it. FastMCP does not hand-set the capability. The single-handler shape mirrors the SDK's own `completion/complete` surface and FastMCP's existing client-side `Client.complete()`, and it slots into the `@mcp.tool`/`@mcp.prompt`/`@mcp.resource` decorator lineup as another server-level `@mcp.<verb>` registration rather than inventing a per-argument sub-decorator idiom. It works identically on the handshake and modern (`2026-07-28`) eras, since `completion/complete` is a request/response method that flows on every era. The authoring types — `PromptReference`, `ResourceTemplateReference`, `CompletionArgument`, `CompletionContext`, and `Completion` — are imported from `mcp_types`, not `fastmcp.types`.
230+
231+
*Verify:* `fastmcp_slim/fastmcp/server/completions.py` (handler type + `normalize_completion`), `fastmcp_slim/fastmcp/server/server.py` (`completion` decorator, `add_completion_handler`), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_complete`), `tests/server/test_completions.py`, `docs/servers/completions.mdx`.
232+
205233
## Client
206234

207235
The `fastmcp.Client` public API is largely preserved. The client stays a wrapper around `mcp.ClientSession`; the first-class `mcp.client.Client` is deliberately not adopted. Two client-surface changes are called out below: the connection `mode` default flips to `"auto"`, and `extensions=` / `result_claims=` are newly surfaced.

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
"pages": [
145145
"servers/elicitation",
146146
"servers/sampling",
147+
"servers/completions",
147148
"servers/progress",
148149
"servers/logging",
149150
"servers/pagination",

docs/servers/completions.mdx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: Argument Completion
3+
sidebarTitle: Completions
4+
description: Suggest values for prompt arguments and resource template parameters as the user types.
5+
icon: list-check
6+
---
7+
8+
import { VersionBadge } from "/snippets/version-badge.mdx"
9+
10+
<VersionBadge version="4.0.0" />
11+
12+
Argument completion lets a server suggest values while a user fills in a prompt argument or a resource template parameter. As the user types, the client sends a `completion/complete` request naming the prompt or template, the argument being completed, and the partial value so far. The server answers with candidate strings, which the client offers as autocomplete suggestions.
13+
14+
This is the server side of the feature. A client requests completions with [`Client.complete()`](/clients/client); this page covers how a server answers.
15+
16+
## Register a completion handler
17+
18+
A server has a single completion handler, registered with the `@mcp.completion` decorator. The handler receives every completion request and switches on which reference and argument is being completed.
19+
20+
```python
21+
from fastmcp import FastMCP
22+
from mcp_types import PromptReference
23+
24+
mcp = FastMCP("Completion Server")
25+
26+
27+
@mcp.prompt
28+
def write_poem(theme: str) -> str:
29+
return f"Write a poem about {theme}"
30+
31+
32+
@mcp.completion
33+
def complete(ref, argument, context):
34+
if isinstance(ref, PromptReference) and ref.name == "write_poem":
35+
if argument.name == "theme":
36+
options = ["nature", "love", "adventure"]
37+
return [o for o in options if o.startswith(argument.value)]
38+
return None
39+
```
40+
41+
The handler is called with three values:
42+
43+
- `ref`: which component is being completed — a `PromptReference` (carrying the prompt `name`) or a `ResourceTemplateReference` (carrying the template `uri`).
44+
- `argument`: a `CompletionArgument` with the argument `name` and the partial `value` typed so far.
45+
- `context`: an optional `CompletionContext` carrying the values of arguments the user has already supplied (see [Using already-supplied arguments](#using-already-supplied-arguments)).
46+
47+
Filter your candidates against `argument.value` so the suggestions narrow as the user types. Returning `None` means "I have no suggestions for this reference and argument" — the client receives an empty list, which is the correct answer for a reference the server does not recognize.
48+
49+
<Tip>
50+
Registering a completion handler declares the server's completions capability during the handshake. A server with no handler does not advertise the capability, and a client that checks capabilities before calling will skip completion requests entirely. This works the same way on both the handshake and modern protocol eras.
51+
</Tip>
52+
53+
## Completing resource template parameters
54+
55+
The same handler answers completion for resource template parameters. A `ResourceTemplateReference` identifies the template by its URI template, and `argument.name` is the parameter being completed.
56+
57+
```python
58+
from fastmcp import FastMCP
59+
from mcp_types import ResourceTemplateReference
60+
61+
mcp = FastMCP("Completion Server")
62+
63+
REPOS = ["fastmcp", "prefect", "marvin"]
64+
65+
66+
@mcp.resource("github://{owner}/{repo}")
67+
def repo_readme(owner: str, repo: str) -> str:
68+
return f"README for {owner}/{repo}"
69+
70+
71+
@mcp.completion
72+
def complete(ref, argument, context):
73+
if isinstance(ref, ResourceTemplateReference):
74+
if ref.uri == "github://{owner}/{repo}" and argument.name == "repo":
75+
return [r for r in REPOS if r.startswith(argument.value)]
76+
return None
77+
```
78+
79+
Because a single handler answers for every prompt and template, a server that completes several components branches on `ref` first, then on `argument.name`. Grouping the branches by reference keeps the handler readable as it grows.
80+
81+
## Using already-supplied arguments
82+
83+
Completions often depend on values the user has already entered. A repository suggestion, for example, depends on which owner was chosen. The client sends those resolved values in the completion context, and the handler reads them from `context.arguments`.
84+
85+
```python
86+
from fastmcp import FastMCP
87+
from mcp_types import ResourceTemplateReference
88+
89+
mcp = FastMCP("Completion Server")
90+
91+
REPOS_BY_OWNER = {
92+
"prefecthq": ["fastmcp", "prefect", "marvin"],
93+
"python": ["cpython", "mypy"],
94+
}
95+
96+
97+
@mcp.resource("github://{owner}/{repo}")
98+
def repo_readme(owner: str, repo: str) -> str:
99+
return f"README for {owner}/{repo}"
100+
101+
102+
@mcp.completion
103+
def complete(ref, argument, context):
104+
if isinstance(ref, ResourceTemplateReference) and argument.name == "repo":
105+
owner = context.arguments.get("owner") if context and context.arguments else None
106+
repos = REPOS_BY_OWNER.get(owner or "", [])
107+
return [r for r in repos if r.startswith(argument.value)]
108+
return None
109+
```
110+
111+
Here the suggestions for `repo` are scoped to the `owner` the user already selected. The context is only present once at least one argument has been resolved, so guard against `context` being `None`.
112+
113+
## Returning results
114+
115+
A handler may return any of three things:
116+
117+
- A list of strings — the simplest form, wrapped into a completion response automatically.
118+
- `None` — treated as an empty completion, for references and arguments the handler does not recognize.
119+
- A `Completion` object — when you want to include pagination hints alongside the values.
120+
121+
The MCP protocol caps a single response at 100 values. When more candidates exist, return a `Completion` and set `total` (how many candidates match in all) and `has_more` (whether values were truncated) so the client can indicate that the list is partial.
122+
123+
```python
124+
from fastmcp import FastMCP
125+
from mcp_types import Completion, PromptReference
126+
127+
mcp = FastMCP("Completion Server")
128+
129+
ALL_CITIES = ["Paris", "Prague", "Portland", "Phoenix", "Perth"]
130+
131+
132+
@mcp.prompt
133+
def pick_city(city: str) -> str:
134+
return f"Tell me about {city}"
135+
136+
137+
def search_cities(prefix: str) -> list[str]:
138+
# A real lookup might return thousands of matches; ALL_CITIES stands in.
139+
return [c for c in ALL_CITIES if c.startswith(prefix)]
140+
141+
142+
@mcp.completion
143+
def complete(ref, argument, context):
144+
if isinstance(ref, PromptReference) and argument.name == "city":
145+
matches = search_cities(argument.value)
146+
return Completion(
147+
values=matches[:100],
148+
total=len(matches),
149+
has_more=len(matches) > 100,
150+
)
151+
return None
152+
```
153+
154+
## Accessing the request context
155+
156+
A completion handler may be sync or async, and it can reach the active request through FastMCP's dependency functions the same way any handler does. Use [`get_context()`](/servers/context) to access session information, authentication, or server state while computing suggestions.
157+
158+
```python
159+
from fastmcp import FastMCP
160+
from fastmcp.server.dependencies import get_context
161+
from mcp_types import PromptReference
162+
163+
mcp = FastMCP("Completion Server")
164+
165+
166+
@mcp.completion
167+
async def complete(ref, argument, context):
168+
ctx = get_context()
169+
await ctx.debug(f"Completing {argument.name!r} for {ref}")
170+
...
171+
```
172+
173+
## Authorization
174+
175+
Completion runs behind the server's connection-level authentication: an unauthenticated client never reaches the handler. It is independent of per-component `auth=`, though. FastMCP does not resolve the referenced prompt or resource template, so a completion request is not filtered by that component's visibility the way `prompts/get` or a resource read is — the single handler answers for whatever reference the client names.
176+
177+
A completion response carries only candidate strings for one argument, never component content or schema, so this exposes nothing about a hidden component on its own. If a handler computes candidates that should themselves be restricted — matching a prompt hidden from unauthorized callers, say — check the auth context inside the handler (via [`get_context()`](/servers/context)) and return `None` when the caller is not permitted.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Server-side argument completion for FastMCP.
2+
3+
A completion request names a reference — a specific prompt or resource
4+
template — and the argument being completed, plus a context of the argument
5+
values already supplied. The server answers with candidate string values.
6+
7+
FastMCP surfaces this as a single server-level handler registered with
8+
``@mcp.completion``, mirroring the MCP SDK's own ``completion/complete`` shape
9+
and FastMCP's client-side ``Client.complete()``. The handler receives the
10+
reference, the argument, and the optional context, and returns candidates for
11+
whichever reference/argument pair it recognizes.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from collections.abc import Awaitable, Callable
17+
18+
import mcp_types
19+
20+
CompletionReference = mcp_types.PromptReference | mcp_types.ResourceTemplateReference
21+
"""The reference a completion request targets: a prompt or a resource template."""
22+
23+
CompletionValues = mcp_types.Completion | list[str] | tuple[str, ...] | None
24+
"""What a completion handler may return.
25+
26+
- ``Completion`` — used verbatim (carries the optional ``total`` / ``has_more``
27+
pagination hints).
28+
- ``list[str]`` / ``tuple[str, ...]`` — wrapped into a ``Completion``. A bare
29+
``str`` is deliberately excluded: it satisfies ``Sequence[str]`` but is almost
30+
always a mistake, and ``normalize_completion`` rejects it at runtime — naming
31+
concrete collections keeps the annotation and the runtime guard in agreement.
32+
- ``None`` — treated as "no candidates" (an empty completion).
33+
"""
34+
35+
CompletionHandler = Callable[
36+
[
37+
CompletionReference,
38+
mcp_types.CompletionArgument,
39+
mcp_types.CompletionContext | None,
40+
],
41+
Awaitable[CompletionValues] | CompletionValues,
42+
]
43+
"""A server's completion handler.
44+
45+
Called with the reference, the argument being completed, and the optional
46+
context of already-supplied argument values. May be sync or async.
47+
"""
48+
49+
50+
# The MCP completion contract caps `values` at 100 candidates per response.
51+
MAX_COMPLETION_VALUES = 100
52+
53+
54+
def normalize_completion(result: CompletionValues) -> mcp_types.Completion:
55+
"""Coerce a handler's return value into a wire ``Completion``.
56+
57+
A returned ``str`` is rejected: it is almost always a mistake (the value
58+
would iterate into one-character candidates), so it raises rather than
59+
silently producing surprising output.
60+
61+
The MCP contract caps a completion at 100 values, so a longer result is
62+
truncated to the first 100 with ``has_more`` set — a handler that returns
63+
thousands of matches emits a conforming response rather than an oversized
64+
one that strict clients reject.
65+
"""
66+
if result is None:
67+
return mcp_types.Completion(values=[])
68+
if isinstance(result, str):
69+
raise TypeError(
70+
"A completion handler returned a str; return a list of strings "
71+
"(for example, [value]) or a Completion instead."
72+
)
73+
if isinstance(result, mcp_types.Completion):
74+
completion = result
75+
else:
76+
completion = mcp_types.Completion(values=list(result))
77+
78+
if len(completion.values) > MAX_COMPLETION_VALUES:
79+
total = (
80+
completion.total if completion.total is not None else len(completion.values)
81+
)
82+
return mcp_types.Completion(
83+
values=completion.values[:MAX_COMPLETION_VALUES],
84+
total=total,
85+
has_more=True,
86+
)
87+
return completion

fastmcp_slim/fastmcp/server/mixins/mcp_operations.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
from __future__ import annotations
44

5+
import inspect
56
from collections.abc import Sequence
6-
from typing import TYPE_CHECKING, Any, TypeVar
7+
from typing import TYPE_CHECKING, Any, TypeVar, cast
78

89
import mcp_types
910
from mcp.server.context import ServerRequestContext
1011
from mcp.shared.exceptions import MCPError
1112
from mcp_types import (
1213
INVALID_PARAMS,
1314
CallToolRequestParams,
15+
CompleteRequestParams,
1416
EmptyResult,
1517
GetPromptRequestParams,
1618
PaginatedRequestParams,
@@ -25,9 +27,14 @@
2527
NotFoundError,
2628
to_mcp_error,
2729
)
30+
from fastmcp.server.completions import CompletionValues, normalize_completion
2831
from fastmcp.server.dependencies import bind_request_context, extract_version_spec
2932
from fastmcp.server.tasks.config import TaskMeta
3033
from fastmcp.tools.base import InputRequiredToolResult
34+
from fastmcp.utilities.async_utils import (
35+
call_sync_fn_in_threadpool,
36+
is_coroutine_function,
37+
)
3138
from fastmcp.utilities.logging import get_logger
3239
from fastmcp.utilities.pagination import paginate_sequence
3340
from fastmcp.utilities.versions import VersionSpec, dedupe_with_versions
@@ -391,3 +398,54 @@ async def _on_set_logging_level(
391398
session_id = _log_level_session_key(rc.session)
392399
self._client_log_levels[session_id] = params.level
393400
return EmptyResult()
401+
402+
async def _on_complete(
403+
self: FastMCP,
404+
ctx: ServerRequestContext,
405+
params: CompleteRequestParams,
406+
) -> mcp_types.CompleteResult:
407+
"""Handle MCP 'completion/complete' requests.
408+
409+
Routes to the server's registered completion handler (set via
410+
``@mcp.completion``). The handler switches on the reference and argument
411+
and returns candidate values. A handler that does not recognize the
412+
reference/argument returns ``None`` or an empty sequence, which becomes
413+
an empty completion rather than an error — an unknown reference is not a
414+
protocol failure. This handler is registered on the low-level server
415+
only once a completion handler exists, so the completions capability is
416+
declared exactly when the server can answer.
417+
"""
418+
with bind_request_context(ctx):
419+
logger.debug(f"[{self.name}] Handler called: complete %s", params.ref)
420+
handler = self._completion_handler
421+
if handler is None:
422+
return mcp_types.CompleteResult(
423+
completion=mcp_types.Completion(values=[])
424+
)
425+
426+
if is_coroutine_function(handler):
427+
raw = handler(params.ref, params.argument, params.context)
428+
else:
429+
# A sync handler may perform blocking work (a database lookup,
430+
# say); run it in a threadpool so it does not stall the event
431+
# loop, matching how sync tools/prompts/resources are invoked.
432+
raw = await call_sync_fn_in_threadpool(
433+
handler, params.ref, params.argument, params.context
434+
)
435+
result = await raw if inspect.isawaitable(raw) else raw
436+
completion = normalize_completion(cast(CompletionValues, result))
437+
return mcp_types.CompleteResult(completion=completion)
438+
439+
def _register_completion_handler(self: FastMCP) -> None:
440+
"""Register the low-level ``completion/complete`` handler.
441+
442+
Called when a completion handler is set (via
443+
``add_completion_handler``) so the SDK derives the completions
444+
capability from the handler's presence. Registration is idempotent —
445+
re-registering replaces the handler.
446+
"""
447+
self._mcp_server.add_request_handler(
448+
"completion/complete",
449+
CompleteRequestParams,
450+
self._on_complete,
451+
)

0 commit comments

Comments
 (0)