poi-plugin-mcp 0.2.1 → 0.2.3

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 CHANGED
@@ -61,8 +61,15 @@ The default port is `17777`.
61
61
  | `/master` | Master ship, equipment, ship type, equipment type, and mission data |
62
62
  | `/event` | Event ship tag definitions plus owned ships' current sally area |
63
63
  | `/planner` | Ship Info deck planner areas and ship assignments |
64
+ | `/screenshot` | In-memory PNG capture of the game WebView |
64
65
  | `/all` | Combined basic runtime data |
65
66
 
67
+ `/screenshot` accepts `GET` only. It uses Poi's existing
68
+ `screenshot::get` WebContents capture path and returns PNG base64 in JSON. It
69
+ does not save a file, write the clipboard, capture the desktop, or appear in
70
+ MCP resources and tools. Its response disables CORS and uses
71
+ `Cache-Control: no-store`.
72
+
66
73
  ## MCP Endpoint
67
74
 
68
75
  The same local server also exposes a JSON-RPC MCP endpoint:
@@ -23,6 +23,7 @@ function createBridgeController(options = {}) {
23
23
  function createCurrentBridge() {
24
24
  return createBridge({
25
25
  getStore: options.getStore,
26
+ captureScreenshot: options.captureScreenshot,
26
27
  port: settings.port,
27
28
  portFile: options.portFile,
28
29
  logger,
@@ -2,6 +2,8 @@ const fs = require('fs')
2
2
  const http = require('http')
3
3
  const os = require('os')
4
4
  const path = require('path')
5
+ const packageJson = require('../package.json')
6
+ const { createPoiScreenshotProvider } = require('./poi-screenshot')
5
7
 
6
8
  const DEFAULT_PORT = 17777
7
9
  const DEFAULT_PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
@@ -19,6 +21,7 @@ function createPoiDataBridge(options = {}) {
19
21
  const portFile = options.portFile || DEFAULT_PORT_FILE
20
22
  const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
21
23
  const logger = options.logger || console
24
+ let captureScreenshot = options.captureScreenshot || null
22
25
 
23
26
  let server = null
24
27
  let actualPort = 0
@@ -31,11 +34,17 @@ function createPoiDataBridge(options = {}) {
31
34
  return store
32
35
  }
33
36
 
34
- function sendJson(res, statusCode, data) {
35
- res.writeHead(statusCode, {
36
- 'Access-Control-Allow-Origin': '*',
37
+ function sendJson(res, statusCode, data, options = {}) {
38
+ const headers = {
37
39
  'Content-Type': 'application/json',
38
- })
40
+ }
41
+ if (options.allowCors !== false) {
42
+ headers['Access-Control-Allow-Origin'] = '*'
43
+ }
44
+ if (options.noStore) {
45
+ headers['Cache-Control'] = 'no-store'
46
+ }
47
+ res.writeHead(statusCode, headers)
39
48
  res.end(JSON.stringify(data))
40
49
  }
41
50
 
@@ -66,7 +75,7 @@ function createPoiDataBridge(options = {}) {
66
75
  })
67
76
  }
68
77
 
69
- function handleRequest(req, res) {
78
+ async function handleRequest(req, res) {
70
79
  try {
71
80
  if (req.url === '/shutdown') {
72
81
  sendJson(res, 200, { status: 'shutting down' })
@@ -86,6 +95,37 @@ function createPoiDataBridge(options = {}) {
86
95
  return
87
96
  }
88
97
 
98
+ if (endpoint === '/screenshot') {
99
+ if (req.method !== 'GET') {
100
+ sendJson(
101
+ res,
102
+ 405,
103
+ { error: 'Screenshot endpoint only accepts GET requests.' },
104
+ { allowCors: false, noStore: true },
105
+ )
106
+ return
107
+ }
108
+ try {
109
+ if (!captureScreenshot) {
110
+ captureScreenshot = createPoiScreenshotProvider()
111
+ }
112
+ sendJson(
113
+ res,
114
+ 200,
115
+ await captureScreenshot(),
116
+ { allowCors: false, noStore: true },
117
+ )
118
+ } catch (error) {
119
+ sendJson(
120
+ res,
121
+ 503,
122
+ { error: error.message },
123
+ { allowCors: false, noStore: true },
124
+ )
125
+ }
126
+ return
127
+ }
128
+
89
129
  const store = readStore()
90
130
  const info = store.info
91
131
 
@@ -228,7 +268,7 @@ function handleMcpMessage(message, readStore, plannerFile) {
228
268
  resources: { subscribe: false },
229
269
  tools: {},
230
270
  },
231
- serverInfo: { name: 'poi-plugin-mcp', version: '0.2.0' },
271
+ serverInfo: { name: 'poi-plugin-mcp', version: packageJson.version },
232
272
  })
233
273
 
234
274
  case 'notifications/initialized':
@@ -333,8 +373,16 @@ const MCP_TOOLS = [
333
373
  },
334
374
  {
335
375
  name: 'get_all',
336
- description: 'Get account basics, fleets, ships, equipment, resources, quests, airbase, and names.',
337
- inputSchema: { type: 'object', properties: {} },
376
+ description: 'Get account basics, fleets, ships, equipment, resources, quests, airbase, and names. Optionally include master, event, and planner data.',
377
+ inputSchema: {
378
+ type: 'object',
379
+ properties: {
380
+ include: {
381
+ type: 'array',
382
+ items: { type: 'string', enum: ['master', 'event', 'planner'] },
383
+ },
384
+ },
385
+ },
338
386
  },
339
387
  ]
340
388
 
@@ -349,7 +397,7 @@ function callMcpTool(toolName, args, readStore, plannerFile) {
349
397
  case 'get_resources':
350
398
  return { value: readBridgeData('/resources', readStore, plannerFile) }
351
399
  case 'get_all':
352
- return { value: readBridgeData('/all', readStore, plannerFile) }
400
+ return { value: buildAllPayload(args || {}, readStore, plannerFile) }
353
401
  default:
354
402
  return { error: `Unknown tool: ${toolName}` }
355
403
  }
@@ -446,28 +494,69 @@ function buildFleetStatus(args, readStore) {
446
494
 
447
495
  function searchShips(args, readStore) {
448
496
  const store = readStore()
497
+ const master = extractMasterData(store)
449
498
  const ships = Object.values((store.info && store.info.ships) || {}).filter((ship) => {
450
499
  if (!ship) return false
451
500
  if (args.minLevel != null && ship.api_lv < Number(args.minLevel)) return false
452
501
  if (args.maxLevel != null && ship.api_lv > Number(args.maxLevel)) return false
453
502
  if (args.minMorale != null && ship.api_cond < Number(args.minMorale)) return false
454
503
  return true
455
- })
504
+ }).map((ship) => enrichShip(ship, master))
456
505
 
457
506
  return { total: ships.length, ships }
458
507
  }
459
508
 
460
509
  function searchEquipment(args, readStore) {
461
510
  const store = readStore()
511
+ const master = extractMasterData(store)
462
512
  const equipment = Object.values((store.info && store.info.equips) || {}).filter((equip) => {
463
513
  if (!equip) return false
464
514
  if (args.minLevel != null && (equip.api_level || 0) < Number(args.minLevel)) return false
465
515
  return true
466
- })
516
+ }).map((equip) => enrichEquipment(equip, master))
467
517
 
468
518
  return { total: equipment.length, equipment }
469
519
  }
470
520
 
521
+ function buildAllPayload(args, readStore, plannerFile) {
522
+ const payload = readBridgeData('/all', readStore, plannerFile)
523
+ const include = Array.isArray(args.include) ? new Set(args.include) : new Set()
524
+
525
+ if (include.has('master')) payload.master = readBridgeData('/master', readStore, plannerFile)
526
+ if (include.has('event')) payload.event = readBridgeData('/event', readStore, plannerFile)
527
+ if (include.has('planner')) payload.planner = readBridgeData('/planner', readStore, plannerFile)
528
+
529
+ return payload
530
+ }
531
+
532
+ function enrichShip(ship, master) {
533
+ const masterShip = master.ships && master.ships[ship.api_ship_id]
534
+ const shipType = masterShip && master.shipTypes && master.shipTypes[masterShip.api_stype]
535
+
536
+ return {
537
+ ...ship,
538
+ instanceId: ship.api_id,
539
+ masterId: ship.api_ship_id,
540
+ name: (masterShip && masterShip.api_name) || '',
541
+ typeName: (shipType && shipType.api_name) || '',
542
+ }
543
+ }
544
+
545
+ function enrichEquipment(equip, master) {
546
+ const masterEquip = master.equipment && master.equipment[equip.api_slotitem_id]
547
+ const typeIds = masterEquip && Array.isArray(masterEquip.api_type) ? masterEquip.api_type : []
548
+ const typeId = typeIds[2] || typeIds[1] || typeIds[0]
549
+ const equipType = typeId && master.equipmentTypes && master.equipmentTypes[typeId]
550
+
551
+ return {
552
+ ...equip,
553
+ instanceId: equip.api_id,
554
+ masterId: equip.api_slotitem_id,
555
+ name: (masterEquip && masterEquip.api_name) || '',
556
+ typeName: (equipType && equipType.api_name) || '',
557
+ }
558
+ }
559
+
471
560
  function jsonRpcResult(id, result) {
472
561
  return { jsonrpc: JSONRPC_VERSION, id, result }
473
562
  }
@@ -547,6 +636,8 @@ function extractEventData(store) {
547
636
  const tag = tags[area - 1] || emptyTag(area)
548
637
 
549
638
  ships[ship.api_id] = {
639
+ instanceId: ship.api_id,
640
+ masterId: ship.api_ship_id,
550
641
  shipId: ship.api_id,
551
642
  modelId: ship.api_ship_id,
552
643
  area,
@@ -0,0 +1,104 @@
1
+ const DEFAULT_MAX_BASE64_LENGTH = 16 * 1024 * 1024
2
+
3
+ function createPoiScreenshotProvider(options = {}) {
4
+ const getStore = options.getStore || defaultGetStore
5
+ const ipcRenderer = options.ipcRenderer || require('electron').ipcRenderer
6
+ const devicePixelRatio = options.devicePixelRatio == null
7
+ ? defaultDevicePixelRatio()
8
+ : options.devicePixelRatio
9
+ const now = options.now || (() => new Date())
10
+ const maxBase64Length = options.maxBase64Length == null
11
+ ? DEFAULT_MAX_BASE64_LENGTH
12
+ : options.maxBase64Length
13
+
14
+ if (!Number.isFinite(devicePixelRatio) || devicePixelRatio <= 0) {
15
+ throw new Error('devicePixelRatio must be a positive finite number')
16
+ }
17
+ if (!Number.isInteger(maxBase64Length) || maxBase64Length <= 0) {
18
+ throw new Error('maxBase64Length must be a positive integer')
19
+ }
20
+
21
+ return async function capturePoiScreenshot() {
22
+ const layout = getStore('layout.webview')
23
+ if (
24
+ !layout ||
25
+ !layout.ref ||
26
+ typeof layout.ref.getWebContentsId !== 'function'
27
+ ) {
28
+ throw new Error('Poi game WebView is not ready')
29
+ }
30
+ if (
31
+ !Number.isInteger(layout.width) ||
32
+ layout.width <= 0 ||
33
+ !Number.isInteger(layout.height) ||
34
+ layout.height <= 0
35
+ ) {
36
+ throw new Error('Poi game WebView dimensions must be positive integers')
37
+ }
38
+
39
+ const webContentsId = layout.ref.getWebContentsId()
40
+ if (!Number.isInteger(webContentsId) || webContentsId <= 0) {
41
+ throw new Error('Poi game WebContents id is invalid')
42
+ }
43
+
44
+ const rect = {
45
+ x: 0,
46
+ y: 0,
47
+ width: Math.floor(layout.width * devicePixelRatio),
48
+ height: Math.floor(layout.height * devicePixelRatio),
49
+ }
50
+ const actualSize = {
51
+ width: layout.width,
52
+ height: layout.height,
53
+ }
54
+ const dataUrl = await ipcRenderer.invoke(
55
+ 'screenshot::get',
56
+ webContentsId,
57
+ rect,
58
+ actualSize,
59
+ )
60
+ const prefix = 'data:image/png;base64,'
61
+ if (typeof dataUrl !== 'string' || !dataUrl.startsWith(prefix)) {
62
+ throw new Error('Poi screenshot did not return a PNG data URL')
63
+ }
64
+
65
+ const dataBase64 = dataUrl.slice(prefix.length)
66
+ if (
67
+ dataBase64.length === 0 ||
68
+ !/^[A-Za-z0-9+/]+={0,2}$/.test(dataBase64)
69
+ ) {
70
+ throw new Error('Poi screenshot returned invalid base64 data')
71
+ }
72
+ if (dataBase64.length > maxBase64Length) {
73
+ throw new Error(`Poi screenshot exceeds ${maxBase64Length} base64 characters`)
74
+ }
75
+
76
+ return {
77
+ capturedAt: now().toISOString(),
78
+ mimeType: 'image/png',
79
+ dataBase64,
80
+ }
81
+ }
82
+ }
83
+
84
+ function defaultGetStore(path) {
85
+ if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
86
+ return window.getStore(path)
87
+ }
88
+ return null
89
+ }
90
+
91
+ function defaultDevicePixelRatio() {
92
+ if (
93
+ typeof window !== 'undefined' &&
94
+ Number.isFinite(window.devicePixelRatio) &&
95
+ window.devicePixelRatio > 0
96
+ ) {
97
+ return window.devicePixelRatio
98
+ }
99
+ return 1
100
+ }
101
+
102
+ module.exports = {
103
+ createPoiScreenshotProvider,
104
+ }
package/mcp-server.js CHANGED
@@ -57,6 +57,7 @@ const os = require('os')
57
57
  const path = require('path')
58
58
  const fs = require('fs')
59
59
  const http = require('http')
60
+ const packageJson = require('./package.json')
60
61
 
61
62
  const PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
62
63
 
@@ -139,6 +140,53 @@ function getInjectScript() {
139
140
  ].join('\n')
140
141
  }
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
+
142
190
  // ─── MCP Protocol ────────────────────────────────────────────────────────────
143
191
 
144
192
  // Minimum viable MCP stdio server — no external dependencies
@@ -161,7 +209,7 @@ function sendLog(text) {
161
209
  // ─── Main ────────────────────────────────────────────────────────────────────
162
210
 
163
211
  async function main() {
164
- sendLog('POI MCP Server v0.1.0')
212
+ sendLog(`POI MCP Server v${packageJson.version}`)
165
213
  sendLog('Checking POI data API...')
166
214
 
167
215
  const port = getPoiPort()
@@ -226,23 +274,25 @@ async function main() {
226
274
 
227
275
  search_ships: async (args) => {
228
276
  const ships = await fetchFromPoi('/ships')
277
+ const master = await fetchMasterData()
229
278
  const results = Object.values(ships).filter(s => {
230
279
  if (!s) return false
231
280
  if (args.minLevel != null && s.api_lv < args.minLevel) return false
232
281
  if (args.maxLevel != null && s.api_lv > args.maxLevel) return false
233
282
  if (args.minMorale != null && s.api_cond < args.minMorale) return false
234
283
  return true
235
- })
284
+ }).map(s => enrichShip(s, master))
236
285
  return { total: results.length, ships: results }
237
286
  },
238
287
 
239
288
  search_equipment: async (args) => {
240
289
  const equips = await fetchFromPoi('/equipment')
290
+ const master = await fetchMasterData()
241
291
  const results = Object.values(equips).filter(e => {
242
292
  if (!e) return false
243
293
  if (args.minLevel != null && (e.api_level || 0) < args.minLevel) return false
244
294
  return true
245
- })
295
+ }).map(e => enrichEquipment(e, master))
246
296
  return { total: results.length, equipment: results }
247
297
  },
248
298
 
@@ -250,14 +300,15 @@ async function main() {
250
300
  return await fetchFromPoi('/resources')
251
301
  },
252
302
 
253
- get_all: async () => {
254
- return await fetchFromPoi('/all')
303
+ get_all: async (args) => {
304
+ return await fetchAllData(args)
255
305
  }
256
306
  }
257
307
 
258
308
  const resourceUris = [
259
309
  'poi://fleets', 'poi://ships', 'poi://equipment',
260
- 'poi://resources', 'poi://quests', 'poi://airbase', 'poi://basic', 'poi://all'
310
+ 'poi://resources', 'poi://quests', 'poi://airbase', 'poi://basic',
311
+ 'poi://names', 'poi://master', 'poi://event', 'poi://planner', 'poi://all'
261
312
  ]
262
313
 
263
314
  // ── JSON-RPC over stdio ──────────────────────────────────────────────
@@ -282,7 +333,7 @@ async function main() {
282
333
  resources: { subscribe: false },
283
334
  tools: {}
284
335
  },
285
- serverInfo: { name: 'poi-mcp', version: '0.1.0' }
336
+ serverInfo: { name: 'poi-mcp', version: packageJson.version }
286
337
  })
287
338
  break
288
339
 
@@ -357,8 +408,16 @@ async function main() {
357
408
  },
358
409
  {
359
410
  name: 'get_all',
360
- description: '获取所有数据(舰队/舰娘/装备/资源/任务/陆航)',
361
- inputSchema: { type: 'object', properties: {} }
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
+ }
362
421
  }
363
422
  ]
364
423
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.1",
4
- "description": "Poi data bridge for local KanColle inventory tools.",
3
+ "version": "0.2.3",
4
+ "description": "Poi read-only data and game WebView capture bridge for local KanColle tools.",
5
5
  "main": "index.js",
6
6
  "keywords": [
7
7
  "poi-plugin",
@@ -24,7 +24,7 @@
24
24
  "poiPlugin": {
25
25
  "title": "MCP 数据桥",
26
26
  "id": "mcp_data_bridge",
27
- "description": "Expose Poi fleet, equipment, master, event tag, and planner data on local HTTP for CLI/MCP agents.",
27
+ "description": "Expose Poi state and in-memory game WebView captures on a local read-only bridge.",
28
28
  "icon": "fa/database",
29
29
  "priority": 53
30
30
  },