Off Shoring MCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Off Shoring MCPload the story context and append my last turn as completed"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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_turnrequires anidempotency_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
/healthzroute 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 dependenciesThe 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 --statusUsage
# 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.envStop 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-stdioConnecting a client
Two connection shapes are possible:
Streamable HTTP (recommended): point the MCP client at
http://127.0.0.1:8787/mcpand 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:
list_stories, thencreate_storyif there is nothing to continue.get_story_contextto load the active branch, the stored assignments (user_roleis the character the human plays,assistant_rolethe one the model plays), the setup, the profiles, the summaries and a bounded window of turns.Continue the scene in the conversation.
append_turnthe turns that were actually settled, withstatus="completed"and a freshidempotency_key. Say something is saved only after the tool confirms it.To revise instead of overwrite,
branch_storyat the intended turn and save the alternate text there. The original branch is left untouched.export_storywhen a readable copy is wanted;delete_storyonly withconfirm=trueand 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 |
| Compact summaries of stored stories, newest update first. |
| Create a story with setup, assignments and profiles. |
| Active branch, assignments, profiles, summaries and a bounded turn window. |
| One bounded page of turns from a specific branch. |
| Change title, setup, assignments, profiles or summaries. |
| Save one explicit turn (the only way dialogue is written). |
| Fork at a turn into a new branch; never erases the original. |
| Persist which branch is active. |
| Render the story as JSON or Markdown, returned inline. |
| Delete a story's local records (needs |
Design rules that the tools enforce:
Every mutation takes an
expected_revision; a stale write is refused with astale_revisionerror instead of clobbering newer state.Every
append_turntakes anidempotency_key, so a retry cannot duplicate a turn; reusing a key with different arguments is refused.Only
status="completed"turns are canon.draft,cancelledandfailedturns 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.jsonand.../export.md— the same documentsexport_storyproduces, 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 /healthzreports 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_FILEorOFFSHORING_MCP_JWKS_JSON. The signature is verified locally; thenonealgorithm and the symmetricHS*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 |
|
| Where the local story database lives. |
|
| This resource server's own URL; the default audience. |
| — | Issuer identifier of the authorization server; must match the token's |
| — | JWKS endpoint for local signature verification. |
| — | Path to a JWKS file for local signature verification. |
| — | Inline JWKS document. |
| — | RFC 7662 introspection endpoint. |
| — | Client id for introspection. |
| — | Client secret for introspection. |
| resource URL | Expected audience of the access token. |
| — | Scopes every access token must hold (space- or comma-separated). |
|
| Accepted signature algorithms. Never add |
|
|
|
|
| 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_storyremoves 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.relayIts 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 templatesTests
PYTHONPATH=src:. .venv/bin/python -m pytestThe 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
mcpPython 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.
This server cannot be deployed
Maintenance
Related MCP Connectors
- mcpOAuthco.aistoryhub
Remote MCP server for AIStoryHub: stories, chapters, story bible, Voiceprints, AI generation.
Manage your tabletop RPG campaign from any MCP client: worlds, sessions, quests, lore, recaps.
OAuth-protected, read-only-by-default MCP server for provenance-labeled QuillCaddie project memory.
- GentkeyOAuthcom.gentkey
One MCP URL for all your connectors — scoped writes, enforced constraints, and a full audit trail.
Related MCP Servers
- AlicenseAqualityDmaintenanceA local MCP server for maintaining internal consistency in fiction writing, with constraint-based fact checking, branching, and author-oracle conflict resolution.27MIT
- AlicenseBqualityBmaintenanceEnables 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.14MIT
- AlicenseNot gradedqualityBmaintenanceEnables 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
- AlicenseNot gradedqualityBmaintenanceEnables 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