Skip to content

MCPServer

register_tool registers a kernel spec as a tool, register_tools every spec in a kernel SpecRegistry, and register_resource a kernel RETRIEVE selector as a resource.

MCPServer

A pluggable MCP server serving kernel specs.

The server owns its tool, resource and prompt registries, an auth backend, and a session store — all instance state, no module-level singletons. A tool or a resource is a django_service_specs spec, registered by passing it in:

from django_service_specs import SelectorKind, SelectorSpec, ServiceSpec

server = MCPServer(name="my-app")
server.register_tool(
    ServiceSpec(service=create_invoice, permissions=[IsBilling()]),
    name="invoices.create",
)
server.register_resource(
    SelectorSpec(
        kind=SelectorKind.RETRIEVE, selector=get_invoice, permissions=[IsBilling()]
    ),
    name="invoice",
    uri_template="invoices://{pk}",
)

A SpecRegistry registers in one call, register_tools, with the MCP knobs per tool in overrides.

Mount the URLs in your URL conf the admin.site.urls way — .urls is a namespaced (patterns, app_name, namespace) triple path() mounts directly (no include()):

urlpatterns = [path("mcp/", server.urls)]
# reverse("mcp:endpoint") · reverse("mcp:protected-resource-metadata")

What every spec on the mount may read -- a tenant, a locale, a clock -- is registered once as pool_seeds=, a kernel PoolSeeds:

server = MCPServer(
    pool_seeds=DEFAULT_POOL_SEEDS.extend(tenant=lambda *, user: user.tenant),
)

Every spec the server runs receives the seeds (tools over either transport, call_tool / acall_tool, task workers and resource reads), every operation condition it asks is asked with them, tools/list availability included, and a registered name is reserved: a client argument cannot occupy it, and a spec read or a URI-template variable naming it is refused at registration.

What the server tells a model in its own words -- the handle description and line, a paged tool's scope sentence, the missing-argument message -- is conventions=, an AgentConventions. Change one sentence and keep the rest:

server = MCPServer(
    conventions=AgentConventions(handle_line="Pass ids to other tools; never show them."),
)

The server still decides where each sentence appears; the conventions decide what it says, on every route the seeds above reach.

config property

config: MCPConfig

This server's resolved scalars — a frozen snapshot taken at construction.

pool_seeds property

pool_seeds: PoolSeeds

The pool_seeds= every dispatch and condition on this server receives.

conventions property

conventions: AgentConventions

The conventions= every listing and refusal on this server is worded with.

auth_backend property

auth_backend: MCPAuthBackend | None

The auth_backend= given at construction, or None when there is none.

urls property

urls: tuple[list[URLPattern], str, str]

Sync URL patterns. Suitable for any deployment (WSGI or ASGI).

Returns the namespaced (patterns, app_name, namespace) triple path() mounts directly — path("mcp/", server.urls), the admin.site.urls idiom — so the endpoints reverse within the namespace (reverse("mcp:endpoint")). Use async_urls instead when running under ASGI to get non-blocking dispatch for the I/O-bound handlers.

async_urls property

async_urls: tuple[list[URLPattern], str, str]

Async URL patterns for ASGI deployments.

The namespaced triple (like urls), but tools/call, resources/read and prompts/get dispatch through async-native runners. Sync collaborators (auth backend, session store, custom permissions) are bridged via asgiref.sync.sync_to_async, so a fully sync stack still works; async-native ones are detected by signature and called directly.

register_tool

register_tool(
    spec: ServiceSpec | SelectorSpec,
    *,
    name: str,
    description: str | None = None,
    title: str | None = None,
    display_name: str | None = None,
    display_description: str | None = None,
    icons: tuple[Icon, ...] = (),
    annotations: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    ui: UIToolMeta | None = None,
    content_kind: ToolContentKind = ToolContentKind.TEXT,
    content_mime_type: str | None = None,
    include_structured_content: bool | None = None,
    include_output_schema: bool | None = None,
    task_policy: TaskPolicy = TaskPolicy.FORBIDDEN,
    invalidates: tuple[str, ...] | list[str] = (),
    paginate: bool = False,
    max_page_size: int | None | UnsetType = UNSET,
    max_result_bytes: int | None | UnsetType = UNSET,
    dispatch_timeout: float | None | UnsetType = UNSET,
    always_listed: bool = False,
    rate_limits: list[Any] | None = None,
    scopes: str | Sequence[str] | None = None,
    unknown_arguments: UnknownArguments = UnknownArguments.REJECT,
    server_arguments: ServerArguments | None = None,
) -> KernelToolBinding

