Skip to content
Conduit

mcp server

write-like-me-mcp

Open-source MCP server that learns your personal writing style from a local document corpus and serves it to agents so generated prose matches your voice. Fully local, no network.

gwicho38/write-like-me-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "write-like-me-mcp": {
      "command": "uvx",
      "args": [
        "write-like-me-mcp"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 26, 2026

write-like-me-mcp

An MCP (Model Context Protocol) server that learns your personal writing style from a local document corpus and gives Claude self-updating style context — so the prose it produces on your behalf actually sounds like you.

100% local. Nothing leaves your machine. The server reads your documents on-device, derives statistical style metrics, and serves them to Claude over stdio. There is no network call and no LLM code path in v0.1 — the actual rewriting is done by the Claude you're already talking to, guided by the style context this server provides.

How It Works

  1. You point it at your writing — a directory (or set of files) of things you've written: essays, emails, posts, docs, notes.
  2. It learns your voice locally — sentence rhythm, lexical diversity, contraction habits, punctuation tics, signature phrases, formality markers — and writes a profile.json plus a searchable excerpt index (examples.db).
  3. It watches for changes — edit or add to your corpus and the profile and index rebuild automatically (debounced).
  4. Claude uses it as style context — before writing for you, Claude loads your profile and can pull real excerpts of your writing for grounding, then matches your voice. You never upload anything.

Privacy & Local-Only Posture

This is the whole point of the project, so it is worth being explicit:

GuaranteeHow it's enforced
No network accessThe server has no HTTP client and no LLM call. A regression test runs the full tool flow with all sockets patched to raise.
No verbatim corpus storedprofile.json holds derived statistics + short (≤3-token) signature phrases only — never your sentences. Enforced by a dispersion floor + a dedicated test.
Excerpts stay localfind_writing_examples serves excerpts live from a local SQLite index and returns source basenames only — never absolute paths.
Private data stays out of the repoThe default data dir is ~/.write-like-me (outside any clone). If you keep config/profile/index inside a clone, those paths are gitignored.
Clean build artifactsuv build ships only the package + packaging metadata — no corpus, no profile.json, no *.db. Verified by a test.
Secret scanningsecret-scan.yml runs both TruffleHog and Gitleaks.

Install

Requirement: Python ≥ 3.10. You must tell the server where your writing lives — via a config file, the WRITE_LIKE_ME_CONFIG env var, or ~/.write-like-me.json.

Option A: Install from GitHub

pip install git+https://github.com/gwicho38/write-like-me-mcp.git

Option B: Run without installing (uvx)

uvx write-like-me-mcp /path/to/your/write-like-me.json

Option C: Clone and install locally

git clone https://github.com/gwicho38/write-like-me-mcp.git
cd write-like-me-mcp
pip install -e .

Configuration

The server is driven by a small JSON config that names your writing sources and which profile is active.

write-like-me.json example

{
  "active_profile": "default",
  "data_dir": "~/.write-like-me",
  "profiles": {
    "default": {
      "sources": ["~/writing", "~/notes"],
      "exclude": ["drafts/", "_Index_of_*"],
      "file_extensions": [".md", ".markdown", ".txt", ".docx", ".pdf", ".html", ".htm"]
    },
    "work": {
      "sources": ["~/work/memos"]
    }
  },
  "llm": { "enabled": false }
}
FieldRequiredDefaultDescription
active_profileyes—Which entry in profiles to build and serve.
profilesyes—Map of profile name → settings. The schema is multi-profile-ready; v0.1 builds only the active one.
profiles.<name>.sourcesyes—Directories and/or files to learn from (~ expanded).
profiles.<name>.excludeno[]Path components or globs to skip. Matches a whole component ("drafts", "drafts/"), a component glob ("_Index_of_*"), or the full path ("*/archive/*"). Bare substrings do not match.
profiles.<name>.file_extensionsnosee table belowWhich extensions to analyze.
data_dirno~/.write-like-meWhere profile.json + examples.db are written.
llm.enablednofalseReserved for a future server-side rewrite path; always false in v0.1.

Config discovery priority

The config file is resolved at startup with this priority (first match wins):

PriorityMethodExample
1 (highest)CLI argumentwrite-like-me-mcp /path/to/write-like-me.json
2Environment variableWRITE_LIKE_ME_CONFIG=/path/to/write-like-me.json
3Home-dir config file~/.write-like-me.json

If no config is found, the server exits with a clear error explaining how to create one — it never silently defaults to a directory.

Setup

Claude Code (CLI)

claude mcp add write-like-me -- write-like-me-mcp /path/to/your/write-like-me.json

That's it. The server starts automatically with every claude session.

To change config, remove and re-add:

claude mcp remove write-like-me
claude mcp add write-like-me -- write-like-me-mcp /new/path/to/write-like-me.json

Claude Desktop

Add this to your Claude Desktop config file:

OSConfig path
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "write-like-me": {
      "command": "write-like-me-mcp",
      "args": ["/path/to/your/write-like-me.json"]
    }
  }
}

