essay-stats-mcp 0.1.2 → 0.1.3
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 +5 -0
- package/README.md +67 -9
- package/build/src/core/tools.js +1 -1
- package/build/src/server.js +1 -1
- package/build/src/text.js +66 -0
- package/build/src/tools.js +71 -7
- package/llms.txt +2 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
- New tool sentence_variety shows how much sentence lengths change in an essay, with short, medium and long counts and runs of sentences that are about the same length.
|
|
6
|
+
- All five tools are now marked read only, idempotent and closed world, so clients can run them without asking you first.
|
|
7
|
+
|
|
3
8
|
## 0.1.2
|
|
4
9
|
|
|
5
10
|
- Add worked examples to word_frequency and style_flags errors
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Essay Stats MCP
|
|
1
|
+
# Essay Stats MCP: Essay Word Count and Writing Checker
|
|
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
|
|
|
@@ -20,7 +20,11 @@ There is no account, no API key and no network call. Your essay text never leave
|
|
|
20
20
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Claude Code users can use the one line above. For other apps, copy the snippet for your client. It needs Node.js 20 or newer.
|
|
24
|
+
|
|
25
|
+
### Claude Desktop
|
|
26
|
+
|
|
27
|
+
Open Settings, Developer, Edit Config, and add this to `claude_desktop_config.json`. Then restart Claude Desktop.
|
|
24
28
|
|
|
25
29
|
```json
|
|
26
30
|
{
|
|
@@ -33,14 +37,40 @@ For Claude Code, use the one line above. For other apps, add this to the MCP con
|
|
|
33
37
|
}
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
### Cursor
|
|
41
|
+
|
|
42
|
+
Add this to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for all projects.
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"essay-stats": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["-y", "essay-stats-mcp"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
### VS Code
|
|
39
56
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
Add this to `.vscode/mcp.json` in your workspace. VS Code uses a `servers` key instead of `mcpServers`.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"servers": {
|
|
62
|
+
"essay-stats": {
|
|
63
|
+
"type": "stdio",
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "essay-stats-mcp"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Windsurf
|
|
72
|
+
|
|
73
|
+
Use the same snippet as Claude Desktop in `~/.codeium/windsurf/mcp_config.json`.
|
|
44
74
|
|
|
45
75
|
## Tools
|
|
46
76
|
|
|
@@ -49,10 +79,11 @@ Where the config lives:
|
|
|
49
79
|
| `count_text` | Words, characters, sentences, paragraphs, reading time and an estimated page count for a font, size and line spacing. |
|
|
50
80
|
| `readability_scores` | Flesch Kincaid grade, Flesch reading ease, Gunning Fog and SMOG. |
|
|
51
81
|
| `style_flags` | Passive voice, repeated sentence openers, filler words and long sentences. |
|
|
82
|
+
| `sentence_variety` | How much sentence lengths change, with short, medium and long counts and runs of similar length. |
|
|
52
83
|
| `check_word_limit` | Compares the text with a word limit. Optional minimum, tolerance, and leaving out references and quotes. |
|
|
53
84
|
| `word_frequency` | The words used most often, with counts and share of the text, to spot overused terms. |
|
|
54
85
|
|
|
55
|
-
All
|
|
86
|
+
All six tools only read the text you give them. Each one returns a short written answer and the same numbers as structured fields.
|
|
56
87
|
|
|
57
88
|
### count_text
|
|
58
89
|
|
|
@@ -68,6 +99,10 @@ Inputs: `text` and optional `max_sentence_words` (default 30). The checks are si
|
|
|
68
99
|
|
|
69
100
|
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
101
|
|
|
102
|
+
### sentence_variety
|
|
103
|
+
|
|
104
|
+
Input: `text`. Returns the shortest, longest and average sentence length in words, the standard deviation, and how many sentences are short (under 10 words), medium (10 to 24) or long (25 or more). It also lists runs of 3 or more sentences in a row whose lengths are within 3 words of the first one in the run, with their sentence numbers. Sentence numbers start at 1 and follow the order of the text.
|
|
105
|
+
|
|
71
106
|
### word_frequency
|
|
72
107
|
|
|
73
108
|
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.
|
|
@@ -76,6 +111,28 @@ Inputs: `text`, and optional `top` (how many words to list, default 10), `min_co
|
|
|
76
111
|
|
|
77
112
|
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.
|
|
78
113
|
|
|
114
|
+
## FAQ: how words and pages are counted
|
|
115
|
+
|
|
116
|
+
### How does it count a word?
|
|
117
|
+
|
|
118
|
+
It splits the text on spaces and line breaks and counts each piece that has at least one letter or digit in it. That is the same idea Word and Google Docs use. A hyphenated word such as well known counts as one word when written with a hyphen, and a number like 2024 counts as one word.
|
|
119
|
+
|
|
120
|
+
### Why can my count differ from Word?
|
|
121
|
+
|
|
122
|
+
Mostly because of what gets pasted. A dash or symbol standing alone between spaces is counted by Word but not here, since it has no letter or digit. Footnotes, headers, footers and text boxes only count if you paste them in. If the two numbers differ by a few words, check those first.
|
|
123
|
+
|
|
124
|
+
### How is the page number worked out?
|
|
125
|
+
|
|
126
|
+
`count_text` divides the word count by a fixed words per page figure for the font you pick, then adjusts for font size and line spacing. It is an estimate, not a layout. It does not know your margins, headings, title page, tables or images, so Word can show a different page count. For a hard page rule, trust the page count in your own document.
|
|
127
|
+
|
|
128
|
+
### What does check_word_limit leave out?
|
|
129
|
+
|
|
130
|
+
Nothing unless you ask. With `exclude_references` it drops everything from a heading line such as References, Works Cited or Bibliography to the end. With `exclude_quotes` it drops lines starting with `>` and text inside double quotes. The answer shows the total, the counted words and how many words were left out, so you can match it to what your instructor counts.
|
|
131
|
+
|
|
132
|
+
### Does the tolerance change the word count?
|
|
133
|
+
|
|
134
|
+
No. The count stays the same. `tolerance_percent` only widens the allowed limit, so a limit of 1500 with 10 percent tolerance allows up to 1650 words. The answer says when you are over the limit but inside the tolerance.
|
|
135
|
+
|
|
79
136
|
## Example prompts
|
|
80
137
|
|
|
81
138
|
- "Count the words in this essay and tell me how many pages it is in Arial 11 point with 1.5 spacing."
|
|
@@ -83,6 +140,7 @@ Inputs: `text`, `limit`, and optional `minimum`, `tolerance_percent`, `exclude_r
|
|
|
83
140
|
- "Find the passive voice and filler words in my draft."
|
|
84
141
|
- "My limit is 1500 words, not counting the works cited or quotes. Am I over?"
|
|
85
142
|
- "Which sentences are over 25 words long?"
|
|
143
|
+
- "Do my sentences all sound the same length?"
|
|
86
144
|
- "Which words do I overuse in this essay?"
|
|
87
145
|
|
|
88
146
|
## How it compares
|
package/build/src/core/tools.js
CHANGED
|
@@ -15,7 +15,7 @@ export function registerTools(server, tools) {
|
|
|
15
15
|
description: tool.description,
|
|
16
16
|
inputSchema: tool.inputSchema,
|
|
17
17
|
outputSchema: tool.outputSchema,
|
|
18
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
18
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
19
19
|
}, async (args) => runTool(tool, args));
|
|
20
20
|
}
|
|
21
21
|
}
|
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.3";
|
|
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
|
@@ -207,3 +207,69 @@ export function wordFrequency(text, includeCommon = false) {
|
|
|
207
207
|
.sort((a, b) => b.count - a.count || a.word.localeCompare(b.word));
|
|
208
208
|
return { total: all.length, unique: new Set(all).size, hits };
|
|
209
209
|
}
|
|
210
|
+
export function sentenceLengths(sentences) {
|
|
211
|
+
return sentences.map((s) => wordCount(s));
|
|
212
|
+
}
|
|
213
|
+
/** Mean, median, longest, shortest and spread (longest minus shortest) of sentence length in words. */
|
|
214
|
+
export function sentenceLengthStats(sentences) {
|
|
215
|
+
const lengths = sentenceLengths(sentences);
|
|
216
|
+
if (lengths.length === 0) {
|
|
217
|
+
return { count: 0, mean: 0, median: 0, longest: 0, shortest: 0, spread: 0 };
|
|
218
|
+
}
|
|
219
|
+
const sorted = [...lengths].sort((a, b) => a - b);
|
|
220
|
+
const mid = Math.floor(sorted.length / 2);
|
|
221
|
+
const median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
222
|
+
const longest = sorted[sorted.length - 1];
|
|
223
|
+
const shortest = sorted[0];
|
|
224
|
+
return {
|
|
225
|
+
count: sorted.length,
|
|
226
|
+
mean: round(sorted.reduce((a, b) => a + b, 0) / sorted.length, 1),
|
|
227
|
+
median,
|
|
228
|
+
longest,
|
|
229
|
+
shortest,
|
|
230
|
+
spread: longest - shortest,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const TRANSITION_PHRASES = [
|
|
234
|
+
"for example", "for instance", "in addition", "in contrast", "in conclusion", "in summary",
|
|
235
|
+
"on the other hand", "as a result", "in other words", "on the contrary", "as well as",
|
|
236
|
+
"in fact", "above all", "after all", "even though", "at the same time", "for this reason",
|
|
237
|
+
"to begin with", "in particular", "of course",
|
|
238
|
+
"however", "therefore", "moreover", "furthermore", "consequently", "nevertheless", "nonetheless",
|
|
239
|
+
"meanwhile", "similarly", "likewise", "finally", "additionally", "thus", "hence", "otherwise",
|
|
240
|
+
"instead", "although", "whereas", "because", "since", "first", "second", "third", "next", "then",
|
|
241
|
+
"also", "still", "yet",
|
|
242
|
+
].sort((a, b) => b.length - a.length);
|
|
243
|
+
/** Counts transition words and phrases in each sentence and lists the sentence numbers that have none. */
|
|
244
|
+
export function transitionWords(sentences) {
|
|
245
|
+
const hits = [];
|
|
246
|
+
const without = [];
|
|
247
|
+
let total = 0;
|
|
248
|
+
sentences.forEach((s, i) => {
|
|
249
|
+
let rest = s.toLowerCase();
|
|
250
|
+
const found = [];
|
|
251
|
+
for (const p of TRANSITION_PHRASES) {
|
|
252
|
+
const re = new RegExp(`\\b${p.replace(/ /g, "\\s+")}\\b`, "g");
|
|
253
|
+
const n = (rest.match(re) ?? []).length;
|
|
254
|
+
for (let k = 0; k < n; k++)
|
|
255
|
+
found.push(p);
|
|
256
|
+
if (n > 0)
|
|
257
|
+
rest = rest.replace(re, " ");
|
|
258
|
+
}
|
|
259
|
+
if (found.length > 0) {
|
|
260
|
+
hits.push({ sentence: i + 1, found });
|
|
261
|
+
total += found.length;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
without.push(i + 1);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
sentences: sentences.length,
|
|
269
|
+
withTransitions: hits.length,
|
|
270
|
+
total,
|
|
271
|
+
perSentence: sentences.length ? round(total / sentences.length, 2) : 0,
|
|
272
|
+
hits,
|
|
273
|
+
withoutTransitions: without,
|
|
274
|
+
};
|
|
275
|
+
}
|
package/build/src/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { counts, findFiller, findPassive, findRepeatedOpeners, pageEstimate, round, splitParagraphs, splitSentences, stripQuotes, stripReferences, wordCount, wordFrequency, } from "./text.js";
|
|
2
|
+
import { counts, findFiller, findPassive, findRepeatedOpeners, pageEstimate, round, sentenceLengths, splitParagraphs, splitSentences, stripQuotes, stripReferences, wordCount, wordFrequency, } from "./text.js";
|
|
3
3
|
const EMPTY_TEXT_HINT = `text is empty. Paste the essay text to analyse. Worked example: { "text": "Your essay content here." }`;
|
|
4
4
|
const textInput = z.string().min(1, EMPTY_TEXT_HINT);
|
|
5
5
|
function requireText(text, toolName) {
|
|
@@ -8,6 +8,7 @@ function requireText(text, toolName) {
|
|
|
8
8
|
check_word_limit: `{ "text": "Your essay content here.", "limit": 1500, "exclude_references": true }`,
|
|
9
9
|
word_frequency: `{ "text": "Your essay content here.", "top": 10, "min_count": 2 }`,
|
|
10
10
|
style_flags: `{ "text": "Your essay content here.", "max_sentence_words": 30 }`,
|
|
11
|
+
sentence_variety: `{ "text": "Your essay content here." }`,
|
|
11
12
|
};
|
|
12
13
|
const example = examples[toolName] ?? `{ "text": "Your essay content here." }`;
|
|
13
14
|
throw new Error(`The text has no words in it. Paste the essay text and try again. Worked example for ${toolName}: ${example}`);
|
|
@@ -16,7 +17,7 @@ function requireText(text, toolName) {
|
|
|
16
17
|
const countText = {
|
|
17
18
|
name: "count_text",
|
|
18
19
|
title: "Count words, sentences and pages",
|
|
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).
|
|
20
|
+
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.\"",
|
|
20
21
|
inputSchema: {
|
|
21
22
|
text: textInput,
|
|
22
23
|
font: z.string().optional().describe("Font name. Default times new roman."),
|
|
@@ -76,7 +77,7 @@ function fleschLabel(score) {
|
|
|
76
77
|
const readabilityScores = {
|
|
77
78
|
name: "readability_scores",
|
|
78
79
|
title: "Readability scores",
|
|
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.
|
|
80
|
+
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.\"",
|
|
80
81
|
inputSchema: { text: textInput },
|
|
81
82
|
outputSchema: {
|
|
82
83
|
words: z.number(),
|
|
@@ -124,7 +125,7 @@ const readabilityScores = {
|
|
|
124
125
|
const styleFlags = {
|
|
125
126
|
name: "style_flags",
|
|
126
127
|
title: "Style flags",
|
|
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.
|
|
128
|
+
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.\"",
|
|
128
129
|
inputSchema: {
|
|
129
130
|
text: textInput,
|
|
130
131
|
max_sentence_words: z.number().int().min(5).max(200).optional().describe("Sentence length that counts as long. Default 30."),
|
|
@@ -187,7 +188,7 @@ const styleFlags = {
|
|
|
187
188
|
const checkWordLimit = {
|
|
188
189
|
name: "check_word_limit",
|
|
189
190
|
title: "Check a word limit",
|
|
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
|
|
191
|
+
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?\"",
|
|
191
192
|
inputSchema: {
|
|
192
193
|
text: textInput,
|
|
193
194
|
limit: z.number().int().positive().describe("Maximum number of words."),
|
|
@@ -270,7 +271,7 @@ const checkWordLimit = {
|
|
|
270
271
|
const wordFrequencyTool = {
|
|
271
272
|
name: "word_frequency",
|
|
272
273
|
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).
|
|
274
|
+
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
275
|
inputSchema: {
|
|
275
276
|
text: textInput,
|
|
276
277
|
top: z.number().int().min(1).max(100).optional().describe("How many words to list. Default 10."),
|
|
@@ -297,4 +298,67 @@ const wordFrequencyTool = {
|
|
|
297
298
|
};
|
|
298
299
|
},
|
|
299
300
|
};
|
|
300
|
-
|
|
301
|
+
const sentenceVariety = {
|
|
302
|
+
name: "sentence_variety",
|
|
303
|
+
title: "Sentence variety",
|
|
304
|
+
description: "Show how much the sentence lengths in an essay change, since a good rhythm mixes short, medium and long sentences. Input: text. Returns the shortest, longest and average sentence length in words, the standard deviation, how many sentences are short (under 10 words), medium (10 to 24) and long (25 or more), and any runs of 3 or more sentences in a row whose lengths are within 3 words of the first one in the run, with their sentence numbers. Sentence numbers start at 1 and follow the order of the text. This is a simple measure of rhythm, so treat it as a prompt to reread. Example: \"Do my sentences all sound the same length?\"",
|
|
305
|
+
inputSchema: { text: textInput },
|
|
306
|
+
outputSchema: {
|
|
307
|
+
sentences: z.number(),
|
|
308
|
+
shortest_words: z.number(),
|
|
309
|
+
longest_words: z.number(),
|
|
310
|
+
average_words: z.number(),
|
|
311
|
+
std_dev_words: z.number(),
|
|
312
|
+
short_count: z.number(),
|
|
313
|
+
medium_count: z.number(),
|
|
314
|
+
long_count: z.number(),
|
|
315
|
+
similar_runs: z.array(z.object({ first: z.number(), last: z.number(), words: z.number() })),
|
|
316
|
+
lengths: z.array(z.number()),
|
|
317
|
+
},
|
|
318
|
+
run: ({ text }) => {
|
|
319
|
+
requireText(text, "sentence_variety");
|
|
320
|
+
const lengths = sentenceLengths(splitSentences(text));
|
|
321
|
+
if (lengths.length === 0) {
|
|
322
|
+
throw new Error("The text needs at least one sentence to measure.");
|
|
323
|
+
}
|
|
324
|
+
const n = lengths.length;
|
|
325
|
+
const avg = lengths.reduce((a, b) => a + b, 0) / n;
|
|
326
|
+
const sd = Math.sqrt(lengths.reduce((a, b) => a + (b - avg) ** 2, 0) / n);
|
|
327
|
+
const shortCount = lengths.filter((l) => l < 10).length;
|
|
328
|
+
const longCount = lengths.filter((l) => l >= 25).length;
|
|
329
|
+
const runs = [];
|
|
330
|
+
let i = 0;
|
|
331
|
+
while (i < n) {
|
|
332
|
+
let j = i;
|
|
333
|
+
while (j + 1 < n && Math.abs(lengths[j + 1] - lengths[i]) <= 3)
|
|
334
|
+
j++;
|
|
335
|
+
if (j - i + 1 >= 3) {
|
|
336
|
+
runs.push({ first: i + 1, last: j + 1, words: lengths[i] });
|
|
337
|
+
i = j + 1;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
i++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const result = {
|
|
344
|
+
sentences: n,
|
|
345
|
+
shortest_words: Math.min(...lengths),
|
|
346
|
+
longest_words: Math.max(...lengths),
|
|
347
|
+
average_words: round(avg, 1),
|
|
348
|
+
std_dev_words: round(sd, 1),
|
|
349
|
+
short_count: shortCount,
|
|
350
|
+
medium_count: n - shortCount - longCount,
|
|
351
|
+
long_count: longCount,
|
|
352
|
+
similar_runs: runs,
|
|
353
|
+
lengths,
|
|
354
|
+
};
|
|
355
|
+
const runText = runs.length
|
|
356
|
+
? runs.map((r) => `sentences ${r.first} to ${r.last} (about ${r.words} words each)`).join(", ")
|
|
357
|
+
: "none";
|
|
358
|
+
return {
|
|
359
|
+
text: `${n} sentences, from ${result.shortest_words} to ${result.longest_words} words, averaging ${result.average_words} with a standard deviation of ${result.std_dev_words}. ${shortCount} short, ${result.medium_count} medium and ${longCount} long. Runs of similar length: ${runText}.`,
|
|
360
|
+
structured: result,
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
export const tools = [countText, readabilityScores, styleFlags, sentenceVariety, checkWordLimit, wordFrequencyTool];
|
package/llms.txt
CHANGED
|
@@ -30,6 +30,7 @@ All tools are read only and work on text passed in the call.
|
|
|
30
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).
|
|
31
31
|
- readability_scores: Flesch Kincaid grade, Flesch reading ease, Gunning Fog and SMOG. Input: text. SMOG is marked less reliable under 30 sentences.
|
|
32
32
|
- style_flags: passive voice, repeated sentence openers, filler words and long sentences. Inputs: text, optional max_sentence_words (default 30).
|
|
33
|
+
- sentence_variety: how much sentence lengths change: shortest, longest, average, standard deviation, counts of short (under 10 words), medium and long (25 or more) sentences, and runs of 3 or more sentences of similar length. Input: text.
|
|
33
34
|
- 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).
|
|
34
35
|
- 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.
|
|
35
36
|
|
|
@@ -50,6 +51,7 @@ All tools are read only and work on text passed in the call.
|
|
|
50
51
|
- 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
52
|
- readability_scores: "Give me the Flesch Kincaid, Gunning Fog and SMOG scores for this paper." Grades are roughly US school grades.
|
|
52
53
|
- style_flags: "Which sentences are over 25 words long?" Uses max_sentence_words 25. Sentence numbers start at 1.
|
|
54
|
+
- sentence_variety: "Do my sentences all sound the same length?" Sentence numbers in similar_runs start at 1.
|
|
53
55
|
- word_frequency: "Show my 15 most used words." Uses top 15.
|
|
54
56
|
- 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.
|
|
55
57
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "essay-stats-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"keywords": [
|
|
27
27
|
"essay stats mcp",
|
|
28
28
|
"essay stats",
|
|
29
|
+
"essay word count",
|
|
29
30
|
"essay word counter",
|
|
30
31
|
"word counter",
|
|
31
32
|
"word count",
|