essay-stats-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +73 -5
- package/build/src/server.js +1 -1
- package/build/src/text.js +15 -0
- package/build/src/tools.js +64 -14
- package/llms.txt +17 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Add worked examples to word_frequency and style_flags errors
|
|
6
|
+
- Add competitor comparison table to README
|
|
7
|
+
- Update the table of sibling MCP servers in the README
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
- Tool descriptions now include example prompts, so clients can show what to ask each tool.
|
|
12
|
+
- New tool word_frequency lists the most used words, with counts and share of the text, to spot overused terms.
|
|
13
|
+
- style_flags now returns a sentence_list with the number of each passive or long sentence, and says that sentence numbers follow the order of the text.
|
|
14
|
+
- Error messages for empty or wordless text and for a minimum above the limit now include a worked example call so a client can fix its input.
|
|
15
|
+
|
|
3
16
|
## 0.1.0
|
|
4
17
|
|
|
5
18
|
- First version.
|
package/README.md
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# Essay Stats MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
You are about to hand in an essay and you are not sure if it is over the word limit, or how many pages it is. A chat assistant tends to guess at word counts. This server counts them properly, on your computer, so your assistant can give you real numbers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Install it in Claude Code with one line:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
claude mcp add essay-stats -- npx -y essay-stats-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Then ask things like:
|
|
12
|
+
|
|
13
|
+
- "My limit is 1500 words, not counting the works cited or quotes. Am I over?"
|
|
14
|
+
- "How many pages is this essay in Arial 11 point with 1.5 spacing?"
|
|
15
|
+
- "Find the passive voice and filler words in my draft."
|
|
16
|
+
|
|
17
|
+
Essay Stats MCP is an essay word counter and writing checker that runs offline. It counts words and pages, gives a readability score, and flags passive voice, filler words, long sentences and repeated sentence openers. It also gives the reading time.
|
|
18
|
+
|
|
19
|
+
There is no account, no API key and no network call. Your essay text never leaves your machine.
|
|
6
20
|
|
|
7
21
|
## Quick start
|
|
8
22
|
|
|
9
|
-
|
|
23
|
+
For Claude Code, use the one line above. For other apps, add this to the MCP config:
|
|
10
24
|
|
|
11
25
|
```json
|
|
12
26
|
{
|
|
@@ -24,7 +38,6 @@ It needs Node.js 20 or newer.
|
|
|
24
38
|
Where the config lives:
|
|
25
39
|
|
|
26
40
|
- Claude Desktop: Settings, Developer, Edit Config (`claude_desktop_config.json`).
|
|
27
|
-
- Claude Code: run `claude mcp add essay-stats -- npx -y essay-stats-mcp`.
|
|
28
41
|
- Cursor: Settings, MCP, or `.cursor/mcp.json` in your project.
|
|
29
42
|
- VS Code: `.vscode/mcp.json`. VS Code uses a `servers` key instead of `mcpServers`.
|
|
30
43
|
- Windsurf: `~/.codeium/windsurf/mcp_config.json`.
|
|
@@ -37,8 +50,9 @@ Where the config lives:
|
|
|
37
50
|
| `readability_scores` | Flesch Kincaid grade, Flesch reading ease, Gunning Fog and SMOG. |
|
|
38
51
|
| `style_flags` | Passive voice, repeated sentence openers, filler words and long sentences. |
|
|
39
52
|
| `check_word_limit` | Compares the text with a word limit. Optional minimum, tolerance, and leaving out references and quotes. |
|
|
53
|
+
| `word_frequency` | The words used most often, with counts and share of the text, to spot overused terms. |
|
|
40
54
|
|
|
41
|
-
All
|
|
55
|
+
All five tools only read the text you give them. Each one returns a short written answer and the same numbers as structured fields.
|
|
42
56
|
|
|
43
57
|
### count_text
|
|
44
58
|
|
|
@@ -52,6 +66,12 @@ Input: `text`. The grades are roughly US school grades, so 12 is the end of high
|
|
|
52
66
|
|
|
53
67
|
Inputs: `text` and optional `max_sentence_words` (default 30). The checks are simple pattern rules. Use them as a prompt to reread a sentence, not as proof of a mistake.
|
|
54
68
|
|
|
69
|
+
Sentence numbers start at 1 and follow the order of the text. The `sentence_list` field gives each passive or long sentence with its number, and the text summary ends with a line listing those numbers, so a reply can point you to the right sentence.
|
|
70
|
+
|
|
71
|
+
### word_frequency
|
|
72
|
+
|
|
73
|
+
Inputs: `text`, and optional `top` (how many words to list, default 10), `min_count` (fewest uses for a word to be listed, default 2) and `include_common_words` (also count small words such as the, and, of and to, default false). Case is ignored, and words under 3 letters are skipped unless `include_common_words` is true.
|
|
74
|
+
|
|
55
75
|
### check_word_limit
|
|
56
76
|
|
|
57
77
|
Inputs: `text`, `limit`, and optional `minimum`, `tolerance_percent`, `exclude_references` and `exclude_quotes`. References are cut from a heading line such as References, Works Cited or Bibliography to the end. Quotes are lines starting with `>` and text inside double quotes.
|
|
@@ -63,6 +83,34 @@ Inputs: `text`, `limit`, and optional `minimum`, `tolerance_percent`, `exclude_r
|
|
|
63
83
|
- "Find the passive voice and filler words in my draft."
|
|
64
84
|
- "My limit is 1500 words, not counting the works cited or quotes. Am I over?"
|
|
65
85
|
- "Which sentences are over 25 words long?"
|
|
86
|
+
- "Which words do I overuse in this essay?"
|
|
87
|
+
|
|
88
|
+
## How it compares
|
|
89
|
+
|
|
90
|
+
Essay Stats MCP does one job: it measures a piece of writing you give it. Other tools in this area do different jobs, so the table below is about what each kind of tool is for, not about which is better.
|
|
91
|
+
|
|
92
|
+
| | Essay Stats MCP | Brightspace MCP servers, such as brightspace-mcp-server | Online grammar and writing checkers | Asking a chat assistant to count |
|
|
93
|
+
| --- | --- | --- | --- | --- |
|
|
94
|
+
| Main job | Word count, page estimate, readability and style flags for an essay | Reading course data from Brightspace, such as courses and assignments | Fixing grammar, spelling and tone | General answers |
|
|
95
|
+
| Works on your own draft text | Yes | No, it works on course data | Yes | Yes |
|
|
96
|
+
| Exact word and page numbers | Yes, counted by code | Not its purpose | Usually a word count only | Often a guess |
|
|
97
|
+
| Word limit check that skips references and quotes | Yes, with `check_word_limit` | No | Rarely | Unreliable |
|
|
98
|
+
| Needs an account or API key | No | Depends on the server, check its README | Often an account | Depends on the app |
|
|
99
|
+
| Sends your text over the network | No | Depends on the server | Usually yes | Yes, to the assistant |
|
|
100
|
+
| Runs as a local MCP server | Yes | Yes | Not usually | Not applicable |
|
|
101
|
+
|
|
102
|
+
Essay Stats MCP does not fix your writing or reach into Brightspace. If you want Brightspace data next to it, see the next section for our own server, brightspace-d2l-mcp.
|
|
103
|
+
|
|
104
|
+
## Works well with
|
|
105
|
+
|
|
106
|
+
If your school uses Brightspace, you can run [brightspace-d2l-mcp](https://www.npmjs.com/package/brightspace-d2l-mcp) next to Essay Stats MCP in the same app. That server has its own setup, so follow its README to install and sign in. Essay Stats MCP does not talk to it and does not need it.
|
|
107
|
+
|
|
108
|
+
With both added to your MCP config, you can ask your assistant to look up the word limit in your assignment details, then pass that number to `check_word_limit` along with your draft. For example:
|
|
109
|
+
|
|
110
|
+
- "Find the word limit for my history essay in Brightspace, then check this draft against it, leaving out the works cited."
|
|
111
|
+
- "Look up my assignment instructions, then tell me if this draft is under the limit and how many pages it is."
|
|
112
|
+
|
|
113
|
+
The assignment details come from the other server. The counting and checking happen here, on your machine, and Essay Stats MCP makes no network calls.
|
|
66
114
|
|
|
67
115
|
## Privacy
|
|
68
116
|
|
|
@@ -76,6 +124,26 @@ npm run build
|
|
|
76
124
|
npm test
|
|
77
125
|
```
|
|
78
126
|
|
|
127
|
+
## More free student MCP servers
|
|
128
|
+
|
|
129
|
+
All free, open source and read only where they touch your accounts. Browse the full list at [brightspacemcp.com/more-mcps](https://brightspacemcp.com/more-mcps/).
|
|
130
|
+
|
|
131
|
+
| Server | What it does |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| [Brightspace MCP](https://github.com/AnonymousOnyx22/brightspace-d2l-mcp) | D2L Brightspace for AI. Due dates with real submission status, grades, a target grade calculator and course files. |
|
|
134
|
+
| [Canvas MCP](https://github.com/AnonymousOnyx22/canvas-lms-student-mcp) | Canvas LMS for AI, read only. Due dates, missing work, grades and announcements. |
|
|
135
|
+
| [Moodle MCP](https://github.com/AnonymousOnyx22/moodle-student-mcp) | Moodle for AI, read only. Due dates, overdue work, grades and announcements. |
|
|
136
|
+
| [Google Classroom MCP](https://github.com/AnonymousOnyx22/google-classroom-student-mcp) | Google Classroom for AI, read only. Todos, due dates, missing work and grades. |
|
|
137
|
+
| [GPA Calculator MCP](https://github.com/AnonymousOnyx22/gpa-calculator-mcp) | GPA, grade conversion and the grade you need on the final, for AI. Works offline, no account. |
|
|
138
|
+
| [Citations MCP](https://github.com/AnonymousOnyx22/student-citations-mcp) | APA, MLA and Chicago citations from a DOI, an ISBN or typed details. No account. |
|
|
139
|
+
| [Study Planner MCP](https://github.com/AnonymousOnyx22/study-planner-mcp) | Exam study schedules, spaced repetition dates, pomodoro plans and assignment milestones. Works offline. |
|
|
140
|
+
| [Paper Finder MCP](https://github.com/AnonymousOnyx22/paper-finder-student-mcp) | Search scholarly papers, find open access copies and related work through OpenAlex. No key needed. |
|
|
141
|
+
| [BibTeX Reference Manager MCP](https://github.com/AnonymousOnyx22/bibtex-reference-manager-mcp) | Clean, convert, dedupe and lint BibTeX, RIS and CSL JSON reference lists, and make cite keys. Works offline. |
|
|
142
|
+
| [Open Textbook Finder MCP](https://github.com/AnonymousOnyx22/open-textbook-finder-mcp) | Find free legal copies of textbooks on OpenStax, DOAB and Open Library, look up editions by ISBN and suggest cheaper older ones. |
|
|
143
|
+
| [STEM Calculator MCP](https://github.com/AnonymousOnyx22/stem-calc-student-mcp) | Chemistry and physics homework math: molar mass, balancing equations, grams to moles, unit conversion and significant figures. Works offline. |
|
|
144
|
+
| [Syllabus to Calendar MCP](https://github.com/AnonymousOnyx22/syllabus-calendar-mcp) | Syllabus to calendar MCP server. Extract dates from syllabus text, build an ics calendar, expand recurring classes and merge calendars. Works offline, no account. |
|
|
145
|
+
| [Job Application Tracker MCP](https://github.com/AnonymousOnyx22/job-tracker-student-mcp) | Job application tracker MCP server. Track internships, jobs, interviews and deadlines offline, see what is coming up, and export your list as CSV or Markdown. No account and no API keys. |
|
|
146
|
+
|
|
79
147
|
## License
|
|
80
148
|
|
|
81
149
|
MIT
|
package/build/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { registerTools } from "./core/tools.js";
|
|
3
3
|
import { tools } from "./tools.js";
|
|
4
|
-
export const VERSION = "0.1.
|
|
4
|
+
export const VERSION = "0.1.2";
|
|
5
5
|
export function createServer() {
|
|
6
6
|
const server = new McpServer({ name: "essay-stats-mcp", version: VERSION });
|
|
7
7
|
registerTools(server, tools);
|
package/build/src/text.js
CHANGED
|
@@ -192,3 +192,18 @@ export function stripQuotes(text) {
|
|
|
192
192
|
});
|
|
193
193
|
return { body, removed: removed.join("\n") };
|
|
194
194
|
}
|
|
195
|
+
const COMMON_WORDS = new Set("a about after all also an and any are as at be because been but by can could did do does for from had has have he her his how i if in into is it its may more most my no not of on one or our out she so some than that the their them then there these they this to up was we were what when which who will with would you your".split(" "));
|
|
196
|
+
/** Counts how often each word is used, ignoring case. Common small words are skipped unless asked for. */
|
|
197
|
+
export function wordFrequency(text, includeCommon = false) {
|
|
198
|
+
const all = words(text).map((w) => w.toLowerCase().replace(/['’]s$/, ""));
|
|
199
|
+
const counted = new Map();
|
|
200
|
+
for (const w of all) {
|
|
201
|
+
if (!includeCommon && (COMMON_WORDS.has(w) || w.length < 3))
|
|
202
|
+
continue;
|
|
203
|
+
counted.set(w, (counted.get(w) ?? 0) + 1);
|
|
204
|
+
}
|
|
205
|
+
const hits = [...counted.entries()]
|
|
206
|
+
.map(([word, count]) => ({ word, count, percent: round((count / all.length) * 100, 2) }))
|
|
207
|
+
.sort((a, b) => b.count - a.count || a.word.localeCompare(b.word));
|
|
208
|
+
return { total: all.length, unique: new Set(all).size, hits };
|
|
209
|
+
}
|
package/build/src/tools.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { counts, findFiller, findPassive, findRepeatedOpeners, pageEstimate, round, splitParagraphs, splitSentences, stripQuotes, stripReferences, wordCount, } from "./text.js";
|
|
3
|
-
const
|
|
4
|
-
|
|
2
|
+
import { counts, findFiller, findPassive, findRepeatedOpeners, pageEstimate, round, splitParagraphs, splitSentences, stripQuotes, stripReferences, wordCount, wordFrequency, } from "./text.js";
|
|
3
|
+
const EMPTY_TEXT_HINT = `text is empty. Paste the essay text to analyse. Worked example: { "text": "Your essay content here." }`;
|
|
4
|
+
const textInput = z.string().min(1, EMPTY_TEXT_HINT);
|
|
5
|
+
function requireText(text, toolName) {
|
|
5
6
|
if (!/[A-Za-z0-9]/.test(text)) {
|
|
6
|
-
|
|
7
|
+
const examples = {
|
|
8
|
+
check_word_limit: `{ "text": "Your essay content here.", "limit": 1500, "exclude_references": true }`,
|
|
9
|
+
word_frequency: `{ "text": "Your essay content here.", "top": 10, "min_count": 2 }`,
|
|
10
|
+
style_flags: `{ "text": "Your essay content here.", "max_sentence_words": 30 }`,
|
|
11
|
+
};
|
|
12
|
+
const example = examples[toolName] ?? `{ "text": "Your essay content here." }`;
|
|
13
|
+
throw new Error(`The text has no words in it. Paste the essay text and try again. Worked example for ${toolName}: ${example}`);
|
|
7
14
|
}
|
|
8
15
|
}
|
|
9
16
|
const countText = {
|
|
10
17
|
name: "count_text",
|
|
11
18
|
title: "Count words, sentences and pages",
|
|
12
|
-
description: "Count the words, characters, sentences and paragraphs in a text, and estimate how many pages it fills at a given font, font size and line spacing. Inputs: text, and optional font (times new roman, arial, calibri, georgia, courier new or verdana, default times new roman), font_size in points (default 12) and spacing (single, 1.5 or double, default double).
|
|
19
|
+
description: "Count the words, characters, sentences and paragraphs in a text, and estimate how many pages it fills at a given font, font size and line spacing. Inputs: text, and optional font (times new roman, arial, calibri, georgia, courier new or verdana, default times new roman), font_size in points (default 12) and spacing (single, 1.5 or double, default double). The page count is an estimate from typical words per page, so a word processor may differ by a little. Also returns an estimated reading time at 238 words per minute. Example: \"Count the words in this essay and estimate the pages in Times New Roman 12 double spaced.",
|
|
13
20
|
inputSchema: {
|
|
14
21
|
text: textInput,
|
|
15
22
|
font: z.string().optional().describe("Font name. Default times new roman."),
|
|
@@ -29,7 +36,7 @@ const countText = {
|
|
|
29
36
|
reading_minutes: z.number(),
|
|
30
37
|
},
|
|
31
38
|
run: ({ text, font = "times new roman", font_size = 12, spacing = "double" }) => {
|
|
32
|
-
requireText(text);
|
|
39
|
+
requireText(text, "count_text");
|
|
33
40
|
const w = wordCount(text);
|
|
34
41
|
const pages = round(pageEstimate(w, font, font_size, spacing), 1);
|
|
35
42
|
const minutes = round(w / 238, 1);
|
|
@@ -69,7 +76,7 @@ function fleschLabel(score) {
|
|
|
69
76
|
const readabilityScores = {
|
|
70
77
|
name: "readability_scores",
|
|
71
78
|
title: "Readability scores",
|
|
72
|
-
description: "Score how hard a text is to read with three standard formulas: Flesch Kincaid grade level (plus the Flesch reading ease score), Gunning Fog index and SMOG index. Each grade is roughly a US school grade, so 12 is the end of high school and 16 is a college degree. Input: text.
|
|
79
|
+
description: "Score how hard a text is to read with three standard formulas: Flesch Kincaid grade level (plus the Flesch reading ease score), Gunning Fog index and SMOG index. Each grade is roughly a US school grade, so 12 is the end of high school and 16 is a college degree. Input: text. SMOG is built for samples of 30 sentences or more, so on shorter text the tool still returns a number but marks it as less reliable. Example: \"Give me the Flesch Kincaid, Gunning Fog and SMOG scores for this paper.",
|
|
73
80
|
inputSchema: { text: textInput },
|
|
74
81
|
outputSchema: {
|
|
75
82
|
words: z.number(),
|
|
@@ -84,7 +91,7 @@ const readabilityScores = {
|
|
|
84
91
|
smog_reliable: z.boolean(),
|
|
85
92
|
},
|
|
86
93
|
run: ({ text }) => {
|
|
87
|
-
requireText(text);
|
|
94
|
+
requireText(text, "readability_scores");
|
|
88
95
|
const c = counts(text);
|
|
89
96
|
if (c.words === 0 || c.sentences === 0) {
|
|
90
97
|
throw new Error("The text needs at least one word and one sentence to score.");
|
|
@@ -117,7 +124,7 @@ const readabilityScores = {
|
|
|
117
124
|
const styleFlags = {
|
|
118
125
|
name: "style_flags",
|
|
119
126
|
title: "Style flags",
|
|
120
|
-
description: "Flag common style problems in an essay: sentences in the passive voice, sentences that start with the same word as a neighbour (or three or more times), filler words and phrases such as very, just, in order to, and sentences that are too long. Inputs: text, and optional max_sentence_words (default 30), the length above which a sentence counts as long. The checks are simple pattern rules, so treat them as prompts to reread, not as errors.",
|
|
127
|
+
description: "Flag common style problems in an essay: sentences in the passive voice, sentences that start with the same word as a neighbour (or three or more times), filler words and phrases such as very, just, in order to, and sentences that are too long. Inputs: text, and optional max_sentence_words (default 30), the length above which a sentence counts as long. Sentence numbers start at 1 and follow the order of the text, so number 3 is the third sentence. The sentence_list field gives each passive or long sentence with its number, and the text summary ends with a line listing those numbers. The checks are simple pattern rules, so treat them as prompts to reread, not as errors. Example: \"Flag passive voice and filler words in my draft.",
|
|
121
128
|
inputSchema: {
|
|
122
129
|
text: textInput,
|
|
123
130
|
max_sentence_words: z.number().int().min(5).max(200).optional().describe("Sentence length that counts as long. Default 30."),
|
|
@@ -131,9 +138,10 @@ const styleFlags = {
|
|
|
131
138
|
filler_count: z.number(),
|
|
132
139
|
long_sentences: z.array(z.object({ number: z.number(), words: z.number(), sentence: z.string() })),
|
|
133
140
|
max_sentence_words: z.number(),
|
|
141
|
+
sentence_list: z.array(z.object({ number: z.number(), passive: z.boolean(), long: z.boolean(), sentence: z.string() })),
|
|
134
142
|
},
|
|
135
143
|
run: ({ text, max_sentence_words = 30 }) => {
|
|
136
|
-
requireText(text);
|
|
144
|
+
requireText(text, "style_flags");
|
|
137
145
|
const sentences = splitSentences(text);
|
|
138
146
|
const passive = findPassive(sentences);
|
|
139
147
|
const openers = findRepeatedOpeners(sentences);
|
|
@@ -142,12 +150,23 @@ const styleFlags = {
|
|
|
142
150
|
.map((s, i) => ({ number: i + 1, words: wordCount(s), sentence: s }))
|
|
143
151
|
.filter((s) => s.words > max_sentence_words);
|
|
144
152
|
const fillerCount = filler.reduce((n, f) => n + f.count, 0);
|
|
153
|
+
let next = 0;
|
|
154
|
+
const longNumbers = new Set(long.map((l) => l.number));
|
|
155
|
+
const sentenceList = sentences
|
|
156
|
+
.map((s, i) => {
|
|
157
|
+
const isPassive = next < passive.length && passive[next].sentence === s;
|
|
158
|
+
if (isPassive)
|
|
159
|
+
next++;
|
|
160
|
+
return { number: i + 1, passive: isPassive, long: longNumbers.has(i + 1), sentence: s };
|
|
161
|
+
})
|
|
162
|
+
.filter((s) => s.passive || s.long);
|
|
145
163
|
const lines = [
|
|
146
164
|
`${sentences.length} sentences checked.`,
|
|
147
165
|
`Passive voice: ${passive.length}${passive.length ? " (" + passive.slice(0, 3).map((p) => `"${p.phrase}"`).join(", ") + (passive.length > 3 ? ", ..." : "") + ")" : ""}.`,
|
|
148
166
|
`Repeated openers: ${openers.length ? openers.map((o) => `"${o.word}" x${o.count}`).join(", ") : "none"}.`,
|
|
149
167
|
`Filler: ${fillerCount ? filler.map((f) => `"${f.text}" x${f.count}`).join(", ") : "none"}.`,
|
|
150
168
|
`Long sentences over ${max_sentence_words} words: ${long.length ? long.map((l) => `#${l.number} (${l.words} words)`).join(", ") : "none"}.`,
|
|
169
|
+
`Passive or long sentence numbers, in text order: ${sentenceList.length ? sentenceList.map((s) => s.number).join(", ") : "none"}.`,
|
|
151
170
|
];
|
|
152
171
|
return {
|
|
153
172
|
text: lines.join("\n"),
|
|
@@ -160,6 +179,7 @@ const styleFlags = {
|
|
|
160
179
|
filler_count: fillerCount,
|
|
161
180
|
long_sentences: long,
|
|
162
181
|
max_sentence_words,
|
|
182
|
+
sentence_list: sentenceList,
|
|
163
183
|
},
|
|
164
184
|
};
|
|
165
185
|
},
|
|
@@ -167,7 +187,7 @@ const styleFlags = {
|
|
|
167
187
|
const checkWordLimit = {
|
|
168
188
|
name: "check_word_limit",
|
|
169
189
|
title: "Check a word limit",
|
|
170
|
-
description: "Compare an essay against a word limit. Inputs: text, limit (the maximum word count), optional minimum (a lower bound), optional tolerance_percent (how far over the limit is still acceptable, default 0, a common rule is 10), optional exclude_references (cut everything from a heading line such as References, Works Cited or Bibliography to the end) and
|
|
190
|
+
description: "Compare an essay against a word limit. Inputs: text, limit (the maximum word count), optional minimum (a lower bound), optional tolerance_percent (how far over the limit is still acceptable, default 0, a common rule is 10), optional exclude_references (cut everything from a heading line such as References, Works Cited or Bibliography to the end) and optional exclude_quotes (leave out block quotes that start with > and text inside double quotes). Returns the counted words, how many are left or over, and what was excluded. Example: \"Is this under 2000 words if I leave out the works cited and quotes?.",
|
|
171
191
|
inputSchema: {
|
|
172
192
|
text: textInput,
|
|
173
193
|
limit: z.number().int().positive().describe("Maximum number of words."),
|
|
@@ -189,9 +209,9 @@ const checkWordLimit = {
|
|
|
189
209
|
words_over: z.number(),
|
|
190
210
|
},
|
|
191
211
|
run: ({ text, limit, minimum, tolerance_percent = 0, exclude_references = false, exclude_quotes = false }) => {
|
|
192
|
-
requireText(text);
|
|
212
|
+
requireText(text, "check_word_limit");
|
|
193
213
|
if (minimum !== undefined && minimum > limit) {
|
|
194
|
-
throw new Error(`minimum (${minimum}) is above limit (${limit}).
|
|
214
|
+
throw new Error(`minimum (${minimum}) is above limit (${limit}). Set minimum to at most the limit. Worked example: { "text": "Your essay content here.", "limit": 1500, "minimum": 1200 }`);
|
|
195
215
|
}
|
|
196
216
|
const total = wordCount(text);
|
|
197
217
|
let body = text;
|
|
@@ -247,4 +267,34 @@ const checkWordLimit = {
|
|
|
247
267
|
};
|
|
248
268
|
},
|
|
249
269
|
};
|
|
250
|
-
|
|
270
|
+
const wordFrequencyTool = {
|
|
271
|
+
name: "word_frequency",
|
|
272
|
+
title: "Word frequency",
|
|
273
|
+
description: "Find the words an essay uses most, to spot overused or repeated terms. Inputs: text, and optional top (how many words to list, default 10), min_count (fewest uses for a word to be listed, default 2) and include_common_words (also count small words such as the, and, of and to, default false). Case is ignored, and words under 3 letters are skipped unless include_common_words is true. Each word comes with its count and its share of all the words in the text. Example: \"Which words do I overuse in this essay?.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
text: textInput,
|
|
276
|
+
top: z.number().int().min(1).max(100).optional().describe("How many words to list. Default 10."),
|
|
277
|
+
min_count: z.number().int().min(1).optional().describe("Fewest uses for a word to be listed. Default 2."),
|
|
278
|
+
include_common_words: z.boolean().optional().describe("Also count small common words. Default false."),
|
|
279
|
+
},
|
|
280
|
+
outputSchema: {
|
|
281
|
+
total_words: z.number(),
|
|
282
|
+
unique_words: z.number(),
|
|
283
|
+
words: z.array(z.object({ word: z.string(), count: z.number(), percent: z.number() })),
|
|
284
|
+
top: z.number(),
|
|
285
|
+
min_count: z.number(),
|
|
286
|
+
},
|
|
287
|
+
run: ({ text, top = 10, min_count = 2, include_common_words = false }) => {
|
|
288
|
+
requireText(text, "word_frequency");
|
|
289
|
+
const f = wordFrequency(text, include_common_words);
|
|
290
|
+
const list = f.hits.filter((h) => h.count >= min_count).slice(0, top);
|
|
291
|
+
const summary = list.length
|
|
292
|
+
? `Most used words: ${list.map((h) => `"${h.word}" x${h.count} (${h.percent} percent)`).join(", ")}.`
|
|
293
|
+
: `No word is used ${min_count} or more times.`;
|
|
294
|
+
return {
|
|
295
|
+
text: `${f.total} words, ${f.unique} different. ${summary}`,
|
|
296
|
+
structured: { total_words: f.total, unique_words: f.unique, words: list, top, min_count },
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
export const tools = [countText, readabilityScores, styleFlags, checkWordLimit, wordFrequencyTool];
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Essay Stats MCP
|
|
2
2
|
|
|
3
|
-
> An MCP server with offline writing metrics for essays. It counts words and pages, scores readability, flags style problems and checks word limits. No account, no API key, no network.
|
|
3
|
+
> An MCP server with offline writing metrics for essays. It counts words and pages, scores readability, flags style problems and checks word limits. No account, no API key, no network. Your essay text never leaves your machine.
|
|
4
|
+
|
|
5
|
+
Claude Code users can install it with one line: `claude mcp add essay-stats -- npx -y essay-stats-mcp`
|
|
4
6
|
|
|
5
7
|
Package: essay-stats-mcp (npm). Registry name: io.github.AnonymousOnyx22/essay-stats-mcp. Runs over stdio. Needs Node.js 20 or newer.
|
|
6
8
|
|
|
@@ -28,6 +30,7 @@ All tools are read only and work on text passed in the call.
|
|
|
28
30
|
- count_text: words, characters, sentences, paragraphs, reading time and estimated pages. Inputs: text, optional font (times new roman, arial, calibri, georgia, courier new, verdana), font_size (default 12) and spacing (single, 1.5, double; default double).
|
|
29
31
|
- readability_scores: Flesch Kincaid grade, Flesch reading ease, Gunning Fog and SMOG. Input: text. SMOG is marked less reliable under 30 sentences.
|
|
30
32
|
- style_flags: passive voice, repeated sentence openers, filler words and long sentences. Inputs: text, optional max_sentence_words (default 30).
|
|
33
|
+
- word_frequency: the most used words with counts and share of the text, to spot overused terms. Inputs: text, optional top (default 10), min_count (default 2) and include_common_words (default false).
|
|
31
34
|
- check_word_limit: compares text with a limit. Inputs: text, limit, optional minimum, tolerance_percent, exclude_references and exclude_quotes. Returns a status of under_minimum, within_limit, within_tolerance or over_limit.
|
|
32
35
|
|
|
33
36
|
## Example prompts
|
|
@@ -35,7 +38,20 @@ All tools are read only and work on text passed in the call.
|
|
|
35
38
|
- "Count the words in this essay and estimate the pages in Times New Roman 12 double spaced."
|
|
36
39
|
- "Give me the Flesch Kincaid, Gunning Fog and SMOG scores for this paper."
|
|
37
40
|
- "Flag passive voice and filler words in my draft."
|
|
41
|
+
- "Which words do I overuse in this essay?"
|
|
38
42
|
- "Is this under 2000 words if I leave out the works cited and quotes?"
|
|
43
|
+
- "My limit is 1500 words, not counting the works cited or quotes. Am I over?"
|
|
44
|
+
- "How many pages is this essay in Arial 11 point with 1.5 spacing?"
|
|
45
|
+
- "What reading level is this essay?"
|
|
46
|
+
- "Which sentences are over 25 words long?"
|
|
47
|
+
|
|
48
|
+
## Tool examples
|
|
49
|
+
|
|
50
|
+
- count_text: "Count the words in this essay and tell me how many pages it is in Arial 11 point with 1.5 spacing." Uses font arial, font_size 11 and spacing 1.5.
|
|
51
|
+
- readability_scores: "Give me the Flesch Kincaid, Gunning Fog and SMOG scores for this paper." Grades are roughly US school grades.
|
|
52
|
+
- style_flags: "Which sentences are over 25 words long?" Uses max_sentence_words 25. Sentence numbers start at 1.
|
|
53
|
+
- word_frequency: "Show my 15 most used words." Uses top 15.
|
|
54
|
+
- check_word_limit: "My limit is 1500 words, not counting the works cited or quotes." Uses limit 1500 with exclude_references and exclude_quotes set to true.
|
|
39
55
|
|
|
40
56
|
## Privacy
|
|
41
57
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "essay-stats-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Essay Stats MCP counts words and pages offline for essays, scores readability, flags passive voice and filler words, and checks word limits against assignment requirements. No account, no network.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -25,16 +25,20 @@
|
|
|
25
25
|
},
|
|
26
26
|
"keywords": [
|
|
27
27
|
"essay stats mcp",
|
|
28
|
+
"essay stats",
|
|
28
29
|
"essay word counter",
|
|
30
|
+
"word counter",
|
|
29
31
|
"word count",
|
|
30
32
|
"page count",
|
|
31
33
|
"readability",
|
|
34
|
+
"readability score",
|
|
32
35
|
"flesch kincaid",
|
|
33
36
|
"gunning fog",
|
|
34
37
|
"smog index",
|
|
35
38
|
"passive voice",
|
|
36
39
|
"writing metrics",
|
|
37
40
|
"word limit",
|
|
41
|
+
"word frequency",
|
|
38
42
|
"reading time",
|
|
39
43
|
"style checker",
|
|
40
44
|
"student writing",
|