ynosis-agent-mcp 0.2.1 → 0.2.2

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,6 +1,6 @@
1
1
  {
2
2
  "name": "ynosis-agent-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "scripts": {
5
5
  "test": "node --test test/*.test.mjs"
6
6
  },
package/src/client.mjs CHANGED
@@ -111,6 +111,20 @@ export class AgentApiClient {
111
111
  return res.json;
112
112
  }
113
113
 
114
+ async requestFacilityDeletion(facilityId, { reason, sourceLink }) {
115
+ const res = await this.request("POST", `/api/agent/facilities/${facilityId}/deletion-requests`, {
116
+ json: { reason, source_link: sourceLink },
117
+ });
118
+ return res.json;
119
+ }
120
+
121
+ async requestSourceDeletion(sourceId, { reason, sourceLink }) {
122
+ const res = await this.request("POST", `/api/agent/sources/${sourceId}/deletion-requests`, {
123
+ json: { reason, source_link: sourceLink },
124
+ });
125
+ return res.json;
126
+ }
127
+
114
128
  async remediateDuplicateFacility(duplicateId, { canonicalId = null, dryRun = false } = {}) {
115
129
  const { remediateDuplicateFacility: remediate } = await import("./facility_remediate.mjs");
116
130
  return remediate(this, duplicateId, { canonicalId, dryRun });
@@ -255,6 +269,7 @@ export const FACILITY_TAG_OPTIONS = [
255
269
  "露天あり",
256
270
  "貸切湯あり",
257
271
  "日帰り入浴可",
272
+ "飲泉可",
258
273
  "共同浴場",
259
274
  "造成泉",
260
275
  "モール泉",
@@ -45,16 +45,28 @@ export function facilitySearchQueries(name) {
45
45
  return queries;
46
46
  }
47
47
 
48
+ async function coreNameInFacilityName(query, facilityName) {
49
+ const core = await normalizeFacilityName(query);
50
+ const normDb = await normalizeFacilityName(facilityName);
51
+ if (core.length < 2) return false;
52
+ return normDb.includes(core);
53
+ }
54
+
48
55
  export async function scoreFacilityItem(candidateName, item) {
49
56
  const dbName = item?.name || "";
50
57
  let score = await nameSimilarity(candidateName, dbName);
58
+ let contains = await coreNameInFacilityName(candidateName, dbName);
51
59
  for (const alias of item?.aliases || []) {
52
60
  const aliasName = typeof alias === "object" ? alias?.name : alias;
53
- if (aliasName) score = Math.max(score, await nameSimilarity(candidateName, String(aliasName)));
61
+ if (aliasName) {
62
+ const aliasStr = String(aliasName);
63
+ score = Math.max(score, await nameSimilarity(candidateName, aliasStr));
64
+ if (await coreNameInFacilityName(candidateName, aliasStr)) contains = true;
65
+ }
54
66
  }
67
+ if (contains) return score >= 0.95 ? 100 : 95;
55
68
  if (score >= 0.95) return 100;
56
69
  if (score >= NAME_SIMILARITY_HIGH) return 90;
57
- if (score >= NAME_SIMILARITY_LOW_WITH_LOCATION_HINT) return 85;
58
70
  return 0;
59
71
  }
60
72
 
@@ -88,6 +100,7 @@ export async function findExistingFacility(
88
100
  lat = null,
89
101
  lng = null,
90
102
  searchLimit = 30,
103
+ expectedAreas = null,
91
104
  } = {}
92
105
  ) {
93
106
  const primary = String(name || "").trim();
@@ -109,9 +122,8 @@ export async function findExistingFacility(
109
122
  }
110
123
  }
111
124
 
112
- let bestScore = 0;
113
- let bestItem = null;
114
- let bestSim = 0;
125
+ /** @type {Array<[number, number, object]>} */
126
+ const candidates = [];
115
127
 
116
128
  for (const q of searchQs) {
117
129
  const params = { q, limit: searchLimit };
@@ -124,19 +136,34 @@ export async function findExistingFacility(
124
136
  }
125
137
  for (const item of res.items || []) {
126
138
  if (prefecture && (item.prefecture || "") !== prefecture) continue;
139
+ if (expectedAreas) {
140
+ const area = String(item.area || "").trim();
141
+ if (area && !expectedAreas.has(area)) continue;
142
+ }
127
143
  const sc = await scoreWithLocation(names, item, lat, lng);
144
+ if (sc < MATCH_SCORE_MIN) continue;
128
145
  let sim = 0;
129
146
  for (const cn of names) {
130
147
  sim = Math.max(sim, await nameSimilarity(cn, item.name || ""));
131
148
  }
132
- if (sc > bestScore) {
133
- bestScore = sc;
134
- bestItem = item;
135
- bestSim = sim;
136
- }
149
+ candidates.push([sc, sim, item]);
137
150
  }
138
151
  }
139
152
 
140
- if (bestScore < MATCH_SCORE_MIN || !bestItem) return null;
153
+ if (!candidates.length) return null;
154
+
155
+ const rank = ([sc, sim, item]) => {
156
+ const unapprovedAgent = item.is_agent_registered && !item.is_agent_approved;
157
+ return [sc - (unapprovedAgent ? 15 : 0), sim, unapprovedAgent ? 0 : 1];
158
+ };
159
+ candidates.sort((a, b) => {
160
+ const ra = rank(a);
161
+ const rb = rank(b);
162
+ for (let i = 0; i < 3; i += 1) {
163
+ if (ra[i] !== rb[i]) return rb[i] - ra[i];
164
+ }
165
+ return 0;
166
+ });
167
+ const [bestScore, bestSim, bestItem] = candidates[0];
141
168
  return { facility: bestItem, match_score: bestScore, name_similarity: bestSim };
142
169
  }
