spilo-mcp
v0.2.0
Published
Spilo MCP server + skill. Wraps the Spilo REST API for any MCP host (Claude Code, luziaclaw, Codex, Cowork, CLI).
Downloads
382
Readme
spilo-mcp
Spilo MCP server + skill. Wraps the Spilo REST API as Model Context Protocol tools so any MCP-capable host — Claude Code, luziaclaw, Codex, Cowork, the MCP CLI — can save, search, and organize content in a user's Spilo account.
One codebase, two transports:
- stdio — single-user mode for local hosts (Claude Code, Codex, Cowork). Credentials live in a local JSON file.
- http — multi-user mode for luziaclaw. Credentials live in Postgres (JWTs encrypted at rest). Service-level Bearer auth + per-user JWT header.
Tool surface is locked in ARCHITECTURE.md. The agent-facing skill spec is in SKILL.md.
Contents
- What's in the box
- Install as a Claude Code MCP (stdio)
- Install as a luziaclaw skill (HTTP)
- Install in Codex / Cowork / other stdio hosts
- Local development
- Environment variables
- Troubleshooting
What's in the box
22 tools, split across four groups:
| Group | Tools |
|---|---|
| Auth (4) | spilo_login_start, spilo_login_verify, spilo_whoami, spilo_logout |
| Content (6) | spilo_add, spilo_search, spilo_list_items, spilo_get_item, spilo_update_item, spilo_delete_item |
| Trash (2) | spilo_trash_item, spilo_restore_item |
| Org (10) | spilo_list_lists, spilo_create_list, spilo_rename_list, spilo_delete_list, spilo_add_to_list, spilo_remove_from_list, spilo_list_reminders, spilo_create_reminder, spilo_update_reminder, spilo_cancel_reminder |
Auth is a two-step WhatsApp code flow. Sessions last 30 days. Every tool returns {ok, data | error} — see SKILL.md for the error contract agents should follow.
Install as a Claude Code MCP (stdio)
Claude Code reads MCP server definitions from ~/.claude.json (user-scoped) or the project-local .claude/settings.json / .mcp.json. Add an entry:
{
"mcpServers": {
"spilo": {
"command": "npx",
"args": ["-y", "spilo-mcp", "--stdio"]
}
}
}The exact config filename for MCP servers in Claude Code is
.mcp.jsonat the project root, or themcpServersblock inside~/.claude.json/.claude/settings.json. Verify against your Claude Code version; the CLI also acceptsclaude mcp add spilo -- npx -y spilo-mcp --stdio.
First run:
- Restart Claude Code (or run
claude mcp listto see the new server is connected). - In a conversation, ask Claude to "save this URL in Spilo: https://example.com". It will call
spilo_add, get backauth_required, and walk you through the WhatsApp login —spilo_login_start→ code delivered on WhatsApp →spilo_login_verify. - Credentials are stored at
~/.config/spilo/credentials.json(mode 600). The session is good for 30 days.
Point at staging / local dev by setting SPILO_API_URL in the env for that server:
{
"mcpServers": {
"spilo": {
"command": "npx",
"args": ["-y", "spilo-mcp", "--stdio"],
"env": { "SPILO_API_URL": "http://localhost:3200" }
}
}
}Install as a luziaclaw skill (HTTP)
luziaclaw runs Spilo MCP as a shared HTTP service. One process, N authenticated users.
Deploy the HTTP server. On the Spilo Hetzner box (or any container that can reach Spilo Postgres):
export SPILO_MCP_TRANSPORT=http export SPILO_MCP_PORT=3400 export SPILO_API_URL=https://spilo.ai export SPILO_MCP_SHARED_SECRET='...' # random 32+ bytes export LUZIACLAW_JWT_SECRET='...' # same HS256 secret luziaclaw signs with export MCP_SESSION_ENCRYPTION_KEY='...' # 32 bytes, hex or base64 export DATABASE_URL=postgres://.../spilo node dist/index.js --httpThe server listens on
:3400and expectsmcp_sessions+mcp_pending_authtables (migrated with the rest of Spilo).Register the MCP server in luziaclaw admin. Add a new row pointing at the public endpoint:
- URL:
https://mcp.spilo.ai/mcp - Auth header:
Authorization: Bearer ${SPILO_MCP_SHARED_SECRET} - Per-user header:
X-LuziaClaw-User-JWT: ${USER_JWT}— injected by luziaclaw per request. The JWT'ssubclaim is the luziaclaw user ID; it is verified withLUZIACLAW_JWT_SECRET.
luziaclaw's per-user JWT forwarding is landing in PR #137 draft — forward-user-jwt (verify the final PR number before rollout).
- URL:
First use per luziaclaw user. The user triggers
spilo_login_startfrom inside luziaclaw, gets a code on WhatsApp, confirms withspilo_login_verify. Their encrypted JWT is stored inmcp_sessionskeyed by their luziaclaw user ID.
Every HTTP request must carry both the shared Bearer secret and the per-user JWT. Either missing → the server rejects the request before tool dispatch.
Install in Codex / Cowork / other stdio hosts
Same pattern as Claude Code, adapted to the host's config format.
- Codex: add to the Codex MCP config (key:
mcpServers, same{ command, args, env }shape). Some Codex builds call this file~/.codex/config.tomlor~/.codex/mcp.json— verify against your version. - Cowork: Cowork sessions read the Claude Code settings and also
~/Library/Application Support/Claude/local-agent-mode-sessions/.... Adding the server to the standard Claude Code config makes it available to Cowork too. - Generic MCP CLI:
npx @modelcontextprotocol/inspector npx -y spilo-mcp --stdiowill launch the inspector against a fresh stdio instance — useful for smoke-testing the tool schemas.
In all cases, the binary is spilo-mcp (from package.json bin), and the only required CLI flag is --stdio (or set SPILO_MCP_TRANSPORT=stdio).
Local development
cd integrations/spilo-mcp
npm install
npm run typecheck
# Run against production Spilo (default SPILO_API_URL=https://spilo.ai)
npm run dev:stdio
# Run against local Spilo dev server
SPILO_API_URL=http://localhost:3200 npm run dev:stdio
# HTTP mode (needs all the http-mode env vars — see table below)
npm run dev:httpBoth dev:* scripts use tsx so there's no build step. For a production build:
npm run build # emits dist/
npm start # node dist/index.js (respects SPILO_MCP_TRANSPORT)The package is excluded from the root tsc --noEmit gate — it has its own tsconfig.json and is built independently in CI.
Environment variables
Copied from ARCHITECTURE.md. Anything marked "http only" is required in HTTP mode and ignored in stdio.
| Var | Required | Default | Purpose |
|---|---|---|---|
| SPILO_API_URL | No | https://spilo.ai | Base URL for Spilo REST |
| SPILO_MCP_TRANSPORT | No | falls back to CLI flag | stdio or http |
| SPILO_MCP_PORT | http only | 3400 | HTTP listen port |
| SPILO_MCP_SHARED_SECRET | http only | — | Service-level Bearer token luziaclaw sends |
| LUZIACLAW_JWT_SECRET | http only | — | HS256 secret to verify X-LuziaClaw-User-JWT |
| MCP_SESSION_ENCRYPTION_KEY | http only | — | AES-256-GCM key (32 bytes, hex or base64) for JWT-at-rest |
| DATABASE_URL | http only | — | Postgres connection (same DB as Spilo) |
| SPILO_MCP_CREDENTIALS_PATH | No | ~/.config/spilo/credentials.json | stdio credentials file |
| AUTH_PENDING_TTL_MINUTES | No | 10 | Lifetime of a login_start pending record |
| LOG_LEVEL | No | info | trace | debug | info | warn | error |
Troubleshooting
"Every tool returns auth_required, even after I logged in."
- stdio: check
~/.config/spilo/credentials.jsonexists and is mode600. If your host runs the MCP server as a different user, the file may be written but unreadable. SetSPILO_MCP_CREDENTIALS_PATHto a shared location. - http: check
mcp_sessionshas a row keyed by the caller's luziaclaw user ID. If not,spilo_login_verifyfailed silently — re-run withLOG_LEVEL=debugand look forupstream_erroron the Spilo side.
"HTTP mode fails to start."
- All four http-only env vars must be set (
SPILO_MCP_SHARED_SECRET,LUZIACLAW_JWT_SECRET,MCP_SESSION_ENCRYPTION_KEY,DATABASE_URL). Missing any one → startup error. MCP_SESSION_ENCRYPTION_KEYmust decode to exactly 32 bytes. Hex strings must be 64 chars; base64 must be 44 chars (=-padded).
"Login code never arrives on WhatsApp."
- Confirm the phone number is E.164 (e.g.
+34621013273, not34621013273or621013273). - The user must have opted into the Spilo WhatsApp bot at least once — the first-time flow goes through https://spilo.ai. If they've never interacted with the bot, the 24h window isn't open and some code-delivery paths silently drop.
- Hit Spilo's
/api/auth/request-codedirectly to rule out MCP-layer issues:curl -X POST https://spilo.ai/api/auth/request-code -H 'Content-Type: application/json' -d '{"phone":"+34..."}'.
"spilo_search returns itemIds that don't resolve."
- Expected in edge cases —
itemIdsis synthesized from agent source/evidence cards and may include items the agent consulted but didn't link. Always callspilo_get_itembefore presenting a specific hit.
"spilo_add with a list name says invalid_input."
- The tool resolves list names by case-insensitive exact match. Typos / plurals don't match. Call
spilo_list_liststo see the user's actual list names, or omit the argument and let the item land uncategorized.
"I'm getting upstream_error from everything."
- Spilo API is reachable but returning 5xx. Check
curl https://spilo.ai/health(should be 200{"status":"ok"}). If it's 503, Spilo is shutting down — wait and retry.
Changelog
0.1.1
spilo_add/spilo_get_item/spilo_list_itemsnow return ashareUrl(public/p/<id>link).spilo_update_itemnow acceptsfavorite,rating,user_status,user_note.- New:
spilo_trash_item,spilo_restore_item,spilo_update_reminder,spilo_cancel_reminder,spilo_rename_list,spilo_delete_list. - Fixed
spilo_delete_itemdescription — it is a SOFT delete (moves to trash), not permanent.
0.1.0
- Initial release. 16 tools across auth/content/org. stdio + Streamable HTTP transports.
