loading…
Search for a command to run...
loading…
Enables MCP-compatible agents to authenticate to a Kanka account and interact with campaigns, entities (CRUD on all 18 entity types, posts, relations), and full
Enables MCP-compatible agents to authenticate to a Kanka account and interact with campaigns, entities (CRUD on all 18 entity types, posts, relations), and full-text search.
A Model Context Protocol (MCP) server for Kanka, the worldbuilding platform. Exposes a small set of tools that let any MCP-compatible agent (Claude Desktop, Claude Code, Cursor, custom clients) authenticate to a Kanka account and work with campaigns, entities, and search.
Status: Phase 4 — feature-complete. 15 tools, full CRUD over all 18 entity types, posts and relations sub-resources, OAuth 2.0 (Authorization Code + PKCE) with transparent refresh, and a client-side full-text search. Backed by a vitest test suite (43 tests across 7 files, including msw-mocked HTTP integration tests).
One command takes you from a fresh clone to a verified setup:
cd kanka-mcp
npm run quickstart
The script will:
npm install and build TypeScript0600 perms — token to ~/.config/kanka-mcp/token or OAuth client/secret to .env in the repo root.mcpb extension — if Claude Desktop is detected, the script offers to build the bundle and open it directly so Claude Desktop's install dialog launches automatically. Because your credentials are already on disk, you can leave every field blank in the install dialog — the server resolves them from ~/.config/kanka-mcp/ at runtime.Re-running npm run quickstart is safe — it'll detect existing credentials and offer to reuse them. Skip the rest of this README unless you want manual control.
Tokens are valid for 365 days.
KANKA_TOKENThe server reads the token from the KANKA_TOKEN environment variable. Pick whichever method fits your workflow:
1. Inline for a single command — quickest way to run the smoke test once:
KANKA_TOKEN="paste-your-token-here" npm run smoke
2. Export for the current shell session — persists for as long as the terminal is open:
export KANKA_TOKEN="paste-your-token-here"
npm run smoke
npm run smoke -- --mutate
3. Persistent across shells — add it to your shell rc file (zsh shown; bash users use ~/.bashrc):
echo 'export KANKA_TOKEN="paste-your-token-here"' >> ~/.zshrc
source ~/.zshrc
⚠️ Avoid this on shared machines — your token is sensitive.
4. Token file (no shell env at all) — drop the token into a 0600 file the server reads as a fallback:
mkdir -p ~/.config/kanka-mcp
printf '%s' 'paste-your-token-here' > ~/.config/kanka-mcp/token
chmod 600 ~/.config/kanka-mcp/token
The server checks KANKA_TOKEN first, then this file.
5. MCP client config — once you've verified the smoke test, supply the token directly to your client (Claude Desktop / Claude Code / etc.) so you never have to touch your shell. See Connect to an MCP client.
echo "$KANKA_TOKEN" | head -c 8 ; echo "…"
Should print the first 8 characters of your token. If it prints … only, the variable isn't set in this shell.
From the repo root:
cd kanka-mcp
npm install
npm run build
This compiles TypeScript to dist/.
The smoke script spawns the built server, performs the MCP handshake, and exercises the read-only path against the real Kanka API. It's the fastest way to confirm your token works end-to-end before configuring an agent.
KANKA_TOKEN="your-token-here" npm run smoke
It will:
kanka_auth_statuskanka_list_campaigns and print the first 5kanka_get_campaign for the first one (or pass a campaign id as the second arg)kanka_list_entities for character and print the first 5Pass an explicit campaign id if you don't want the script to auto-pick:
KANKA_TOKEN="..." npm run smoke -- 12345
--mutate mode (validates Phase 2 CRUD)Add --mutate to also exercise the create/update/get/delete cycle. The script creates a throwaway Note named kanka-mcp smoke test <ISO timestamp>, updates its entry, fetches it back by entity_id (which exercises the dual-ID resolver), and deletes it.
KANKA_TOKEN="..." npm run smoke -- --mutate
KANKA_TOKEN="..." npm run smoke -- 12345 --mutate
The Note appears in your campaign briefly. If the script crashes between create and delete, you may have to remove the Note manually.
Expected output (abridged):
→ initialize
kanka-mcp v0.1.0
→ tools/list
15 tools registered
→ kanka_auth_status
{ authenticated: true, source: 'env' }
→ kanka_list_campaigns
2 campaign(s):
- 12345: Legends of Tolria
- ...
→ kanka_get_campaign(12345)
name: Legends of Tolria
→ kanka_list_entities(12345, character)
N character(s); first 5: ...
✓ smoke test passed
.mcpb extension (easiest)Claude Desktop has a native Extensions UI. The repo ships a bundle (.mcpb file) you can install in two clicks — no JSON editing, no PATH wiring.
The fastest path: run npm run quickstart. After the smoke test passes, the script offers to build the bundle and (on macOS) open it directly — Claude Desktop's install dialog launches automatically, and because your credentials are already saved at ~/.config/kanka-mcp/, you can leave every field in the dialog blank.
Manual flow:
npm run pack:mcpb (produces kanka-mcp-<version>.mcpb in the repo root). Or download a pre-built release from https://github.com/torinvdb/kanka-mcp/releases/latest..mcpb file — Claude Desktop is registered as the handler for the Desktop Extension UTI on macOS, so this launches the install dialog directly. (Equivalent: open kanka-mcp-0.1.0.mcpb.)~/.config/kanka-mcp/token (the server resolves it from disk as a fallback). OAuth fields are only needed if you registered an OAuth client.To upgrade later, repeat with the new .mcpb. Claude Desktop preserves your saved configuration across reinstalls of the same extension name.
claude mcp add kanka-mcp --env KANKA_TIER=subscriber \
-- node /absolute/path/to/kanka-mcp/dist/index.js
(Token resolves automatically from ~/.config/kanka-mcp/token if you ran npm run quickstart.)
If you'd rather edit the config file directly — for example, if you use multiple Kanka accounts and want different config per workspace — edit Claude Desktop's claude_desktop_config.json:
~/Library/Application Support/Claude/claude_desktop_config.json%APPDATA%\Claude\claude_desktop_config.json{
"mcpServers": {
"kanka-mcp": {
"command": "node",
"args": ["/absolute/path/to/kanka-mcp/dist/index.js"],
"env": { "KANKA_TOKEN": "your-token-here" }
}
}
}
The file may not exist yet on a fresh Claude Desktop install — create it with the snippet above. Restart Claude Desktop to pick up the change.
The server speaks MCP over stdio. Any client that can launch a stdio MCP server will work — point it at node /absolute/path/to/kanka-mcp/dist/index.js with KANKA_TOKEN in the environment.
All configuration is via environment variables.
| Variable | Required | Default | Purpose |
|---|---|---|---|
KANKA_TOKEN |
one of token or OAuth | — | Personal API token (Bearer) |
KANKA_TIER |
no | auto-detected | free or subscriber. Sets the initial rate limit before /profile auto-detection kicks in. Rarely needed — the server queries /profile on startup and resizes the bucket to match the API-reported rate_limit (30 free / 90 subscriber). |
KANKA_RATE_LIMIT_PER_MIN |
no | auto-tuned via /profile |
Hard override on the rate-limit bucket capacity. Setting this disables /profile-based auto-tuning so a deliberately conservative value won't be silently raised. |
KANKA_BASE_URL |
no | https://api.kanka.io/1.0 |
Override the API base (for testing) |
KANKA_OAUTH_BASE_URL |
no | https://app.kanka.io |
Override the OAuth host (for testing) |
KANKA_TOKEN_FILE |
no | ~/.config/kanka-mcp/token |
Fallback token location if KANKA_TOKEN is unset |
KANKA_OAUTH_CLIENT_ID |
OAuth | — | OAuth app client id (register at app.kanka.io/settings/api-apps) |
KANKA_OAUTH_CLIENT_SECRET |
OAuth | — | OAuth app client secret |
KANKA_OAUTH_REDIRECT_PORT |
no | random ephemeral | Pin the loopback callback port (useful if your OAuth app's redirect URI is fixed) |
KANKA_OAUTH_TOKEN_FILE |
no | ~/.config/kanka-mcp/oauth.json |
Where access + refresh tokens are persisted |
KANKA_REQUEST_TIMEOUT_MS |
no | 30000 |
Per-request HTTP timeout; aborts the fetch and returns NETWORK_ERROR |
KANKA_MAX_RESPONSE_BYTES |
no | 10485760 (10 MiB) |
Hard cap on response body size; oversized responses are rejected |
KANKA_LOG_LEVEL |
no | info |
trace, debug, info, warn, error, fatal |
The server logs to stderr (stdout is reserved for MCP frames).
When making an API call, the server picks a token in this order:
~/.config/kanka-mcp/oauth.json) — refreshed transparently on 401 or within 24h of expiryKANKA_TOKEN environment variable~/.config/kanka-mcp/token fileMost users only need a Personal API token. Use OAuth when you want a long-running login that can refresh itself, or when you're delegating access to a Kanka account that's not yours.
http://localhost:<port>/cb (Kanka's URL validator rejects 127.0.0.1 — use localhost). Pin a port via KANKA_OAUTH_REDIRECT_PORT and use the same port here.export KANKA_OAUTH_CLIENT_ID="paste-issued-client-id" # number/UUID, NOT the app name
export KANKA_OAUTH_CLIENT_SECRET="paste-issued-secret"
export KANKA_OAUTH_REDIRECT_PORT=53117
npm run smoke -- --oauth), call kanka_oauth_login. The server opens your browser to Kanka's authorize page. After approval, tokens are persisted to KANKA_OAUTH_TOKEN_FILE (0600 perms) and used automatically.kanka_auth_logout to clear the stored tokens.Tokens are stored as a JSON file with restrictive permissions; we deliberately avoid native keyring dependencies for portability.
Auth & discovery
| Tool | Purpose |
|---|---|
kanka_auth_status |
Report whether a token is configured and where it was loaded from |
kanka_oauth_login |
Run the OAuth Authorization Code + PKCE flow; persists tokens locally |
kanka_auth_logout |
Clear stored OAuth tokens |
kanka_describe_entity_type |
Return the JSON Schema for an entity type's create/update payload |
Campaigns
| Tool | Purpose |
|---|---|
kanka_list_campaigns |
List campaigns the authenticated user has access to |
kanka_get_campaign |
Fetch metadata for one campaign by id |
Search
| Tool | Purpose |
|---|---|
kanka_search |
Native Kanka name search — fast, but matches names only |
kanka_full_text_search |
Client-side full-text search across entry HTML. Paginates typed list endpoints, strips HTML, and matches locally. Costs API budget — narrow types and max_pages_per_type to keep it cheap. Supports regex: true and case_sensitive: true. |
Entities (CRUD)
| Tool | Purpose |
|---|---|
kanka_list_entities |
Paginated list of entities, optionally filtered by type and arbitrary query filters |
kanka_get_entity |
Fetch a single entity by type-scoped id OR global entity_id (resolves the dual-ID system transparently) |
kanka_create_entity |
Create an entity. data is validated client-side against the per-type Zod schema before sending |
kanka_update_entity |
Partial PATCH on an existing entity |
kanka_delete_entity |
Permanently delete an entity. Requires confirm: true |
Sub-resources — both follow a unified action: list | get | create | update | delete shape. They hang off the global entity_id, never the type-scoped id.
| Tool | Purpose |
|---|---|
kanka_posts |
List/read/create/update/delete posts (sub-notes) attached to an entity |
kanka_relations |
List/read/create/update/delete typed links between entities (with attitude, two_way, etc.) |
Call kanka_describe_entity_type first whenever you're about to send a data payload — it returns the exact JSON Schema for that type, including which fields are required and any constraints. Some types have type-specific required fields beyond name:
calendar: requires weekday (array of at least 2 strings)conversation: requires target_id (1 = users, 2 = characters)dice_roll: requires parameters (e.g. "1d20+3")kanka_list_entities accepts an optional since parameter (ISO 8601 timestamp) and returns a sync token in the response. To walk only what's changed:
// 1st call — full pull, save the returned token
{ "tool": "kanka_list_entities", "args": { "campaign_id": 113176, "entity_type": "character" } }
// → { "data": [...all 109 characters...], "sync": "2026-05-08T18:30:00.000Z" }
// 2nd call later — pass back the token to get only deltas
{ "tool": "kanka_list_entities", "args": {
"campaign_id": 113176, "entity_type": "character",
"since": "2026-05-08T18:30:00.000Z"
}}
// → { "data": [...only entities updated since...], "sync": "2026-05-08T19:42:11.000Z" }
Backed by Kanka's native ?lastSync= query parameter — efficient for long-running agent workflows that don't want to refetch entire entity lists on every turn.
character, location, family, organisation, object, note, event, calendar, creature, race, quest, map, journal, ability, tag, conversation, dice_roll, timeline
MCP Client <—stdio JSON-RPC—> kanka-mcp (Node)
├─ Tool layer (15 tools)
├─ Service layer (id-resolver, full-text-search, html strip)
├─ Kanka HTTP client (token-bucket rate limiter, retry, error map)
└─ Auth (composite: OAuth → env token → file token)
│
└─ HTTPS → api.kanka.io/1.0 + app.kanka.io/oauth/*
The Kanka API exposes every entity through both a type-scoped id (used by /characters/{id}) and a global entity_id (used by /entities/{id} and as the parent of posts/relations). The server resolves between the two transparently — pass whichever one you have.
Rate limiting is conservative: a token bucket sized to the configured tier with exponential backoff on 429. Adjust KANKA_RATE_LIMIT_PER_MIN if you have headroom.
kanka_create_entity/update/delete), all 18 entity schemas, posts & relationskanka_full_text_search with HTML strippingnpm run check pipeline, ESLint guard against stdout pollution, TtlCache wired for the campaigns listmultipart/form-data; v1 accepts pre-uploaded image UUIDs only)npm run dev # tsx watch mode
npm run typecheck # tsc --noEmit
npm run lint # eslint (bans `console` to protect stdout / MCP framing)
npm run test # vitest run — unit + msw HTTP integration tests
npm run test:watch # vitest in watch mode
npm run check # typecheck + lint + test (run this before committing)
npm run build # compile to dist/
npm run smoke # end-to-end smoke against the live Kanka API
npm run smoke -- --mutate # additionally exercises CRUD
npm run smoke -- --oauth # exercises the OAuth flow
npm run validate:mcpb # validate manifest.json against the MCPB schema
npm run pack:mcpb # build kanka-mcp-<version>.mcpb for Claude Desktop
Push a vX.Y.Z tag to trigger .github/workflows/release.yml. The workflow runs the full check pipeline, builds the .mcpb, and attaches it to a GitHub Release with install instructions. Users download from the Releases tab.
npm version patch # bumps package.json + creates a git tag
git push --follow-tags
Tests live next to the code they cover (*.test.ts). The tsconfig.json excludes them from dist/, and eslint.config.js excludes them from the no-console rule so test files can log freely.
| File | Coverage |
|---|---|
src/client/rate-limiter.test.ts |
Token bucket, burst window, penalty, refill wait |
src/client/errors.test.ts |
Status-code → KankaError mapping, structured 422 details |
src/client/pagination.test.ts |
Cursor encode/decode, paginateAll generator |
src/client/http.test.ts |
msw-mocked HTTP: query encoding, 401 + refresh hook, 422 fields, 429 retry, 204 |
src/services/html.test.ts |
HTML strip + snippet extraction |
src/services/id-resolver.test.ts |
Cache hit/miss, forget(), unknown-type rejection |
src/schemas/index.test.ts |
Required-field enforcement per type, describeEntityType JSON Schema output |
Two GitHub Actions workflows live under .github/workflows/:
ci.yml — runs on every push/PRTypecheck, lint, full vitest suite, and build. No secrets needed; safe to run on fork PRs. Fails the merge if any step regresses.
integration.yml — live API smoke against your own campaignRuns npm run smoke against the real Kanka API. Triggers:
workflow_dispatch) from the Actions tab — optional mutate checkbox to additionally run the create/update/delete cycle, optional campaign_id to pin the test targetRequires one secret on the repository:
| Setting | Where | Value |
|---|---|---|
KANKA_TOKEN |
Settings → Secrets and variables → Actions → Secrets | Your Personal API token |
KANKA_TIER (optional) |
Settings → Secrets and variables → Actions → Variables | subscriber if you have a Boosted/Premium account; defaults to free |
The workflow is gated to workflow_dispatch and schedule triggers only — it deliberately never runs on pull_request or push, so a fork PR can't ever trigger a run that would expose or consume your token. The --mutate job creates a throwaway Note named kanka-mcp smoke test <ISO timestamp> and deletes it; if a run crashes between create and delete, the leftover Note is named so you can find and remove it manually.
See SECURITY.md for the threat model and disclosure policy.
Hardening defaults baked into the server:
KANKA_REQUEST_TIMEOUT_MS, default 30 s) — every HTTP call to Kanka is wrapped in an AbortController; hangs surface as NETWORK_ERROR rather than blocking the agent indefinitely.KANKA_MAX_RESPONSE_BYTES, default 10 MiB) — both the declared Content-Length and streamed bytes are checked; oversized responses are rejected before they OOM the process./profile, reads the API-reported rate_limit, and resizes the bucket automatically (30 rpm free / 90 rpm subscriber). KANKA_RATE_LIMIT_PER_MIN overrides and disables auto-tuning.0600 mode under ~/.config/kanka-mcp/ (created 0700).state compared via crypto.timingSafeEqual, loopback callback bound to 127.0.0.1.Authorization headers and any field named *token*, *secret*, etc., before writing to stderr.console.* in src/ so a future contributor can't accidentally corrupt MCP framing.npm audit --omit=dev --audit-level=high runs on every push/PR; the workflow fails on high-severity advisories in production deps.Run npm audit locally any time:
npm audit # all deps (may show low-severity dev-only items)
npm audit --omit=dev # production deps only — should always be 0
Note:
npm audit(without flags) currently surfaces a few low-severity advisories from dev tooling (@anthropic-ai/mcpb→@inquirer/prompts→tmp). These are interactive-CLI components used only when packing the extension bundle locally; they never run at server runtime and aren't shipped in the.mcpb. Production deps remain at zero advisories — see CI for the authoritative gate.
MIT
Выполни в терминале:
claude mcp add kanka-mcp -- npx Безопасность
Низкий рискАвтоматическая эвристика по публичным данным — не гарантия безопасности.