#
tokens: 4759/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | v18.19.0 
2 | 
```

--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------

```
1 | pnpm-lock.yaml linguist-generated=true
2 | package-lock.json linguist-generated=true
3 | 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
 1 | # EditorConfig is awesome: https://EditorConfig.org
 2 | 
 3 | # top-most EditorConfig file
 4 | root = true
 5 | 
 6 | # Unix-style newlines with a newline ending every file
 7 | [*]
 8 | end_of_line = lf
 9 | insert_final_newline = true
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | 
13 | # TypeScript/JavaScript files
14 | [*.{ts,tsx,js,jsx,json}]
15 | indent_style = space
16 | indent_size = 2
17 | quote_type = double
18 | 
19 | # Markdown files
20 | [*.md]
21 | trim_trailing_whitespace = false 
22 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # .gitignore file created for free with Tower (https://www.git-tower.com/)
  2 | # For suggestions or improvements please contact us using [email protected]
  3 | # Generated on 2025-04-06
  4 | # Includes: typescript, javascript, node, vscode, macos
  5 | .vscode/
  6 | 
  7 | ### Universal ###
  8 | # Common files that should be ignored in all projects
  9 | 
 10 | # Logs
 11 | *.log
 12 | # Temporary files
 13 | *.tmp
 14 | *~
 15 | 
 16 | *.bak
 17 | # Environment files (containing secrets, API keys, credentials)
 18 | .env
 19 | *.env
 20 | .env.*
 21 | 
 22 | # Local configuration that shouldn't be shared
 23 | *.local
 24 | 
 25 | ### Typescript ###
 26 | # typescript specific files
 27 | 
 28 | *.tsbuildinfo
 29 | node_modules/
 30 | dist/
 31 | 
 32 | ### Javascript ###
 33 | # javascript specific files
 34 | 
 35 | node_modules/
 36 | *.log
 37 | npm-debug.log*
 38 | yarn-debug.log*
 39 | .cache/
 40 | 
 41 | dist/
 42 | coverage/
 43 | 
 44 | ### Node ###
 45 | # Node.js dependency directory, logs, and environment files
 46 | 
 47 | # Dependencies
 48 | node_modules/
 49 | jspm_packages/
 50 | bower_components/
 51 | web_modules/
 52 | 
 53 | # Logs
 54 | logs
 55 | *.log
 56 | *.launch
 57 | connect.lock/
 58 | 
 59 | libpeerconnection.log
 60 | .history/
 61 | npm-debug.log*
 62 | yarn-debug.log*
 63 | yarn-error.log*
 64 | 
 65 | lerna-debug.log*
 66 | .pnpm-debug.log*
 67 | # Diagnostic reports
 68 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 69 | # Runtime data
 70 | 
 71 | pids
 72 | *.pid
 73 | *.seed
 74 | *.pid.lock
 75 | # Environment
 76 | 
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | # Package management
 82 | 
 83 | .npm
 84 | yarn.lock
 85 | .yarn-integrity
 86 | .yarn/cache
 87 | 
 88 | .yarn/unplugged
 89 | .yarn/build-state.yml
 90 | .yarn/install-state.gz
 91 | .pnp.*
 92 | # Coverage & Test
 93 | 
 94 | coverage/
 95 | lib-cov
 96 | *.lcov
 97 | .nyc_output
 98 | # Build output
 99 | 
