MCP server
Questi contenuti non sono ancora disponibili nella tua lingua.
Kept runs an MCP server at POST /mcp on the same process as
the API. One tool set serves both MCP clients and Kept’s own assistant: the contracts live in
packages/mcp, the handlers in apps/server/src/tools/. The design is in the engineering spec
§2.5 MCP tools and §2.6 webhooks
and decisions D63, D124 and D180 in the
product design.
Where the code is
Section titled “Where the code is”| Path | What it holds |
|---|---|
packages/mcp/src/tools.ts |
TOOL_DEFS: each tool’s name, title, description, scope, module and zod input and output |
packages/mcp/src/output.ts |
The answer envelope and its size fit |
packages/mcp/src/vocabulary.ts |
The server instructions and the kept://vocabulary resource |
apps/server/src/mcp/routes.ts |
Mounts POST /mcp: Host and Origin checks, then the bearer token |
apps/server/src/mcp/server.ts |
Builds one McpServer per request with the tools the token may call |
apps/server/src/mcp/auth.ts |
Verifies a personal token or an OAuth access token |
apps/server/src/tools/registry.ts, tools/handlers/ |
One handler per tool |
apps/server/src/oauth/ |
The OAuth authorization server, consent and CIMD fetches |
apps/server/src/tokens/ |
Personal tokens and the per-token rate limiter |
The tools
Section titled “The tools”Every tool takes an optional location_id (optional when the principal has one location) and
answers {data, as_of, next_cursor?} or {error, hint}. Ids are UUIDs or 6-character short IDs.
User-written strings in outputs sit under an untrusted key. Lists default to 20 items.
| Tool | Scope | Module |
|---|---|---|
capabilities |
read | core |
list_locations |
read | core |
search_things |
read | core |
where_is |
read | core |
get_thing |
read | core |
list_contents |
read | core |
thing_history |
read | core |
find_documents |
read | core |
upcoming |
read | schedules |
add_thing |
write | core |
update_thing |
write | core |
move_thing |
write | core |
mark_seen |
write | core |
create_place |
write | core |
attach_link |
write | core |
log_reading |
write | core |
log_service |
write | core |
lend_thing, return_thing, borrow_thing |
write | lending |
complete_schedule, snooze_schedule |
write | schedules |
add_warranty, open_claim, update_claim |
write | warranties |
log_fuel |
write | fuel |
adjust_stock |
write | consumables |
No tool trashes, deletes, merges, transfers ownership, reveals a secret, uploads a file or runs
SQL (D58, D63, D124). packages/mcp/src/tools.test.ts walks the names and fails on one that
does. A tool that needs a file, such as a receipt, returns a short link from attach_link that
opens the capture sheet in the web app.
A tool is offered only in locations where its module is on and, for a write tool, where the
principal may write. capabilities lists what each location allows. Roles are checked on every
call, never cached in a token (D180).
Transport
Section titled “Transport”Streamable HTTP, stateless (D63), with @modelcontextprotocol/server. mcp/server.ts calls
createMcpHandler with a factory that builds a fresh McpServer for each HTTP request and
registers only the tools that token may call. GET /mcp and DELETE /mcp answer 405. The route
is mounted by hand on Fastify, as the
MCP SDK spike
decided, so the security headers, the request log and x-request-id still apply.
Before the handler: the Host must be the public URL’s host (or a former one), and the Origin
must be absent or Kept’s own. Both refusals are 403. Each answer carries the envelope once, as
text, with no structuredContent, so a reply isn’t sent twice.
Authentication
Section titled “Authentication”The bearer token is the only credential: /mcp takes no session cookie and needs no CSRF token.
A failure is 401 with WWW-Authenticate: Bearer.
Personal tokens start kpt_ and are made in Settings → Connections (POST /api/v1/tokens).
Each has a scope, read or write, and a list of locations. A token never goes above its
creator’s role: a viewer’s token only reads. The same token works on the REST API.
OAuth is on only when the public URL is HTTPS (D125); http is allowed on a loopback host for
development. Kept is the authorization server, through Better Auth’s jwt(), mcp() and cimd()
plugins (apps/server/src/oauth/plugin.ts):
- clients identify with a Client ID Metadata Document (CIMD): their
client_idis a URL Kept fetches through its SSRF guard. Dynamic Client Registration is off; - authorization code with PKCE, and a consent screen where the person picks the scope
(
kept:read,kept:write) and the locations; - access tokens are JWTs for
<public URL>/mcp, an hour long. The grant row holds the scope and locations and is read on every call, so revoking it in Connections works at once; - discovery documents are served at
/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resource(and/.well-known/oauth-protected-resource/mcp).
The OAuth and CIMD spike proved the flow with the official MCP client against a local fixture. Connecting hosted clients (claude.ai, ChatGPT) needs a public HTTPS URL and is still a pending check by the maintainer.
Limits and audit. One per-token limiter counts both API requests and MCP tool calls (reads
120 a minute, writes 30 a minute; tokens/rate.ts). Every write a tool makes is audited as the
token.
Answer size. A tool’s answer stays under 8 KB of JSON. A list is shortened (with
next_cursor), then the largest fields go, each listed in trimmed; ids, short IDs, names and a
write’s undo ids (audit_event_ids, undo_until) are never removed. A write has already saved
when it answers, so its answer is never an error: if even that won’t fit, it is cut to its ids and
undo ids, with trimmed: ["*"]. A client must not retry a write because of trimmed.
Connecting a client
Section titled “Connecting a client”When you create a token, the response includes ready-made client settings: the endpoint and the bearer header, nothing client-specific.
{ "url": "https://kept.example.org/mcp", "headers": { "Authorization": "Bearer kpt_…" }}Put those two values wherever your MCP client takes a remote server’s URL and headers. The token is shown once.
Adding a tool
Section titled “Adding a tool”- The contract. Add an entry to
TOOL_DEFSinpackages/mcp/src/tools.tswithtool({…}): a snake_casename, atitle, adescriptionwritten as the question it answers (English; the model reads it, the UI never shows it),scope,module(nullfor the core),since, and zodinputandoutput. User-written strings in the output go underuntrusted. - The handler. Write it in
apps/server/src/tools/handlers/as aHandler<'your_tool'>with theactionit needs (a role permission frompackages/shared),subjectLocation, andrun, which calls the same service function the REST route calls. Register it inHANDLERSinapps/server/src/tools/registry.ts. A contract without a handler is never offered. - Tests.
packages/mcp/src/tools.test.tspins the table of names, scopes and modules, so update it. Add handler tests beside the others (apps/server/src/tools/tools.test.ts) and an end-to-end call inapps/server/src/mcp/mcp.test.tsif the tool needs one. - The assistant gets the tool too. If it is a write tool, it shows as a proposal card there,
which the web draws from the arguments (
apps/web/src/assistant/confirm-rows.ts).
Webhooks
Section titled “Webhooks”Location webhooks are built (apps/server/src/webhooks/). Owners and admins manage them on a
location (/api/v1/locations/:id/webhooks); tokens can’t. Events are WEBHOOK_EVENTS:
thing.created, thing.updated, thing.moved, thing.trashed, thing.restored,
thing.lifecycle_changed, reading.logged, reminder.due.
Payloads never carry values: only ids, the event, the time, the actor and the names of the
changed fields. A receiver fetches the entity with its own token. Each POST is signed:
Kept-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">. A failed delivery is
retried 10 times over about 24 hours, then marked gave_up and the hook marked failing. A hook
is disabled when its creator loses the role (D180).