Skip to main content

MCP & OAuth Integration

Inkweld exposes creative writing project data to AI assistants through the Model Context Protocol (MCP). Authentication is handled via OAuth 2.1 with PKCE, enabling secure third-party access to user projects.

Architecture​

AI Client (Claude, etc.)
│
├── Discovery: GET /.well-known/oauth-protected-resource
│ └── Finds authorization_servers → AS metadata
│
├── DCR: POST /oauth/register
│ └── Registers as OAuth client (one-time)
│
├── Auth: Browser redirect → /oauth/authorize
│ └── User selects projects + grants consent
│
├── Token: POST /oauth/token (PKCE exchange)
│ └── Receives JWT access_token + refresh_token
│
└── MCP: POST /api/v1/ai/mcp (Streamable HTTP)
└── JSON-RPC with Bearer token

MCP Endpoint​

PropertyValue
URL/api/v1/ai/mcp
TransportStreamable HTTP (stateless, POST for JSON-RPC)
Protocol Version2026-07-28
AuthBearer token (OAuth JWT). There are no long-lived API keys

The server implements the stateless MCP protocol (2026-07-28): modern clients have no initialize handshake and no Mcp-Session-Id header. Every request carries its protocol version, client info, and client capabilities in _meta, and the server identifies itself in each result's _meta. GET/DELETE on the endpoint return 405 Method Not Allowed. Stateless 2026-07-28 clients should call server/discover first to learn the supported protocol versions and capabilities.

For backward compatibility, the server is dual-era: legacy clients (those using the pre-stateless initialize handshake, e.g. MCP Inspector 0.22.0) are still served the classic initialize response with an Mcp-Session-Id header, while requests that carry their protocol version in _meta are served statelessly.

Available Tools​

Read Operations​

ToolDescription
list_projectsList authorized projects (call first to find project keys)
get_project_schemasGet a project's element schemas
get_project_treeGet the element tree structure for a project
search_elementsFull-text search across project elements
search_worldbuildingSearch worldbuilding content with optional full data
search_relationshipsFind relationships for a specific element
get_element_fullGet complete element data including worldbuilding
get_document_contentGet prose content from a document element
get_relationships_graphGet all relationships as a graph structure
get_project_metadataGet project metadata and settings
get_publish_plansGet saved publish/export configurations
list_project_mediaList project media files and media:// URLs
list_image_profilesList enabled image profiles and profile IDs for generation
get_media_contentRead media:// content as image, text, or base64

Write Operations​

ToolDescription
create_elementCreate new story elements
update_elementModify existing elements
delete_elementRemove elements from the tree
replace_all_elementsReplace the whole element tree in one call (destructive, see below)
move_elementsMove elements to a new parent
reorder_elementChange element position within siblings
sort_elementsSort children alphabetically
update_worldbuildingUpdate worldbuilding data for an element
update_document_contentReplace prose content of a document element
create_relationshipCreate relationships between elements
delete_relationshipRemove a relationship
tag_elementAdd, remove, or set tags on an element
create_snapshotCreate a snapshot of a document's current state

replace_all_elements (requires write:elements) deletes every existing element and replaces them with the supplied list, so it is meant for bulk restructuring or initial setup, not routine edits. The elements must be in positional order: children immediately follow their parent and each has level = parent level + 1. The tool normalizes order values and parentIds from that ordering. Prefer create_element, move_elements and reorder_element when you only need to change part of the tree.

When creating a worldbuilding entry via create_element, pass schemaId along with type: "WORLDBUILDING" so the element is template-bound (for example, character-v1 or location-v1). If schemaId is omitted, the element is treated as generic worldbuilding.

Image Operations​

ToolDescription
apply_imageUnified image tool: generate (AI), upload (base64), or reference existing media — and optionally assign to project cover or element image

Call list_image_profiles first and pass profileId when using source: "generate".

Discovering Projects​

