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 +1 -1
- package/src/client.mjs +15 -0
- package/src/facility_match.mjs +38 -11
- package/src/server.mjs +41 -3
package/package.json
CHANGED
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
|
"モール泉",
|
package/src/facility_match.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
133
|
-
bestScore = sc;
|
|
134
|
-
bestItem = item;
|
|
135
|
-
bestSim = sim;
|
|
136
|
-
}
|
|
149
|
+
candidates.push([sc, sim, item]);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
if (
|
|
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.
|
|
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 {
|