Writing Tools MCP Server
by wdm0006
README.md
# Writing Tools MCP Server
This is a Model Context Protocol (MCP) server designed to provide various text analysis tools, assisting users in improving their writing. It is **optimized for Claude Desktop** with one-click installation via MCP bundles, and also works with other MCP-compatible tools like Cursor and Windsurf.
MCP servers act as a secure bridge or interface, enabling AI models and language assistants to interact with local applications, tools, or data on a user's machine. This server leverages that protocol to offer its specialized writing-specific analysis capabilities to connected AI clients.
## Features
This server provides the following text analysis tools:
* **`list_tools`**: List all available tools in this server.
* **`character_count`**: Return the number of characters in the input text.
* **`word_count`**: Return the number of words in the input text.
* **`spellcheck`**: Return a list of misspelled words in the input text.
* **`readability_score`**: Return readability scores (Flesch, Kincaid, Fog) for the text, section, or paragraph level. Successful responses include a `findings` array.
* **`reading_time`**: Return the estimated reading time for the text, section, or paragraph level.
* **`keyword_density`**: Calculate the density of a given keyword in the text. Returns `{"keyword", "density", "findings"}`.
* **`keyword_frequency`**: Count how often each keyword appears in the text (optionally removing stopwords). Returns `{"frequencies", "findings"}`.
* **`top_keywords`**: Identify the most frequently used keywords in the text. Returns `{"keywords", "findings"}`.
* **`keyword_context`**: Extract sentences or phrases where a specific keyword appears. Returns `{"keyword", "sentences", "findings"}`.
* **`passive_voice_detection`**: Detect passive voice constructions in the text.
* **`perplexity_analysis`**: Analyze text for perplexity and burstiness to detect AI-generated content using GPT-2. Successful responses include a `findings` array.
* **`stylometric_analysis`**: Analyze stylometric features (sentence length, length-robust lexical diversity and vocabulary rarity, POS ratios and bigrams, six readability grade-level formulas, syntactic complexity, punctuation idiosyncrasies, hedge/booster rate, per-function-word/Burrows' Delta profile, character n-gram profile) for AI detection, against a built-in or custom baseline. Successful responses include a `findings` array.
* **`stylometric_delta`**: Profile a draft and its revision against the same baseline and report what the revision moved: per-statistic z-score deltas, an `improved`/`regressed`/`unchanged` verdict per dimension, and a `findings` array for the revised text.
* **`analyze_sections`**: Run a selected subset of the analysis tools on every markdown section of a document, plus a whole-document rollup. Each section carries its key, heading level, rendered text, an impact-ordered `findings` array located at `section:<key>`, and per-tool results.
## Install
```bash
# Run directly from GitHub (no install needed)
uvx --from git+https://github.com/wdm0006/writing-tools-mcp writing-tools-mcp
# Or install from source
git clone https://github.com/wdm0006/writing-tools-mcp
cd writing-tools-mcp
uv sync
uv run run_server.py
```
The spaCy `en_core_web_sm` model is not published to PyPI (Explosion distributes
model wheels through [GitHub releases](https://github.com/explosion/spacy-models/releases)),
so it is not listed as a package dependency. It is downloaded automatically on
first use; to pre-install it - for example to keep test runs hermetic - run:
```bash
uv pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl
```
## MCP Client Configuration
```json
{
"mcpServers": {
"writingtools": {
"command": "uvx",
"args": ["--from", "git+https://github.com/wdm0006/writing-tools-mcp", "writing-tools-mcp"]
}
}
}
```
## Server Configuration
The server reads an optional `.mcp-config.yaml` from its working directory. Unknown keys and
wrongly typed values are ignored with a warning on stderr, and every missing key falls back to
the default below.
```yaml
perplexity:
model_name: "gpt2" # Hugging Face model used for perplexity analysis
max_length: 512 # Token window per chunk
overlap: 50 # Token overlap between chunks
device: "cpu" # "cpu" pins the model to CPU
language: "en" # Only "en" is supported
thresholds:
ppl_max: 25.0 # Perplexity at or below this counts as an AI signal
burstiness_min: 2.5 # Burstiness below this counts as an AI signal
stylometry:
default_baseline: "brown_corpus" # Used when a stylometric_analysis call omits baseline
custom_baselines_dir: "server/data/baselines/custom_baselines" # Relative paths resolve against the server's working directory
thresholds:
warning_z: 2.0 # |z| for a warning
error_z: 3.0 # |z| for an error
ai_confidence_threshold: 0.7 # Confidence needed to flag AI authorship
model:
keep_warm_seconds: 0 # Seconds to keep spaCy/GPT-2 resident between calls; 0 unloads after every call
logging:
level: "INFO" # CRITICAL, ERROR, WARNING, INFO, or DEBUG
format: "%(asctime)s - %(levelname)s - %(message)s" # Standard `logging` format string
```
Set `logging.level: "DEBUG"` when reporting a problem. Logs are always written to stderr — stdout
carries the MCP JSON-RPC stream — and an unrecognized level falls back to `INFO` with a warning
rather than stopping the server.
`stylometry.default_baseline` selects the baseline `stylometric_analysis` measures against when a call
omits the `baseline` argument, and `stylometry.custom_baselines_dir` is where custom baselines are
saved and loaded from - relative paths resolve against the server's working directory, the same place
`.mcp-config.yaml` is read from. Every `stylometric_analysis` response reports the baseline actually
measured against in its `baseline_used` field. A config that still carries the removed
`stylometry.features` key logs an unknown-key warning on stderr and starts normally with the rest of
its settings applied. `perplexity.language` remains accepted but unread; language is chosen per call
through the `perplexity_analysis` argument.
### Keep-warm models (`model.keep_warm_seconds`)
By default (`0`) every model-backed tool call unloads the spaCy pipeline and the GPT-2 weights the
moment it finishes: memory stays flat, and each call pays the model load. Setting
`keep_warm_seconds` to a positive number turns that end-of-call cleanup into TTL-based eviction —
for that many seconds after a model-backed call, the models stay resident and the next call skips
the load entirely; the first call to finish after the window lapses runs the normal eviction.
Eviction is checked on tool calls, not on a timer, so an idle server holds the memory until its
next call. Explicit cleanup (the server's `cleanup_models` path) still unloads immediately
regardless of this setting.
The trade is memory for latency: with the default `gpt2` model, keeping both models resident costs
roughly 100 MB of RSS (~55 MB for the spaCy pipeline and ~40 MB for the GPT-2 weights, measured on
Linux; larger models configured via `perplexity.model_name` cost proportionally more). The default
`0` keeps the lowest possible footprint and is the historical behavior — a load and an unload on
every model-backed call.
### Detector calibration
The detection thresholds above are measured against the committed benchmark corpus:
[`docs/calibration.md`](docs/calibration.md) publishes confusion matrices for the shipped flags and
precision/recall/FPR at target true-positive rates for each detector's single measurement. On that
sample (110 human / 110 machine documents), the shipped stylometry flag detects about 9% of
machine text at a 1% false-positive rate and the shipped perplexity flag fires on none of it —
higher recall is available at higher false-positive cost, per the published tables. The file is
regenerated with `uv run benchmarks/calibrate_detection.py` from the benchmark scores; CI fails
when it drifts, so a threshold change must re-derive the published numbers.
## Custom Baselines
`stylometric_analysis` compares a text's features against a baseline (`brown_corpus` by default)
and flags whatever is an outlier relative to it. Brown Corpus is 1961 published news and fiction;
it answers "does this read like typical published prose," which is often not the question you
actually want answered. A more useful question for judging your own drafts is "does this read like
*my own* pre-existing writing" - answered by building a baseline from a corpus of your own text.
```bash
uv run scripts/build_baseline.py my_own_voice path/to/txt/files/
```
Each `*.txt` file in the directory is treated as one document (strip front matter, markdown, and
code fences first - the script analyzes exactly the text it's given). The baseline is saved under
`stylometry.custom_baselines_dir` from your `.mcp-config.yaml` (the shipped config points it at
`server/data/baselines/custom_baselines`, relative to the server's working directory; without a
configured directory, the `custom_baselines/` folder inside the `server` package is used) and is
immediately usable:
```
stylometric_analysis(text, baseline="my_own_voice")
```
By default the builder (`server.stylometry.build_baseline_from_texts`) only computes mean/std for
a curated, length-robust feature set: `avg_sentence_len`, `sentence_len_std`, `fog`/`kincaid`/`smog`/
`coleman_liau`/`ari`/`dale_chall` (six readability grade-level formulas), `mtld`/`mattr`/`mtld_lemma`
(length-robust lexical diversity, on surface forms and lemmas respectively), `mean_word_frequency`
(vocabulary *rarity*, via `wordfreq` - distinct from diversity: how common the words used are, not
how many distinct words there are), `word_len_std`, `lexical_density`, five punctuation-idiosyncrasy
ratios (`semicolon_ratio`, `em_dash_ratio`, `ellipsis_ratio`, `exclamation_ratio`,
`parenthetical_rate`), `hedge_rate`/`booster_rate` (epistemic-marker word categories),
`mean_dependency_distance`/`subordinate_clause_ratio` (syntactic complexity read off the dependency
parse), the `ADP`/`DET` POS ratios, a curated 10-bigram POS-sequence profile (see below), a
Burrows'-Delta-style per-function-word frequency profile (see below), and a character n-gram
orthographic profile (see below). Type-token ratio and the hapax legomena rate are deliberately left
out: both fall monotonically as a document gets longer, for any author, so comparing them across a
corpus of mixed document lengths mostly measures length rather than style - `mtld`/`mattr`/`mtld_lemma`
exist specifically as length-robust replacements for them (McCarthy & Jarvis 2010; Covington & McFall
2010). `fourgram_repetition_rate` and `zipf_slope` are computed but *not* in the default set: unlike
ttr/hapax, we haven't verified whether they vary with length, so they're opt-in only. Pass
`--all-features` to include every feature `extract_features` computes, length-confounded or not.
**Burrows' Delta.** Rather than one aggregate `function_word_ratio`, the builder also tracks each of
`StylemetricAnalyzer`'s ~100 function words individually (mean/std per word across the corpus).
`stylometric_analysis` z-scores each word against its own baseline entry, then reduces all of them
to one number - `burrows_delta`, the mean absolute z-score across every word scored - the classic
Burrows' Delta statistic (Burrows 2002), built for exactly this kind of small, single-author corpus.
A large `burrows_delta` (above the usual warning z-threshold) raises a `distinct_function_word_profile`
flag. Pass `function_words=[]` to `build_baseline_from_texts` (or a custom word list) to change or
skip this dimension.
**POS bigrams.** Published authorship-attribution work reports POS-tag bigrams/trigrams
discriminating authors substantially better than single-tag POS ratios alone. The builder tracks a
small, curated 10-bigram subset by default (`DEFAULT_ROBUST_POS_BIGRAMS` - noun- and
verb-phrase-initiation patterns like `DET_NOUN`, `VERB_ADP`), scored the same way as `pos_ratios`
under a `posbi_` prefix, rather than all ~289 possible tag combinations - most bigrams are too sparse
per document (a handful of occurrences in an 800-word post) to average reliably. Pass `pos_bigrams=`
to change the tracked set, or `[]` to skip this dimension.
**Character n-gram profile.** A PAN/CLEF-style orthographic fingerprint: character 4-gram relative
frequencies, normalized per document. Individual n-grams are too sparse to z-score the way pos_ratios
or function words are (most 4-grams occur 0-2 times in a typical post), so this is compared as a
*whole profile* instead - `calculate_char_ngram_similarity` computes the cosine similarity between a
draft's profile and the baseline's aggregate profile (kept to the top `char_ngram_top_k` n-grams by
corpus-wide frequency, default 300, to bound the baseline's file size). `stylometric_analysis` surfaces
this as a top-level `char_ngram_similarity` (not part of `z_scores` or `flags` - there's no calibrated
threshold for it yet). Note this is sensitive to vocabulary/topic, not just style: a post using very
different subject-matter vocabulary from the baseline corpus will score a low similarity for that
reason alone, not necessarily because of authorship. Pass `char_ngram_top_k=0` to skip this dimension.
`server/data/baselines/custom_baselines/mcginniscommawill_pre2020.json` ships as a worked example: 102
pre-2020 posts from [mcginniscommawill.com](https://mcginniscommawill.com), built with this script.
## Domain Baselines
Three shipped baselines cover genres that don't read like 1961 news and fiction, so
`stylometric_analysis` can measure against prose closer to what you're writing. Load them by name
exactly like the Brown Corpus baseline — `stylometric_analysis(text, baseline="essays")` — or make
one the default in `.mcp-config.yaml`:
```yaml
stylometry:
default_baseline: "technical_docs"
```
Each baseline carries 27 statistics (vs Brown Corpus's 9), built with the default length-robust
feature set. That set supports 9 of the 12 AI indicators (`uniform_sentences`,
`unusual_sentence_length`, `pos_anomalies`, `unusual_reading_level`, `low_mtld`,
`distinct_function_word_profile` via Burrows' Delta, `unusual_vocabulary_rarity`,
`unusual_hedge_rate`, `unusual_booster_rate`) - five more than Brown Corpus's statistics support
(`low_mtld`, `distinct_function_word_profile`, `unusual_vocabulary_rarity`, `unusual_hedge_rate`,
`unusual_booster_rate` are unreachable against its 9-statistic set). The remaining three
(`low_ttr`, `low_hapax`, `function_word_anomaly`) need length-confounded statistics the robust
builder deliberately omits; they fire only against Brown Corpus, and only there.
| Baseline | Register | Documents | Sources (all US public domain) |
| --- | --- | --- | --- |
| `essays` | reflective essay prose — first-person argumentative/expository writing | 144 | Montaigne, *Essays* (Cotton trans., 1877 ed., PG #3600); Emerson, *Essays, First Series* (1841, PG #2944); Emerson, *Essays, Second Series* (1844, PG #2945); Chesterton, *Orthodoxy* (1908, PG #130) |
| `technical_docs` | instructional how-to and explanatory documentation | 834 | Milton & Wohlers, *A Course in Wood Turning* (1919, PG #15460); Noyes, *Handwork in Wood* (1910, PG #20846); Anderson, *Electricity for the Farm* (1915, PG #27257); *The Boy Mechanic, Vol. 1* (1913, PG #12655) |
| `scientific_prose` | expository scientific writing | 52 | Darwin, *On the Origin of Species* (1859, PG #1228); Faraday, *Experimental Researches in Electricity, Vol. 1* (1831–1852, PG #14474); Einstein, *Relativity* (Lawson trans., 1920, PG #30155) |
**License.** Every source work was published in the US before 1929 and is in the public domain;
Project Gutenberg's license terms govern the *electronic transcriptions*, not the underlying texts.
The shipped baselines contain only aggregate feature statistics (means and standard deviations) —
no corpus text is redistributed. The corpora themselves are never committed; they're re-assembled
from the pinned sources (see below).
**Provenance and reproducibility.** Corpus assembly is scripted and deterministic:
`scripts/fetch_domain_corpora.py` downloads each source from Project Gutenberg (or a mirror),
verifies every download's SHA-256 against the pin recorded in its `WORKS` table, strips Gutenberg
boilerplate and illustration captions, splits each work into documents at work-specific heading
patterns (indexes and transcriber notes are skipped), and writes numbered `pg<id>_nnnn.txt` files.
Running it twice produces byte-identical corpora, and rebuilding a baseline from the same corpora
produces a byte-identical JSON (function-word statistics are serialized in a canonical order). To
rebuild:
```bash
uv run python scripts/fetch_domain_corpora.py --corpora-root data/corpora
uv run scripts/build_baseline.py essays data/corpora/essays --description "..."
```
Per-baseline source lists, heading/stop rules, and SHA-256 pins live in the `WORKS` table at the
top of `scripts/fetch_domain_corpora.py`. The baselines ship in the wheel
(`server/data/baselines/*.json`) and are covered by integration tests
(`tests/test_domain_baselines.py`) for loadability, z-score generation, default-baseline
configuration, and wheel bundling.
## Building the Bundle
To create a `.mcpb` bundle for distribution:
```bash
make build-mcpb
```
This creates `writing-tools-mcp.mcpb` which can be installed in Claude Desktop.
## Usage Examples
You can configure any MCP client (like Claude.ai, Windsurf, or Cursor) to connect to it. Here are some example prompts you could give to an AI assistant connected to this MCP server:
**General Analysis:**
* "List the available writing tools." (Calls `list_tools`)
* "Analyze the text below for readability using the standard scores." (Provide text, calls `readability_score`)
* "Check this document for spelling mistakes." (Provide text, calls `spellcheck`)
* "How long would it take someone to read this blog post?" (Provide text, calls `reading_time`)
**Keyword Analysis:**
* "What are the top 5 keywords in the following abstract?" (Provide text, calls `top_keywords` with `top_n=5`)
* "Calculate the keyword density for 'artificial intelligence' in this paper." (Provide text, calls `keyword_density` with `keyword="artificial intelligence"`)
* "Show me all sentences containing the term 'MCP'." (Provide text, calls `keyword_context` with `keyword="MCP"`)
* "Search the web for pages based on the top 5 keyworkds in this text, and compare those pages to mine" (Provide text, calls `top_keywords` with `top_n=5`, then passes that to a different web search tool if available)
**Style and Structure:**
* "Identify any sentences using passive voice in my draft." (Provide text, calls `passive_voice_detection`)
* "What's the word count for this paragraph?" (Provide text, calls `word_count`)
* "Get the readability scores for each section of this document." (Provide markdown text, calls `readability_score` with `level="section"`)
* "Break this document into sections and tell me which section needs the most work." (Provide markdown text, calls `analyze_sections`)
**AI Detection:**
* "Analyze this text for signs of AI generation using perplexity analysis." (Provide text, calls `perplexity_analysis`)
* "Check if this essay was written by AI using stylometric analysis." (Provide text, calls `stylometric_analysis`)
* "Compare the writing style of this text against human writing baselines." (Provide text, calls `stylometric_analysis`)
* "Is this text too uniform in sentence structure to be human-written?" (Provide text, calls both AI detection tools)
## Revision Prompts
Three MCP prompts support an analyze → revise → verify loop:
* **`guided_revision`**: Render an impact-ordered revision brief for a document. Pass the `findings` array from any analysis tool as the optional `findings` argument (JSON string); every finding's `rule`, `location`, `message`, and `fix_hint` is listed, highest-impact first. Omit it and the brief tells you which tools to run first.
* **`writing_checklist`**: A pre-flight drafting checklist (structure, sentence variety, hedging and boosters, readability, keywords, voice) to apply while writing.
* **`verify_revision`**: Render a `stylometric_delta` response as a revision verdict. Pass the tool's full response as the `delta` argument (JSON string); the verdict groups every statistic into `improved`/`regressed`/`unchanged` with the z-score movement behind each, lists the revised text's findings, and closes with a verification protocol (fix the regressions first, re-run, stop when they clear or plateau). Malformed input degrades into a note instead of an error.
Seven analysis tools (`readability_score`, `perplexity_analysis`, `stylometric_analysis`, `keyword_density`, `keyword_frequency`, `top_keywords`, `keyword_context`) attach a `findings` array to successful responses — located, actionable observations with `rule`, `location`, `message`, and `fix_hint` fields, where fix hints coach the fix rather than restate the flaw. Stylometric findings are honestly scoped to the chosen baseline: indicators the baseline cannot measure produce no finding. Error responses are unchanged, and `passive_voice_detection` still returns a plain list of sentences.
## Preference-Scoring Evaluation
Whether "revisions move prose toward the baseline" is strong enough to power a writing-quality
score was measured directly, not assumed: [`docs/preference-foundation.md`](docs/preference-foundation.md)
scores 13 public-domain edit pairs (1818→1831 Frankenstein, Origin of Species editions, the Alice
manuscript fair copy, Dorian Gray 1890→1891) with the `stylometric_delta` machinery. The delta
direction agreed with the edited variant at coin-flip rates (pair level 41.7%, 95% CI [15%, 72%];
verdict level 47.6%, 95% CI [41%, 55%]), so the go/no-go call is **no-go** on a production
preference score — the evaluation is deterministic and reproducible, and the report names the
evidence that would reverse the call.
## Reference Writing Loop
`examples/writing_loop.py` is a runnable reference agent that wires the loop end-to-end against the live stdio server: a weak draft goes to `stylometric_analysis`, its findings render into a `guided_revision` brief, a revision is produced, and `stylometric_delta` verifies the revision actually moved the statistics toward the baseline — with `verify_revision` rendering the verdict prompt. It exits `0` when verification is clear, `1` when the revision regressed statistics the loop should have caught, and `2` when the loop itself failed (server unreachable, tools missing, error envelopes).
```bash
uv run python examples/writing_loop.py # draft -> revise -> verify, exit 0
uv run python examples/writing_loop.py --demo-failure # revision regresses the draft; exit 1
```
A committed transcript of both runs lives in `examples/writing_loop.transcript.md`: the happy path clears with 14 improved / 0 regressed statistics against the `brown_corpus` baseline, and the failure-mode run shows the loop refusing to sign off on a revision that moved nine statistics further from the baseline.
## Tool Reference
Below is a detailed reference for each tool provided by the server.
---
**`list_tools`**
* **Description**: List all available tools in this server.
* **Parameters**: None
* **Returns**: `list[str]` - A list of tool names.
---
**`character_count`**
* **Description**: Return the number of characters in the input text.
* **Parameters**:
* `text` (`str`): The input text.
* **Returns**: `int` - The total character count.
---
**`word_count`**
* **Description**: Return the number of words in the input text.
* **Parameters**:
* `text` (`str`): The input text.
* **Returns**: `int` - The total word count (based on whitespace splitting).
---
**`spellcheck`**
* **Description**: Return a list of misspelled words in the input text.
* **Parameters**:
* `text` (`str`): The input text.
* **Returns**: `list[str]` - A list of words identified as potentially misspelled.
---
**`readability_score`**
* **Description**: Return readability scores using Flesch Reading Ease, Flesch-Kincaid Grade Level, and Gunning Fog index.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `level` (`str`, optional): Granularity of analysis. Options:
* `"full"` (default): Score the entire text.
* `"section"`: Score the full text and each markdown section (identified by `#` headings) separately.
* `"paragraph"`: Score the full text and each paragraph (separated by blank lines) separately.
* **Returns**: `dict` - A dictionary containing the scores. Structure depends on the `level` parameter. For `"full"`, it returns `{"flesch": float, "kincaid": float, "fog": float}`. For other levels, it returns nested dictionaries. Returns `None` for scores if the text segment is too short.
---
**`reading_time`**
* **Description**: Return the estimated reading time for the input text (based on `textstat`). Markdown markup is stripped before estimating, so syntax characters and link/image target URLs do not count toward the time.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `level` (`str`, optional): Granularity of analysis. Options:
* `"full"` (default): Calculate for the entire text.
* `"section"`: Calculate for the full text and each markdown section.
* `"paragraph"`: Calculate for the full text and each paragraph.
* **Returns**: `dict` - A dictionary containing the estimated reading time in minutes. Structure depends on the `level` parameter.
---
**`keyword_density`**
* **Description**: Calculate the density of a given keyword in the text (case-insensitive, lemmatized). Multi-word keywords are matched as complete, contiguous phrases.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `keyword` (`str`): The keyword or phrase to search for.
* **Returns**: `dict` - `{"keyword": str, "density": float, "findings": list}` — the density percentage ( (keyword count / total words) * 100 ) plus actionable findings.
---
**`keyword_frequency`**
* **Description**: Count how often each keyword (token) appears in the text.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `remove_stopwords` (`bool`, optional, default=`True`): Whether to exclude common English stopwords (e.g., 'the', 'a', 'is').
* **Returns**: `dict` - `{"frequencies": {keyword: count, ...}, "findings": list}` — the frequency map (counts under `frequencies`) plus actionable findings.
---
**`top_keywords`**
* **Description**: Identify the most frequently used keywords in the text.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `top_n` (`int`, optional, default=`10`): The number of top keywords to return.
* `remove_stopwords` (`bool`, optional, default=`True`): Whether to exclude common English stopwords.
* **Returns**: `dict` - `{"keywords": [[keyword, count], ...], "findings": list}` — keyword/count pairs sorted by frequency in descending order, plus actionable findings.
---
**`keyword_context`**
* **Description**: Extract sentences where a specific keyword (case-insensitive, lemmatized) appears. Multi-word keywords are matched as complete, contiguous phrases.
* **Parameters**:
* `text` (`str`): The text to search within.
* `keyword` (`str`): The keyword or phrase to find.
* **Returns**: `dict` - `{"keyword": str, "sentences": list[str], "findings": list}` — the matching sentences plus actionable findings.
---
**`passive_voice_detection`**
* **Description**: Detect sentences containing passive voice constructions (based on a simplified pattern matching using spaCy).
* **Parameters**:
* `text` (`str`): The text to analyze.
* **Returns**: `list[str]` - A list of sentences identified as potentially containing passive voice.
---
**`perplexity_analysis`**
* **Description**: Analyze text for perplexity and burstiness to detect AI-generated content using GPT-2. Computes document-level and sentence-level perplexity along with burstiness (variance of perplexity across sentences). Low perplexity combined with low burstiness is a statistical signal used by AI detectors.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `language` (`str`, optional, default=`"en"`): Language code (only "en" supported currently).
* **Returns**: `dict` - Analysis results including:
* `doc_ppl` (`float | null`): Document-level perplexity score; `null` when no sentence could be scored
* `doc_burstiness` (`float | null`): Burstiness score (standard deviation of sentence perplexities); `null` when fewer than two sentences were scored, since the standard deviation is undefined there
* `sentences` (`list`): Sentence-level perplexity scores
* `config` (`dict`): Model configuration and thresholds
* `flags` (`dict`): AI detection flags with confidence and explanations
---
**`stylometric_analysis`**
* **Description**: Analyze text for stylometric features and detect AI-generated content. Computes sentence length distribution, lexical diversity (TTR/Hapax, plus the length-robust `mtld`/`mattr`/`mtld_lemma`) and vocabulary rarity (`mean_word_frequency`, via `wordfreq`), POS ratios and a curated POS-bigram profile, six readability grade-level formulas (Fog, Kincaid, SMOG, Coleman-Liau, ARI, Dale-Chall), syntactic complexity from the dependency parse (`mean_dependency_distance`, `subordinate_clause_ratio`), punctuation idiosyncrasies (semicolon/em-dash/ellipsis/exclamation/parenthetical rate), hedge/booster epistemic-marker rates, n-gram repetition and Zipf-slope, a per-function-word frequency profile reduced to a Burrows' Delta score, and a character n-gram orthographic profile compared via cosine similarity. Flags outliers relative to a baseline (built-in `brown_corpus`, or a [custom baseline](#custom-baselines) built from your own writing) using z-score analysis.
* **Parameters**:
* `text` (`str`): The text to analyze.
* `baseline` (`str`, optional): Baseline corpus name for comparison. When omitted, the configured `stylometry.default_baseline` (or `"brown_corpus"`) is used; the response's `baseline_used` field names the baseline actually measured against. See [Custom Baselines](#custom-baselines) to build your own.
* `language` (`str`, optional, default=`"en"`): Language code (only "en" supported currently).
* **Returns**: `dict` - Stylometric analysis including:
* `features` (`dict`): Extracted stylometric features (sentence length, TTR/hapax/`mtld`/`mattr`/`mtld_lemma`, `mean_word_frequency`, `word_len_std`, `lexical_density`, POS ratios, `pos_bigram_ratios`, `fog`/`kincaid`/`smog`/`coleman_liau`/`ari`/`dale_chall`, `mean_dependency_distance`, `subordinate_clause_ratio`, punctuation-idiosyncrasy ratios, `hedge_rate`/`booster_rate`, `fourgram_repetition_rate`, `zipf_slope`, `function_word_freqs`, etc. - `char_ngram_profile` is computed internally for `char_ngram_similarity` below but omitted here, as a several-hundred-entry intermediate)
* `z_scores` (`dict`): Z-scores of features against the baseline, including per-word `fw_<word>` scores and the aggregate `burrows_delta`, and per-bigram `posbi_<tag>_<tag>` scores
* `flags` (`dict`): AI detection flags with confidence levels and explanations
* `sentence_analysis` (`list`): Per-sentence analysis with z-scores
* `char_ngram_similarity` (`float | null`): Cosine similarity between this text's character n-gram profile and the baseline's (see [Custom Baselines](#custom-baselines)); `null` when the baseline has no character n-gram profile (e.g. `brown_corpus`)
* `baseline_used` (`str`): Name of the baseline the analysis was measured against - the explicit `baseline` argument, the configured `stylometry.default_baseline`, or `"brown_corpus"`
* `config` (`dict`): Baseline information and analysis thresholds
---
**`stylometric_delta`**
* **Description**: Verify a revision: profile a draft (`text_a`) and its revision (`text_b`) against one baseline and report what the revision actually moved. Both texts run through the same `stylometric_analysis` pipeline against the same baseline, and every statistic the baseline can measure in **both** texts is reported as a movement. A statistic `improved` when the revision moved its z-score closer to zero (into the baseline's range), `regressed` when the movement pushed it further out, and `unchanged` otherwise - the sign never matters, only the distance.
* **Parameters**:
* `text_a` (`str`): The draft text (the "before").
* `text_b` (`str`): The revised text (the "after").
* `baseline` (`str`, optional): Baseline corpus name both texts are measured against. When omitted, the configured `stylometry.default_baseline` (or `"brown_corpus"`) is used; the response's `baseline_used` field names the baseline actually used. See [Custom Baselines](#custom-baselines) to build your own.
* **Returns**: `dict` - Delta analysis including:
* `baseline_used` (`str`): The baseline both texts were measured against.
* `deltas` (`list`): One `{statistic, z_a, z_b, delta, direction}` entry per shared statistic (sorted by name), where `delta = z_b - z_a` and `direction` is `"increased"`, `"decreased"`, or `"none"` - the raw movement, independent of whether it helped.
* `verdict` (`list`): One `{statistic, verdict}` entry per delta, running parallel to `deltas`, with verdict `"improved"`, `"regressed"`, or `"unchanged"`.
* `text_b_analysis` (`dict`): The revised text's full `stylometric_analysis` response, so the verdict carries its own evidence.
* `findings` (`list`): Actionable, located observations about the **revised** text (see [Revision Prompts](#revision-prompts)); pass the whole response to the `verify_revision` prompt to render it as a revision verdict.
**`analyze_sections`**
* **Description**: Run a selected subset of the analysis tools on every markdown section of a document, plus a whole-document rollup. Sections come from the same parser as the section/paragraph analysis levels: heading-keyed and hierarchy-aware (a subsection's body folds into its parent), with pre-first-heading content preserved as its own `_leading_content` section (heading level 0). Each section entry carries `key`, `heading_level`, the rendered section `text`, an impact-ordered `findings` array (located at `section:<key>`), and per-tool `results` — each exactly the response that tool returns for the section text at its default settings. The `rollup` carries each selected tool's whole-document response (findings included), identical to the standalone tool's output, so per-section views can be checked against whole-document analysis. An empty document yields no sections with the rollup still computed; a document with no headings yields the single `_leading_content` section.
* **Parameters**:
* `text` (`str`): The markdown document to analyze.
* `tools` (`list[str]`, optional): The analysis tools to run per section. Choose from: `readability`, `word_count`, `character_count`, `reading_time`, `spellcheck`, `passive_voice`, `perplexity`, `stylometry`. Defaults to everything except the GPT-2 tools (`perplexity` and `stylometry` stay opt-in since they are the expensive tier). Unknown names yield `{"error": ...}` naming the valid menu.
* `baseline` (`str`, optional): Baseline name passed through to the per-section and rollup stylometry runs (ignored unless `stylometry` is selected). See [Custom Baselines](#custom-baselines).
* **Returns**: `dict` - Section batch analysis including:
* `sections` (`list`): One entry per section, in document order: `key` (`str`), `heading_level` (`int`, 0 for leading content), `text` (`str`), `findings` (`list`, impact-ordered — same shape the standalone tools attach), and `results` (`dict`, per-tool raw responses)
* `rollup` (`dict`): Each selected tool's whole-document response — identical to the standalone tool's output, `findings` included
* `tools_used` (`list`): The validated selection
* `section_count` (`int`): Number of sections analyzed
---
## Detector Benchmark
`benchmarks/` is an offline evaluation surface, separate from the MCP server. It scores
the AI-detection code against committed labeled corpora and writes a report, so the
shipped thresholds can be argued about with numbers instead of intuition. It changes no
tool, default, or threshold.
```bash
uv run benchmarks/run_benchmark.py
```
See [`benchmarks/README.md`](benchmarks/README.md) for what the corpora are, what the
report contains, and how far the results reproduce. Corpus text is third-party and is
licensed separately from this repository's code.
## Contributing
Contributions are welcome! Please open an issue or submit a pull request.
## License
This is MIT licensed
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessSlow