ynosis-agent-mcp 0.2.0 → 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/README.md +4 -1
- package/package.json +1 -1
- package/src/client.mjs +25 -0
- package/src/facility_match.mjs +38 -11
- package/src/facility_remediate.mjs +102 -0
- package/src/server.mjs +78 -3
package/README.md
CHANGED
|
@@ -35,10 +35,13 @@ Ynosis Agent API 用の **MCP(stdio)**。Cursor など Node がある環境
|
|
|
35
35
|
## 施設登録フロー(Python MCP と同一)
|
|
36
36
|
|
|
37
37
|
```
|
|
38
|
-
find_existing_facility → update_facility
|
|
38
|
+
find_existing_facility → update_facility(既存・認定・タグ不足時も編集依頼)
|
|
39
39
|
→ resolve_place_candidates → prepare_facility_for_register → register_facility(新規)
|
|
40
|
+
誤登録整理: remediate_duplicate_facility(正規へ依頼 + withdraw)
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
主要ツール: `find_existing_facility`, `prepare_facility_for_register`, `resolve_place_candidates`, `withdraw_facility`, `remediate_duplicate_facility`, `get_tag_options`
|
|
44
|
+
|
|
42
45
|
## 運営: npm 公開
|
|
43
46
|
|
|
44
47
|
リポジトリ内手順: [docs/agent/publishing.md](../../docs/agent/publishing.md#npmynosis-agent-mcp)
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -106,6 +106,30 @@ export class AgentApiClient {
|
|
|
106
106
|
return findExisting(this, name, prefecture, options);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
async withdrawFacility(facilityId) {
|
|
110
|
+
const res = await this.request("POST", `/api/agent/facilities/${facilityId}/withdraw`);
|
|
111
|
+
return res.json;
|
|
112
|
+
}
|
|
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
|
+
|
|
128
|
+
async remediateDuplicateFacility(duplicateId, { canonicalId = null, dryRun = false } = {}) {
|
|
129
|
+
const { remediateDuplicateFacility: remediate } = await import("./facility_remediate.mjs");
|
|
130
|
+
return remediate(this, duplicateId, { canonicalId, dryRun });
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
async prepareFacilityPayload(payload, { place = null, maxResults = 5 } = {}) {
|
|
110
134
|
const { filterValidTags, resolvePlaceForPayload } = await import("./facility_prepare.mjs");
|
|
111
135
|
const prepared = { ...payload };
|
|
@@ -245,6 +269,7 @@ export const FACILITY_TAG_OPTIONS = [
|
|
|
245
269
|
"露天あり",
|
|
246
270
|
"貸切湯あり",
|
|
247
271
|
"日帰り入浴可",
|
|
272
|
+
"飲泉可",
|
|
248
273
|
"共同浴場",
|
|
249
274
|
"造成泉",
|
|
250
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
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Agent 重複誤登録の整理(Python ynosis_agent.facility_remediate と同期)。 */
|
|
2
|
+
|
|
3
|
+
import { MATCH_SCORE_MIN, facilitySearchQueries, scoreFacilityItem } from "./facility_match.mjs";
|
|
4
|
+
import { mergeFacilityUpdates } from "./facility_prepare.mjs";
|
|
5
|
+
|
|
6
|
+
function appendNote(existing, line) {
|
|
7
|
+
const base = String(existing || "").trim();
|
|
8
|
+
if (base.includes(line)) return base;
|
|
9
|
+
return base ? `${base}\n${line}` : line;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function findCanonicalFacility(client, duplicate, { searchLimit = 30 } = {}) {
|
|
13
|
+
const dupId = Number(duplicate.id);
|
|
14
|
+
const name = String(duplicate.name || "").trim();
|
|
15
|
+
const prefecture = duplicate.prefecture;
|
|
16
|
+
if (!name) return null;
|
|
17
|
+
|
|
18
|
+
let bestScore = 0;
|
|
19
|
+
let bestItem = null;
|
|
20
|
+
const queries = facilitySearchQueries(name);
|
|
21
|
+
|
|
22
|
+
for (const q of queries) {
|
|
23
|
+
const params = { q, limit: searchLimit };
|
|
24
|
+
if (prefecture) params.prefecture = prefecture;
|
|
25
|
+
let res;
|
|
26
|
+
try {
|
|
27
|
+
res = await client.searchFacilities(params);
|
|
28
|
+
} catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const item of res.items || []) {
|
|
32
|
+
if (Number(item.id) === dupId) continue;
|
|
33
|
+
if (prefecture && (item.prefecture || "") !== prefecture) continue;
|
|
34
|
+
const sc = await scoreFacilityItem(name, item);
|
|
35
|
+
if (sc > bestScore) {
|
|
36
|
+
bestScore = sc;
|
|
37
|
+
bestItem = item;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (bestScore < MATCH_SCORE_MIN || !bestItem) return null;
|
|
43
|
+
return bestItem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function remediateDuplicateFacility(
|
|
47
|
+
client,
|
|
48
|
+
duplicateId,
|
|
49
|
+
{ canonicalId = null, dryRun = false } = {}
|
|
50
|
+
) {
|
|
51
|
+
const duplicate = await client.getFacility(duplicateId);
|
|
52
|
+
if (duplicate.is_agent_approved) {
|
|
53
|
+
return { ok: false, error: "承認済みのため取り下げ不可", duplicate_id: duplicateId };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let canonical = canonicalId != null ? await client.getFacility(canonicalId) : await findCanonicalFacility(client, duplicate);
|
|
57
|
+
if (!canonical) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
action: "no_canonical",
|
|
61
|
+
duplicate_id: duplicateId,
|
|
62
|
+
duplicate_name: duplicate.name,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const canonId = Number(canonical.id);
|
|
67
|
+
canonical = await client.getFacility(canonId);
|
|
68
|
+
const marker =
|
|
69
|
+
`[Agent重複整理] 未承認ID ${duplicateId}「${duplicate.name}」は` +
|
|
70
|
+
` 当施設(ID ${canonId})と同一の誤登録。Agent 側で取り下げ${dryRun ? "予定" : "済み"}。`;
|
|
71
|
+
const proposed = { ...duplicate, notes: appendNote(canonical.notes, marker) };
|
|
72
|
+
const updates = mergeFacilityUpdates(canonical, proposed) || {};
|
|
73
|
+
if (proposed.notes !== canonical.notes) updates.notes = proposed.notes;
|
|
74
|
+
|
|
75
|
+
const result = {
|
|
76
|
+
ok: true,
|
|
77
|
+
action: "remediate",
|
|
78
|
+
duplicate_id: duplicateId,
|
|
79
|
+
duplicate_name: duplicate.name,
|
|
80
|
+
canonical_id: canonId,
|
|
81
|
+
canonical_name: canonical.name,
|
|
82
|
+
updates,
|
|
83
|
+
dry_run: dryRun,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (dryRun) return result;
|
|
87
|
+
|
|
88
|
+
let editRequestId = null;
|
|
89
|
+
if (!Object.keys(updates).length) {
|
|
90
|
+
result.warning = "正規施設への追記差分なし(認定・タグは既に付与済みの可能性)";
|
|
91
|
+
} else {
|
|
92
|
+
const resp = await client.updateFacility(canonId, updates);
|
|
93
|
+
editRequestId = resp.edit_request_id;
|
|
94
|
+
if (!editRequestId) {
|
|
95
|
+
result.warning = "編集依頼が作成されなかった(差分なし扱い)";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const withdraw = await client.withdrawFacility(duplicateId);
|
|
99
|
+
result.edit_request_id = editRequestId;
|
|
100
|
+
result.withdraw = withdraw;
|
|
101
|
+
return result;
|
|
102
|
+
}
|
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、" +
|
|
@@ -88,6 +88,81 @@ server.tool(
|
|
|
88
88
|
}
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
+
server.tool(
|
|
92
|
+
"withdraw_facility",
|
|
93
|
+
"未承認 Agent 新規施設を取り下げ(削除)。重複誤登録の整理用。",
|
|
94
|
+
{ facility_id: z.number() },
|
|
95
|
+
async ({ facility_id }) => {
|
|
96
|
+
try {
|
|
97
|
+
return textResult(dump(await client().withdrawFacility(facility_id)));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return textResult(errText(e));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
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
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
"remediate_duplicate_facility",
|
|
144
|
+
"正規施設へ編集依頼(備考に重複整理メモ)→ 誤登録を withdraw。",
|
|
145
|
+
{
|
|
146
|
+
duplicate_id: z.number(),
|
|
147
|
+
canonical_id: z.number().optional(),
|
|
148
|
+
dry_run: z.boolean().optional(),
|
|
149
|
+
},
|
|
150
|
+
async ({ duplicate_id, canonical_id, dry_run }) => {
|
|
151
|
+
try {
|
|
152
|
+
return textResult(
|
|
153
|
+
dump(
|
|
154
|
+
await client().remediateDuplicateFacility(duplicate_id, {
|
|
155
|
+
canonicalId: canonical_id ?? null,
|
|
156
|
+
dryRun: dry_run ?? false,
|
|
157
|
+
})
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return textResult(errText(e));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
91
166
|
server.tool(
|
|
92
167
|
"find_existing_facility",
|
|
93
168
|
"登録前に既存施設を照合(未承認含む)。ヒット時は update_facility、無ければ新規 register_facility。",
|
|
@@ -122,7 +197,7 @@ server.tool(
|
|
|
122
197
|
|
|
123
198
|
server.tool(
|
|
124
199
|
"register_facility",
|
|
125
|
-
"施設を1件登録する(FacilityCreate 形式)。新規前に find_existing_facility → prepare_facility_for_register
|
|
200
|
+
"施設を1件登録する(FacilityCreate 形式)。新規前に find_existing_facility → prepare_facility_for_register 推奨。既存ヒットで編集依頼になる場合は source_link 必須。",
|
|
126
201
|
{ facility: dict },
|
|
127
202
|
async ({ facility }) => {
|
|
128
203
|
try {
|
|
@@ -208,7 +283,7 @@ server.tool(
|
|
|
208
283
|
|
|
209
284
|
server.tool(
|
|
210
285
|
"update_facility",
|
|
211
|
-
"既存施設を部分更新する(Agent キー時は編集依頼)。",
|
|
286
|
+
"既存施設を部分更新する(Agent キー時は編集依頼)。updates に source_link(根拠 URL・http/https)必須。",
|
|
212
287
|
{ facility_id: z.number(), updates: dict },
|
|
213
288
|
async ({ facility_id, updates }) => {
|
|
214
289
|
try {
|