Alternative: Environment Variable

Instead of passing the config path as an argument:

export WRITE_LIKE_ME_CONFIG="/path/to/your/write-like-me.json"

What Gets Into the Corpus

A file extension is not a promise of prose, and prose is not a promise of your prose. Two independent gates decide what shapes a profile.

The prose gate (automatic). Every extracted document is checked by prose_filter.is_prose before it reaches the profile or the excerpt index. A document is rejected when it is:

ReasonWhat it catches
binarycontrol characters or U+FFFD — a DRM'd ebook renamed .md, a truncated download
not-prosefewer than 55% of non-whitespace characters are letters — topology dumps, config, matrices
encodedmore than 5% of tokens over 30 characters — base64, hashes, minified data
unsegmentedover 80 words per sentence — syslogs and word lists with no terminal punctuation
too-shortunder 15 words — a stub cannot characterize a style

Identical documents are also collapsed by content hash, so eight copies of one file under eight project paths count once. Every build reports what it threw away in skipped_non_prose, skipped_reasons and skipped_duplicates, next to skipped_unreadable for files that yielded no text at all.

The exclude list (yours). Whether a document is your writing is an authorship question no heuristic settles, so it lives in the profile's exclude. Entries are literal path components (node_modules) or globs (*-master, *.azw.md, Solutions Manual*) — third-party material in a personal corpus shares a shape more often than a name. Cloned course repos, ebooks, web clippings and starter code belong here.

Left ungated, both failures are quiet and severe: a corpus of ~7,300 files that included published books, syslogs and one binary blob reported an average sentence of 62 words against a median of 12, and offered "x x" and "0 0" as the author's characteristic phrases.

Supported File Types

FormatExtensionsNotes
Plain text.txtUTF-8 with encoding fallback
Markdown.md, .markdownUTF-8 with encoding fallback; leading YAML/TOML frontmatter and fenced code blocks are stripped so metadata and code do not count as prose
PDF.pdfExtracted with PyMuPDF
Word.docxHeadings + tables preserved
HTML.html, .htmBoilerplate (script/style/nav) stripped
Rich Text.rtfNot supported in v0.1 — the plain reader would leak RTF control codes into the corpus and corrupt the metrics; proper handling is deferred.

Multilingual Corpora

Several style metrics only mean something against the grammar of the language a document is written in. Passive voice is found by looking for auxiliary verbs; hedging and formality come from closed word lists; an apostrophe marks a stylistic contraction in English but a mandatory elision in French. Applying one language's tables to another does not lose signal quietly — it reports confident, wrong numbers.

