Skip to main content
Glama
thenobody0817

Off Shoring MCP

Off Shoring MCP

A story-state MCP server: it stores persistent fictional scene state — stories, character assignments, branches, turns and editable summaries — and exposes it through a small, validated set of MCP tools.

It is deliberately not a storyteller:

  • it has no model and no inference backend;

  • it cannot see the conversation it is being used in — it only sees the arguments a client actually sends to a tool;

  • it writes nothing unless a write tool is called, and it never treats anything spoken only in the chat as saved.

The conversation happens in the MCP client. This server is the store behind it, not the voice in it.

Features

  • Ten MCP tools for listing, creating, reading, updating, branching and exporting stories — all of them validated, all of them revision-checked.

  • Branching: fork a story at a turn into a new branch without touching the original; a branch sees its ancestors' turns only up to its fork point.

  • Optimistic concurrency: every mutation takes an expected_revision, so a stale write is refused instead of clobbering newer state.

  • Idempotency: append_turn requires an idempotency_key, so a retry cannot duplicate a turn.

  • Canon rules: only status="completed" turns count as settled story; drafts, cancellations and failures are stored but never treated as canon.

  • Editable summaries that are derived context and never replace the original turns.

  • Structured tool errors with stable codes, so a client can react instead of guessing.

  • A real HTTP authorization boundary: the server is an OAuth 2.1 resource server that verifies bearer tokens (local JWKS or RFC 7662 introspection). It fails closed — there is no development bypass.

  • An unauthenticated /healthz route that reports only whether authorization is configured.

  • A separate local relay (off_shoring_mcp.relay) for passing explicit instructions and results between local agents over stdio.

Related MCP server: storyai

Requirements

  • Python 3.10 or newer (developed on 3.14).

  • Linux, macOS or Windows — anything with SQLite, which ships with Python.

  • No network access is required to run or to test: the auth tests use local loopback stand-ins.

Runtime dependencies are pinned in requirements.txt; the test dependencies add pytest and anyio in requirements-dev.txt.

Install

git clone <this-repository> off-shoring-mcp
cd off-shoring-mcp
python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt   # runtime + test dependencies

The package is run from source — there is no build or install step. Put src/ on the path when running:

PYTHONPATH=src .venv/bin/python -m off_shoring_mcp --status

Usage

# See the resolved configuration and exit (no database is created):
PYTHONPATH=src .venv/bin/python -m off_shoring_mcp --status

# Serve over Streamable HTTP on loopback (default) at http://127.0.0.1:8787/mcp:
PYTHONPATH=src .venv/bin/python -m off_shoring_mcp \
    --transport streamable-http --host 127.0.0.1 --port 8787 --path /mcp \
    --db var/off-shoring.sqlite3

# Seed the configuration from a file (values already in the environment win):
PYTHONPATH=src .venv/bin/python -m off_shoring_mcp --env-file config/off-shoring.env

Stop the server with Ctrl-C. The HTTP transport binds 127.0.0.1 by default and refuses to bind a routable address while no authorization server is configured.

The stdio transport has no network boundary at all, so it only starts when you acknowledge that explicitly:

PYTHONPATH=src .venv/bin/python -m off_shoring_mcp --transport stdio --allow-local-stdio

Connecting a client

Two connection shapes are possible:

  • Streamable HTTP (recommended): point the MCP client at http://127.0.0.1:8787/mcp and send a bearer access token issued for this resource server (see Authorization). This is the only transport with an authentication boundary.

  • stdio: configure the client to launch the process itself with --transport stdio --allow-local-stdio. Appropriate only when the client owns the process; there is no token check on that path.

A suggested working loop

The server can describe this itself through its offshoring://guide resource and its prompt:

  1. list_stories, then create_story if there is nothing to continue.

  2. get_story_context to load the active branch, the stored assignments (user_role is the character the human plays, assistant_role the one the model plays), the setup, the profiles, the summaries and a bounded window of turns.

  3. Continue the scene in the conversation.

  4. append_turn the turns that were actually settled, with status="completed" and a fresh idempotency_key. Say something is saved only after the tool confirms it.

  5. To revise instead of overwrite, branch_story at the intended turn and save the alternate text there. The original branch is left untouched.

  6. export_story when a readable copy is wanted; delete_story only with confirm=true and the current revision.

Honesty note: the server does not know whether a client is actually connected, and it cannot observe the live conversation. A "saved" turn exists only after a successful append_turn call returns its turn id.

Tools

Tool

Purpose

list_stories

Compact summaries of stored stories, newest update first.

create_story

Create a story with setup, assignments and profiles.

get_story_context

Active branch, assignments, profiles, summaries and a bounded turn window.

read_turns

One bounded page of turns from a specific branch.

