# 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);
}
}
```