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/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
- const PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
63
-
64
- // ─── POI HTTP API Client ─────────────────────────────────────────────────────
65
-
66
- function getPoiPort() {
67
- try {
68
- return parseInt(fs.readFileSync(PORT_FILE, 'utf8').trim(), 10)
69
- } catch (_) {
70
- return null
71
- }
72
- }
73
-
74
- function fetchFromPoi(endpoint) {
75
- return new Promise((resolve, reject) => {
76
- const port = getPoiPort()
77
- if (!port) {
78
- return reject(new Error(
79
- 'POI data API not found.\n\n' +
80
- 'Please:\n' +
81
- ' 1. Open POI → F12 (DevTools) → Console tab\n' +
82
- ' 2. Paste this script and press Enter:\n\n' +
83
- '─── PASTE THIS INTO POI CONSOLE ───\n' +
84
- getInjectScript() +
85
- '\n─── END ───\n\n' +
86
- ' 3. Then run this MCP server again.'
87
- ))
88
- }
89
- http.get(`http://127.0.0.1:${port}${endpoint}`, (res) => {
90
- let data = ''
91
- res.on('data', chunk => data += chunk)
92
- res.on('end', () => {
93
- try { resolve(JSON.parse(data)) } catch (e) { reject(e) }
94
- })
95
- }).on('error', reject).setTimeout(10000, function() {
96
- this.destroy()
97
- reject(new Error('Request timed out'))
98
- })
99
- })
100
- }
101
-
102
- function getInjectScript() {
103
- return [
104
- '(function(){',
105
- 'var p=17777;',
106
- 'var s=require("http").createServer(function(q,r){',
107
- ' r.setHeader("Access-Control-Allow-Origin","*");',
108
- ' r.setHeader("Content-Type","application/json");',
109
- ' try{',
110
- ' var st=window.getStore();',
111
- ' if(!st||!st.info)throw new Error("Store not ready");',
112
- ' var u=q.url;',
113
- ' if(u==="/health"){r.end(JSON.stringify({status:"ok"}))}',
114
- ' else if(u==="/fleets"){r.end(JSON.stringify(st.info.fleets||[]))}',
115
- ' else if(u==="/ships"){r.end(JSON.stringify(st.info.ships||{}))}',
116
- ' else if(u==="/equipment"){r.end(JSON.stringify(st.info.equips||{}))}',
117
- ' else if(u==="/resources"){r.end(JSON.stringify(st.info.resources||[]))}',
118
- ' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:st.info.quests?.activeQuests||{},records:st.info.quests?.records||{}}))}',
119
- ' else if(u==="/airbase"){r.end(JSON.stringify(st.info.airbase||[]))}',
120
- ' else if(u==="/basic"){r.end(JSON.stringify(st.info.basic||{}))}',
121
- ' else if(u==="/all"){r.end(JSON.stringify({',
122
- ' basic:st.info.basic,',
123
- ' fleets:st.info.fleets,',
124
- ' ships:st.info.ships,',
125
- ' equipment:st.info.equips,',
126
- ' resources:st.info.resources,',
127
- ' quests:{activeQuests:st.info.quests?.activeQuests,records:st.info.quests?.records},',
128
- ' airbase:st.info.airbase',
129
- ' }))}',
130
- ' else{r.writeHead(404);r.end("Not found")}',
131
- ' }catch(e){r.writeHead(500);r.end(e.message)}',
132
- '});',
133
- 's.listen(p,"127.0.0.1",function(){',
134
- ' var d=require("path").join(require("os").homedir(),".poi-mcp");',
135
- ' try{require("fs").mkdirSync(d,{recursive:true})}catch(e){}',
136
- ' require("fs").writeFileSync(require("path").join(d,"port"),String(p),"utf8");',
137
- ' console.log("[poi-mcp] API: http://127.0.0.1:"+p+" (/fleets /ships /equipment /resources /quests /airbase /basic /all)");',
138
- '});',
139
- '})()'
140
- ].join('\n')
141
- }
142
-
143
- async function fetchMasterData() {
144
- try {
145
- return await fetchFromPoi('/master')
146
- } catch (_) {
147
- return { ships: {}, equipment: {}, shipTypes: {}, equipmentTypes: {} }
148
- }
149
- }
150
-
151
- function enrichShip(ship, master) {
152
- const masterShip = master.ships && master.ships[ship.api_ship_id]
153
- const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
154
-
155
- return {
156
- ...ship,
157
- instanceId: ship.api_id,
158
- masterId: ship.api_ship_id,
159
- name: (masterShip && masterShip.api_name) || '',
160
- typeName: (shipType && shipType.api_name) || '',
161
- }
162
- }
163
-
164
- function enrichEquipment(equip, master) {
165
- const masterEquip = master.equipment && master.equipment[equip.api_slotitem_id]
166
- const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
167
- const typeId = typeIds[2] || typeIds[1] || typeIds[0]
168
- const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
169
-
170
- return {
171
- ...equip,
172
- instanceId: equip.api_id,
173
- masterId: equip.api_slotitem_id,
174
- name: (masterEquip && masterEquip.api_name) || '',
175
- typeName: (equipType && equipType.api_name) || '',
176
- }
177
- }
178
-
179
- async function fetchAllData(args = {}) {
180
- const payload = await fetchFromPoi('/all')
181
- const include = Array.isArray(args.include) ? new Set(args.include) : new Set()
182
-
183
- if (include.has('master')) payload.master = await fetchFromPoi('/master')
184
- if (include.has('event')) payload.event = await fetchFromPoi('/event')
185
- if (include.has('planner')) payload.planner = await fetchFromPoi('/planner')
186
-
187
- return payload
188
- }
189
-
190
- // ─── MCP Protocol ────────────────────────────────────────────────────────────
191
-
192
- // Minimum viable MCP stdio server no external dependencies
193
-
194
- const JSONRPC_VERSION = '2.0'
195
- let reqId = 0
196
-
197
- function send(id, result, error) {
198
- const msg = { jsonrpc: JSONRPC_VERSION }
199
- if (id != null) msg.id = id
200
- if (error) msg.error = { code: error.code || -32603, message: error.message }
201
- else msg.result = result
202
- process.stdout.write(JSON.stringify(msg) + '\n')
203
- }
204
-
205
- function sendLog(text) {
206
- console.error('[poi-mcp] ' + text)
207
- }
208
-
209
- // ─── Main ────────────────────────────────────────────────────────────────────
210
-
211
- async function main() {
212
- sendLog(`POI MCP Server v${packageJson.version}`)
213
- sendLog('Checking POI data API...')
214
-
215
- const port = getPoiPort()
216
- if (!port) {
217
- sendLog('NOT CONNECTED POI DevTools script not running')
218
- sendLog('')
219
- sendLog('=== 请在 POI 中执行以下步骤 ===')
220
- sendLog('1. 启动 POI,进入游戏母港')
221
- sendLog('2. F12 打开 DevTools → Console 标签')
222
- sendLog('3. 粘贴下面一整段脚本,按回车:')
223
- sendLog('')
224
- console.error(getInjectScript())
225
- sendLog('')
226
- sendLog('4. 关闭 DevTools,重新运行本命令')
227
- process.exit(1)
228
- }
229
-
230
- // Verify the API is responding
231
- try {
232
- const health = await fetchFromPoi('/health')
233
- sendLog(`Connected to POI API on port ${port}: ${health.status}`)
234
- } catch (err) {
235
- sendLog(`ERROR: POI API on port ${port} is not responding: ${err.message}`)
236
- sendLog('Make sure POI is running and the script was pasted into DevTools Console.')
237
- process.exit(1)
238
- }
239
-
240
- // ── MCP Request Handler ──────────────────────────────────────────────
241
-
242
- const toolHandlers = {
243
- get_fleet_status: async (args) => {
244
- const fleets = await fetchFromPoi('/fleets')
245
- if (!Array.isArray(fleets)) return { error: 'No fleet data' }
246
- const fleet = fleets[args.fleetId - 1]
247
- if (!fleet) return { error: `Fleet #${args.fleetId} not found` }
248
-
249
- // Enrich with ship names and equipment
250
- const ships = await fetchFromPoi('/ships')
251
- const equips = await fetchFromPoi('/equipment')
252
- return {
253
- id: fleet.api_id,
254
- name: fleet.api_name,
255
- mission: fleet.api_mission,
256
- ships: (fleet.api_ship || []).filter(id => id > 0).map(sid => {
257
- const s = ships[sid]
258
- if (!s) return { id: sid }
259
- return {
260
- id: s.api_id,
261
- shipId: s.api_ship_id,
262
- level: s.api_lv,
263
- hp: `${s.api_nowhp}/${s.api_maxhp}`,
264
- morale: s.api_cond,
265
- locked: s.api_locked,
266
- slotItems: (s.api_slot || []).filter(eid => eid > 0).map(eid => {
267
- const e = equips[eid]
268
- return e ? { equipId: e.api_slotitem_id, level: e.api_level || 0, prof: e.api_alv || 0 } : null
269
- }).filter(Boolean)
270
- }
271
- })
272
- }
273
- },
274
-
275
- search_ships: async (args) => {
276
- const ships = await fetchFromPoi('/ships')
277
- const master = await fetchMasterData()
278
- const results = Object.values(ships).filter(s => {
279
- if (!s) return false
280
- if (args.minLevel != null && s.api_lv < args.minLevel) return false
281
- if (args.maxLevel != null && s.api_lv > args.maxLevel) return false
282
- if (args.minMorale != null && s.api_cond < args.minMorale) return false
283
- return true
284
- }).map(s => enrichShip(s, master))
285
- return { total: results.length, ships: results }
286
- },
287
-
288
- search_equipment: async (args) => {
289
- const equips = await fetchFromPoi('/equipment')
290
- const master = await fetchMasterData()
291
- const results = Object.values(equips).filter(e => {
292
- if (!e) return false
293
- if (args.minLevel != null && (e.api_level || 0) < args.minLevel) return false
294
- return true
295
- }).map(e => enrichEquipment(e, master))
296
- return { total: results.length, equipment: results }
297
- },
298
-
299
- get_resources: async () => {
300
- return await fetchFromPoi('/resources')
301
- },
302
-
303
- get_all: async (args) => {
304
- return await fetchAllData(args)
305
- }
306
- }
307
-
308
- const resourceUris = [
309
- 'poi://fleets', 'poi://ships', 'poi://equipment',
310
- 'poi://resources', 'poi://quests', 'poi://airbase', 'poi://basic',
311
- 'poi://names', 'poi://master', 'poi://event', 'poi://planner', 'poi://all'
312
- ]
313
-
314
- // ── JSON-RPC over stdio ──────────────────────────────────────────────
315
-
316
- let buffer = ''
317
- process.stdin.setEncoding('utf8')
318
- process.stdin.on('data', async (chunk) => {
319
- buffer += chunk
320
- const lines = buffer.split('\n')
321
- buffer = lines.pop() || ''
322
- for (const line of lines) {
323
- if (!line.trim()) continue
324
- try {
325
- const req = JSON.parse(line)
326
- const { id, method, params } = req
327
-
328
- switch (method) {
329
- case 'initialize':
330
- send(id, {
331
- protocolVersion: '2024-11-05',
332
- capabilities: {
333
- resources: { subscribe: false },
334
- tools: {}
335
- },
336
- serverInfo: { name: 'poi-mcp', version: packageJson.version }
337
- })
338
- break
339
-
340
- case 'notifications/initialized':
341
- case 'notifications/cancelled':
342
- break
343
-
344
- case 'ping':
345
- send(id, {})
346
- break
347
-
348
- case 'resources/list':
349
- send(id, {
350
- resources: resourceUris.map(uri => ({
351
- uri, name: uri.replace('poi://', ''), mimeType: 'application/json'
352
- }))
353
- })
354
- break
355
-
356
- case 'resources/read': {
357
- const uri = params?.uri
358
- const endpoint = '/' + uri.replace('poi://', '')
359
- const data = await fetchFromPoi(endpoint)
360
- send(id, {
361
- contents: [{
362
- uri,
363
- mimeType: 'application/json',
364
- text: JSON.stringify(data, null, 2)
365
- }]
366
- })
367
- break
368
- }
369
-
370
- case 'tools/list':
371
- send(id, {
372
- tools: [
373
- {
374
- name: 'get_fleet_status',
375
- description: '获取舰队详细编成',
376
- inputSchema: {
377
- type: 'object',
378
- properties: { fleetId: { type: 'number', description: '舰队编号 1-4' } },
379
- required: ['fleetId']
380
- }
381
- },
382
- {
383
- name: 'search_ships',
384
- description: '搜索舰娘',
385
- inputSchema: {
386
- type: 'object',
387
- properties: {
388
- minLevel: { type: 'number' },
389
- maxLevel: { type: 'number' },
390
- minMorale: { type: 'number', description: '最低士气()' }
391
- }
392
- }
393
- },
394
- {
395
- name: 'search_equipment',
396
- description: '搜索装备',
397
- inputSchema: {
398
- type: 'object',
399
- properties: {
400
- minLevel: { type: 'number', description: '最低改修★' }
401
- }
402
- }
403
- },
404
- {
405
- name: 'get_resources',
406
- description: '获取资源概况',
407
- inputSchema: { type: 'object', properties: {} }
408
- },
409
- {
410
- name: 'get_all',
411
- description: '获取所有数据(舰队/舰娘/装备/资源/任务/陆航),可选包含 master/event/planner',
412
- inputSchema: {
413
- type: 'object',
414
- properties: {
415
- include: {
416
- type: 'array',
417
- items: { type: 'string', enum: ['master', 'event', 'planner'] }
418
- }
419
- }
420
- }
421
- }
422
- ]
423
- })
424
- break
425
-
426
- case 'tools/call': {
427
- const toolName = params?.name
428
- const toolArgs = params?.arguments || {}
429
- const handler = toolHandlers[toolName]
430
- if (handler) {
431
- const result = await handler(toolArgs)
432
- send(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] })
433
- } else {
434
- send(id, null, { code: -32602, message: `Unknown tool: ${toolName}` })
435
- }
436
- break
437
- }
438
-
439
- default:
440
- send(id, null, { code: -32601, message: `Unknown method: ${method}` })
441
- }
442
- } catch (err) {
443
- // Malformed JSON — ignore
444
- }
445
- }
446
- })
447
-
448
- process.stdin.on('end', () => process.exit(0))
449
- process.on('SIGINT', () => process.exit(0))
450
- process.on('SIGTERM', () => process.exit(0))
451
- }
452
-
453
- main().catch(err => {
454
- console.error('[poi-mcp] Fatal:', err.message)
455
- process.exit(1)
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
+ })