update_story

Change title, setup, assignments, profiles or summaries.

append_turn

Save one explicit turn (the only way dialogue is written).

branch_story

Fork at a turn into a new branch; never erases the original.

select_branch

Persist which branch is active.

export_story

Render the story as JSON or Markdown, returned inline.

delete_story

Delete a story's local records (needs confirm=true).

Design rules that the tools enforce:

  • Every mutation takes an expected_revision; a stale write is refused with a stale_revision error instead of clobbering newer state.

  • Every append_turn takes an idempotency_key, so a retry cannot duplicate a turn; reusing a key with different arguments is refused.

  • Only status="completed" turns are canon. draft, cancelled and failed turns are stored but never treated as settled story.

  • A branch sees its ancestors' turns only up to its fork point, plus its own. Sibling branches never leak into each other.

  • Summaries are derived and editable; they never replace the original turns.

  • Stored text, including anything written as an instruction to the model, is fictional story content, never configuration. There is no content blacklist.

Failures are returned as structured tool errors so a client can react:

{"ok": false, "error": {"code": "stale_revision", "message": "...", "details": {}}}

Codes: story_not_found, branch_not_found, turn_not_found, stale_revision, invalid_fork_point, invalid_argument, input_too_large, idempotency_conflict, confirmation_required, export_too_large.

Resources and prompt

  • offshoring://guide — the usage guidance the server sends as instructions.

  • offshoring://story/{story_id}/export.json and .../export.md — the same documents export_story produces, as resources.

  • Prompt offshoring_scene_turn — guidance for continuing the stored scene. It is guidance only and calls no tool by itself.

Authorization

Off Shoring is an OAuth 2.1 resource server: it verifies bearer tokens. It is not a login service and not a token issuer. Something else — an authorization server / identity provider you choose — issues the tokens this code validates.

The boundary is always on and fails closed:

  • Every MCP call over HTTP requires a valid bearer token. There is no development bypass and no unauthenticated fallback.

  • With no usable auth configuration the server still starts, advertises an obvious placeholder issuer, and rejects every protected call with 401.

  • Tokens are checked for signature (or introspection status), expiry, issuer, audience/resource and required scopes. The audience is enforced inside the verifier for both verification modes.

  • An unauthenticated GET /healthz reports only whether auth is configured. The protected-resource metadata document is served at /.well-known/oauth-protected-resource/mcp.

Two verification modes are implemented; configure exactly one:

  • JWKS (local): set one of OFFSHORING_MCP_JWKS_URI, OFFSHORING_MCP_JWKS_FILE or OFFSHORING_MCP_JWKS_JSON. The signature is verified locally; the none algorithm and the symmetric HS* family are refused.

  • Introspection (RFC 7662): set OFFSHORING_MCP_INTROSPECTION_ENDPOINT (and optional client credentials). The issuer verifies the signature; this server still enforces expiry, issuer, audience and scope from the response.

Configuration

All configuration comes from OFFSHORING_MCP_* environment variables. See config/off-shoring.example.env for a blank template; --env-file seeds the variables from a file without overwriting values that are already in the environment.

Variable

Default

Meaning

OFFSHORING_MCP_DB_PATH

var/off-shoring.sqlite3

Where the local story database lives.

OFFSHORING_MCP_RESOURCE_URL

http://127.0.0.1:8787/mcp

This resource server's own URL; the default audience.

OFFSHORING_MCP_ISSUER_URL

—

Issuer identifier of the authorization server; must match the token's iss claim.

OFFSHORING_MCP_JWKS_URI

—

JWKS endpoint for local signature verification.

OFFSHORING_MCP_JWKS_FILE

—

Path to a JWKS file for local signature verification.

OFFSHORING_MCP_JWKS_JSON

—

Inline JWKS document.

OFFSHORING_MCP_INTROSPECTION_ENDPOINT

—

RFC 7662 introspection endpoint.

OFFSHORING_MCP_INTROSPECTION_CLIENT_ID

—

Client id for introspection.

OFFSHORING_MCP_INTROSPECTION_CLIENT_SECRET

—

Client secret for introspection.

OFFSHORING_MCP_AUDIENCE

resource URL

Expected audience of the access token.

OFFSHORING_MCP_REQUIRED_SCOPES

—

Scopes every access token must hold (space- or comma-separated).

OFFSHORING_MCP_ALLOWED_ALGORITHMS

RS256,RS384,…,EdDSA

Accepted signature algorithms. Never add none or an HS* algorithm.

OFFSHORING_MCP_LOG_LEVEL

INFO

debug, info, warning or error.

OFFSHORING_MCP_MAX_REQUEST_BODY_BYTES

1000000

Maximum accepted HTTP request body.

