MCP Server
A group of tools a Model Context Protocol server offers. External MCP clients (Claude, Cursor, …) authenticate with an access token from the app’s identity provider and call the configured tools directly. Not a page: it renders nothing and is not referenced from any tab.
Placement
Headless placement: a module with this concept renders nothing, so it is declared under items: and served by a handler that names it with module: rather than being placed in a tab segment or a handler’s chat: slot. That handler’s authentication: decides whose access tokens the endpoint honors and, through its rule, which of those callers it admits: roles: for the holders of any of the roles the authenticator declares, from its users-table columns or its membership table, and no key for anyone who signs in. An app may declare at most one such handler.
Outline Display: Expose {{tools}} to external MCP clients
Details
A module with this concept is declared under items: and served by a
handler of type: mcp, which names it with module:. Referencing it
from a tab segment or a chat: slot is a type error, and so is a page
handler naming it.
Tool names are what a client calls, and they have to stay distinct
within the module: a name contributed twice – easy to do when
ingredientRef: splices shared tool lists into tools: – would appear
twice in tools/list while every call to it reached whichever comes
first, so the spec checker rejects it. By default a tool is named after
what it acts on (lookup_Orders), so two lookups on the same table
collide however they are otherwise configured; give an entry name:
(a sibling of kind:, 1-64 characters of letters, digits, underscore,
or hyphen) to set the advertised name yourself and tell them apart.
Beside the name, tools/list carries each tool’s title, the
human-readable name a client shows: the tool’s toolTitle setting, or
derived from the wire name when unset (search_open_orders is “Search
Open Orders”). Titles must be distinct within the module too, derived
ones included, since a person tells tools apart by them. An entry may
also carry category:, a free-text label that groups tools in Nectry’s
own spec views; it is internal only and never appears in
tools/list. The name the endpoint reports to clients is the handler’s
title:, not a module setting.
That handler’s authentication: decides whose access tokens the
endpoint honors, exactly as it decides who may load a page, and its
optional query: decides which of those callers are admitted at all,
with the same [Username] semantics a page query has. It must name
an OIDC-backed authenticator whose API is configured with issuer,
introspection_url, and audience; password, SecretCode, and Dummy
authenticators have no external issuer and are rejected.
Set audience to this app’s own MCP endpoint URL – https://<the app's host and path>/mcp. The endpoint derives the absolute URLs in its
discovery documents from that setting, so an opaque identifier there
compiles but leaves the endpoint advertising a metadata URL that
resolves nowhere.
The handler serves two HTTP endpoints: /mcp, which speaks JSON-RPC
over POST, and /.well-known/oauth-protected-resource, which tells
callers which identity provider issues tokens for this app. A 401 from
/mcp carries the absolute URL of the second in its WWW-Authenticate
challenge, which is how MCP requires clients to discover it. A 401
means the identity provider judged the token and refused it, or the
handler’s query: did not admit the resolved caller; where the
provider could not be reached at all, the endpoint answers 503 with a
Retry-After instead, so a client keeps a token that was never in
question. The app is an OAuth resource server only: it issues no tokens
and runs no consent screen. Every request must carry
Authorization: Bearer <token>, and a token minted for a different
audience is rejected. Because those paths are fixed, an app may declare
at most one mcp handler.
The tools: list draws on the same ingredient family Chat uses:
table lookups, inserts, updates and deletes, and Tool pipelines
built from row-action steps. A tool that must do several things –
look something up and then act on it, call an API and record the
result, check a rule before writing – is one Tool: its
parameters declare what the caller supplies, its rowAction wires the
steps, and its outputMapping picks what returns to the model. That
ingredient’s steps decide its approval gate where the spec leaves
requiresApproval unset, but an MCP surface applies no such gate, so
put the policy in the pipeline instead: a Require placed before the
step it protects refuses the call with its message, and If condition
branches when both outcomes are normal.
Tools run unattended – there is no user in the loop to approve a call –
so requiresApproval gates nothing here; it warns rather than failing, so
one tool list can be shared with a Chat concept, which does gate it. The handler’s
roles: decides who may call at all; scope what an admitted caller can
reach with rowFilters (which see the token’s subject as currentUser)
and with column selection, exactly as you would for Chat. A caller resolves to the same app user their browser session would,
so per-user row filters and audit attribution behave identically on both
paths.
Between the handler’s admission and those row filters sits an entry’s
roles:, a sibling of name: and category: naming roles the
handler’s authenticator declares: a caller must hold any one of them to
see and call that tool. A caller holding none finds the tool absent
from tools/list, and calling it anyway answers exactly as for a name
that never existed. Absent means every admitted caller; [] means
nobody.
Settings
Optional
tools: ingredient slot ofllmQueryToolIngredients(many)
Tools external MCP clients can call