Register a kernel spec as a tool: a ServiceSpec or a SelectorSpec.

spec is a django_service_specs spec, and its class says which kind of tool this is. A service tool's arguments are the spec's parameters at the top level of arguments, as the kernel's spec_input_schema advertises them; a selector tool reads, and a LIST one paginates under paginate=True. The call runs through the kernel's dispatch / adispatch, which asks the spec's own permissions of the principal the auth backend resolved for the request.

Anything that is not a kernel spec is refused with a TypeError. A knob of the other kind of tool is refused: paginate / max_page_size on a service tool, invalidates on a selector tool.

A LIST selector tool registered without paginate=True warns UnboundedListWarning, and REQUIRE_LIST_PAGINATION refuses it. A paginated one whose spec declares a read named page or limit is refused, since the pagination takes both names from the arguments before the selector runs. A presenter whose audience markings cannot be projected (two labels, say) is refused naming the tool, rather than failing every tools/list of the server.

A declaration no call on this server can run as written is refused naming the tool, by the kernel's check_seeds with the server's pool_seeds: a parameter named like a registered seed or the call's own request or view, a positional-only parameter with no default, a keyword the callable requires that nothing fills, and an optional read over a keyword the callable requires. A read server_arguments binds a value to is filled on every call, so it is never refused as optional.

display_name and display_description are labels for in-process consumers, read off the binding and never sent on the wire.

scopes are the OAuth scopes the caller's token must carry, every one of them; a token missing one is answered -32006 with every declared scope in requiredScopes, as a prompt's or a view's scope gate answers. A bare string is one scope. scopes=() is refused, since it would read as a gate and admit every caller. The scope gate is a transport check and does not count as the tool's guard for REQUIRE_TOOL_PERMISSIONS, which reads the spec's own permissions.

task_policy means the same for a service tool and a selector tool: OPTIONAL answers a client that declared the tasks extension with a task handle and runs inline for one that did not, and REQUIRED refuses the latter with -32021. The spec's own checks are asked before the task is created, and the worker runs the call, under either policy, through the same dispatch a direct call takes, under the server's pool_seeds and conventions.

A refusal from dispatch is answered on the wire as kernel_refusals words it: NotPermitted and PrincipalUnavailable as -32006, InvalidArguments, ServiceValidationError and a bare DispatchError as the validation_error result, an AdditionalInputRequired as MRTR's input_required, and any other ServiceError as the service_error result.

register_tools

register_tools(
    registry: SpecRegistry, *, overrides: Mapping[str, Mapping[str, Any]] | None = None
) -> tuple[KernelToolBinding, ...]

Register every kernel spec in a SpecRegistry as a tool, in order.

Each entry registers through register_tool under its registry name, with the knobs overrides holds under that name, so an entry and a direct register_tool of the same spec are the same tool. An overrides key the registry does not hold raises ValueError, as a typo rather than a no-op, and an entry that is not a kernel ServiceSpec or SelectorSpec (a SpecSource among them) is refused as register_tool refuses it. A refusal of any entry registers none of them: every entry is built and checked first.

Returns the bindings in registration order.

call_tool

call_tool(
    name: str,
    arguments: dict[str, Any] | None = None,
    *,
    user: Any,
    request: Any = None,
) -> ToolResult

Invoke a registered tool off the HTTP / JSON-RPC path.

The transport-neutral entry point: hand a tool name, a flat arguments dict and the acting user, and get back the same ToolResult the wire handlers build. An in-process consumer calls this instead of re-implementing dispatch.

The call runs through the kernel's dispatch as on the wire: the binding's server_arguments in place, the spec's own permissions asked of user, then the spec run and presented, a paged tool's pagination included. The transport-level checks are not consulted: the scopes= gate and rate limits read a token, and there is none in process. For those, and for tool listing, use acall_tool / list_tools.

A refusal the wire answers -32006 (NotPermitted, PrincipalUnavailable) is raised for the caller to map to its own wire; every other refusal comes back as the isError result the wire serves.

request is the caller's own Django request when there is one, and the call never writes its user to it: the spec is handed a copy whose user is user, so a view handing the request it is serving keeps serving as its own user after the call. The copy shares the handed request's body, stream and parsed POST and FILES, so a body a spec reads is read for the caller, and an object a spec mutates through it, such as META or the session, is the caller's; an attribute a spec sets or deletes changes the copy alone. With no request, a minimal POST HttpRequest bearing user is synthesised.