Input bounds are fixed in code (src/off_shoring_mcp/config.py): 500 characters for a title, 20 000 for a setup or a profile description, 100 000 for a turn, 50 000 for a summary, 100 turns per page and a 1 MB request body.

Data and privacy

  • All state lives in one local SQLite database (default var/off-shoring.sqlite3, git-ignored).

  • Exports are returned inline in the tool result. The tools accept no filesystem path and expose no server-local path; an export is not a downloadable file.

  • Exports and stored turns can contain private fictional dialogue; treat the returned text and the database as sensitive.

  • delete_story removes a story's branches, turns, profiles, summaries and write records. It cannot be undone.

The local handoff relay

off_shoring_mcp.relay is a separate, local-only MCP server for passing explicit instructions and results between agents that can launch a stdio MCP server. It does not change the story server and never opens a network port. It uses the existing Python environment and stores its queue under $XDG_STATE_HOME/off-shoring-relay/handoffs.sqlite3 (or ~/.local/state/off-shoring-relay/handoffs.sqlite3) with private directory and database permissions. Handoff text can be sensitive; do not put credentials or secrets in it.

Run it locally with:

PYTHONPATH=src .venv/bin/python -m off_shoring_mcp.relay

Its tools are submit_handoff, read-only list_handoffs, claim_next_handoff, complete_handoff, read_handoff, and read-only clipboard_status (reports the host clipboard byte count and MIME types, never its text). A first queue status check does not create the database. A consumer gets a five-minute claim lease; if it exits, another consumer can reclaim the task.

Ready-to-review, project-scoped client examples are in config/empryo.project.example.json and config/opencode.project.example.json. They are inert templates: replace the placeholder path with the absolute path to your checkout, then copy them to the client's own project configuration to make the relay start when that project opens. This project never changes application configuration on its own.

Development

src/off_shoring_mcp/
├── __main__.py     CLI: transports, --status, safety refusals
├── config.py       environment configuration, input bounds
├── db.py           SQLite connections, transactions, migrations
├── migrations/     001_init.sql schema
├── models.py       typed tool inputs and outputs
├── errors.py       structured tool error codes
├── store.py        domain logic: revisions, idempotency, lineage, pagination
├── export.py       Markdown renderer
├── auth.py         bearer-token verifiers (JWKS and introspection)
├── server.py       tools, resources, prompt, /healthz
└── relay.py        local stdio MCP handoff queue, separate from story storage
tests/              store, protocol, HTTP auth, verifier, config, CLI, relay tests
config/             example env file and client configuration templates

Tests

PYTHONPATH=src:. .venv/bin/python -m pytest

The suite covers the store directly, the tools and resources over the real MCP client protocol (in-process transport), the HTTP authorization boundary against a real loopback server, the token verifiers claim by claim, the CLI and the relay. The in-process and stdio transports have no HTTP authentication boundary, so they are not auth tests: authentication is tested only over real HTTP, with test-only signing keys generated under tests/.

Limitations and known issues

  • No model, no inference, no story generation. The server stores; it does not write.

  • No observation of the live conversation. State changes only when a tool is called.

  • No public hosting, tunnels or non-loopback listener is set up or supported here. A routable bind requires a configured authorization server.

  • No multi-user accounts. The boundary authenticates a token, not a person, and the database is a single local store.

  • No HTTPS. Anything beyond loopback needs TLS in front of the server.

  • No token issuance. You must bring your own identity provider; the server only verifies.

  • No content filtering. Stored text is never inspected or moderated — by design, since it is fiction.

  • The relay only exchanges text between local clients. It does not connect to any remote service, and it has not been validated against a live third-party deployment.

Acknowledgements

  • The Model Context Protocol and the mcp Python SDK, which provide the server, tools, resources and the transports used here.

  • PyJWT for JWT signature verification and httpx for RFC 7662 introspection.

  • Pydantic for the typed tool inputs and outputs, via the MCP SDK.

  • SQLite, which ships with Python.

License

MIT — see LICENSE.

Copyright (c) 2026 the nobody.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    A local MCP server for maintaining internal consistency in fiction writing, with constraint-based fact checking, branching, and author-oracle conflict resolution.
    27
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables writers and AI agents to preserve continuity in long-form fiction by maintaining a narrative knowledge graph and exposing MCP tools for querying outlines, entities, references, and consistency diagnostics.
    14
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables long-form web novel writing workflows by ingesting chapters into a state database, running deterministic consistency checks, querying entity histories, replaying extraction results, and retrieving conflict reports via MCP tools.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP clients to create and manage long-form story projects with isolated databases, persistent drafts, canonical continuity checks, controlled updates, and local semantic retrieval through MCP tools, resources, and prompts.
    MIT