ynosis-agent-mcp 0.2.2 → 0.2.3
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/package.json +2 -2
- package/src/client.mjs +56 -7
- package/src/server.mjs +19 -13
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ynosis-agent-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "node --test test/*.test.mjs"
|
|
6
6
|
},
|
|
7
7
|
"description": "MCP stdio server for the Ynosis Agent API (facilities and spring analyses)",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"bin": {
|
|
10
|
-
"ynosis-agent-mcp": "
|
|
10
|
+
"ynosis-agent-mcp": "src/server.mjs"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"src",
|
package/src/client.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { File } from "node:buffer";
|
|
2
|
+
|
|
1
3
|
const DEFAULT_BASE = "https://ynosis.jp";
|
|
4
|
+
const DEFAULT_REGISTER_MIN_INTERVAL_MS = 20_000;
|
|
2
5
|
|
|
3
6
|
function resolveKey() {
|
|
4
7
|
return process.env.YNOSIS_AGENT_API_KEY || process.env.AGENT_API_KEY || "";
|
|
@@ -13,6 +16,26 @@ function resolveBase() {
|
|
|
13
16
|
return raw.replace(/\/$/, "");
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @type {number | null} */
|
|
24
|
+
let lastRegisterRelatedAt = null;
|
|
25
|
+
|
|
26
|
+
export async function paceRegisterRelated(minIntervalMs = DEFAULT_REGISTER_MIN_INTERVAL_MS) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
if (lastRegisterRelatedAt != null && minIntervalMs > 0) {
|
|
29
|
+
const wait = minIntervalMs - (now - lastRegisterRelatedAt);
|
|
30
|
+
if (wait > 0) await sleep(wait);
|
|
31
|
+
}
|
|
32
|
+
lastRegisterRelatedAt = Date.now();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function appendUploadFile(form, fieldName, buf, filename, contentType) {
|
|
36
|
+
form.append(fieldName, new File([buf], filename, { type: contentType }));
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
function imageContentType(filePath) {
|
|
17
40
|
const suffix = filePath.toLowerCase().split(".").pop();
|
|
18
41
|
if (suffix === "png") return "image/png";
|
|
@@ -22,6 +45,14 @@ function imageContentType(filePath) {
|
|
|
22
45
|
return "image/jpeg";
|
|
23
46
|
}
|
|
24
47
|
|
|
48
|
+
function preparedImageHeaders(filePath) {
|
|
49
|
+
const suffix = filePath.toLowerCase().split(".").pop();
|
|
50
|
+
if (suffix === "jpg" || suffix === "jpeg") {
|
|
51
|
+
return { "X-Ynosis-Image-Prepared": "1" };
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
25
56
|
export class AgentApiError extends Error {
|
|
26
57
|
constructor(statusCode, detail) {
|
|
27
58
|
super(`HTTP ${statusCode}: ${detail}`);
|
|
@@ -52,12 +83,17 @@ export class AgentApiClient {
|
|
|
52
83
|
return headers;
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
async request(method, path, { json, form } = {}) {
|
|
86
|
+
async request(method, path, { json, form, extraHeaders } = {}) {
|
|
56
87
|
const url = `${this.baseUrl}${path}`;
|
|
57
88
|
const ctrl = new AbortController();
|
|
58
89
|
const timer = setTimeout(() => ctrl.abort(), form ? Math.max(this.timeoutMs, 180_000) : this.timeoutMs);
|
|
59
90
|
try {
|
|
60
|
-
const
|
|
91
|
+
const formHeaders = { "X-API-Key": this.apiKey, ...(extraHeaders || {}) };
|
|
92
|
+
const init = {
|
|
93
|
+
method,
|
|
94
|
+
headers: form ? formHeaders : this.headers(json != null),
|
|
95
|
+
signal: ctrl.signal,
|
|
96
|
+
};
|
|
61
97
|
if (form) init.body = form;
|
|
62
98
|
else if (json != null) init.body = JSON.stringify(json);
|
|
63
99
|
const res = await fetch(url, init);
|
|
@@ -191,21 +227,31 @@ export class AgentApiClient {
|
|
|
191
227
|
const res = await this.request("POST", "/api/agent/register", { json: payload });
|
|
192
228
|
return res.json;
|
|
193
229
|
}
|
|
230
|
+
await paceRegisterRelated();
|
|
194
231
|
const buf = await readFile(imagePath);
|
|
195
232
|
const form = new FormData();
|
|
196
233
|
form.append("payload", JSON.stringify(payload));
|
|
197
|
-
|
|
198
|
-
|
|
234
|
+
const name = basename(imagePath);
|
|
235
|
+
appendUploadFile(form, "file", buf, name, imageContentType(imagePath));
|
|
236
|
+
const res = await this.request("POST", "/api/agent/register", {
|
|
237
|
+
form,
|
|
238
|
+
extraHeaders: preparedImageHeaders(imagePath),
|
|
239
|
+
});
|
|
199
240
|
return res.json;
|
|
200
241
|
}
|
|
201
242
|
|
|
202
243
|
async analyzeImage(imagePath) {
|
|
203
244
|
const { readFile } = await import("node:fs/promises");
|
|
204
245
|
const { basename } = await import("node:path");
|
|
246
|
+
await paceRegisterRelated();
|
|
205
247
|
const buf = await readFile(imagePath);
|
|
206
248
|
const form = new FormData();
|
|
207
|
-
|
|
208
|
-
|
|
249
|
+
const name = basename(imagePath);
|
|
250
|
+
appendUploadFile(form, "file", buf, name, imageContentType(imagePath));
|
|
251
|
+
const res = await this.request("POST", "/api/agent/analyze", {
|
|
252
|
+
form,
|
|
253
|
+
extraHeaders: preparedImageHeaders(imagePath),
|
|
254
|
+
});
|
|
209
255
|
return res.json;
|
|
210
256
|
}
|
|
211
257
|
|
|
@@ -215,6 +261,7 @@ export class AgentApiClient {
|
|
|
215
261
|
}
|
|
216
262
|
|
|
217
263
|
async parseTranscript(rawText, llmJson) {
|
|
264
|
+
await paceRegisterRelated();
|
|
218
265
|
const body = { raw_text: rawText };
|
|
219
266
|
if (llmJson != null) body.llm_json = llmJson;
|
|
220
267
|
const res = await this.request("POST", "/api/agent/parse", { json: body });
|
|
@@ -229,9 +276,11 @@ export class AgentApiClient {
|
|
|
229
276
|
async uploadSourceImage(sourceId, imagePath) {
|
|
230
277
|
const { readFile } = await import("node:fs/promises");
|
|
231
278
|
const { basename } = await import("node:path");
|
|
279
|
+
await paceRegisterRelated();
|
|
232
280
|
const buf = await readFile(imagePath);
|
|
233
281
|
const form = new FormData();
|
|
234
|
-
|
|
282
|
+
const name = basename(imagePath);
|
|
283
|
+
appendUploadFile(form, "file", buf, name, imageContentType(imagePath));
|
|
235
284
|
const res = await this.request("POST", `/api/sources/${sourceId}/image`, { form });
|
|
236
285
|
return res.json;
|
|
237
286
|
}
|
package/src/server.mjs
CHANGED
|
@@ -29,19 +29,20 @@ function textResult(text) {
|
|
|
29
29
|
|
|
30
30
|
const server = new McpServer({
|
|
31
31
|
name: "ynosis-agent",
|
|
32
|
-
version: "0.2.
|
|
32
|
+
version: "0.2.3",
|
|
33
33
|
instructions:
|
|
34
34
|
"Ynosis(温泉データベース)への施設・成分表登録・検索用ツール。" +
|
|
35
35
|
"新規施設登録前: find_existing_facility で重複確認 → 既存なら update_facility、" +
|
|
36
36
|
"新規のみ resolve_place_candidates → LLM が同一候補を選び prepare_facility_for_register → register_facility(推測禁止)。" +
|
|
37
37
|
"検索は search_facilities / get_facility / find_existing_facility。" +
|
|
38
38
|
"施設登録は register_facilities_batch または register_facility。" +
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
39
|
+
"成分表一括は CLI register-batch 推奨(3件ごと60秒休憩)。MCP は1件ずつ。" +
|
|
40
|
+
"成分表は手元 Vision なら get_parse_prompt → 手元 LLM → parse_transcript(llm_json 推奨)、" +
|
|
41
|
+
"または analyze_source_image(Ynosis OCR・負荷大)。" +
|
|
42
|
+
"その後 register_source。新規源泉は image_path 必須。ツール呼び出し間隔20秒以上。" +
|
|
43
|
+
"並列禁止。本番512MBのため4件以上を休憩なしで連打しない。" +
|
|
43
44
|
"YNOSIS_AGENT_API_KEY と座標候補用 GOOGLE_MAPS_API_KEY が必要。" +
|
|
44
|
-
"CLI は PyPI
|
|
45
|
+
"CLI は PyPI ynosis-agent 0.2.6+。CSV 一括は CLI / MCP。",
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
const dict = z.record(z.any());
|
|
@@ -296,7 +297,7 @@ server.tool(
|
|
|
296
297
|
|
|
297
298
|
server.tool(
|
|
298
299
|
"register_source",
|
|
299
|
-
"成分表付きで施設・源泉を登録する。新規源泉は image_path
|
|
300
|
+
"成分表付きで施設・源泉を登録する。新規源泉は image_path 必須。3件以上は60秒休憩。HEICは事前JPEG化推奨。",
|
|
300
301
|
{ payload: dict, image_path: z.string().optional() },
|
|
301
302
|
async ({ payload, image_path }) => {
|
|
302
303
|
try {
|
|
@@ -328,13 +329,18 @@ server.tool(
|
|
|
328
329
|
}
|
|
329
330
|
);
|
|
330
331
|
|
|
331
|
-
server.tool(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
332
|
+
server.tool(
|
|
333
|
+
"analyze_source_image",
|
|
334
|
+
"成分表画像を Ynosis OCR で解析する(DB 未書込)。並列禁止。連続呼び出しは20秒以上空ける。一括は CLI register-batch 推奨。",
|
|
335
|
+
{ image_path: z.string() },
|
|
336
|
+
async ({ image_path }) => {
|
|
337
|
+
try {
|
|
338
|
+
return textResult(dump(await client().analyzeImage(image_path)));
|
|
339
|
+
} catch (e) {
|
|
340
|
+
return textResult(errText(e));
|
|
341
|
+
}
|
|
336
342
|
}
|
|
337
|
-
|
|
343
|
+
);
|
|
338
344
|
|
|
339
345
|
server.tool("prepare_parsed", "register_source 用に dissolved_material を mg 換算する。", { parsed: dict }, async ({ parsed }) => {
|
|
340
346
|
try {
|