poi-plugin-mcp 0.2.16 → 0.2.21
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 +290 -201
- package/index.js +3 -0
- package/lib/bridge-controller.js +75 -1
- package/lib/fleet-metrics.js +249 -0
- package/lib/poi-action-events.js +405 -292
- package/lib/poi-api-responses.js +165 -0
- package/lib/poi-auto-interaction-recorder.js +399 -0
- package/lib/poi-data-query.js +254 -0
- package/lib/poi-http-bridge.js +1673 -1320
- package/lib/poi-input.js +366 -302
- package/lib/poi-interaction-recorder.js +1693 -0
- package/lib/poi-telemetry.js +515 -439
- package/lib/poi-webview-runtime.js +1181 -0
- package/lib/settings-view.js +112 -0
- package/lib/settings.js +8 -0
- package/mcp-server.js +542 -456
- package/package.json +3 -3
package/mcp-server.js
CHANGED
|
@@ -1,456 +1,542 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// poi-mcp — MCP Server for KanColle game data
|
|
3
|
-
//
|
|
4
|
-
// 两种使用方式:
|
|
5
|
-
// 方式 A: 配合 POI DevTools 脚本 (推荐, 最稳定)
|
|
6
|
-
// 方式 B: 配合 POI 的 --remote-debugging-port
|
|
7
|
-
//
|
|
8
|
-
// ── 快速开始 ──
|
|
9
|
-
// 1. 启动 POI,进入游戏母港
|
|
10
|
-
// 2. POI 菜单 → 开发工具 → 切换开发工具 (F12)
|
|
11
|
-
// 3. 在 Console 中粘贴下面这段脚本:
|
|
12
|
-
//
|
|
13
|
-
// fetch('https://raw.githubusercontent.com/your/poi-plugin-mcp/main/inject.js')
|
|
14
|
-
// .then(r => r.text())
|
|
15
|
-
// .then(eval)
|
|
16
|
-
//
|
|
17
|
-
// 4. 脚本会自动启动 HTTP 服务并写入端口号到 ~/.poi-mcp/port
|
|
18
|
-
// 5. 然后运行: node mcp-server.js
|
|
19
|
-
//
|
|
20
|
-
// ── 备用方案: 粘贴下面脚本到 Console ──
|
|
21
|
-
// (function(){
|
|
22
|
-
// var port = 17777;
|
|
23
|
-
// var http = new XMLHttpRequest();
|
|
24
|
-
// http.open('GET', 'http://127.0.0.1:' + port + '/health', true);
|
|
25
|
-
// http.onload = function() {
|
|
26
|
-
// if (http.status === 200) console.log('[poi-mcp] Server already running on port', port);
|
|
27
|
-
// };
|
|
28
|
-
// http.send();
|
|
29
|
-
// var s = document.createElement('script');
|
|
30
|
-
// s.src = 'data:text/javascript,' + encodeURIComponent([
|
|
31
|
-
// 'var p='+port+';',
|
|
32
|
-
// 'var gs=function(){return window.getStore()};',
|
|
33
|
-
// 'var s=require("http").createServer(function(q,r){',
|
|
34
|
-
// ' r.setHeader("Access-Control-Allow-Origin","*");',
|
|
35
|
-
// ' r.setHeader("Content-Type","application/json");',
|
|
36
|
-
// ' var u=q.url;',
|
|
37
|
-
// ' if(u==="/health"){r.end('+JSON.stringify(JSON.stringify({status:"ok"}))+')}',
|
|
38
|
-
// ' else if(u==="/fleets"){r.end(JSON.stringify(gs().info.fleets))}',
|
|
39
|
-
// ' else if(u==="/ships"){r.end(JSON.stringify(gs().info.ships))}',
|
|
40
|
-
// ' else if(u==="/equipment"){r.end(JSON.stringify(gs().info.equips))}',
|
|
41
|
-
// ' else if(u==="/resources"){r.end(JSON.stringify(gs().info.resources))}',
|
|
42
|
-
// ' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:gs().info.quests.activeQuests,records:gs().info.quests.records}))}',
|
|
43
|
-
// ' else if(u==="/airbase"){r.end(JSON.stringify(gs().info.airbase))}',
|
|
44
|
-
// ' else if(u==="/basic"){r.end(JSON.stringify(gs().info.basic))}',
|
|
45
|
-
// ' else if(u==="/all"){r.end(JSON.stringify(gs().info))}',
|
|
46
|
-
// ' else{r.writeHead(404);r.end("Not found")}',
|
|
47
|
-
// '});',
|
|
48
|
-
// 's.listen(p,"127.0.0.1",function(){',
|
|
49
|
-
// ' require("fs").writeFileSync("'+require('path').join(os.homedir(),'.poi-mcp','port').replace(/\\/g,'/')+'",String(p),"utf8");',
|
|
50
|
-
// ' console.log("[poi-mcp] API running on http://127.0.0.1:"+p);',
|
|
51
|
-
// '});'
|
|
52
|
-
// ].join(''));
|
|
53
|
-
// document.head.appendChild(s);
|
|
54
|
-
// })();
|
|
55
|
-
|
|
56
|
-
const os = require('os')
|
|
57
|
-
const path = require('path')
|
|
58
|
-
const fs = require('fs')
|
|
59
|
-
const http = require('http')
|
|
60
|
-
const packageJson = require('./package.json')
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'
|
|
114
|
-
'
|
|
115
|
-
'
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'
|
|
119
|
-
'
|
|
120
|
-
'
|
|
121
|
-
' else if(u==="/
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'
|
|
129
|
-
'
|
|
130
|
-
'
|
|
131
|
-
'
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'
|
|
135
|
-
'
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'});',
|
|
139
|
-
'})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
'
|
|
310
|
-
'
|
|
311
|
-
'
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
})
|
|
424
|
-
break
|
|
425
|
-
|
|
426
|
-
case '
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// poi-mcp — MCP Server for KanColle game data
|
|
3
|
+
//
|
|
4
|
+
// 两种使用方式:
|
|
5
|
+
// 方式 A: 配合 POI DevTools 脚本 (推荐, 最稳定)
|
|
6
|
+
// 方式 B: 配合 POI 的 --remote-debugging-port
|
|
7
|
+
//
|
|
8
|
+
// ── 快速开始 ──
|
|
9
|
+
// 1. 启动 POI,进入游戏母港
|
|
10
|
+
// 2. POI 菜单 → 开发工具 → 切换开发工具 (F12)
|
|
11
|
+
// 3. 在 Console 中粘贴下面这段脚本:
|
|
12
|
+
//
|
|
13
|
+
// fetch('https://raw.githubusercontent.com/your/poi-plugin-mcp/main/inject.js')
|
|
14
|
+
// .then(r => r.text())
|
|
15
|
+
// .then(eval)
|
|
16
|
+
//
|
|
17
|
+
// 4. 脚本会自动启动 HTTP 服务并写入端口号到 ~/.poi-mcp/port
|
|
18
|
+
// 5. 然后运行: node mcp-server.js
|
|
19
|
+
//
|
|
20
|
+
// ── 备用方案: 粘贴下面脚本到 Console ──
|
|
21
|
+
// (function(){
|
|
22
|
+
// var port = 17777;
|
|
23
|
+
// var http = new XMLHttpRequest();
|
|
24
|
+
// http.open('GET', 'http://127.0.0.1:' + port + '/health', true);
|
|
25
|
+
// http.onload = function() {
|
|
26
|
+
// if (http.status === 200) console.log('[poi-mcp] Server already running on port', port);
|
|
27
|
+
// };
|
|
28
|
+
// http.send();
|
|
29
|
+
// var s = document.createElement('script');
|
|
30
|
+
// s.src = 'data:text/javascript,' + encodeURIComponent([
|
|
31
|
+
// 'var p='+port+';',
|
|
32
|
+
// 'var gs=function(){return window.getStore()};',
|
|
33
|
+
// 'var s=require("http").createServer(function(q,r){',
|
|
34
|
+
// ' r.setHeader("Access-Control-Allow-Origin","*");',
|
|
35
|
+
// ' r.setHeader("Content-Type","application/json");',
|
|
36
|
+
// ' var u=q.url;',
|
|
37
|
+
// ' if(u==="/health"){r.end('+JSON.stringify(JSON.stringify({status:"ok"}))+')}',
|
|
38
|
+
// ' else if(u==="/fleets"){r.end(JSON.stringify(gs().info.fleets))}',
|
|
39
|
+
// ' else if(u==="/ships"){r.end(JSON.stringify(gs().info.ships))}',
|
|
40
|
+
// ' else if(u==="/equipment"){r.end(JSON.stringify(gs().info.equips))}',
|
|
41
|
+
// ' else if(u==="/resources"){r.end(JSON.stringify(gs().info.resources))}',
|
|
42
|
+
// ' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:gs().info.quests.activeQuests,records:gs().info.quests.records}))}',
|
|
43
|
+
// ' else if(u==="/airbase"){r.end(JSON.stringify(gs().info.airbase))}',
|
|
44
|
+
// ' else if(u==="/basic"){r.end(JSON.stringify(gs().info.basic))}',
|
|
45
|
+
// ' else if(u==="/all"){r.end(JSON.stringify(gs().info))}',
|
|
46
|
+
// ' else{r.writeHead(404);r.end("Not found")}',
|
|
47
|
+
// '});',
|
|
48
|
+
// 's.listen(p,"127.0.0.1",function(){',
|
|
49
|
+
// ' require("fs").writeFileSync("'+require('path').join(os.homedir(),'.poi-mcp','port').replace(/\\/g,'/')+'",String(p),"utf8");',
|
|
50
|
+
// ' console.log("[poi-mcp] API running on http://127.0.0.1:"+p);',
|
|
51
|
+
// '});'
|
|
52
|
+
// ].join(''));
|
|
53
|
+
// document.head.appendChild(s);
|
|
54
|
+
// })();
|
|
55
|
+
|
|
56
|
+
const os = require('os')
|
|
57
|
+
const path = require('path')
|
|
58
|
+
const fs = require('fs')
|
|
59
|
+
const http = require('http')
|
|
60
|
+
const packageJson = require('./package.json')
|
|
61
|
+
const {
|
|
62
|
+
collectFleetMetricShips,
|
|
63
|
+
inspectFleetMetrics,
|
|
64
|
+
moraleMeaning,
|
|
65
|
+
speedFromRaw,
|
|
66
|
+
speedMeaning,
|
|
67
|
+
} = require('./lib/fleet-metrics')
|
|
68
|
+
|
|
69
|
+
const PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
|
|
70
|
+
|
|
71
|
+
// ─── POI HTTP API Client ─────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function getPoiPort() {
|
|
74
|
+
try {
|
|
75
|
+
return parseInt(fs.readFileSync(PORT_FILE, 'utf8').trim(), 10)
|
|
76
|
+
} catch (_) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fetchFromPoi(endpoint) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const port = getPoiPort()
|
|
84
|
+
if (!port) {
|
|
85
|
+
return reject(new Error(
|
|
86
|
+
'POI data API not found.\n\n' +
|
|
87
|
+
'Please:\n' +
|
|
88
|
+
' 1. Open POI → F12 (DevTools) → Console tab\n' +
|
|
89
|
+
' 2. Paste this script and press Enter:\n\n' +
|
|
90
|
+
'─── PASTE THIS INTO POI CONSOLE ───\n' +
|
|
91
|
+
getInjectScript() +
|
|
92
|
+
'\n─── END ───\n\n' +
|
|
93
|
+
' 3. Then run this MCP server again.'
|
|
94
|
+
))
|
|
95
|
+
}
|
|
96
|
+
http.get(`http://127.0.0.1:${port}${endpoint}`, (res) => {
|
|
97
|
+
let data = ''
|
|
98
|
+
res.on('data', chunk => data += chunk)
|
|
99
|
+
res.on('end', () => {
|
|
100
|
+
try { resolve(JSON.parse(data)) } catch (e) { reject(e) }
|
|
101
|
+
})
|
|
102
|
+
}).on('error', reject).setTimeout(10000, function() {
|
|
103
|
+
this.destroy()
|
|
104
|
+
reject(new Error('Request timed out'))
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getInjectScript() {
|
|
110
|
+
return [
|
|
111
|
+
'(function(){',
|
|
112
|
+
'var p=17777;',
|
|
113
|
+
'var s=require("http").createServer(function(q,r){',
|
|
114
|
+
' r.setHeader("Access-Control-Allow-Origin","*");',
|
|
115
|
+
' r.setHeader("Content-Type","application/json");',
|
|
116
|
+
' try{',
|
|
117
|
+
' var st=window.getStore();',
|
|
118
|
+
' if(!st||!st.info)throw new Error("Store not ready");',
|
|
119
|
+
' var u=q.url;',
|
|
120
|
+
' if(u==="/health"){r.end(JSON.stringify({status:"ok"}))}',
|
|
121
|
+
' else if(u==="/fleets"){r.end(JSON.stringify(st.info.fleets||[]))}',
|
|
122
|
+
' else if(u==="/ships"){r.end(JSON.stringify(st.info.ships||{}))}',
|
|
123
|
+
' else if(u==="/equipment"){r.end(JSON.stringify(st.info.equips||{}))}',
|
|
124
|
+
' else if(u==="/resources"){r.end(JSON.stringify(st.info.resources||[]))}',
|
|
125
|
+
' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:st.info.quests?.activeQuests||{},records:st.info.quests?.records||{}}))}',
|
|
126
|
+
' else if(u==="/airbase"){r.end(JSON.stringify(st.info.airbase||[]))}',
|
|
127
|
+
' else if(u==="/basic"){r.end(JSON.stringify(st.info.basic||{}))}',
|
|
128
|
+
' else if(u==="/all"){r.end(JSON.stringify({',
|
|
129
|
+
' basic:st.info.basic,',
|
|
130
|
+
' fleets:st.info.fleets,',
|
|
131
|
+
' ships:st.info.ships,',
|
|
132
|
+
' equipment:st.info.equips,',
|
|
133
|
+
' resources:st.info.resources,',
|
|
134
|
+
' quests:{activeQuests:st.info.quests?.activeQuests,records:st.info.quests?.records},',
|
|
135
|
+
' airbase:st.info.airbase',
|
|
136
|
+
' }))}',
|
|
137
|
+
' else{r.writeHead(404);r.end("Not found")}',
|
|
138
|
+
' }catch(e){r.writeHead(500);r.end(e.message)}',
|
|
139
|
+
'});',
|
|
140
|
+
's.listen(p,"127.0.0.1",function(){',
|
|
141
|
+
' var d=require("path").join(require("os").homedir(),".poi-mcp");',
|
|
142
|
+
' try{require("fs").mkdirSync(d,{recursive:true})}catch(e){}',
|
|
143
|
+
' require("fs").writeFileSync(require("path").join(d,"port"),String(p),"utf8");',
|
|
144
|
+
' console.log("[poi-mcp] API: http://127.0.0.1:"+p+" (/fleets /ships /equipment /resources /quests /airbase /basic /all)");',
|
|
145
|
+
'});',
|
|
146
|
+
'})()'
|
|
147
|
+
].join('\n')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function fetchMasterData() {
|
|
151
|
+
try {
|
|
152
|
+
return await fetchFromPoi('/master')
|
|
153
|
+
} catch (_) {
|
|
154
|
+
return { ships: {}, equipment: {}, shipTypes: {}, equipmentTypes: {} }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function enrichShip(ship, master) {
|
|
159
|
+
const masterShip = master.ships && master.ships[ship.api_ship_id]
|
|
160
|
+
const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
...ship,
|
|
164
|
+
instanceId: ship.api_id,
|
|
165
|
+
masterId: ship.api_ship_id,
|
|
166
|
+
name: (masterShip && masterShip.api_name) || '',
|
|
167
|
+
typeName: (shipType && shipType.api_name) || '',
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function enrichEquipment(equip, master) {
|
|
172
|
+
const masterEquip = master.equipment && master.equipment[equip.api_slotitem_id]
|
|
173
|
+
const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
|
|
174
|
+
const typeId = typeIds[2] || typeIds[1] || typeIds[0]
|
|
175
|
+
const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...equip,
|
|
179
|
+
instanceId: equip.api_id,
|
|
180
|
+
masterId: equip.api_slotitem_id,
|
|
181
|
+
name: (masterEquip && masterEquip.api_name) || '',
|
|
182
|
+
typeName: (equipType && equipType.api_name) || '',
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function projectFleetShip(ship, shipId, position, equips, names, master) {
|
|
187
|
+
if (!ship) return { id: shipId, position }
|
|
188
|
+
|
|
189
|
+
const masterShip = master.ships && master.ships[ship.api_ship_id]
|
|
190
|
+
const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
|
|
191
|
+
const maxHp = Number(ship.api_maxhp) || 0
|
|
192
|
+
const nameMap = (names && names.ships) || {}
|
|
193
|
+
const equipNames = (names && names.equipment) || {}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
position,
|
|
197
|
+
id: ship.api_id,
|
|
198
|
+
shipId: ship.api_ship_id,
|
|
199
|
+
masterId: ship.api_ship_id,
|
|
200
|
+
name: nameMap[ship.api_ship_id] || (masterShip && masterShip.api_name) || '',
|
|
201
|
+
typeName: (shipType && shipType.api_name) || '',
|
|
202
|
+
stype: (masterShip && masterShip.api_stype) || null,
|
|
203
|
+
level: ship.api_lv,
|
|
204
|
+
hp: `${ship.api_nowhp}/${ship.api_maxhp}`,
|
|
205
|
+
hpMod4: maxHp % 4,
|
|
206
|
+
morale: ship.api_cond,
|
|
207
|
+
moraleMeaning: moraleMeaning(ship.api_cond || 0),
|
|
208
|
+
speed: Number(ship.api_soku ?? (masterShip && masterShip.api_soku) ?? 0),
|
|
209
|
+
speedMeaning: speedMeaning(speedFromRaw(Number(ship.api_soku ?? (masterShip && masterShip.api_soku) ?? 0))),
|
|
210
|
+
fuel: ship.api_fuel,
|
|
211
|
+
ammo: ship.api_bull,
|
|
212
|
+
locked: ship.api_locked,
|
|
213
|
+
slotnum: ship.api_slotnum || (ship.api_slot || []).filter((id) => id !== -1).length,
|
|
214
|
+
onslot: Array.isArray(ship.api_onslot) ? ship.api_onslot : [],
|
|
215
|
+
sallyArea: ship.api_sally_area || 0,
|
|
216
|
+
fire: ship.api_karyoku || null,
|
|
217
|
+
torp: ship.api_raisou || null,
|
|
218
|
+
aa: ship.api_taiku || null,
|
|
219
|
+
armor: ship.api_soukou || null,
|
|
220
|
+
luck: ship.api_lucky || null,
|
|
221
|
+
los: ship.api_sakuteki || null,
|
|
222
|
+
asw: ship.api_taisen || null,
|
|
223
|
+
slotItems: (ship.api_slot || [])
|
|
224
|
+
.filter((equipId) => equipId > 0)
|
|
225
|
+
.map((equipId) => describeEquip(equipId, equips, { equipment: equipNames }, master))
|
|
226
|
+
.filter(Boolean),
|
|
227
|
+
expansion: describeExpansion(ship.api_slot_ex, equips, { equipment: equipNames }, master),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function describeEquip(equipId, equips, names, master) {
|
|
232
|
+
if (!equipId || equipId <= 0) return null
|
|
233
|
+
const equip = equips[equipId]
|
|
234
|
+
if (!equip) return { id: equipId, missing: true }
|
|
235
|
+
const masterId = equip.api_slotitem_id
|
|
236
|
+
const masterEquip = master.equipment && master.equipment[masterId]
|
|
237
|
+
const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
|
|
238
|
+
const typeId = typeIds[2] || typeIds[1] || typeIds[0]
|
|
239
|
+
const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
|
|
240
|
+
return {
|
|
241
|
+
id: equip.api_id,
|
|
242
|
+
equipId: masterId,
|
|
243
|
+
name:
|
|
244
|
+
(names.equipment && names.equipment[masterId]) ||
|
|
245
|
+
(masterEquip && masterEquip.api_name) ||
|
|
246
|
+
'',
|
|
247
|
+
typeName: (equipType && equipType.api_name) || '',
|
|
248
|
+
level: equip.api_level || 0,
|
|
249
|
+
prof: equip.api_alv || 0,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function describeExpansion(rawEx, equips, names, master) {
|
|
254
|
+
const raw = Number(rawEx)
|
|
255
|
+
if (!Number.isFinite(raw) || raw === 0) {
|
|
256
|
+
return { raw: Number.isFinite(raw) ? raw : 0, state: 'closed', meaning: '未开孔', item: null }
|
|
257
|
+
}
|
|
258
|
+
if (raw < 0) {
|
|
259
|
+
return { raw, state: 'open_empty', meaning: '已开孔但为空', item: null }
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
raw,
|
|
263
|
+
state: 'equipped',
|
|
264
|
+
meaning: '已装备',
|
|
265
|
+
item: describeEquip(raw, equips, names, master),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function fetchAllData(args = {}) {
|
|
270
|
+
const payload = await fetchFromPoi('/all')
|
|
271
|
+
const include = Array.isArray(args.include) ? new Set(args.include) : new Set()
|
|
272
|
+
|
|
273
|
+
if (include.has('master')) payload.master = await fetchFromPoi('/master')
|
|
274
|
+
if (include.has('event')) payload.event = await fetchFromPoi('/event')
|
|
275
|
+
if (include.has('planner')) payload.planner = await fetchFromPoi('/planner')
|
|
276
|
+
|
|
277
|
+
return payload
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── MCP Protocol ────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
// Minimum viable MCP stdio server — no external dependencies
|
|
283
|
+
|
|
284
|
+
const JSONRPC_VERSION = '2.0'
|
|
285
|
+
let reqId = 0
|
|
286
|
+
|
|
287
|
+
function send(id, result, error) {
|
|
288
|
+
const msg = { jsonrpc: JSONRPC_VERSION }
|
|
289
|
+
if (id != null) msg.id = id
|
|
290
|
+
if (error) msg.error = { code: error.code || -32603, message: error.message }
|
|
291
|
+
else msg.result = result
|
|
292
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sendLog(text) {
|
|
296
|
+
console.error('[poi-mcp] ' + text)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
sendLog(`POI MCP Server v${packageJson.version}`)
|
|
303
|
+
sendLog('Checking POI data API...')
|
|
304
|
+
|
|
305
|
+
const port = getPoiPort()
|
|
306
|
+
if (!port) {
|
|
307
|
+
sendLog('NOT CONNECTED — POI DevTools script not running')
|
|
308
|
+
sendLog('')
|
|
309
|
+
sendLog('=== 请在 POI 中执行以下步骤 ===')
|
|
310
|
+
sendLog('1. 启动 POI,进入游戏母港')
|
|
311
|
+
sendLog('2. 按 F12 打开 DevTools → Console 标签')
|
|
312
|
+
sendLog('3. 粘贴下面一整段脚本,按回车:')
|
|
313
|
+
sendLog('')
|
|
314
|
+
console.error(getInjectScript())
|
|
315
|
+
sendLog('')
|
|
316
|
+
sendLog('4. 关闭 DevTools,重新运行本命令')
|
|
317
|
+
process.exit(1)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Verify the API is responding
|
|
321
|
+
try {
|
|
322
|
+
const health = await fetchFromPoi('/health')
|
|
323
|
+
sendLog(`Connected to POI API on port ${port}: ${health.status}`)
|
|
324
|
+
} catch (err) {
|
|
325
|
+
sendLog(`ERROR: POI API on port ${port} is not responding: ${err.message}`)
|
|
326
|
+
sendLog('Make sure POI is running and the script was pasted into DevTools Console.')
|
|
327
|
+
process.exit(1)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── MCP Request Handler ──────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
const toolHandlers = {
|
|
333
|
+
get_fleet_status: async (args) => {
|
|
334
|
+
const fleets = await fetchFromPoi('/fleets')
|
|
335
|
+
if (!Array.isArray(fleets)) return { error: 'No fleet data' }
|
|
336
|
+
const fleet = fleets[args.fleetId - 1]
|
|
337
|
+
if (!fleet) return { error: `Fleet #${args.fleetId} not found` }
|
|
338
|
+
|
|
339
|
+
const [ships, equips, names, master, basic] = await Promise.all([
|
|
340
|
+
fetchFromPoi('/ships'),
|
|
341
|
+
fetchFromPoi('/equipment'),
|
|
342
|
+
fetchFromPoi('/names').catch(() => ({ ships: {}, equipment: {} })),
|
|
343
|
+
fetchMasterData(),
|
|
344
|
+
fetchFromPoi('/basic').catch(() => ({})),
|
|
345
|
+
])
|
|
346
|
+
const hqLevel = Number(basic && basic.api_level)
|
|
347
|
+
const metrics = Number.isInteger(hqLevel) && hqLevel >= 1
|
|
348
|
+
? inspectFleetMetrics(collectFleetMetricShips(fleet, ships, equips, master), hqLevel)
|
|
349
|
+
: null
|
|
350
|
+
return {
|
|
351
|
+
id: fleet.api_id,
|
|
352
|
+
name: fleet.api_name,
|
|
353
|
+
mission: fleet.api_mission,
|
|
354
|
+
metrics,
|
|
355
|
+
ships: (fleet.api_ship || []).filter(id => id > 0).map((sid, index) =>
|
|
356
|
+
projectFleetShip(ships[sid], sid, index + 1, equips, names, master),
|
|
357
|
+
),
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
search_ships: async (args) => {
|
|
362
|
+
const ships = await fetchFromPoi('/ships')
|
|
363
|
+
const master = await fetchMasterData()
|
|
364
|
+
const results = Object.values(ships).filter(s => {
|
|
365
|
+
if (!s) return false
|
|
366
|
+
if (args.minLevel != null && s.api_lv < args.minLevel) return false
|
|
367
|
+
if (args.maxLevel != null && s.api_lv > args.maxLevel) return false
|
|
368
|
+
if (args.minMorale != null && s.api_cond < args.minMorale) return false
|
|
369
|
+
return true
|
|
370
|
+
}).map(s => enrichShip(s, master))
|
|
371
|
+
return { total: results.length, ships: results }
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
search_equipment: async (args) => {
|
|
375
|
+
const equips = await fetchFromPoi('/equipment')
|
|
376
|
+
const master = await fetchMasterData()
|
|
377
|
+
const results = Object.values(equips).filter(e => {
|
|
378
|
+
if (!e) return false
|
|
379
|
+
if (args.minLevel != null && (e.api_level || 0) < args.minLevel) return false
|
|
380
|
+
return true
|
|
381
|
+
}).map(e => enrichEquipment(e, master))
|
|
382
|
+
return { total: results.length, equipment: results }
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
get_resources: async () => {
|
|
386
|
+
return await fetchFromPoi('/resources')
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
get_all: async (args) => {
|
|
390
|
+
return await fetchAllData(args)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const resourceUris = [
|
|
395
|
+
'poi://fleets', 'poi://ships', 'poi://equipment',
|
|
396
|
+
'poi://resources', 'poi://quests', 'poi://airbase', 'poi://basic',
|
|
397
|
+
'poi://names', 'poi://master', 'poi://event', 'poi://planner', 'poi://all'
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
// ── JSON-RPC over stdio ──────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
let buffer = ''
|
|
403
|
+
process.stdin.setEncoding('utf8')
|
|
404
|
+
process.stdin.on('data', async (chunk) => {
|
|
405
|
+
buffer += chunk
|
|
406
|
+
const lines = buffer.split('\n')
|
|
407
|
+
buffer = lines.pop() || ''
|
|
408
|
+
for (const line of lines) {
|
|
409
|
+
if (!line.trim()) continue
|
|
410
|
+
try {
|
|
411
|
+
const req = JSON.parse(line)
|
|
412
|
+
const { id, method, params } = req
|
|
413
|
+
|
|
414
|
+
switch (method) {
|
|
415
|
+
case 'initialize':
|
|
416
|
+
send(id, {
|
|
417
|
+
protocolVersion: '2024-11-05',
|
|
418
|
+
capabilities: {
|
|
419
|
+
resources: { subscribe: false },
|
|
420
|
+
tools: {}
|
|
421
|
+
},
|
|
422
|
+
serverInfo: { name: 'poi-mcp', version: packageJson.version }
|
|
423
|
+
})
|
|
424
|
+
break
|
|
425
|
+
|
|
426
|
+
case 'notifications/initialized':
|
|
427
|
+
case 'notifications/cancelled':
|
|
428
|
+
break
|
|
429
|
+
|
|
430
|
+
case 'ping':
|
|
431
|
+
send(id, {})
|
|
432
|
+
break
|
|
433
|
+
|
|
434
|
+
case 'resources/list':
|
|
435
|
+
send(id, {
|
|
436
|
+
resources: resourceUris.map(uri => ({
|
|
437
|
+
uri, name: uri.replace('poi://', ''), mimeType: 'application/json'
|
|
438
|
+
}))
|
|
439
|
+
})
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
case 'resources/read': {
|
|
443
|
+
const uri = params?.uri
|
|
444
|
+
const endpoint = '/' + uri.replace('poi://', '')
|
|
445
|
+
const data = await fetchFromPoi(endpoint)
|
|
446
|
+
send(id, {
|
|
447
|
+
contents: [{
|
|
448
|
+
uri,
|
|
449
|
+
mimeType: 'application/json',
|
|
450
|
+
text: JSON.stringify(data, null, 2)
|
|
451
|
+
}]
|
|
452
|
+
})
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case 'tools/list':
|
|
457
|
+
send(id, {
|
|
458
|
+
tools: [
|
|
459
|
+
{
|
|
460
|
+
name: 'get_fleet_status',
|
|
461
|
+
description: '读取一支舰队:舰名、装备、补强、速度、士气、33式索敌、制空。看一队时不要用 get_all。',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: { fleetId: { type: 'number', description: '舰队编号 1-4' } },
|
|
465
|
+
required: ['fleetId']
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'search_ships',
|
|
470
|
+
description: '搜索舰娘',
|
|
471
|
+
inputSchema: {
|
|
472
|
+
type: 'object',
|
|
473
|
+
properties: {
|
|
474
|
+
minLevel: { type: 'number' },
|
|
475
|
+
maxLevel: { type: 'number' },
|
|
476
|
+
minMorale: { type: 'number', description: '最低士气(闪)' }
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: 'search_equipment',
|
|
482
|
+
description: '搜索装备',
|
|
483
|
+
inputSchema: {
|
|
484
|
+
type: 'object',
|
|
485
|
+
properties: {
|
|
486
|
+
minLevel: { type: 'number', description: '最低改修★' }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'get_resources',
|
|
492
|
+
description: '获取资源概况',
|
|
493
|
+
inputSchema: { type: 'object', properties: {} }
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'get_all',
|
|
497
|
+
description: '整包账号转储。读一队请用 get_fleet_status;资源用 get_resources。',
|
|
498
|
+
inputSchema: {
|
|
499
|
+
type: 'object',
|
|
500
|
+
properties: {
|
|
501
|
+
include: {
|
|
502
|
+
type: 'array',
|
|
503
|
+
items: { type: 'string', enum: ['master', 'event', 'planner'] }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
})
|
|
510
|
+
break
|
|
511
|
+
|
|
512
|
+
case 'tools/call': {
|
|
513
|
+
const toolName = params?.name
|
|
514
|
+
const toolArgs = params?.arguments || {}
|
|
515
|
+
const handler = toolHandlers[toolName]
|
|
516
|
+
if (handler) {
|
|
517
|
+
const result = await handler(toolArgs)
|
|
518
|
+
send(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] })
|
|
519
|
+
} else {
|
|
520
|
+
send(id, null, { code: -32602, message: `Unknown tool: ${toolName}` })
|
|
521
|
+
}
|
|
522
|
+
break
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
default:
|
|
526
|
+
send(id, null, { code: -32601, message: `Unknown method: ${method}` })
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
// Malformed JSON — ignore
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
process.stdin.on('end', () => process.exit(0))
|
|
535
|
+
process.on('SIGINT', () => process.exit(0))
|
|
536
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
main().catch(err => {
|
|
540
|
+
console.error('[poi-mcp] Fatal:', err.message)
|
|
541
|
+
process.exit(1)
|
|
542
|
+
})
|