100 | dist/
101 | build/
102 | .next/
103 | out/
104 | .nuxt
105 | 
106 | .output
107 | # Cache & Temporary
108 | .cache/
109 | .temp/
110 | .grunt
111 | 
112 | .lock-wscript
113 | .fusebox/
114 | .dynamodb/
115 | .tern-port
116 | .vscode-test
117 | 
118 | .node_repl_history
119 | .webpack/
120 | # TypeScript
121 | *.tsbuildinfo
122 | # Optional REPL history
123 | 
124 | .node_repl_history
125 | # Additional caches
126 | .eslintcache
127 | .stylelintcache
128 | .parcel-cache
129 | 
130 | .rpt2_cache/
131 | .rts2_cache_cjs/
132 | .rts2_cache_es/
133 | .rts2_cache_umd/
134 | # Misc
135 | 
136 | *.tgz
137 | .serverless/
138 | .vuepress/dist
139 | .temp
140 | .docusaurus
141 | 
142 | .svelte-kit
143 | 
144 | ### Vscode ###
145 | # Visual Studio Code editor settings and workspace files
146 | 
147 | # Visual Studio Code
148 | .vscode/*
149 | !.vscode/settings.json
150 | !.vscode/tasks.json
151 | !.vscode/launch.json
152 | 
153 | !.vscode/extensions.json
154 | !.vscode/*.code-snippets
155 | .history/
156 | *.vsix
157 | .ionide
158 | 
159 | .vs/
160 | *.code-workspace
161 | .vscode-test
162 | .vscodeignore
163 | .vscode/chrome
164 | 
165 | .vscode-server/
166 | .vscode/sftp.json
167 | .vscode/tags
168 | .devcontainer/
169 | 
170 | ### Macos ###
171 | # macOS operating system specific files
172 | 
173 | .DS_Store
174 | .AppleDouble
175 | .LSOverride
176 | Icon
177 | ._*
178 | 
179 | .DocumentRevisions-V100
180 | .fseventsd
181 | .Spotlight-V100
182 | .TemporaryItems
183 | .Trashes
184 | 
185 | .VolumeIcon.icns
186 | .com.apple.timemachine.donotpresent
187 | .AppleDB
188 | .AppleDesktop
189 | Network Trash Folder
190 | 
191 | Temporary Items
192 | .apdisk
193 | *.icloud
194 | .AppleDB/
195 | .AppleDesktop/
196 | 
197 | *.AppleDouble
198 | *.AppleDB
199 | .Spotlight-V100/
200 | .Trashes/
201 | *.swp
202 | 
203 | Network Trash Folder/
204 | .DS_Store?
205 | .Spotlight-V100
206 | .Trashes
207 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # mcp-korean-spell
 2 | 
 3 | `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.
 4 | 
 5 | ![mcp-korean-spell result example in cursor chat](images/example.png)
 6 | 
 7 | ## Tools
 8 | 
 9 | - `fix_korean_spell`: Analyzes and corrects Korean text for spelling and grammar errors
10 | 
11 | ## How to add MCP config to your client
12 | 
13 | ### Using npm
14 | 
15 | 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`)
16 | 
17 | ```javascript
18 | {
19 |   "mcpServers": {
20 |     "korean-spell-checker": {
21 |       "command": "npx",
22 |       "args": [
23 |         "-y",
24 |         "@winterjung/mcp-korean-spell"
25 |       ]
26 |     }
27 |   }
28 | }
29 | ```
30 | 
31 | ## Disclaimer
32 | 
33 | - This tool is based on the [네이버(NAVER) 맞춤법 검사기](https://m.search.naver.com/search.naver?query=맞춤법+검사기).
34 | - This tool is not officially provided and is not affiliated with the company.
35 | - If company stops, changes or blocks providing the service, this tool may not function correctly.
36 | 
37 | ## License
38 | 
39 | This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
40 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "es2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "declaration": true,
 9 |     "strict": true,
10 |     "esModuleInterop": true,
11 |     "skipLibCheck": true,
12 |     "forceConsistentCasingInFileNames": true
13 |   },
14 |   "include": ["src/**/*"],
15 |   "exclude": ["node_modules", "**/*.test.ts", "dist"]
16 | }
17 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
 3 |   "vcs": {
 4 |     "enabled": false,
 5 |     "clientKind": "git",
 6 |     "useIgnoreFile": false
 7 |   },
 8 |   "files": {
 9 |     "ignoreUnknown": false,
10 |     "ignore": []
11 |   },
12 |   "formatter": {
13 |     "enabled": true,
14 |     "indentStyle": "space",
15 |     "indentWidth": 2,
16 |     "ignore": ["dist/**/*"]
17 |   },
18 |   "organizeImports": {
19 |     "enabled": true
20 |   },
21 |   "linter": {
22 |     "enabled": true,
23 |     "rules": {
24 |       "recommended": true
25 |     },
26 |     "ignore": ["dist/**/*"]
27 |   },
28 |   "javascript": {
29 |     "formatter": {
30 |       "quoteStyle": "double",
31 |       "semicolons": "always"
32 |     }
33 |   }
34 | }
35 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@winterjung/mcp-korean-spell",
 3 |   "version": "1.0.1",
 4 |   "description": "Korean spell checker MCP(Model Context Protocol) server",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "mcp-korean-spell": "./dist/index.js"
 8 |   },
 9 |   "files": ["dist", "README.md", "LICENSE"],