package/src/server.mjs CHANGED
@@ -29,7 +29,7 @@ function textResult(text) {
29
29
 
30
30
  const server = new McpServer({
31
31
  name: "ynosis-agent",
32
- version: "0.2.1",
32
+ version: "0.2.2",
33
33
  instructions:
34
34
  "Ynosis(温泉データベース)への施設・成分表登録・検索用ツール。" +
35
35
  "新規施設登録前: find_existing_facility で重複確認 → 既存なら update_facility、" +
@@ -101,6 +101,44 @@ server.tool(
101
101
  }
102
102
  );
103
103
 
104
+ server.tool(
105
+ "request_facility_deletion",
106
+ "施設の削除依頼(管理者承認後に削除)。source_link 必須。",
107
+ {
108
+ facility_id: z.number(),
109
+ reason: z.string(),
110
+ source_link: z.string(),
111
+ },
112
+ async ({ facility_id, reason, source_link }) => {
113
+ try {
114
+ return textResult(
115
+ dump(await client().requestFacilityDeletion(facility_id, { reason, sourceLink: source_link }))
116
+ );
117
+ } catch (e) {
118
+ return textResult(errText(e));
119
+ }
120
+ }
121
+ );
122
+
123
+ server.tool(
124
+ "request_source_deletion",
125
+ "源泉の削除依頼(管理者承認後に削除)。source_link 必須。",
126
+ {
127
+ source_id: z.number(),
128
+ reason: z.string(),
129
+ source_link: z.string(),
130
+ },
131
+ async ({ source_id, reason, source_link }) => {
132
+ try {
133
+ return textResult(
134
+ dump(await client().requestSourceDeletion(source_id, { reason, sourceLink: source_link }))
135
+ );
136
+ } catch (e) {
137
+ return textResult(errText(e));
138
+ }
139
+ }
140
+ );
141
+
104
142
  server.tool(
105
143
  "remediate_duplicate_facility",
106
144
  "正規施設へ編集依頼(備考に重複整理メモ)→ 誤登録を withdraw。",
@@ -159,7 +197,7 @@ server.tool(
159
197
 
160
198
  server.tool(
161
199
  "register_facility",
162
- "施設を1件登録する(FacilityCreate 形式)。新規前に find_existing_facility → prepare_facility_for_register 推奨。",
200
+ "施設を1件登録する(FacilityCreate 形式)。新規前に find_existing_facility → prepare_facility_for_register 推奨。既存ヒットで編集依頼になる場合は source_link 必須。",
163
201
  { facility: dict },
164
202
  async ({ facility }) => {
165
203
  try {
@@ -245,7 +283,7 @@ server.tool(
245
283
 
246
284
  server.tool(
247
285
  "update_facility",
248
- "既存施設を部分更新する(Agent キー時は編集依頼)。",
286
+ "既存施設を部分更新する(Agent キー時は編集依頼)。updates に source_link(根拠 URL・http/https)必須。",
249
287
  { facility_id: z.number(), updates: dict },
250
288
  async ({ facility_id, updates }) => {
251
289
  try {