DOCS
MCP reference
One remote MCP server gives any MCP client — Claude, ChatGPT, Claude Code, your own — the same bundles people see in the web app. This reference is for developers wiring their own client or automations; to use Korpus with Claude or ChatGPT, the manual is enough.
Overview
| Transport | Streamable HTTP, stateless: JSON responses, no session id, no server-sent events. |
|---|---|
| Auth | OAuth 2.1 with PKCE and dynamic client registration. Bearer token on every request. |
| Server | korpus, titled Korpus (or Korpus · <agent> on an agent link). |
| Tools | One per action: list_folder, read_concept, create_concept, update_concept and the rest, plus manual. |
| Resources | Bundles and concepts as korpus:// URIs. |
| Prompts | whats_in_korpus, save_session. |
| Hosting | AWS eu-central-1 (Frankfurt). |
The data model in one breath: a bundle is a folder of concepts; a concept is a markdown document at a path such as auth/token-expiry, with type, tags, relations and a rev. Links in the web app are relations here. See the manual for the rest.
Endpoint
https://mcp.fra.korpus.cloud/mcp
- JSON-RPC over
POSTto/mcp; each request stands alone, so there is no stream to open withGETand no session to end. The token decides the agent; the consent screen lets the person pick it. https://mcp.fra.korpus.cloud/mcp/<agent>signs in as that agent, preselected on the consent screen. A token issued for another agent is refused withagent_mismatch, which names the right URL.- An account lives in the region it signed up in; this endpoint serves Frankfurt accounts.
Connect a client
Claude
As yourself (me): Korpus is listed in Claude's connector directory. Settings → Connectors, search for Korpus, choose Connect, sign in and keep me. Often quicker: Add custom connector, name it Korpus, paste https://mcp.fra.korpus.cloud/mcp/me. It also works in Claude for desktop and mobile; turn it on per chat from the tools menu.
As an agent with rules: add it as its own connector, not through the directory entry. claude.ai → Settings → Connectors → Add custom connector, name it korpus-<agent>, paste https://mcp.fra.korpus.cloud/mcp/<agent>. A connection of its own holds only that agent's rules, so it does its job and nothing else.
Claude Code
claude mcp add --transport http korpus https://mcp.fra.korpus.cloud/mcp
Then run /mcp and sign in.
ChatGPT
Settings → Apps & Connectors → Advanced, turn on Developer mode, then Create. Name it Korpus, paste the URL.
Any other client
{
"mcpServers": {
"korpus": {
"type": "http",
"url": "https://mcp.fra.korpus.cloud/mcp"
}
}
}This is the .mcp.json shape Claude Code and similar clients read; others ask only for the URL. The server name is yours to choose. For an agent with rules, use /mcp/<agent> and name the server korpus-<agent>. The web app's Link an app and the get_agent_connect tool print these lines for you.
Authentication
A client with MCP OAuth support needs nothing configured: it discovers everything from the first 401.
- Discovery. An unauthenticated request answers
401withWWW-Authenticate: Bearer resource_metadata="https://mcp.fra.korpus.cloud/.well-known/oauth-protected-resource"(RFC 9728). Authorization server metadata is at/.well-known/oauth-authorization-server(RFC 8414) and/.well-known/openid-configuration. - Registration.
POST /oauth/register(RFC 7591). Public clients only:token_endpoint_auth_methodmust benone. Redirect URIs must behttps, orhttpon localhost, 127.0.0.1 or [::1]. Registrations expire after 90 days. - Authorize.
GET /oauth/authorizewithresponse_type=code, PKCES256and scopemcp. The person signs in to the web app, picks the agent the client acts as (me, an existing one, or a new one) and presses Allow. The request is good for 10 minutes; the code for 5. - Token.
POST /oauth/tokenwithauthorization_codeorrefresh_token. The access token lives 1 hour, the refresh token 30 days. Refresh tokens rotate on every use; one presented twice revokes the whole grant.
Each approval is a grant, listed under its agent on the web app's Agents page. Disconnect there ends it at once; the next call answers 401. Deleting the agent ends all of its grants.
Instructions
On initialize the server sends instructions a client passes to its model:
- Which Korpus deployment the client reached.
- A short overview: the data model, which tools only look, the change description every write carries, how to find things (list_folder first), how usage is metered.
- The person's plan, free or Plus. Every tool works on either; only the limits differ.
- The agent's name and job, and for an agent with rules, the rules.
Everything longer sits behind the manual tool, so a client that drops the instructions can still read them. Topics:
start | First moves in a bundle you do not know. |
concepts | Paths, fields, rev, create versus update, change descriptions, sources. |
find | list_folder first; search, regex and body search. |
relations | Relation types and links between concepts. |
sharing | Roles and invitations. |
catalog | The public page of a published template: pitch, prompts, how it works, screenshots. |
agents | Agents, rules, path patterns, rights. |
learning | A bundle that teaches the agents using it: read AGENTS and the last retro first, write a retro and apply one change last. |
limits | Ceilings and how usage is metered. |
errors | Every error code and what to do about it. |
Tools
One tool per action, each with a title a client shows and hints it sorts by. The reading tools carry readOnlyHint, so most clients let a person allow them for good; every other tool asks, and the ones that remove or end something also carry destructiveHint. Every tool is openWorldHint: false. An agent with rules gets no managing tool at all.
- Change descriptions.
create_concept,update_concept,delete_concept,add_relationandremove_relationtake a requiredchange_description: one line, what changed and why. It is the log; nothing else records why a concept moved. - Index and log. Every folder has an
index(read_index: the titles and descriptions of its concepts) and alog(read_log: its changes, newest first). Both are generated from the content as OKF 0.2 §8 and §9 describe, never stored, so no concept may be namedindexorlog. - Revisions.
read_conceptreturns a concept'srev. Pass it back onupdate_conceptand the call fails withrev_mismatchif someone changed the concept meanwhile. Without it the update lands and says whose revision it changed.update_conceptchanges only the fields given;""clears a text field.
On the wire
A tool call is an ordinary tools/call. Its answer is a tool result: the first content item is plain text, followed by up to 50 resource_links to the concepts it names. There is no structuredContent.
POST /mcp
Authorization: Bearer <access token>
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": { "name": "create_concept", "arguments": {
"bundle": "01M3…", "path": "README",
"type": "Guide", "title": "Research",
"body": "# Research\nWhat this bundle is for.",
"change_description": "Said what this bundle is for" } } }
200 OK
{ "jsonrpc": "2.0", "id": 7, "result": {
"isError": false,
"content": [
{ "type": "text", "text": "ok README created, rev 1" }
] } }A call that fails answers the same way with isError: true and the text <code>: <message>. Only transport and account problems are JSON-RPC or HTTP errors: 401 without a valid token, 403 for a token of another agent (-32003 agent_mismatch) or a blocked account (-32003 account_blocked).
Every tool
Parameters with ? are optional. The input schemas (tools/list) carry the same fields with a description each; the manual topics explain the behaviour.
Field shapes
| Field | Shape | Meaning |
|---|---|---|
bundle | string | A bundle id (a 26-character ULID) from list_bundles. Names are not accepted. |
path | string | Bundle-relative, segments of A–Z a–z 0–9 . _ - starting with a letter or digit, / between, no .md, depth ≤ 10. Umlauts are spelled out (ä → ae), other characters become -. A last segment named index or log is refused. |
type | string | Free text: Decision, Runbook, Guide, … |
status | string | stable | draft | deprecated |
tags | string[] | At most 32. Replaces all tags when given. |
relations | [{type, to}] | type = a relation type of the bundle, to = a path in it (may not exist yet). Replaces all relations when given. |
extra | object | Any other frontmatter keys. Replaces all of them when given. |
folder | string | A folder path, e.g. auth. Left out: the bundle root. |
rev | integer | From read_concept. The update fails with rev_mismatch if the concept changed since. |
change_description | string | One line, what changed and why. Required on every create, update, delete and relation change; it is the log. |
since | string | ISO 8601 UTC, e.g. 2026-09-19T10:00:00Z. |
rights | string[] | Any of create, read, update, delete. |
cursor | string | Opaque. A page with more ends its count line with (more) and its text with a line cursor <token>; pass it back with the same filters. |
Reading readOnly · idempotent · every agent
| Tool | Parameters and effect |
|---|---|
manualHow Korpus works | topic?The manual, by topic. One topic, or all of them with no argument. |
list_bundlesList bundles | The bundles you see, with your role and how many concepts each holds. |
get_usageShow usage | How full each allowance is (write and read rate, 5-hour session, week, space), as fractions. Never refused. |
list_folderList folder | bundle, folder?One folder: its concepts (path, type, title, description) and subfolders. The way to find things; start here. |
read_indexRead folder index | bundle, folder?The folder's index.md: its concepts' titles and descriptions by type, and its subfolders. Generated from the concepts, never stored. |
read_logRead change log | bundle, path?, since?The log.md of a folder (every change under it), a concept (its own) or the bundle: kind, what changed, who, newest first. Generated from the change records, never stored. |
search_conceptsSearch concepts | bundle, q?, regex?, case_sensitive?, body?, digest?, type?, tag?, tags?, status?, stale?, stale_within_days?, date_from?, date_to?, prefix?, limit?, cursor?Search paths, titles, descriptions and tags, ignoring case and umlauts. body: also the text, quoting the line that hit. digest: a line under each hit saying what it is about. tags: all of them. stale_within_days: what falls due. date_from/date_to: on the concept's date. Pages of 50, up to 200. |
read_conceptRead concept | bundle, pathA whole concept as markdown with its rev, and what links to it. |
list_activityShow recent activity | bundle?, since?, mentions_only?What is new since your last visit, and where you were @-mentioned. |
list_relation_typesList relation types | bundleThe bundle's link vocabulary. Read it before writing a relation. |
check_relationsCheck relations | bundleRelations pointing at concepts that do not exist, and relation types nothing uses. |
list_membersList members | bundleWho can see the bundle, and with which role. |
get_rightsShow my rights | bundle, pathWhat you may do at one path, and which of your rules say so. |
list_agentsList agents | Your agents and their rules. A restricted agent sees only itself. |
list_invitationsList invitations | Invitations waiting for you. Me agent only. |
get_agent_connectGet agent setup link | name, bundle?Setup for one agent: its URL, how to add it in Claude (me from the connector directory or as a custom connector, any other agent as its own custom connector), a claude mcp add line, a .mcp.json snippet, and a CLAUDE.md block with a SessionStart hook so every session reads the bundle first (given bundle, both name it). Me agent only. |
Writing asks · every agent, inside its rules
| Tool | Parameters and effect |
|---|---|
create_conceptCreate concept | bundle, path, type, body, title?, description?, tags?, relations?, status?, stale_after?, date?, extra?, change_descriptionA new concept. Refused if the path is taken. A path with umlauts or spaces is made valid (schäfer → schaefer) and the answer names it. |
update_conceptUpdate concept | bundle, path, …fields?, rev?, change_descriptionChange the fields you send; the rest stay. Pass the rev you read to fail on a concurrent change; required when sending tags, relations or extra. |
delete_conceptDelete concept | bundle, path, change_descriptionDelete one concept. |
add_relationAdd relation | bundle, from, type, to, change_descriptionLink one concept to another, keeping its other links; reads from <type> to, and the answer echoes it. Nothing is written if the link is there. |
remove_relationRemove relation | bundle, from, type, to, change_descriptionRemove one link, keeping the others. |
Managing asks · me agent only
| Tool | Parameters and effect |
|---|---|
create_bundleCreate bundle | name, description?A new bundle you own, with a starter set of relation types (relates_to, depends_on, supersedes, contains, precedes). The server waits up to about 15 seconds for its setup; if it is not done by then, the next call on it waits. |
create_relation_typeDefine relation type | bundle, name, label, kind, inverse_label?, description?Declare or replace one kind of link: symmetric, directed (needs inverse_label) or unidirectional, with a line on when to use it. |
invite_memberInvite member | bundle, who, roleInvite @username or an email as viewer or contributor. Nothing is shared until they accept. |
accept_invitationAccept invitation | bundleAccept an invitation waiting for you. |
decline_invitationDecline invitation | bundleDecline it. |
create_agentCreate agent | name, description?, rulesPrepare an agent with its job and rules. |
update_agentUpdate agent | name, description?, rules?Change an agent; rules replace all of its rules. Holds from its next request. |
delete_agentDelete agent | nameDelete an agent and sign out every app connected as it. |
add_agent_ruleAdd agent rule | name, bundle, path, rightsAdd one rule; the same bundle and path merge their rights. |
remove_agent_ruleRemove agent rule | name, bundle, pathRemove one rule, named exactly as list_agents prints it. |
Resources and prompts
| URI | What |
|---|---|
korpus://bundle/{bundle_id} | A bundle: name, your role, count, description and its root folder. Listed by resources/list. |
korpus://bundle/{bundle_id}/{+path} | A concept: its rev and markdown. A template; concepts are not listed. |
Both are text/markdown and go through the same access checks as the tools.
| Prompt | What it does |
|---|---|
whats_in_korpus | What's in Korpus: Lists your bundles, then the index of each. Reads only. |
save_session | Save what we learned: Reads what is there, proposes what to write, waits for your OK, then writes it, saying what changed. |
Agents and rights
Every token belongs to one agent, and every write is stamped with it: @you via <agent>.
meis built in, fixed and holds the person's full rights. Nothing creates, changes or deletes it.- Other agents carry a job and rules. At most 50 agents per account, 50 rules each; names are 1–32 characters of
a-z 0-9 . _ -.
Rules
{ "bundle": "<id> | *", "path": "<pattern>", "rights": ["create", "read", "update", "delete"] }readis always implied.- Patterns:
*alone is the whole bundle;*or**as the last segment is everything below that folder, at any depth (the two mean the same there);**elsewhere is any number of folders;*inside a segment matches within that segment. No.md. - Rules only subtract: the person's role in the bundle is checked first.
- Rules are read on every request, so a change holds from the next call.
| Pattern | Matches | Does not match |
|---|---|---|
notes/* | notes/a, notes/a/b | notes, other/notes/a |
**/test/* | test/a, x/y/test/a/b | test, x/tests/a |
adr/0* | adr/0007-queues | adr/0007/notes, adr/1001 |
AGENTS | AGENTS | x/AGENTS |
| Tools | Right needed on the path |
|---|---|
read_concept, list_folder, read_index, read_log, search_concepts | read |
create_concept | create |
update_concept, add_relation, remove_relation | update (on the from concept) |
delete_concept | delete |
An agent with rules never gets create_bundle, create_relation_type, invitations, get_agent_connect or agent management. A refusal answers restricted and names what the agent may do instead; get_rights asks in advance.
Errors
A failure reads <code>: <message>, sometimes followed by details as JSON and a pointer to a manual topic. The message names the fix.
| Code | Meaning |
|---|---|
rev_mismatch | The concept changed since you read it. Read again, merge, update with the new rev the message names. |
rev_required | An update replaced tags, relations or extra without rev. Read, merge, pass rev. details: current_rev |
change_description_required | A create, update or delete said nothing about what changed. One line, at most 200 characters. |
not_found | Not there, or not yours to see. Not a permission hint. |
restricted | The agent's rules refuse it. The message lists what it may do. Do not retry; the person widens the rules. |
rate_limited | Too much too fast, or the session or week is used up. details: limit (writes, reads, session, week), retry_after (seconds) |
quota_exceeded | Out of room or over a count. details: limit (space, bundles; publications is web only), max? |
bundle_not_ready | A new bundle is still being set up after the server waited for it (rare). Retry after 10 s. details: status, retry_after_seconds? |
bundle_busy | The bundle is held still while a version of it is published (a web-only action); reads work. details: retry_after_seconds |
published_read_only | A published catalog version (web only), which nobody writes. Edit its source bundle instead. |
account_blocked | The account is blocked. Every request is refused. |
internal_error | Our fault. Retry once, then tell support. |
More codes
invalid_path, invalid_concept, invalid_frontmatter, invalid_name | A path, field or name does not fit its rules (charset, depth, a last segment named index or log). |
invalid_access, invalid_agent | An agent or rule is malformed: pattern charset, an unknown right, a .md in a pattern. |
invalid_relation_type | The relation type is not declared in this bundle: list_relation_types, then create_relation_type. |
invalid_cursor, invalid_regex, invalid_query | Start the search again without the cursor; the regex uses a construct outside the accepted subset; a filter such as date_from is not a day. |
body_too_large, too_many_tags, too_many_relations | Over a ceiling; see Limits. |
bundle_not_empty, conflict | The state does not allow it right now. |
user_not_found, member_not_found, invitation_not_found, owner_stays, email_taken, username_taken | Sharing and people: nobody by that name in the bundle, no such invitation, or the owner trying to leave. |
Retry hints: rate_limited carries retry_after, bundle_not_ready and bundle_busy carry retry_after_seconds, all in seconds. A new bundle is ready within 60 seconds; retry every 10. For session and week limits the wait can be hours: stop and tell the person. manual topic=errors explains every code.
Limits and usage
| Limit | Value |
|---|---|
| Concept body | 300 KB |
| change_description | 200 characters |
| Path | depth 10, 900 bytes |
| Tags per concept | 32 |
| Relations per concept | 32 |
| Relation types per bundle | 16 |
| Search page | 50, up to 200 |
| Regex length | 200 characters |
| Agents per account / rules per agent | 50 / 50 |
| Bundle set-up | up to 60 s |
Usage is metered by what a call costs to serve. Writes and reads each have a short-term rate, and every call spends from a 5-hour session and a weekly allowance: a read counts 1, a write 3, a body search 10. Over a limit, the call answers rate_limited with retry_after. The allowances differ by plan (free or Plus) and are not published; get_usage shows how full each one is, and is never refused. Space is counted per person, over the bundles they created.
Not on MCP
Publishing to the catalog, using a template, admin screens for Korpus staff and billing are in the web app only. Questions: support@korpus.cloud.