Raises KeyError when no tool is registered under name.

list_tools

list_tools(
    cursor: str | None = None,
    *,
    user: Any,
    request: Any = None,
    scopes: Sequence[str] | None = None,
    include_unavailable: bool = False,
) -> dict[str, Any] | JsonRpcError

List the tools this server exposes, exactly as the wire would.

The in-process twin of a tools/list request: one page of the tool catalog with the same inputSchema the HTTP transport advertises (the spec's parameters, a paginated tool's page / limit and the additionalProperties policy), the same per-caller listing-permission filter (FILTER_LISTINGS_BY_PERMISSIONS) and the same opaque-cursor pagination — pass the returned nextCursor back for the next page. A JsonRpcError signals a bad cursor.

scopes are the caller's granted scopes; pass them so a tool registered with scopes= is visible under FILTER_LISTINGS_BY_PERMISSIONS exactly as it would be on the wire. A tool whose operation-scope affordances condition is unmet for user right now is left out, as on the wire, unless include_unavailable=True, which lists every tool the caller may see and asks no condition. That is for a consumer that builds its tool definitions once and asks unavailable_tools each step: it needs a definition for a tool that is unavailable when it lists and becomes available later. The wire has no such parameter.

Unlike call_tool (the spec core) this is the full transport surface. Under an event loop use alist_tools — a listing permission filter or an affordance condition that hits the DB raises SynchronousOnlyOperation from a sync call on the loop.

alist_tools async

alist_tools(
    cursor: str | None = None,
    *,
    user: Any,
    request: Any = None,
    scopes: Sequence[str] | None = None,
    include_unavailable: bool = False,
) -> dict[str, Any] | JsonRpcError

Async list_tools — safe to call from an event loop.

Listing itself is pure Python, but the per-caller permission filter (FILTER_LISTINGS_BY_PERMISSIONS) and a tool's operation-scope affordances condition may each run a DB-backed check, which raises SynchronousOnlyOperation when reached synchronously from within an event loop. The whole sync handler therefore runs in Django's thread-sensitive executor.

unavailable_tools

unavailable_tools(
    *, user: Any, request: Any = None, scopes: Sequence[str] | None = None
) -> dict[str, Affordance]

The tools list_tools leaves out for this caller right now, and why.

Maps each omitted tool's name to the operation-scope Affordance whose condition is unmet: its code and its reason, the sentence written for people and models alike. The answer comes from the same pass tools/list makes, over the same tools the caller may see (FILTER_LISTINGS_BY_PERMISSIONS included), so a name is here exactly when the listing omits it, and a tool hidden from this caller is never named with its reason.

For an in-process consumer that lists once with include_unavailable=True and asks this each step: it offers its model what a fresh tools/list would, and can tell the model why the rest are missing, which a listing cannot, since it only leaves them out. always_listed=True tools are never here, as they are never left out. Advisory like the listing: acall_tool still enforces every condition.

Under an event loop use aunavailable_tools; a condition or listing permission that queries raises SynchronousOnlyOperation from here.

aunavailable_tools async

aunavailable_tools(
    *, user: Any, request: Any = None, scopes: Sequence[str] | None = None
) -> dict[str, Affordance]

Async unavailable_tools -- safe to call from an event loop.

Runs in Django's thread-sensitive executor, as alist_tools does, since a condition or a listing permission may query. A server none of whose tools declares an operation-scope condition answers {} without the hop: that is decided from the declarations alone, and a consumer asking on every model step should not pay a thread switch per step to learn it.

acall_tool async

acall_tool(
    name: str,
    arguments: dict[str, Any] | None = None,
    *,
    user: Any,
    request: Any = None,
    scopes: Sequence[str] | None = None,
) -> dict[str, Any] | JsonRpcError

Invoke a tool off the HTTP path with full transport semantics (async).

The in-process twin of a tools/call request: routes through the same async handler the wire uses, so the scopes= gate and the rate limits apply — what call_tool omits — beside the spec's own permissions and the pagination of a LIST tool. Returns the wire's result payload (a dict carrying content / structuredContent / isError), or a JsonRpcError for a protocol fault (unknown tool, malformed arguments shape, denied permission).

request is the originating Django request when there is one, handled as call_tool handles it: the call works on a copy bearing user and never writes its user to the handed request. A minimal one is synthesised otherwise. scopes populate the synthetic token so a tool registered with scopes= is invokable in-process just as it is on the wire.

run_task

run_task(task_id: str) -> None