10 |   "engines": {
11 |     "node": ">=18.0.0",
12 |     "pnpm": "v10.7.1"
13 |   },
14 |   "scripts": {
15 |     "test": "ts-node src/naver_speller_test.ts",
16 |     "start": "ts-node src/index.ts",
17 |     "build": "tsc && chmod +x dist/index.js",
18 |     "dev": "ts-node src/index.ts",
19 |     "watch": "tsc --watch",
20 |     "format": "biome format . --write",
21 |     "check": "biome check ."
22 |   },
23 |   "keywords": ["korean", "spell checker"],
24 |   "author": "winterjung",
25 |   "license": "Apache-2.0",
26 |   "repository": {
27 |     "type": "git",
28 |     "url": "git+https://github.com/winterjung/mcp-korean-spell.git"
29 |   },
30 |   "bugs": {
31 |     "url": "https://github.com/winterjung/mcp-korean-spell/issues"
32 |   },
33 |   "homepage": "https://github.com/winterjung/mcp-korean-spell#readme",
34 |   "dependencies": {
35 |     "@modelcontextprotocol/sdk": "^1.8.0",
36 |     "cheerio": "^1.0.0",
37 |     "zod": "^3.24.2"
38 |   },
39 |   "devDependencies": {
40 |     "@biomejs/biome": "1.9.4",
41 |     "@types/cheerio": "^0.22.35",
42 |     "@types/node": "^22.14.0",
43 |     "ts-node": "^10.9.2",
44 |     "typescript": "^5.8.3"
45 |   }
46 | }
47 | 
```

--------------------------------------------------------------------------------
/src/naver_speller.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { load as loadCheerio } from "cheerio";
  2 | 
  3 | const SPELLER_PROVIDER_URL =
  4 |   "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";
  5 | const PASSPORT_KEY_REGEX = /SpellerProxy\?passportKey=([a-zA-Z0-9]+)/;
  6 | const SPELLER_API_URL_BASE =
  7 |   "https://m.search.naver.com/p/csearch/ocontent/util/SpellerProxy?passportKey=";
  8 | const MAX_CHUNK_LENGTH = 300;
  9 | 
 10 | interface NaverSpellerResponse {
 11 |   message: {
 12 |     result: {
 13 |       html: string;
 14 |       errata_count: number;
 15 |       origin_html: string;
 16 |       notag_html: string;
 17 |     };
 18 |     error?: string;
 19 |   };
 20 | }
 21 | 
 22 | function simpleHtmlUnescape(text: string): string {
 23 |   return text
 24 |     .replace(/&lt;/g, "<")
 25 |     .replace(/&gt;/g, ">")
 26 |     .replace(/<br>/g, "\n")
 27 |     .replace(/&amp;/g, "&")
 28 |     .replace(/&quot;/g, '"')
 29 |     .replace(/&#39;/g, "'")
 30 |     .replace(/&nbsp;/g, " ");
 31 | }
 32 | 
 33 | export class NaverSpellChecker {
 34 |   private spellerApiUrl: string | null = null;
 35 | 
 36 |   private async fetchPassportKey(): Promise<string> {
 37 |     const response = await fetch(SPELLER_PROVIDER_URL);
 38 |     const html = await response.text();
 39 |     if (!response.ok) {
 40 |       throw new Error(
 41 |         `HTTP error! status: ${response.status}, response: ${html}`,
 42 |       );
 43 |     }
 44 | 
 45 |     let passportKey: string | undefined;
 46 |     const $ = loadCheerio(html);
 47 |     $("script").each((_: any, element: any) => {
 48 |       const scriptContent = $(element).html();
 49 |       const match = scriptContent?.match(PASSPORT_KEY_REGEX);
 50 |       if (match?.[1]) {
 51 |         passportKey = match[1];
 52 |         return false;
 53 |       }
 54 |     });
 55 | 
 56 |     if (!passportKey) {
 57 |       throw new Error("Passport key not found in the HTML content.");
 58 |     }
 59 |     return passportKey;
 60 |   }
 61 | 
 62 |   private async updateSpellerApiUrl(): Promise<void> {
 63 |     const passportKey = await this.fetchPassportKey();
 64 |     this.spellerApiUrl = `${SPELLER_API_URL_BASE}${passportKey}&color_blindness=0&q=`;
 65 |   }
 66 | 
 67 |   async correctText(text: string): Promise<string> {
 68 |     const chunks = this.chunkText(text);
 69 |     let corrected = "";
 70 |     for (const chunk of chunks) {
 71 |       const correctedChunk = await this.correctChunk(chunk);
 72 |       if (corrected.endsWith(" ")) {
 73 |         corrected += correctedChunk;
 74 |       } else {
 75 |         corrected += ` ${correctedChunk}`;
 76 |       }
 77 |     }
 78 |     return corrected;
 79 |   }
 80 | 
 81 |   async correctChunk(text: string): Promise<string> {
 82 |     if (!this.spellerApiUrl) {
 83 |       await this.updateSpellerApiUrl();
 84 |       if (!this.spellerApiUrl) {
 85 |         throw new Error(
 86 |           "Failed to initialize Speller API URL. Cannot proceed.",
 87 |         );
 88 |       }
 89 |     }
 90 | 
 91 |     const encodedText = encodeURIComponent(text);
 92 |     const url = this.spellerApiUrl + encodedText;
 93 | 
 94 |     const response = await fetch(url);
 95 |     const responseText = await response.text();
 96 |     if (!response.ok) {
 97 |       throw new Error(
 98 |         `HTTP error! status: ${response.status}, response: ${responseText}`,
 99 |       );
100 |     }
101 | 
102 |     const data: NaverSpellerResponse = JSON.parse(responseText);
103 | 
104 |     if (data.message.error) {
105 |       if (data.message.error === "유효한 키가 아닙니다.") {
106 |         this.spellerApiUrl = null;
107 |         throw new Error("Try again with a new passport key.");
108 |       }
109 |       throw new Error(`failed to check spelling: ${data.message.error}`);
110 |     }
111 | 
112 |     return simpleHtmlUnescape(data.message.result.notag_html);
113 |   }
114 | 
115 |   private chunkText(text: string): string[] {
116 |     if (text.length <= MAX_CHUNK_LENGTH) {
117 |       return [text];
118 |     }
119 | 
120 |     const chunks: string[] = [];
121 |     let currentChunk = "";
122 |     const words = text.split(/(\s+)/);
123 | 
124 |     for (const word of words) {
125 |       if (!word) continue;
126 | 
127 |       if (word.length > MAX_CHUNK_LENGTH) {
128 |         // If a single "word" is too long, split it forcefully
129 |         if (currentChunk) {
130 |           chunks.push(currentChunk);
131 |           currentChunk = "";
132 |         }
133 |         for (let i = 0; i < word.length; i += MAX_CHUNK_LENGTH) {
134 |           chunks.push(word.substring(i, i + MAX_CHUNK_LENGTH));
135 |         }
136 |         continue;
137 |       }
138 |       if (currentChunk.length + word.length > MAX_CHUNK_LENGTH) {
139 |         chunks.push(currentChunk);
140 |         currentChunk = word.trimStart();
141 |         continue;
142 |       }
143 |       currentChunk += word;
144 |     }
145 | 
146 |     if (currentChunk) {
147 |       chunks.push(currentChunk);
148 |     }
149 | 
150 |     return chunks.filter((chunk) => chunk.length > 0);
151 |   }
152 | }
153 | 
```