# Directory Structure ``` ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .nvmrc ├── biome.json ├── images │ └── example.png ├── LICENSE ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── README.md ├── src │ ├── index.ts │ ├── naver_speller.test.ts │ └── naver_speller.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- ``` v18.19.0 ``` -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` pnpm-lock.yaml linguist-generated=true package-lock.json linguist-generated=true ``` -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- ``` # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true # TypeScript/JavaScript files [*.{ts,tsx,js,jsx,json}] indent_style = space indent_size = 2 quote_type = double # Markdown files [*.md] trim_trailing_whitespace = false ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # .gitignore file created for free with Tower (https://www.git-tower.com/) # For suggestions or improvements please contact us using [email protected] # Generated on 2025-04-06 # Includes: typescript, javascript, node, vscode, macos .vscode/ ### Universal ### # Common files that should be ignored in all projects # Logs *.log # Temporary files *.tmp *~ *.bak # Environment files (containing secrets, API keys, credentials) .env *.env .env.* # Local configuration that shouldn't be shared *.local ### Typescript ### # typescript specific files *.tsbuildinfo node_modules/ dist/ ### Javascript ### # javascript specific files node_modules/ *.log npm-debug.log* yarn-debug.log* .cache/ dist/ coverage/ ### Node ### # Node.js dependency directory, logs, and environment files # Dependencies node_modules/ jspm_packages/ bower_components/ web_modules/ # Logs logs *.log *.launch connect.lock/ libpeerconnection.log .history/ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Environment .env.development.local .env.test.local .env.production.local .env.local # Package management .npm yarn.lock .yarn-integrity .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # Coverage & Test coverage/ lib-cov *.lcov .nyc_output # Build output dist/ build/ .next/ out/ .nuxt .output # Cache & Temporary .cache/ .temp/ .grunt .lock-wscript .fusebox/ .dynamodb/ .tern-port .vscode-test .node_repl_history .webpack/ # TypeScript *.tsbuildinfo # Optional REPL history .node_repl_history # Additional caches .eslintcache .stylelintcache .parcel-cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Misc *.tgz .serverless/ .vuepress/dist .temp .docusaurus .svelte-kit ### Vscode ### # Visual Studio Code editor settings and workspace files # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets .history/ *.vsix .ionide .vs/ *.code-workspace .vscode-test .vscodeignore .vscode/chrome .vscode-server/ .vscode/sftp.json .vscode/tags .devcontainer/ ### Macos ### # macOS operating system specific files .DS_Store .AppleDouble .LSOverride Icon ._* .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk *.icloud .AppleDB/ .AppleDesktop/ *.AppleDouble *.AppleDB .Spotlight-V100/ .Trashes/ *.swp Network Trash Folder/ .DS_Store? .Spotlight-V100 .Trashes ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # mcp-korean-spell `mcp-korean-spell` is a MCP(Model Context Protocol) server designed for Korean spell checking, providing a reliable tool for writers to integrate spell checking capabilities into their documents and texts.  ## Tools - `fix_korean_spell`: Analyzes and corrects Korean text for spelling and grammar errors ## How to add MCP config to your client ### Using npm To configure this spell checker in your MCP client, add the following to your [`~/.cursor/mcp.json`](cursor://settings/) or `claude_desktop_config.json` (MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`) ```javascript { "mcpServers": { "korean-spell-checker": { "command": "npx", "args": [ "-y", "@winterjung/mcp-korean-spell" ] } } } ``` ## Disclaimer - This tool is based on the [네이버(NAVER) 맞춤법 검사기](https://m.search.naver.com/search.naver?query=맞춤법+검사기). - This tool is not officially provided and is not affiliated with the company. - If company stops, changes or blocks providing the service, this tool may not function correctly. ## License This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "es2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "declaration": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "**/*.test.ts", "dist"] } ``` -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- ```json { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { "ignoreUnknown": false, "ignore": [] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "ignore": ["dist/**/*"] }, "organizeImports": { "enabled": true }, "linter": { "enabled": true, "rules": { "recommended": true }, "ignore": ["dist/**/*"] }, "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "always" } } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@winterjung/mcp-korean-spell", "version": "1.0.1", "description": "Korean spell checker MCP(Model Context Protocol) server", "type": "module", "bin": { "mcp-korean-spell": "./dist/index.js" }, "files": ["dist", "README.md", "LICENSE"], "engines": { "node": ">=18.0.0", "pnpm": "v10.7.1" }, "scripts": { "test": "ts-node src/naver_speller_test.ts", "start": "ts-node src/index.ts", "build": "tsc && chmod +x dist/index.js", "dev": "ts-node src/index.ts", "watch": "tsc --watch", "format": "biome format . --write", "check": "biome check ." }, "keywords": ["korean", "spell checker"], "author": "winterjung", "license": "Apache-2.0", "repository": { "type": "git", "url": "git+https://github.com/winterjung/mcp-korean-spell.git" }, "bugs": { "url": "https://github.com/winterjung/mcp-korean-spell/issues" }, "homepage": "https://github.com/winterjung/mcp-korean-spell#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.8.0", "cheerio": "^1.0.0", "zod": "^3.24.2" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@types/cheerio": "^0.22.35", "@types/node": "^22.14.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" } } ``` -------------------------------------------------------------------------------- /src/naver_speller.ts: -------------------------------------------------------------------------------- ```typescript import { load as loadCheerio } from "cheerio"; const SPELLER_PROVIDER_URL = "https://m.search.naver.com/search.naver?query=%EB%A7%9E%EC%B6%A4%EB%B2%95%EA%B2%80%EC%82%AC%EA%B8%B0"; const PASSPORT_KEY_REGEX = /SpellerProxy\?passportKey=([a-zA-Z0-9]+)/; const SPELLER_API_URL_BASE = "https://m.search.naver.com/p/csearch/ocontent/util/SpellerProxy?passportKey="; const MAX_CHUNK_LENGTH = 300; interface NaverSpellerResponse { message: { result: { html: string; errata_count: number; origin_html: string; notag_html: string; }; error?: string; }; } function simpleHtmlUnescape(text: string): string { return text .replace(/</g, "<") .replace(/>/g, ">") .replace(/<br>/g, "\n") .replace(/&/g, "&") .replace(/"/g, '"') .replace(/'/g, "'") .replace(/ /g, " "); } export class NaverSpellChecker { private spellerApiUrl: string | null = null; private async fetchPassportKey(): Promise<string> { const response = await fetch(SPELLER_PROVIDER_URL); const html = await response.text(); if (!response.ok) { throw new Error( `HTTP error! status: ${response.status}, response: ${html}`, ); } let passportKey: string | undefined; const $ = loadCheerio(html); $("script").each((_: any, element: any) => { const scriptContent = $(element).html(); const match = scriptContent?.match(PASSPORT_KEY_REGEX); if (match?.[1]) { passportKey = match[1]; return false; } }); if (!passportKey) { throw new Error("Passport key not found in the HTML content."); } return passportKey; } private async updateSpellerApiUrl(): Promise<void> { const passportKey = await this.fetchPassportKey(); this.spellerApiUrl = `${SPELLER_API_URL_BASE}${passportKey}&color_blindness=0&q=`; } async correctText(text: string): Promise<string> { const chunks = this.chunkText(text); let corrected = ""; for (const chunk of chunks) { const correctedChunk = await this.correctChunk(chunk); if (corrected.endsWith(" ")) { corrected += correctedChunk; } else { corrected += ` ${correctedChunk}`; } } return corrected; } async correctChunk(text: string): Promise<string> { if (!this.spellerApiUrl) { await this.updateSpellerApiUrl(); if (!this.spellerApiUrl) { throw new Error( "Failed to initialize Speller API URL. Cannot proceed.", ); } } const encodedText = encodeURIComponent(text); const url = this.spellerApiUrl + encodedText; const response = await fetch(url); const responseText = await response.text(); if (!response.ok) { throw new Error( `HTTP error! status: ${response.status}, response: ${responseText}`, ); } const data: NaverSpellerResponse = JSON.parse(responseText); if (data.message.error) { if (data.message.error === "유효한 키가 아닙니다.") { this.spellerApiUrl = null; throw new Error("Try again with a new passport key."); } throw new Error(`failed to check spelling: ${data.message.error}`); } return simpleHtmlUnescape(data.message.result.notag_html); } private chunkText(text: string): string[] { if (text.length <= MAX_CHUNK_LENGTH) { return [text]; } const chunks: string[] = []; let currentChunk = ""; const words = text.split(/(\s+)/); for (const word of words) { if (!word) continue; if (word.length > MAX_CHUNK_LENGTH) { // If a single "word" is too long, split it forcefully if (currentChunk) { chunks.push(currentChunk); currentChunk = ""; } for (let i = 0; i < word.length; i += MAX_CHUNK_LENGTH) { chunks.push(word.substring(i, i + MAX_CHUNK_LENGTH)); } continue; } if (currentChunk.length + word.length > MAX_CHUNK_LENGTH) { chunks.push(currentChunk); currentChunk = word.trimStart(); continue; } currentChunk += word; } if (currentChunk) { chunks.push(currentChunk); } return chunks.filter((chunk) => chunk.length > 0); } } ```