Memo runs a Model Context Protocol server at:
https://memo.matters.ai/api/mcp
Any MCP-aware client — claude.ai's "Add custom MCP", the Claude CLI, Cursor, VS Code agents, or your own scripts — can drive the same tool surface a memo owner uses by hand. Thirteen tools cover the whole owner API: project CRUD, file CRUD, publish, link CRUD, visit reads, and visitor URL resolution.
Streamable HTTP (the modern MCP transport). One request/response per HTTP round-trip; no long-poll, no SSE, no session handshake — the same shape works in browser fetch, Node, curl, and serverless functions.
| Request | Method | Body | Returns |
|---|---|---|---|
tools/list | POST | {"jsonrpc":"2.0","id":1,"method":"tools/list"} | JSON-RPC envelope with all tool schemas |
tools/call | POST | {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{...}} | JSON-RPC envelope with the tool result |
initialize | POST | {...method:"initialize"...} | Server info + protocol version |
ping | POST | {...method:"ping"...} | {} |
Protocol version header: MCP-Protocol-Version: 2025-06-18.
Two bearer formats, both resolved through one helper (src/lib/auth/bearer.ts). Both surface the same tool capabilities; they just differ in how they're issued and which audit-log actor tag they get.
| Format | Prefix | Issuance | Audit actor |
|---|---|---|---|
| Personal Access Token | mp_… | Self-serve at /u/<username>/settings/tokens | mcp_pat:<token_id> |
| OAuth 2.0 access token | ma_… | Issued by /oauth/token after PKCE consent | mcp_oauth:<access_token_id> |
Both are sha256-hashed before storage (raw never sits in the DB), checked timing-safely on every request, and rate-limited to 60 calls/minute per (bearerId, IP) pair via Upstash sliding window.
curl -X POST https://memo.matters.ai/api/mcp \
-H "Authorization: Bearer mp_…" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
PATs:
projects:read|write, links:read|write, visits:read).last_used_at for inactivity audit.Memo speaks the full OAuth 2.0 Authorization Code + PKCE flow with Dynamic Client Registration (RFC 7591). When you point claude.ai at https://memo.matters.ai/api/mcp:
/api/mcp with no bearer → memo returns 401 with a WWW-Authenticate: Bearer realm="memo", resource_metadata="…/.well-known/oauth-protected-resource" header.https://memo.matters.ai./.well-known/oauth-authorization-server → gets authorize, token, revoke, and register URLs./oauth/register with its client_name, redirect_uris, etc. — memo writes a new row in oauth_clients and returns a client_id./oauth/authorize?client_id=…&redirect_uri=…&scope=…&code_challenge=…&code_challenge_method=S256&state=… in your browser.authorize.astro, tick scopes, hit Authorize./oauth/authorize/grant → mints a one-shot 10-minute code in oauth_authorization_codes, 303s back to claude.ai's redirect_uri?code=…&state=….code_verifier to /oauth/token → memo verifies PKCE, marks the code consumed, mints an access token (ma_…, 1h TTL) + refresh token (mr_…, 30d TTL, single-use, rotated on every refresh)./api/mcp with the access token as a bearer, and we're off.All four OAuth tables on memo's side (oauth_clients, oauth_authorization_codes, oauth_access_tokens, oauth_refresh_tokens) are documented in Data Model.
| Endpoint | Spec | Purpose |
|---|---|---|
/.well-known/oauth-protected-resource | RFC 9728 | Tells clients which authorization server protects /api/mcp. |
/.well-known/oauth-authorization-server | RFC 8414 | The authorize/token/revoke/register URLs. |
/oauth/register | RFC 7591 | Dynamic Client Registration. claude.ai uses this on first contact. |
/oauth/authorize | RFC 6749 + 7636 | Consent screen, mints an authorization code. |
/oauth/token | RFC 6749 | Exchanges code → access+refresh, or refreshes. |
/oauth/revoke | RFC 7009 | Revokes either token type. |
v3 added 26 tools on top of v2's 13. Claude now has full owner-account power: every action available in the owner UI is wired to a matching MCP tool.
Every tool checks the calling bearer's scopes before executing and writes an audit_logs row with the action + actor tag on state changes.
| Tool name | Scope | Description |
|---|---|---|
list_projects | projects:read | Every project owned by the caller. |
create_project | projects:write | Create a new project. Returns the row. |
delete_project | projects:write | Drop a project + all its versions, files, links. |
list_files | projects:read | List staged file paths under a project. |
read_file | projects:read | UTF-8 contents of a staged file. |
write_file | projects:write | Create or overwrite a staged file. Path-validated. |
delete_file | projects:write | Delete a single staged file. |
publish | projects:write | Compile staged TS/TSX, copy artifacts to R2, mint a version row. |
list_links | links:read | Every access link owned by the caller. |
create_link | links:write | Mint a shareable link for a published version. Argon2-hashes password if given. |
update_link | links:write | Toggle active/name/watermark/allowDownloads. |
get_visits | visits:read | Recent visits across the caller's links, optionally scoped. |
preview_url | links:read | Resolve /<slug> for any owned link. |
| Tool name | Scope | Description |
|---|---|---|
rename_project | projects:write | Rename a project. |
set_project_notes | projects:write | Set the owner-only Markdown notes shown above the editor. |
list_versions | projects:read | Every published version of a project. |
rollback_version | projects:write | Make a prior version current. Existing links stay bound to their original version. |
diff_versions | projects:read | Categorised added/removed/changed/unchanged file diff between two versions. |
read_version_file | projects:read | Read a file inside a specific published version. |
upload_files | projects:write | Bulk-create staged files (base64-encoded). |
update_link_full | links:write | Patch any subset of a link's config — every field memo supports. |
delete_link | links:write | Permanently delete a link. |
list_agreements | projects:read | Every NDA template owned by the caller. |
create_agreement | projects:write | Create a new NDA. Version hash frozen at create time. |
read_agreement | projects:read | Full agreement row including content. |
delete_agreement | projects:write | Delete an agreement (refuses if attached to active link). |
list_signatures | visits:read | List signatures by agreement or by link. |
get_visit_events | visits:read | Per-event detail from the Mongo time-series store. |
get_replay_url | visits:read | rrweb replay URL + LogRocket URL for one visit. |
get_visit_recording_events | visits:read | Raw rrweb event stream for a visit. |
get_performance | visits:read | Aggregate performance for a project. |
get_utilization | visits:read | Per-visitor utilization. |
rename_username | projects:write | Change the caller's memo username. |
list_connections | projects:read | OAuth-authorized apps holding tokens for this account. |
revoke_connection | projects:write | Revoke an OAuth access token + all its refresh tokens. |
list_webhooks | projects:read | List outbound webhook subscriptions. |
create_webhook | projects:write | Create a webhook. Returns the one-shot HMAC secret. |
delete_webhook | projects:write | Delete a webhook subscription. |
export_visits_csv | visits:read | Return every visit for a project as inline CSV text. |
The full JSON Schema for every tool is returned by tools/list. We don't embed it here so this doc never drifts from the source of truth — query the server directly.
Every state-changing tool writes an audit_logs row:
{
"actor_user_id": "<owner uuid>",
"action": "project.publish",
"target_type": "version",
"target_id": "<version uuid>",
"meta": { "actor": "mcp_oauth:<access_token_id>", ... }
}
The actor field in meta is the only place that records which token did the thing, so query by meta->>'actor' to attribute a change to a specific PAT or OAuth connection.
Revocation flows:
/u/<username>/settings/tokens. Token rows get revoked_at set; next /api/mcp request returns 401./u/<username>/settings/connections (or the client calls /oauth/revoke). Both the access token and every refresh token from the same client get revoked_at set in one transaction.For copy-paste setup:
https://memo.matters.ai/api/mcp./oauth/register, and pops a new browser tab on memo's /oauth/authorize.<name>, create a file index.html with a yellow CTA hero" and it will call tools/list → tools/call list_projects / tools/call write_file / etc./<your-username>/settings/connections.For copy-paste local development (no OAuth needed):
Visit /<your-username>/settings/tokens and mint a PAT with the scopes you want.
Drop it into ~/.claude/mcp.json:
{
"mcpServers": {
"memo": {
"url": "https://memo.matters.ai/api/mcp",
"headers": { "Authorization": "Bearer mp_…" }
}
}
}
Restart the Claude CLI / IDE agent and memo shows up under available servers.