Skip to main content
Glama
wdm0006

Writing Tools MCP Server

by wdm0006

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.

Related MCP server: hypernym-mcp-server

Install

# 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), 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:

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

{
  "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.

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

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

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:

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:

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

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 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 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); 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 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); 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.

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

uv run benchmarks/run_benchmark.py

See 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

Related MCP Connectors

Related MCP Servers