Execute a queued task. This is what a worker calls.

The other end of task_executor.enqueue, and the whole public surface of the worker side:

@shared_task
def run_mcp_task(task_id: str) -> None:
    my_server.run_task(task_id)

Everything it needs comes out of the store: the tool, the arguments, and the authorization context to re-check them under. Nothing is returned — the client learns the outcome by polling tasks/get.

Safe to call for an id that is unknown, already claimed or already finished: each is a no-op. Queues deliver at least once, and a retried delivery must not run a mutation twice.

Raises when the server has no task store: a worker calling this on a server that cannot run tasks would otherwise fail as silence — the job "succeeds" and the client polls a handle forever.

notify_resource_updated async

notify_resource_updated(uri: str) -> int

Tell every subscriber watching uri that it changed.

The explicit trigger, and the one that always works. Call it from wherever the write actually happens — a management command, a Celery job, a save() override, a signal handler you wrote yourself:

await server.notify_resource_updated(f"invoices://{invoice.pk}")

Returns how many subscribers were reached — a diagnostic, not a guarantee: 0 means nobody was listening, which is the ordinary case and not an error. Notifications are best-effort by design; a client that misses one re-reads the resource.

A URI, not a template. Publish the concrete URI that changed, and the collection URI too if watchers of the collection should hear about it — matching is exact, deliberately (see topic_for_resource).

Publish after the transaction commits. Inside transaction.atomic() this announces a change that may still roll back, and a subscriber that re-reads immediately sees the old value — worse than no notification at all. transaction.on_commit is the Django-native answer.

notify_list_changed async

notify_list_changed(kind: NotificationKind) -> int

Tell subscribers that one of the catalogs changed.

Registration happens once at configuration time, so the set of registered bindings is fixed for the life of the process. Two things still change a catalog under a running client: a server that registers tools from data — a plugin loader, a per-tenant catalog — and a tool whose operation-scope affordances condition flips, which tools/list answers on every request and so starts or stops listing it. Call this with TOOLS_LIST_CHANGED after the event that flipped the condition, once it has committed. It reaches only clients this server can push to: a subscription broker configured and a modern-era caller listening, the same two things listChanged is advertised on.

register_resource

register_resource(
    spec: SelectorSpec,
    /,
    *,
    name: str,
    uri_template: str,
    description: str | None = None,
    title: str | None = None,
    icons: tuple[Icon, ...] = (),
    mime_type: str = "application/json",
    encoding: ResourceEncoding = ResourceEncoding.JSON,
    rate_limits: list[Any] | None = None,
    annotations: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    always_listed: bool = False,
    cache_ttl_ms: int | UnsetType = UNSET,
    completions: dict[str, Callable[..., Any]] | None = None,
    scopes: str | Sequence[str] | None = None,
) -> KernelResourceBinding

Register a kernel RETRIEVE SelectorSpec as an MCP resource.

register_resource(spec, uri_template=..., name=...) takes a django_service_specs spec positionally. The read runs it through the kernel's dispatch / adispatch, with the URI-template variables as its arguments, so the spec's reads name them and its presenter renders the row. Anything that is not a kernel RETRIEVE SelectorSpec is refused with a TypeError. scopes gate the read as they gate register_tool.

meta is the generic _meta bundle for this resource's listing entry — resources/list for a concrete URI, resources/templates/list for a template — and for the contents block resources/read returns.

encoding decides how what the presenter returns becomes the read body. JSON pretty-prints it; TEXT returns it verbatim, and it must be a str; BLOB base64-encodes it into the contents entry's blob instead of text, and it must be bytes, bytearray or memoryview. A value of the wrong type is answered at read time as an INTERNAL_ERROR naming what produced it. Anything whose mime_type is not JSON — Markdown, CSV, plain text — wants TEXT, or the document comes back wrapped in a quoted string literal, and an image wants BLOB: a presenter rendering the row as Markdown pairs with encoding=TEXT and mime_type="text/markdown". mime_type is advertised as given and never inferred from or checked against the encoding, so TEXT left at the default application/json serves a str that is already a JSON document as it is. The result ceiling and the cache hints apply to every encoding. A TEXT or BLOB resource is refused, naming both, beside a spec declaring affordances, which the kernel adds to a presented object, or allow_none, under which a missing row presents as None without the presenter running: neither is a document or bytes. For an HTML view use register_ui_resource, which sets both.

A uri_template variable is a caller-controlled name that reaches the read's arguments, so one named after a pool seed (user, request, data …) or declared twice raises here rather than letting a URI segment stand in for the authenticated identity. So does a variable no read of the spec declares, and one the server fills for the call. A spec no read can run as written is refused naming the resource, as register_tool refuses one. A read the URI template names is filled on every read, as a value server_arguments binds is on every call, so it is never refused as optional.

A resource whose spec declares no permissions is refused for the same reason a tool is — see REQUIRE_TOOL_PERMISSIONS. The same selector exposed as a resource is as reachable as it is exposed as a tool.

register_ui_resource

register_ui_resource(
    *,
    name: str,
    uri: str,
    template_name: str | None = None,
    body_template_name: str | None = None,
    diagnostics: bool | None = None,
    html: str | None = None,
    render: Callable[[], str] | None = None,
    description: str | None = None,
    title: str | None = None,
    icons: tuple[Icon, ...] = (),
    ui: UIResourceMeta | None = None,
    mime_type: str = UI_RESOURCE_MIME_TYPE,
    permissions: Sequence[PermissionCheck] | None = None,
    scopes: str | Sequence[str] | None = None,
    rate_limits: list[Any] | None = None,
    annotations: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    always_listed: bool = False,
    cache_ttl_ms: int | UnsetType = UNSET,
) -> ResourceBinding

Register an interactive HTML view (an MCP App) as a resource.

A tool links to the view and a host renders it inline in the chat, inside a sandboxed iframe it constructs itself. The iframe and the CSP enforcement are the host's and are deliberately not implemented here.

