yahoo-furigana-mcp 1.0.0

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/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # Yahoo! ふりがな API MCP サーバ
2
+
3
+ Yahoo! JAPAN テキスト解析の[ふりがなAPI(V2)](https://developer.yahoo.co.jp/webapi/jlp/furigana/v2/furigana.html)を利用したMCPサーバです。
4
+
5
+ 日本語テキストにふりがな(ひらがな読み)やローマ字を付けることができます。
6
+
7
+ ## 必要な環境
8
+
9
+ - Node.js 18以上
10
+ - Yahoo! JAPAN デベロッパーネットワークの Client ID(アプリケーションID)
11
+ - [Yahoo! ID連携 v2 アプリケーションの登録](https://developer.yahoo.co.jp/yconnect/v2/registration.html)から取得できます
12
+
13
+ ## セットアップ
14
+
15
+ ### 1. 依存関係のインストール
16
+
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ ### 2. ビルド
22
+
23
+ ```bash
24
+ npm run build
25
+ ```
26
+
27
+ ## Claude Desktop での設定
28
+
29
+ ### 方法1: npx経由で実行(推奨)
30
+
31
+ ローカルにリポジトリを配置せず、npm経由で実行する方法です。
32
+
33
+ `claude_desktop_config.json` に以下を追加してください:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "yahoo-furigana": {
39
+ "command": "npx",
40
+ "args": ["-y", "yahoo-furigana-mcp"],
41
+ "env": {
42
+ "YAHOO_CLIENT_ID": "あなたのClient ID"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### 方法2: ローカルから実行
50
+
51
+ リポジトリをクローンして実行する方法です。
52
+
53
+ `claude_desktop_config.json` に以下を追加してください:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "yahoo-furigana": {
59
+ "command": "node",
60
+ "args": ["/path/to/yahoo-furigana-mcp/dist/index.js"],
61
+ "env": {
62
+ "YAHOO_CLIENT_ID": "あなたのClient ID"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ `/path/to/yahoo-furigana-mcp` は実際のパスに置き換えてください。
70
+
71
+ ## 提供するツール
72
+
73
+ ### gen_furigana
74
+
75
+ 日本語テキストにふりがなを付けます。
76
+
77
+ #### パラメータ
78
+
79
+ | 名前 | 型 | 必須 | 説明 |
80
+ |------|-----|------|------|
81
+ | `text` | string | ○ | ふりがなを付けたい日本語テキスト |
82
+ | `grade` | number | - | 学年指定(1-8)。指定した学年までに習う漢字にはふりがなを付けません |
83
+ | `output_format` | string | - | 出力形式(デフォルト: `bracket`) |
84
+
85
+ #### output_format の値
86
+
87
+ | 値 | 説明 | 出力例 |
88
+ |----|------|--------|
89
+ | `bracket` | 括弧形式(デフォルト) | `漢字(かんじ)` |
90
+ | `ruby` | HTMLルビ形式 | `<ruby>漢字<rt>かんじ</rt></ruby>` |
91
+ | `roman` | ローマ字付き詳細形式 | `漢字: かんじ (kanji)` |
92
+
93
+ #### grade の値
94
+
95
+ | 値 | 対象 |
96
+ |----|------|
97
+ | 1 | 小学1年生までに習う漢字 |
98
+ | 2 | 小学2年生までに習う漢字 |
99
+ | 3 | 小学3年生までに習う漢字 |
100
+ | 4 | 小学4年生までに習う漢字 |
101
+ | 5 | 小学5年生までに習う漢字 |
102
+ | 6 | 小学6年生までに習う漢字 |
103
+ | 7 | 中学生までに習う漢字 |
104
+ | 8 | それ以上 |
105
+
106
+ #### 使用例
107
+
108
+ **bracket形式(デフォルト):**
109
+
110
+ ```
111
+ 入力: "漢字の読み方を教えてください"
112
+ 出力: "漢字(かんじ)の読(よ)み方(かた)を教(おし)えてください"
113
+ ```
114
+
115
+ **ruby形式:**
116
+
117
+ ```
118
+ 入力: "漢字の読み方"
119
+ 出力: "<ruby>漢字<rt>かんじ</rt></ruby>の<ruby>読<rt>よ</rt></ruby>み<ruby>方<rt>かた</rt></ruby>"
120
+ ```
121
+
122
+ ## 特徴
123
+
124
+ - **自動チャンク分割**: 4KBを超える長いテキストも自動的に分割して処理します。文の区切り(。!?など)で分割するため、自然な結果が得られます。
125
+
126
+ ## npm公開(開発者向け)
127
+
128
+ このパッケージをnpmに公開する手順:
129
+
130
+ ```bash
131
+ # ビルド
132
+ npm run build
133
+
134
+ # パッケージの内容を確認
135
+ npm pack --dry-run
136
+
137
+ # npmにログイン(初回のみ)
138
+ npm login
139
+
140
+ # 公開
141
+ npm publish
142
+ ```
143
+
144
+ 公開後、ユーザーは `npx yahoo-furigana-mcp` でローカルにリポジトリを配置せずに利用できます。
145
+
146
+ ## 制限事項
147
+
148
+ - Yahoo! JAPAN APIの利用規約に従ってください
149
+
150
+ ## ライセンス
151
+
152
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ // Client ID from environment variable
6
+ const CLIENT_ID = process.env.YAHOO_CLIENT_ID;
7
+ if (!CLIENT_ID) {
8
+ console.error("Error: YAHOO_CLIENT_ID environment variable is required");
9
+ process.exit(1);
10
+ }
11
+ const API_ENDPOINT = "https://jlp.yahooapis.jp/FuriganaService/V2/furigana";
12
+ // Maximum text size per request (in bytes) - leaving room for JSON overhead
13
+ const MAX_CHUNK_SIZE = 3000;
14
+ // Function to call Yahoo Furigana API
15
+ async function getFurigana(text, grade) {
16
+ const requestBody = {
17
+ id: "1",
18
+ jsonrpc: "2.0",
19
+ method: "jlp.furiganaservice.furigana",
20
+ params: {
21
+ q: text,
22
+ },
23
+ };
24
+ if (grade !== undefined && grade >= 1 && grade <= 8) {
25
+ requestBody.params.grade = grade;
26
+ }
27
+ const response = await fetch(API_ENDPOINT, {
28
+ method: "POST",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ "User-Agent": `Yahoo AppID: ${CLIENT_ID}`,
32
+ },
33
+ body: JSON.stringify(requestBody),
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
37
+ }
38
+ return (await response.json());
39
+ }
40
+ // Split text into chunks at sentence boundaries
41
+ function splitTextIntoChunks(text) {
42
+ const encoder = new TextEncoder();
43
+ const textBytes = encoder.encode(text);
44
+ // If text fits in one chunk, return as is
45
+ if (textBytes.length <= MAX_CHUNK_SIZE) {
46
+ return [text];
47
+ }
48
+ const chunks = [];
49
+ // Split by sentence-ending punctuation (Japanese and English)
50
+ const sentences = text.split(/(?<=[。.!?\n])/);
51
+ let currentChunk = "";
52
+ for (const sentence of sentences) {
53
+ const testChunk = currentChunk + sentence;
54
+ const testBytes = encoder.encode(testChunk);
55
+ if (testBytes.length > MAX_CHUNK_SIZE) {
56
+ if (currentChunk) {
57
+ chunks.push(currentChunk);
58
+ currentChunk = sentence;
59
+ }
60
+ else {
61
+ // Single sentence is too long, split by characters
62
+ let remaining = sentence;
63
+ while (remaining) {
64
+ let end = remaining.length;
65
+ while (encoder.encode(remaining.slice(0, end)).length > MAX_CHUNK_SIZE && end > 1) {
66
+ end = Math.floor(end / 2);
67
+ }
68
+ // Try to find a better break point
69
+ while (end < remaining.length && encoder.encode(remaining.slice(0, end + 1)).length <= MAX_CHUNK_SIZE) {
70
+ end++;
71
+ }
72
+ chunks.push(remaining.slice(0, end));
73
+ remaining = remaining.slice(end);
74
+ }
75
+ }
76
+ }
77
+ else {
78
+ currentChunk = testChunk;
79
+ }
80
+ }
81
+ if (currentChunk) {
82
+ chunks.push(currentChunk);
83
+ }
84
+ return chunks;
85
+ }
86
+ // Process text with chunking support
87
+ async function processTextWithChunking(text, grade, format) {
88
+ const chunks = splitTextIntoChunks(text);
89
+ if (chunks.length === 1) {
90
+ // Single chunk, process normally
91
+ const response = await getFurigana(text, grade);
92
+ if (response.error) {
93
+ throw new Error(`APIエラー: ${response.error.message} (code: ${response.error.code})`);
94
+ }
95
+ if (!response.result) {
96
+ throw new Error("結果が取得できませんでした");
97
+ }
98
+ return formatResult(response.result, format);
99
+ }
100
+ // Multiple chunks, process each and combine
101
+ const results = [];
102
+ for (let i = 0; i < chunks.length; i++) {
103
+ const response = await getFurigana(chunks[i], grade);
104
+ if (response.error) {
105
+ throw new Error(`APIエラー (チャンク ${i + 1}/${chunks.length}): ${response.error.message}`);
106
+ }
107
+ if (!response.result) {
108
+ throw new Error(`結果が取得できませんでした (チャンク ${i + 1}/${chunks.length})`);
109
+ }
110
+ results.push(formatResult(response.result, format));
111
+ }
112
+ // For roman format, join with newlines; for others, join directly
113
+ return format === "roman" ? results.join("\n") : results.join("");
114
+ }
115
+ // Format with bracket notation: 漢字(かんじ)
116
+ function formatBracket(result) {
117
+ const parts = [];
118
+ for (const word of result.word) {
119
+ if (word.furigana && word.surface !== word.furigana) {
120
+ parts.push(`${word.surface}(${word.furigana})`);
121
+ }
122
+ else if (word.subword) {
123
+ const subParts = word.subword
124
+ .map((sw) => {
125
+ if (sw.furigana && sw.surface !== sw.furigana) {
126
+ return `${sw.surface}(${sw.furigana})`;
127
+ }
128
+ return sw.surface;
129
+ })
130
+ .join("");
131
+ parts.push(subParts);
132
+ }
133
+ else {
134
+ parts.push(word.surface);
135
+ }
136
+ }
137
+ return parts.join("");
138
+ }
139
+ // Format with HTML ruby tags: <ruby>漢字<rt>かんじ</rt></ruby>
140
+ function formatRuby(result) {
141
+ const parts = [];
142
+ for (const word of result.word) {
143
+ if (word.furigana && word.surface !== word.furigana) {
144
+ parts.push(`<ruby>${word.surface}<rt>${word.furigana}</rt></ruby>`);
145
+ }
146
+ else if (word.subword) {
147
+ const subParts = word.subword
148
+ .map((sw) => {
149
+ if (sw.furigana && sw.surface !== sw.furigana) {
150
+ return `<ruby>${sw.surface}<rt>${sw.furigana}</rt></ruby>`;
151
+ }
152
+ return sw.surface;
153
+ })
154
+ .join("");
155
+ parts.push(subParts);
156
+ }
157
+ else {
158
+ parts.push(word.surface);
159
+ }
160
+ }
161
+ return parts.join("");
162
+ }
163
+ // Format with roman characters
164
+ function formatWithRoman(result) {
165
+ const lines = [];
166
+ for (const word of result.word) {
167
+ const surface = word.surface;
168
+ const furigana = word.furigana || "";
169
+ const roman = word.roman || "";
170
+ if (word.subword) {
171
+ const subDetails = word.subword
172
+ .map((sw) => ` - ${sw.surface}: ${sw.furigana || ""} (${sw.roman || ""})`)
173
+ .join("\n");
174
+ lines.push(`${surface}:\n${subDetails}`);
175
+ }
176
+ else if (furigana || roman) {
177
+ lines.push(`${surface}: ${furigana} (${roman})`);
178
+ }
179
+ else {
180
+ lines.push(`${surface}`);
181
+ }
182
+ }
183
+ return lines.join("\n");
184
+ }
185
+ // Create MCP server
186
+ const server = new McpServer({
187
+ name: "yahoo-furigana",
188
+ version: "1.0.0",
189
+ });
190
+ // Format result based on output format
191
+ function formatResult(result, format) {
192
+ switch (format) {
193
+ case "ruby":
194
+ return formatRuby(result);
195
+ case "roman":
196
+ return formatWithRoman(result);
197
+ case "bracket":
198
+ default:
199
+ return formatBracket(result);
200
+ }
201
+ }
202
+ // Register the furigana tool
203
+ server.tool("gen_furigana", "日本語テキストにふりがな(ひらがな読み)を付けます。漢字かな混じりのテキストを入力すると、各単語の読み方を返します。", {
204
+ text: z.string().describe("ふりがなを付けたい日本語テキスト"),
205
+ grade: z
206
+ .number()
207
+ .min(1)
208
+ .max(8)
209
+ .optional()
210
+ .describe("学年指定(1-8)。指定した学年までに習う漢字にはふりがなを付けません。1=小1, 2=小2, ..., 6=小6, 7=中学, 8=それ以上"),
211
+ output_format: z
212
+ .enum(["bracket", "ruby", "roman"])
213
+ .optional()
214
+ .describe("出力形式。bracket=括弧形式「漢字(かんじ)」、ruby=HTMLルビ形式「<ruby>漢字<rt>かんじ</rt></ruby>」、roman=ローマ字付き詳細形式(デフォルト: bracket)"),
215
+ }, async ({ text, grade, output_format }) => {
216
+ try {
217
+ const format = output_format || "bracket";
218
+ const formatted = await processTextWithChunking(text, grade, format);
219
+ return {
220
+ content: [
221
+ {
222
+ type: "text",
223
+ text: formatted,
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ catch (error) {
229
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: `エラーが発生しました: ${errorMessage}`,
235
+ },
236
+ ],
237
+ isError: true,
238
+ };
239
+ }
240
+ });
241
+ // Start the server
242
+ async function main() {
243
+ const transport = new StdioServerTransport();
244
+ await server.connect(transport);
245
+ console.error("Yahoo Furigana MCP server started");
246
+ }
247
+ main().catch((error) => {
248
+ console.error("Fatal error:", error);
249
+ process.exit(1);
250
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "yahoo-furigana-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Yahoo! JAPAN Furigana API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "yahoo-furigana-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "zod": "^3.23.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "typescript": "^5.0.0"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "furigana",
29
+ "japanese",
30
+ "yahoo",
31
+ "api"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/analekt/yahoo-furigana-mcp.git"
36
+ },
37
+ "author": "",
38
+ "license": "MIT"
39
+ }