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 CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "ynosis-agent-mcp",
3
- "version": "0.2.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": "./src/server.mjs"
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 init = { method, headers: form ? { "X-API-Key": this.apiKey } : this.headers(json != null), signal: ctrl.signal };
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
- form.append("file", new Blob([buf], { type: imageContentType(imagePath) }), basename(imagePath));
198
- const res = await this.request("POST", "/api/agent/register", { form });
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
- form.append("file", new Blob([buf], { type: imageContentType(imagePath) }), basename(imagePath));
208
- const res = await this.request("POST", "/api/agent/analyze", { form });
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
- form.append("file", new Blob([buf], { type: imageContentType(imagePath) }), basename(imagePath));
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.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
- "成分表は手元 Vision なら get_parse_prompt → 手元 LLM → parse_transcript、" +
40
- "または analyze_source_image(Ynosis OCR)。" +
41
- "その後 register_source。新規源泉は image_path 必須。" +
42
- "OCR / サーバー抽出の parse は並列禁止。" +
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 の ynosis-agent。CSV ファイル一括は CLI / MCP。",
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("analyze_source_image", "成分表画像を Ynosis OCR で解析する(DB 未書込)。並列禁止。", { image_path: z.string() }, async ({ image_path }) => {
332
- try {
333
- return textResult(dump(await client().analyzeImage(image_path)));
334
- } catch (e) {
335
- return textResult(errText(e));
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 {