The ui/* postMessage bridge is not in that list, and reading it as though it were is what this parameter set exists to prevent. The bridge has two ends: the host runs its end, and the document runs the other. That second end is a view's mandatory startup handshake, every one of its failure modes is silent, and one of them leaves the frame hidden with the view unable to say why.

So give exactly one content source, and prefer the first:

  • body_template_name — a Django template holding the view's markup only. The package wraps it in a document whose bridge is already written and already correct. diagnostics= decides whether that bridge writes a protocol failure into the document as well as logging it; None follows settings.DEBUG, because the text is written for whoever wrote the view and a rendered view's audience is whoever is using the product.
  • template_name — a Django template holding a whole document, whose bridge is then yours to write.
  • html — a literal document, same.
  • render — a callable returning one, which must take no arguments: one that declares a parameter is refused at registration, because the read path would fill it from a pool carrying request and user.

Keep tenant data out of the view. Hosts may prefetch and cache a view before any tool call, so it is a shell that hydrates itself at runtime from tool results — which is also why the template renders with no context.

ui= is the typed UIResourceMeta — CSP origins, browser permissions, publisher domain, border preference — which serialises into _meta under the extension's key. meta= remains available for other extensions; passing both ui= and that same key inside meta= raises, rather than letting one silently win.

The result is an ordinary ResourceBinding, so it shares one URI namespace with data resources (a collision raises as always), appears in resources/list, and honours permissions / scopes / always_listed. Views default to unguarded — the MCP session is already authenticated and a view is a static asset, not tenant data.

permissions are kernel PermissionChecks, the instances a spec carries, each asked has_permission(principal, binding) with the view's ResourceBinding as the spec; scopes are the OAuth scopes the caller's token must carry, every one, asked first. Both gate the listing, the read and a subscription alike.

register_prompt

register_prompt(
    *,
    name: str,
    render: Callable[..., Any],
    description: str | None = None,
    title: str | None = None,
    icons: tuple[Icon, ...] = (),
    arguments: list[PromptArgument] | None = None,
    completions: dict[str, Callable[..., Any]] | None = None,
    permissions: Sequence[PermissionCheck] | None = None,
    scopes: str | Sequence[str] | None = None,
    rate_limits: list[Any] | None = None,
    annotations: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    always_listed: bool = False,
) -> PromptBinding

Register a render callable as an MCP prompt.

render receives the prompt arguments as kwargs (plus request and user if it declares them) and returns either a string, a list of strings, a list of PromptMessage, or a coroutine yielding any of those — the dispatch layer normalises the result.

request and user are seeded over the client's arguments at prompts/get, so an argument named after one of them never reaches render in the seed's place.

permissions are kernel PermissionChecks, the instances a spec carries: a prompt is not a spec, so each is asked has_permission(principal, binding) with the prompt's PromptBinding as the spec, and a check on the principal alone answers a prompt as it answers a spec. scopes are the OAuth scopes the caller's token must carry, every one, asked first; a bare string is one scope. Both gate prompts/list, prompts/get and a completion alike.

A prompt with neither is refused like a tool — see REQUIRE_TOOL_PERMISSIONS; permissions=[Unrestricted()] is the way to say it is open. A render callable reads whatever its author gave it access to, so nothing about a prompt makes it safe by construction.

meta is the generic _meta bundle for this prompt's prompts/list entry — see register_tool.

prompt

prompt(
    *,
    name: str | None = None,
    description: str | None = None,
    title: str | None = None,
    icons: tuple[Icon, ...] = (),
    arguments: list[PromptArgument] | None = None,
    completions: dict[str, Callable[..., Any]] | None = None,
    permissions: Sequence[PermissionCheck] | None = None,
    scopes: str | Sequence[str] | None = None,
    rate_limits: list[Any] | None = None,
    annotations: dict[str, Any] | None = None,
    meta: dict[str, Any] | None = None,
    always_listed: bool = False,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]

Decorator form: register the wrapped callable as a prompt.

notify async

notify(session_id: str, payload: Any) -> bool

Push a JSON-RPC payload to a session's open SSE stream.

Returns True if a subscriber was present, False if no client is connected — a missed push is not generally an error, since clients pull state via tools/call round-trips. The broker enforces one subscriber per session: re-subscribing replaces the old queue silently.

With a SSEReplayBuffer configured the payload is recorded before publishing, so the frame carries an event ID the SSE generator emits on the wire (id: <id>\ndata: <payload>\n\n) and a later reconnect with Last-Event-ID drains what it missed before resuming live mode. Without a buffer there are no id: lines and resume is disabled.

Multi-process deployments need an out-of-process broker to fan out across workers; the in-process broker only sees its own.

AgentConventions dataclass

The sentences this server writes for a model, one field per sentence.

Passed as MCPServer(conventions=...), per server, because two servers in one project can be talking to different readers. AgentConventions() is the default, and its fields carry the wording a server uses when it is given none; change one field and every other sentence stays the package's, so a later correction to a line you did not override still reaches you.

The server still decides whether each sentence appears: a tool with no handle gets no handle line, a tool that returns no page no scope sentence. A field changes what it says, never when. None drops a sentence where dropping one leaves a well-formed answer.

Every field is a str.format template, rendered wherever it lands whether or not it has a placeholder, so a literal brace is written twice, {{ or }}, in every field alike. The placeholders a field accepts are the ones listed on it, and only missing_arguments accepts one. Anything the server could not render raises ImproperlyConfigured naming the field when the conventions are built, so a typo fails when the server is, rather than inside the first call that reaches it. This is the Pydantic-AI toolset's rule for its AgentConventions, so one template reads the same on both transports.

Some of these sentences state facts about how the server behaves: that a selection applies to each item and never to the envelope, that the envelope's keys are items, page, totalPages and hasNext. Wording you supply owns keeping those facts true.

Raises:

Type Description
ImproperlyConfigured

A field uses a placeholder it does not accept, is not a valid format string, cannot be rendered with the values it will be given ({names:q}), is neither a string nor None, or is missing_arguments set to None or to an empty or whitespace-only string.

handle_field_description class-attribute instance-attribute

handle_field_description: str | None = HANDLE_DESCRIPTION

The outputSchema description of a handle field that declares none of its own, beside the field in every tool's output schema. None leaves such a field undescribed. A FieldMarking.handle("...") keeps its own words whatever this says. No placeholders.

handle_line class-attribute instance-attribute

handle_line: str | None = HANDLE_LINE

The line appended to the description of a tool whose output carries a handle, after Identify records by `<label>`. when the presenter declares a label. None drops the line and that prefix with it. No placeholders.

query_param_on_pages class-attribute instance-attribute

query_param_on_pages: str | None = PAGED_QUERY_PARAM_SCOPE

What a read-shaping parameter, one of a spec's shapes, applies to on a tool that returns a page. Appended to the param's description in the tool's inputSchema, and to the isError message when a value it supplied is refused while that tool's result is rendered. None drops it from both. No placeholders.

missing_arguments class-attribute instance-attribute

missing_arguments: str = MISSING_ARGUMENTS

The message of a validation_error result for a call that left out arguments the tool's selectors cannot run without: a selector parameter with no default or a service tool's target lookup. {names} is the missing names, sorted, each in backticks, joined with ", "; the detail stays keyed by each name. A validator's own refusal, a missing field included, keeps "Invalid arguments". Never None, empty or only whitespace: it is the result's whole message.