49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
117 lines
5.6 KiB
Python
117 lines
5.6 KiB
Python
"""`docs/servers/completions.md`: every claim the page makes, proved against the real SDK."""
|
|
|
|
import pytest
|
|
from inline_snapshot import snapshot
|
|
from mcp_types import (
|
|
Completion,
|
|
CompletionContext,
|
|
CompletionsCapability,
|
|
ErrorData,
|
|
PromptReference,
|
|
ResourceTemplateReference,
|
|
)
|
|
|
|
from docs_src.completions import tutorial001, tutorial002, tutorial003
|
|
from mcp import Client, MCPError
|
|
|
|
# See test_index.py for why this is a per-module mark and not a conftest hook.
|
|
pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")]
|
|
|
|
TEMPLATE_REF = ResourceTemplateReference(uri="github://repos/{owner}/{repo}")
|
|
PROMPT_REF = PromptReference(name="review_code")
|
|
|
|
|
|
async def test_a_server_with_no_handler_has_no_completions_capability() -> None:
|
|
"""tutorial001: there is something worth completing, but no handler and no advertised capability."""
|
|
async with Client(tutorial001.mcp) as client:
|
|
(template,) = (await client.list_resource_templates()).resource_templates
|
|
assert template.uri_template == "github://repos/{owner}/{repo}"
|
|
(prompt,) = (await client.list_prompts()).prompts
|
|
assert prompt.name == "review_code"
|
|
assert client.server_capabilities.completions is None
|
|
|
|
|
|
async def test_completing_without_a_handler_is_method_not_found() -> None:
|
|
"""tutorial001: nothing handles `completion/complete`, so the request is a JSON-RPC error."""
|
|
async with Client(tutorial001.mcp) as client:
|
|
with pytest.raises(MCPError) as excinfo:
|
|
await client.complete(ref=PROMPT_REF, argument={"name": "language", "value": "py"})
|
|
assert excinfo.value.error == ErrorData(code=-32601, message="Method not found", data="completion/complete")
|
|
|
|
|
|
async def test_registering_the_handler_advertises_the_capability() -> None:
|
|
"""tutorial002: `@mcp.completion()` is the whole declaration; the capability is derived from it."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
assert client.server_capabilities.completions == CompletionsCapability()
|
|
|
|
|
|
async def test_prompt_argument_completion_filters_on_the_typed_prefix() -> None:
|
|
"""tutorial002: the handler returns the languages that start with `argument.value`."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
result = await client.complete(ref=PROMPT_REF, argument={"name": "language", "value": "py"})
|
|
assert result.completion == snapshot(Completion(values=["python"]))
|
|
|
|
|
|
async def test_empty_value_returns_every_suggestion() -> None:
|
|
"""tutorial002: an empty prefix matches everything, so the client gets the whole list."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
result = await client.complete(ref=PROMPT_REF, argument={"name": "language", "value": ""})
|
|
assert result.completion.values == ["go", "javascript", "python", "rust", "typescript"]
|
|
|
|
|
|
async def test_returning_none_is_an_empty_list_not_an_error() -> None:
|
|
"""tutorial002: an argument the handler does not recognise produces `values=[]`, never a failure."""
|
|
async with Client(tutorial002.mcp) as client:
|
|
result = await client.complete(ref=PROMPT_REF, argument={"name": "code", "value": "x"})
|
|
assert result.completion == snapshot(Completion(values=[]))
|
|
result = await client.complete(ref=TEMPLATE_REF, argument={"name": "repo", "value": ""})
|
|
assert result.completion.values == []
|
|
|
|
|
|
async def test_context_arguments_resolve_a_dependent_parameter() -> None:
|
|
"""tutorial003: the already-resolved `owner` arrives in `context.arguments` and picks the repo list."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
result = await client.complete(
|
|
ref=TEMPLATE_REF,
|
|
argument={"name": "repo", "value": ""},
|
|
context_arguments={"owner": "modelcontextprotocol"},
|
|
)
|
|
assert result.completion == snapshot(Completion(values=["python-sdk", "typescript-sdk", "inspector"]))
|
|
|
|
|
|
async def test_the_typed_prefix_still_filters_a_dependent_parameter() -> None:
|
|
"""tutorial003: `argument.value` narrows the owner's repos exactly as it narrows a prompt argument."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
result = await client.complete(
|
|
ref=TEMPLATE_REF,
|
|
argument={"name": "repo", "value": "py"},
|
|
context_arguments={"owner": "modelcontextprotocol"},
|
|
)
|
|
assert result.completion.values == ["python-sdk"]
|
|
|
|
|
|
def test_context_arguments_is_optional() -> None:
|
|
"""tutorial003: `context.arguments` is `dict[str, str] | None`; the handler's `None` guard is required."""
|
|
assert CompletionContext.model_fields["arguments"].annotation == (dict[str, str] | None)
|
|
assert CompletionContext().arguments is None
|
|
|
|
|
|
async def test_no_context_means_no_suggestions() -> None:
|
|
"""tutorial003: without a resolved `owner` (or with an unknown one) the handler has nothing to offer."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
result = await client.complete(ref=TEMPLATE_REF, argument={"name": "repo", "value": ""})
|
|
assert result.completion.values == []
|
|
result = await client.complete(
|
|
ref=TEMPLATE_REF,
|
|
argument={"name": "repo", "value": ""},
|
|
context_arguments={"owner": "nobody"},
|
|
)
|
|
assert result.completion.values == []
|
|
|
|
|
|
async def test_the_prompt_branch_is_untouched_by_the_new_one() -> None:
|
|
"""tutorial003: adding the resource-template branch leaves prompt-argument completion as it was."""
|
|
async with Client(tutorial003.mcp) as client:
|
|
result = await client.complete(ref=PROMPT_REF, argument={"name": "language", "value": "type"})
|
|
assert result.completion.values == ["typescript"]
|