Overview#
Narrareach exposes a Model Context Protocol server so AI clients can read your drafts, manage your scheduling queue, and browse your inspiration library on your behalf. The endpoint is stateless, authenticated with a bearer token, and speaks JSON-RPC 2.0 over HTTP.
- Endpoint
- POST /api/mcp
- Auth
- Authorization: Bearer nrr_mcp_…
- Protocol
- 2025-06-18
- Content-Type
- application/json
Quickstart#
- Open
Settings → Developerin Narrareach and click Create. Copy the token — it's shown once. - Add Narrareach to your MCP client using one of the recipes below.
- Call
tools/listto confirm the 15 tools appear, then start asking your agent to list drafts or schedule posts.
Access tokens#
Each token is tied to the user who created it and inherits that user's permissions. There are no roles or per-tool scopes in v0.1. A token looks like nrr_mcp_… and holds 256 bits of random entropy.
Narrareach stores only the SHA-256 hash — you will never be able to retrieve the plaintext again after the creation screen. Treat tokens like passwords: don't commit them, don't share them, and revoke unused ones.
Revoking tokens#
From Settings → Developer, click Revoke next to any active token. Revocation takes effect immediately on the next request — there is no cache.
Claude Desktop#
Edit claude_desktop_config.json and add a narrareach entry under mcpServers. On macOS this file lives at ~/Library/Application Support/Claude/claude_desktop_config.json; on Windows %APPDATA%\Claude\claude_desktop_config.json.
Claude Code#
Use the claude mcp add command with the HTTP transport. Verify afterwards with claude mcp list.
Cursor#
Cursor's MCP config lives in Settings → MCP or in ~/.cursor/mcp.json. The schema matches Claude Desktop.
Generic HTTP MCP clients#
Any client that supports Streamable HTTP works. There are no sessions — each request is independent. You don't need to track Mcp-Session-Id, handle resumable SSE, or call DELETE /mcp.
Transport#
Narrareach implements a stateless subset of the MCP Streamable HTTP transport, sufficient for tools-only servers:
| Feature | Supported |
|---|---|
| POST /api/mcp (JSON requests) | Yes |
| JSON-RPC batches | Yes |
| Notifications (no id) → 202 Accepted | Yes |
| Bearer token auth | Yes |
| CORS preflight (OPTIONS) | Yes |
| GET /api/mcp (server-initiated SSE) | No |
| DELETE /api/mcp (session termination) | No |
| Mcp-Session-Id header | No |
| Resources / prompts / sampling | No |
Methods#
The server answers four JSON-RPC methods:
initialize— Handshake. Returns the server info and capabilities.ping— Liveness check. Returns{}.tools/list— Returns the full tool catalog with JSON Schemas.tools/call— Invokes a named tool with its arguments.
Tool results always include both a content text block (for LLMs that rely on text output) and a structuredContent field with the same data as a typed object.
Tool catalog#
All tools are scoped to the token owner. Every underlying database query re-checks ownership — tokens cannot be used to touch another user's data even by guessing ids.
list_drafts#
List the user's drafts, newest first. Excludes archived unless you pass status: "ARCHIVED". Returns a compact summary — call get_draft for full content.
| Argument | Type | Description |
|---|---|---|
| limit | integer (1–100) | Max drafts to return. Default 25. |
| status | enum | One of DRAFT, PARTIALLY_PUBLISHED, PUBLISHED, ARCHIVED. |
| query | string | Case-insensitive substring match against the draft title. |
get_draft#
Fetch a single draft including full HTML + ProseMirror JSON content.
| Argument | Type | Description |
|---|---|---|
| id* | string | Draft id. |
create_draft#
Create a new draft owned by the token holder. Newly created drafts are tagged with meta.createdVia = "mcp" so you can distinguish them from in-app drafts later.
| Argument | Type | Description |
|---|---|---|
| title* | string | Draft title. Max 500 chars. |
| contentHtml | string | Starting HTML body. Max 200,000 chars. |
update_draft#
Patch an existing draft. Only supplied fields are changed.
| Argument | Type | Description |
|---|---|---|
| id* | string | Draft id. |
| title | string | New title. |
| contentHtml | string | New HTML body. |
list_scheduled_posts#
List scheduled posts ordered by scheduled time ascending.
| Argument | Type | Description |
|---|---|---|
| limit | integer (1–100) | Default 25. |
| status | enum | PENDING | PUBLISHING | PUBLISHED | FAILED | CANCELLED. |
| from | ISO datetime | Only return posts at/after this time. |
| to | ISO datetime | Only return posts at/before this time. |
cancel_scheduled_post#
Cancel a pending scheduled post. Already-published posts error; already-cancelled posts return alreadyCancelled: true.
| Argument | Type | Description |
|---|---|---|
| id* | string | ScheduledPost id. |
list_inspiration_posts#
List posts the user has saved to their inspiration library, newest first. Each post's content is truncated to 1,500 characters to fit in context windows.
| Argument | Type | Description |
|---|---|---|
| limit | integer (1–100) | Default 25. |
| platform | enum | SUBSTACK | LINKEDIN. |
| tag | string | Only posts containing this tag. |
get_user_profile#
Return the authenticated user's profile, subscription, and connected platforms. Takes no arguments.
Error codes#
Narrareach uses the standard JSON-RPC 2.0 error envelope. Errors thrown by a tool handler (e.g. "Draft not found") are returned as a successful RPC response with isError: true — not as a transport error — so the model can recover.
| Code | Meaning | When it happens |
|---|---|---|
| -32700 | Parse error | Request body is not valid JSON. HTTP 400. |
| -32600 | Invalid Request | Missing jsonrpc: "2.0" or method. |
| -32601 | Method not found | Unknown method or unknown tool name. |
| -32602 | Invalid params | tools/call missing name, or args fail validation. |
| -32603 | Internal error | Unhandled server exception. |
| -32001 | Unauthorized | Missing/invalid/revoked/expired token. HTTP 401. |
Security model#
- Token storage. Only the SHA-256 hash and a 12-character prefix for display are stored. Plaintext exists only in the creation response and your own memory thereafter.
- Per-user scoping. Every tool handler filters Prisma queries by
userId. Tokens cannot access another user's data. - Revocation. Revoking a token sets
revokedAt; the next lookup refuses it. There is no cache. - Clerk boundary.
/api/mcpuses bearer auth only./api/mcp/tokens/*(the management routes) use Clerk sessions and cannot be called with an MCP token. - CORS.
Access-Control-Allow-Origin: *is set on the endpoint so browser-based clients can connect. Since the only accepted auth is a bearer token the user explicitly pasted, there is no cookie or session a malicious origin could ride on.
Limits#
| Limit | Value |
|---|---|
| Active tokens per user | 10 |
| limit arg on list tools | 1–100 |
| title on create_draft / update_draft | 500 chars |
| contentHtml on create_draft / update_draft | 200,000 chars |
| Token name | 80 chars |
Troubleshooting#
- 401 Unauthorized
- Your token is missing, malformed, revoked, or expired. Double-check the header:
Bearer nrr_mcp_…(case-sensitive scheme, single space, full token including thenrr_mcp_prefix). - 405 Method Not Allowed on GET /api/mcp
- Narrareach does not implement server-initiated SSE streams. Use POST only. Any MCP client worth its salt will fall back automatically.
- Tool call returns isError: true
- That's not a transport error — the tool ran and reported a business-logic error. Read
content[0].textfor details. - -32602 Invalid params on tools/call
- Your arguments don't match the tool's input schema. Call
tools/listto see the exact schema and retry.
Changelog#
- 15 tools covering drafts, notes, scheduling, inspiration, analytics, and profile
- Stateless Streamable HTTP transport (POST-only)
- Per-user bearer tokens with SHA-256 hashed storage