Every other tool takes a project argument in username/slug form, so start with project discovery. Some MCP clients (for example Claude's connector) only surface tools, not resources, which is why discovery and schema access are available as tools too.

ToolArgumentsRequired permissionReturns
list_projectsnonenonetotalProjects and, per project: username, slug, projectKey, role, permissions, permissionSummary
get_project_schemasproject (required)read:schemastotal and schemas: the worldbuilding element types (for example Character, Location) with id, name, icon, description, tabs/fields and defaults

list_projects only reports the caller's own grants, so it is always available to an authorized session. Example call and result:

{ "name": "list_projects", "arguments": {} }
{
"totalProjects": 1,
"projects": [
{
"username": "alice",
"slug": "my-novel",
"projectKey": "alice/my-novel",
"role": "editor",
"permissions": ["read:project", "read:elements", "write:elements"],
"permissionSummary": "..."
}
]
}

Call get_project_schemas before create_element or update_worldbuilding to learn the valid schema ids and field keys:

{ "name": "get_project_schemas", "arguments": { "project": "alice/my-novel" } }

Every worldbuilding element also has two identity fields outside its schema, description and image (listed as identityFields in the result). update_worldbuilding resolves a bare key against the element's schema first: if the schema defines a description field, "description" writes that field. Use "identity.description" / "identity.image" to write the identity field, and "worldbuilding.<key>" to force a schema field.

Available Resources​

Resources list the projects the user has authorized access to:

  • Project List — inkweld://projects lists all authorized projects (same data as list_projects)
  • Individual Projects — inkweld://project/{user}/{slug} provides project details and available permissions
  • Sub-resources — elements, worldbuilding and schemas under each project; schemas returns the same data as get_project_schemas
tip

Call list_projects first, then use tools like search_elements, create_element and update_element to work with project content. Pass the project key (e.g., alice/my-novel) as the project parameter to specify which project to operate on.

OAuth 2.1 Flow​

Discovery Endpoints​

EndpointRFCPurpose
/.well-known/oauth-protected-resourceRFC 9728Protected Resource Metadata
/.well-known/oauth-authorization-serverRFC 8414Authorization Server Metadata
/.well-known/openid-configurationOIDCOpenID Connect discovery (fallback)

Supported Standards​

  • OAuth 2.1 with mandatory PKCE (S256)
  • Dynamic Client Registration (RFC 7591) — no pre-registration needed
  • Token Rotation — refresh tokens are rotated on each use with a grace period
  • JWT Access Tokens — HS256 signed, contain session and user info

Scopes​

ScopeDescription
mcp:toolsAccess to MCP tools
mcp:resourcesAccess to MCP resources
read:projectRead project metadata
read:elementsRead story elements
read:worldbuildingRead worldbuilding data
read:schemasRead element schemas
write:elementsCreate/modify elements
write:worldbuildingModify worldbuilding data

Grant Model​

During the consent flow, users select which projects to share and choose an access level per project:

RolePermissions
Viewerread:project, read:elements, read:worldbuilding, read:schemas
EditorAll viewer permissions + write:elements, write:worldbuilding
AdminAll permissions

Users may instead choose "Access all my projects" plus a default access level. In that mode the session (mcp_oauth_sessions.access_all_projects / default_role) expands to every project the user owns (current and future) at the default role, instead of an explicit project_collaborators row per project. Explicit per-project grants are still honoured as overrides where present. This choice is carried from the consent page through the authorization code (mcp_oauth_codes) to the session.

Dual-Runtime Support​

The MCP/OAuth system runs on both:

  • Bun (local development) — uses LevelDB for Yjs document storage
  • Cloudflare Workers (production) — uses Durable Objects for Yjs documents

Environment variables (BASE_URL, DATABASE_KEY, etc.) are accessed via c.env on Workers with a process.env fallback for Bun.

Connected Apps Management​

Users manage authorized OAuth clients from User Settings → Authorized Apps (also deep-linkable at /settings). Both entry points render the same shared ProjectGrantListComponent used by the consent page, so there is a single implementation of the project + access-level UI:

  • View all connected apps
  • Change access levels per project
  • Toggle "Access all my projects" and set a default access level
  • Add / revoke individual project access
  • Revoke an app entirely

The OAuth consent page and the Authorized Apps management tab both use the shared ProjectGrantListComponent (frontend/src/app/components/project-grant-list/) so project-grant editing stays consistent across the two flows.

Configuration​

Required Environment Variables​

VariablePurpose
DATABASE_KEYJWT signing secret (min 32 chars). Falls back to SESSION_SECRET when unset, so most deployments only set SESSION_SECRET
BASE_URLPublic server URL, used as the OAuth issuer and in discovery metadata. Optional: the discovery endpoints fall back to the request's own origin, but set it explicitly behind a reverse proxy or on Cloudflare Workers, where the origin the server sees may not be the public one
ALLOWED_ORIGINSComma-separated frontend URLs (the first is used for the consent page redirect)

See the Configuration reference for details. MCP_ENABLED turns MCP off entirely.

Token Lifetimes​

TokenLifetime
Access Token1 hour
Refresh Token30 days
Authorization Code5 minutes