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 +152 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +250 -0
- package/package.json +39 -0
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
|
package/dist/index.d.ts
ADDED
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
|
+
}
|