So the analyzer detects each document's language and reports metrics per language:

  • Detection is stopword-based: no dependency, no model file, no network, and fully deterministic. Supported: English, French, Spanish, Portuguese, Italian.
  • Abstention over guessing. A document with too few function words — a list of names and dates, a table of numbers, a code dump — or one that does not favour a language clearly over the runner-up is left out of the breakdown rather than bucketed on thin evidence.
  • profile.languages maps each detected language to its own doc_count, total_words, passive_voice_rate, contraction_rate, and formality_markers.
  • Top-level metrics describe the dominant language, recorded as metadata.dominant_language. A single-language corpus is therefore reported exactly as before.
  • apply_style matches the draft's language. A French draft is compared against your French metrics when your corpus has them; the response's matched_language says which set was used, or null when the draft's language is not represented and the dominant metrics were used instead.

Tokenization is Unicode-aware, so accented words (réunion, niño) count as one whole token rather than being truncated at the first accent.

Available Tools

Once configured, Claude gains access to these five tools:

ToolDescription
get_style_profileReturn the full learned style profile (sentence rhythm, vocabulary, punctuation, signature phrases, and a ready-to-use style guide). Call this before writing prose on the user's behalf.
find_writing_examplesFTS5/BM25 search for representative real excerpts of the user's writing, for few-shot grounding. Returns basenames only.
apply_styleCompare a draft to the user's profile and return a concrete rewrite brief — draft-vs-author metric gaps plus the style guide. The calling LLM does the rewrite.
analyze_writing_style(Re)scan the corpus and rebuild profile.json + examples.db. Runs automatically at startup and on corpus changes; call explicitly to force a rebuild.
style_statusReport profile/index health, readiness, document count, and whether the (reserved) LLM path is enabled.

Example Usage

Once set up, ask Claude things like:

  • "Draft a reply to this email so it sounds like me."
  • "Rewrite this paragraph in my voice — check my style profile first."
  • "Write a short post about X the way I'd write it; pull a couple of my real examples for reference."

Claude will load your style profile, optionally retrieve real excerpts, and match your voice — all from context this server computes locally.

Architecture

Claude (Desktop / Code / API)
    │
    ▼  stdio transport (stdout = protocol; stderr = logs)
write-like-me MCP Server
    ├── FastMCP            (5 tool definitions)
    ├── Style Analyzer     (sentence/lexical/punctuation/phrase metrics)
    ├── StyleProfile       (derived-stats model → profile.json)
    ├── Excerpt Index      (SQLite FTS5 + BM25, paragraph excerpts → examples.db)
    ├── Watchdog Watcher   (debounced corpus-change rebuilds)
    └── Text Extractors    (TXT/MD, PDF, DOCX, HTML)
            │
            ▼
    Your local corpus              Your local data dir
    ~/writing, ~/notes, ...   →    ~/.write-like-me/<profile>/
                                     ├── profile.json   (derived stats only)
                                     └── examples.db    (local excerpt index)

Nothing in this diagram crosses the machine boundary: there is no network edge.

Version Boundary: v0.1 → v0.2

v0.1 is intentionally local-only and LLM-free:

  • apply_style returns a rewrite brief (metric gaps + style guide); the Claude you're already talking to performs the actual rewrite. A server-side LLM apply_style that returns finished rewritten prose is deferred to a future version.
  • The config carries a reserved llm block (enabled: false) so this can be added later without a schema change. In v0.1 it is always disabled, and a test guarantees no socket is ever opened.

Development

git clone https://github.com/gwicho38/write-like-me-mcp.git
cd write-like-me-mcp
uv sync --extra dev

# Run the local quality gate (lint, type-check, tests)
uv run ruff check .
uv run mypy src
uv run pytest

# Run the server directly against a test config
uv run write-like-me-mcp /path/to/test/write-like-me.json

# Inspect interactively with the MCP Inspector
npx @modelcontextprotocol/inspector write-like-me-mcp /path/to/test/write-like-me.json

Testing notes

  • Tests are hermetic: builds and data dirs go to tmp_path, and HOME is monkeypatched so the real home is never touched.
  • The OSS-safety suite (tests/test_oss_safety.py) and the no-verbatim-corpus suite (tests/test_no_verbatim_corpus.py) encode the privacy guarantees in the table above — they are the project's reason for existing, so keep them green.

License

MIT — see LICENSE.

↑↓ navigate openesc close