Skip to main content
Glama
index.ts19.2 kB
#!/usr/bin/env node /** * mcp-character-tools * * A comprehensive MCP server providing character and text analysis tools * to help LLMs work with individual characters, which they struggle with * due to tokenization. */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // Import tool implementations import { countLetter, countLetters, countSubstring, letterFrequency, } from './tools/counting.js'; import { spellWord, charAt, nthCharacter, wordLength, reverseText, } from './tools/spelling.js'; import { compareTexts, analyzeSentence, batchCount, } from './tools/analysis.js'; import { getTrickyWords, getTrickyWordByName, } from './resources/tricky-words.js'; // Create the MCP server const server = new McpServer({ name: "mcp-character-tools", version: "1.0.0", }); // ============================================================================ // COUNTING TOOLS // ============================================================================ server.registerTool( "count_letter", { title: "Count Letter", description: `Count occurrences of a specific letter in text. Returns the count, positions, a visual breakdown showing where each letter appears, and a density percentage. Args: - text (string): The text to analyze - letter (string): The single letter to count - case_sensitive (boolean): Whether to match case exactly (default: false) Returns: count, positions array, visual breakdown, and density summary. Example: count_letter("strawberry", "r") → count: 3, positions: [2, 5, 8]`, inputSchema: z.object({ text: z.string().min(1).describe("The text to analyze"), letter: z.string().length(1).describe("The letter to count"), case_sensitive: z.boolean().default(false).describe("Match case exactly"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = countLetter({ text: params.text, letter: params.letter, case_sensitive: params.case_sensitive, }); return { content: [{ type: "text" as const, text: `${result.density}\n\n${result.visual}\n\nResult: ${JSON.stringify({ count: result.count, positions: result.positions })}` }], }; } ); server.registerTool( "count_letters", { title: "Count Multiple Letters", description: `Count occurrences of multiple letters at once. Efficiently counts several letters in a single call. Args: - text (string): The text to analyze - letters (string[]): Array of letters to count - case_sensitive (boolean): Match case exactly (default: false) Returns: Results for each letter with counts and positions. Example: count_letters("strawberry", ["r", "s", "e"]) → r: 3, s: 1, e: 1`, inputSchema: z.object({ text: z.string().min(1).describe("The text to analyze"), letters: z.array(z.string().length(1)).min(1).describe("Letters to count"), case_sensitive: z.boolean().default(false).describe("Match case exactly"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = countLetters({ text: params.text, letters: params.letters, case_sensitive: params.case_sensitive, }); const summary = result.results .map(r => `'${r.letter}': ${r.count} at [${r.positions.join(', ')}]`) .join('\n'); return { content: [{ type: "text" as const, text: `Letter counts in "${result.text}":\n${summary}\n\nTotal: ${result.total_matches}` }], }; } ); server.registerTool( "count_substring", { title: "Count Substring", description: `Count occurrences of a substring or pattern in text. Can count overlapping or non-overlapping matches. Args: - text (string): The text to search in - substring (string): The pattern to find - case_sensitive (boolean): Match case exactly (default: false) - overlapping (boolean): Count overlapping matches (default: false) Returns: count and positions of each match. Example: count_substring("banana", "ana", overlapping=true) → count: 2, positions: [1, 3]`, inputSchema: z.object({ text: z.string().min(1).describe("The text to search in"), substring: z.string().min(1).describe("The pattern to find"), case_sensitive: z.boolean().default(false).describe("Match case exactly"), overlapping: z.boolean().default(false).describe("Count overlapping matches"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = countSubstring({ text: params.text, substring: params.substring, case_sensitive: params.case_sensitive, overlapping: params.overlapping, }); return { content: [{ type: "text" as const, text: `Found "${result.substring}" ${result.count} time(s) in "${result.text}" at positions: [${result.positions.join(', ')}]` }], }; } ); server.registerTool( "letter_frequency", { title: "Letter Frequency", description: `Get frequency distribution of all characters in text. Provides a complete breakdown of character frequencies. Args: - text (string): The text to analyze - case_sensitive (boolean): Distinguish upper/lowercase (default: false) - include_spaces (boolean): Include spaces in count (default: false) - include_punctuation (boolean): Include punctuation (default: false) - letters_only (boolean): Only count a-z letters (default: true) Returns: Frequency map, sorted list, most/least common characters. Example: letter_frequency("hello") → h: 1, e: 1, l: 2, o: 1`, inputSchema: z.object({ text: z.string().min(1).describe("The text to analyze"), case_sensitive: z.boolean().default(false).describe("Distinguish upper/lowercase"), include_spaces: z.boolean().default(false).describe("Include spaces"), include_punctuation: z.boolean().default(false).describe("Include punctuation"), letters_only: z.boolean().default(true).describe("Only count a-z letters"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = letterFrequency({ text: params.text, case_sensitive: params.case_sensitive, include_spaces: params.include_spaces, include_punctuation: params.include_punctuation, letters_only: params.letters_only, }); return { content: [{ type: "text" as const, text: `Frequency analysis of "${result.text}":\n\n${result.frequency_table}\n\nMost common: ${result.most_common.map(c => `'${c.char}': ${c.count}`).join(', ')}` }], }; } ); // ============================================================================ // SPELLING TOOLS // ============================================================================ server.registerTool( "spell_word", { title: "Spell Word", description: `Break text into individual characters with optional indices. Perfect for verifying character-by-character content. Args: - text (string): The text to spell out - include_indices (boolean): Include position numbers (default: true) Returns: Array of characters, indexed list, spelled out string. Example: spell_word("cat") → ['c', 'a', 't'] with indices [0:'c', 1:'a', 2:'t']`, inputSchema: z.object({ text: z.string().min(1).describe("The text to spell out"), include_indices: z.boolean().default(true).describe("Include position numbers"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = spellWord({ text: params.text, include_indices: params.include_indices, }); return { content: [{ type: "text" as const, text: `"${result.text}" (${result.length} chars): ${result.spelled_out}` }], }; } ); server.registerTool( "char_at", { title: "Character At Index", description: `Get the character at a specific index (0-based). Supports negative indices. Args: - text (string): The text to index into - index (number): Position (0-based, negative counts from end) Returns: The character at that position, or error if out of bounds. Example: char_at("hello", 1) → 'e'; char_at("hello", -1) → 'o'`, inputSchema: z.object({ text: z.string().min(1).describe("The text to index into"), index: z.number().int().describe("Position (0-based, negative from end)"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = charAt({ text: params.text, index: params.index, }); const text = result.valid ? `Character at index ${result.index} in "${result.text}" is '${result.character}'` : result.error || "Invalid index"; return { content: [{ type: "text" as const, text }], }; } ); server.registerTool( "nth_character", { title: "Nth Character", description: `Get the nth character (1-based, human-friendly numbering). "What's the 3rd letter?" uses position=3. Args: - text (string): The text to examine - position (number): Which character (1 = first, 2 = second, etc.) - from_end (boolean): Count from end instead (default: false) Returns: The character and a human-readable description. Example: nth_character("hello", 2) → 'e' (the 2nd character)`, inputSchema: z.object({ text: z.string().min(1).describe("The text to examine"), position: z.number().int().min(1).describe("Which character (1-based)"), from_end: z.boolean().default(false).describe("Count from end instead"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = nthCharacter({ text: params.text, position: params.position, from_end: params.from_end, }); return { content: [{ type: "text" as const, text: result.valid ? result.description : result.error || "Invalid position" }], }; } ); server.registerTool( "word_length", { title: "Word Length", description: `Get the exact length of text with detailed breakdown. Args: - text (string): The text to measure - count_spaces (boolean): Include spaces in length (default: true) Returns: Total length, length without spaces, space count, word count. Example: word_length("hello world") → 11 total, 10 without spaces, 2 words`, inputSchema: z.object({ text: z.string().describe("The text to measure"), count_spaces: z.boolean().default(true).describe("Include spaces in length"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = wordLength({ text: params.text, count_spaces: params.count_spaces, }); return { content: [{ type: "text" as const, text: result.description }], }; } ); server.registerTool( "reverse_text", { title: "Reverse Text", description: `Reverse text character-by-character or word-by-word. Also detects if the text is a palindrome. Args: - text (string): The text to reverse - reverse_words_only (boolean): Reverse word order only, not characters (default: false) Returns: Reversed text, palindrome detection. Example: reverse_text("hello") → "olleh"; reverse_text("racecar") → "racecar" (palindrome!)`, inputSchema: z.object({ text: z.string().min(1).describe("The text to reverse"), reverse_words_only: z.boolean().default(false).describe("Reverse word order only"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = reverseText({ text: params.text, reverse_words_only: params.reverse_words_only, }); return { content: [{ type: "text" as const, text: result.description }], }; } ); // ============================================================================ // ANALYSIS TOOLS // ============================================================================ server.registerTool( "compare_texts", { title: "Compare Texts", description: `Compare letter frequencies between two texts. Useful for analyzing similarity or differences. Args: - text1 (string): First text - text2 (string): Second text - case_sensitive (boolean): Distinguish case (default: false) Returns: Common characters, unique to each, frequency comparison, similarity score. Example: compare_texts("hello", "world") → common: ['l', 'o'], unique_to_text1: ['h', 'e'], etc.`, inputSchema: z.object({ text1: z.string().min(1).describe("First text"), text2: z.string().min(1).describe("Second text"), case_sensitive: z.boolean().default(false).describe("Distinguish case"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = compareTexts({ text1: params.text1, text2: params.text2, case_sensitive: params.case_sensitive, }); return { content: [{ type: "text" as const, text: `${result.summary}\n\nCommon: [${result.common_characters.join(', ')}]\nOnly in text1: [${result.unique_to_text1.join(', ')}]\nOnly in text2: [${result.unique_to_text2.join(', ')}]` }], }; } ); server.registerTool( "analyze_sentence", { title: "Analyze Sentence", description: `Analyze a sentence word-by-word for a specific letter. Shows exactly how many times a letter appears in each word. Args: - text (string): The sentence to analyze - letter (string): The letter to count - case_sensitive (boolean): Match case exactly (default: false) Returns: Per-word breakdown with counts and positions. Example: analyze_sentence("The strawberry was very ripe", "r") → per-word counts`, inputSchema: z.object({ text: z.string().min(1).describe("The sentence to analyze"), letter: z.string().length(1).describe("The letter to count"), case_sensitive: z.boolean().default(false).describe("Match case exactly"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = analyzeSentence({ text: params.text, letter: params.letter, case_sensitive: params.case_sensitive, }); return { content: [{ type: "text" as const, text: `${result.summary}\n\n${result.breakdown_table}` }], }; } ); server.registerTool( "batch_count", { title: "Batch Count", description: `Count a letter across multiple words at once. Efficiently process a list of words. Args: - words (string[]): Array of words to analyze - letter (string): The letter to count - case_sensitive (boolean): Match case exactly (default: false) Returns: Results for each word, totals, sorted by count. Example: batch_count(["strawberry", "raspberry", "blueberry"], "r") → individual and total counts`, inputSchema: z.object({ words: z.array(z.string().min(1)).min(1).describe("Words to analyze"), letter: z.string().length(1).describe("The letter to count"), case_sensitive: z.boolean().default(false).describe("Match case exactly"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const result = batchCount({ words: params.words, letter: params.letter, case_sensitive: params.case_sensitive, }); const breakdown = result.results .map(r => `"${r.word}": ${r.count} at [${r.positions.join(', ')}]`) .join('\n'); return { content: [{ type: "text" as const, text: `${result.summary}\n\n${breakdown}` }], }; } ); // ============================================================================ // TRICKY WORDS TOOLS // ============================================================================ server.registerTool( "get_tricky_words", { title: "Get Tricky Words", description: `Get a list of words that are commonly miscounted by LLMs. These are words with double letters, repeated patterns, or other features that cause counting errors. Returns: List of tricky words with correct counts and explanations. Example: Returns "strawberry" with explanation that it has 3 r's, not 2.`, inputSchema: z.object({}).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async () => { const result = getTrickyWords(); const summary = result.words .slice(0, 10) .map(w => `"${w.word}" '${w.letter}': ${w.count} (often miscounted as ${w.common_mistake})`) .join('\n'); return { content: [{ type: "text" as const, text: `${result.total_words} tricky words in database.\n\nExamples:\n${summary}\n\n...and ${result.total_words - 10} more.` }], }; } ); server.registerTool( "check_tricky_word", { title: "Check Tricky Word", description: `Look up a specific word to see if it's a commonly miscounted word. Args: - word (string): The word to check Returns: Information about common mistakes if it's a tricky word, or empty if not. Example: check_tricky_word("strawberry") → explains the 3 r's and common mistake of counting 2`, inputSchema: z.object({ word: z.string().min(1).describe("The word to check"), }).strict(), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, }, async (params) => { const entries = getTrickyWordByName(params.word); const is_tricky = entries.length > 0; let text: string; if (is_tricky) { text = entries .map(e => `"${e.word}" has ${e.count} '${e.letter}'(s) at positions [${e.positions.join(', ')}].\nCommon mistake: counting ${e.common_mistake} instead.\n${e.explanation}`) .join('\n\n'); } else { text = `"${params.word}" is not in the tricky words database. Use count_letter to analyze it.`; } return { content: [{ type: "text" as const, text }], }; } ); // ============================================================================ // SERVER STARTUP // ============================================================================ async function main(): Promise<void> { const transport = new StdioServerTransport(); await server.connect(transport); console.error("mcp-character-tools v1.0.0 running on stdio"); } main().catch((error: unknown) => { console.error("Server error:", error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

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

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Aaryan-Kapoor/mcp-character-tools'

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