Skip to main content
Glama
gwicho38

write-like-me-mcp

by gwicho38

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:

Guarantee

How it's enforced

No network access

The 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 stored

profile.json holds derived statistics + short (≤3-token) signature phrases only — never your sentences. Enforced by a dispersion floor + a dedicated test.

Excerpts stay local

find_writing_examples serves excerpts live from a local SQLite index and returns source basenames only — never absolute paths.

Private data stays out of the repo

The 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 artifacts

uv build ships only the package + packaging metadata — no corpus, no profile.json, no *.db. Verified by a test.

Secret scanning

secret-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 }
}

Field

Required

Default

Description

active_profile

yes

Which entry in profiles to build and serve.

profiles

yes

Map of profile name → settings. The schema is multi-profile-ready; v0.1 builds only the active one.

profiles.<name>.sources

yes

Directories and/or files to learn from (~ expanded).

profiles.<name>.exclude

no

[]

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_extensions

no

see table below

Which extensions to analyze.

data_dir

no

~/.write-like-me

Where profile.json + examples.db are written.

llm.enabled

no

false

Reserved 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):

Priority

Method

Example

1 (highest)

CLI argument

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

2

Environment variable

WRITE_LIKE_ME_CONFIG=/path/to/write-like-me.json

3

Home-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:

OS

Config 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"

Supported File Types

Format

Extensions

Notes

Plain text

.txt

UTF-8 with encoding fallback

Markdown

.md, .markdown

UTF-8 with encoding fallback; leading YAML/TOML frontmatter and fenced code blocks are stripped so metadata and code do not count as prose

PDF

.pdf

Extracted with PyMuPDF

Word

.docx

Headings + tables preserved

HTML

.html, .htm

Boilerplate (script/style/nav) stripped

Rich Text

.rtf

Not 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:

Tool

Description

get_style_profile

Return 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_examples

FTS5/BM25 search for representative real excerpts of the user's writing, for few-shot grounding. Returns basenames only.

apply_style

Compare 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_status

Report 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.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gwicho38/write-like-me-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server