poi-plugin-mcp 0.2.10 → 0.2.12

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,11 +61,15 @@ token is never returned by an HTTP endpoint.
61
61
  | `/equipment` | Raw owned equipment instance data |
62
62
  | `/resources` | Resource array |
63
63
  | `/quests` | Active quests and quest records |
64
+ | `/quest-list` | Latest complete quest-page telemetry |
65
+ | `/quest-action` | Latest successful quest start or stop request |
66
+ | `/equipment-action` | Latest successful equipment mutation request |
64
67
  | `/airbase` | Land base air squadron data |
65
68
  | `/names` | Ship, equipment, and mission name maps |
66
- | `/master` | Master ship, equipment, ship type, equipment type, and mission data |
69
+ | `/master` | Master ship, equipment, ship type, equipment type, mission, and bounded equipment compatibility data |
67
70
  | `/event` | Event ship tag definitions plus owned ships' current sally area |
68
71
  | `/planner` | Ship Info deck planner areas and ship assignments |
72
+ | `/battle` | Observed battle packets, official settlement, and compact Prophet state |
69
73
  | `/screenshot` | In-memory PNG capture of the game WebView |
70
74
  | `/input/status` | Whether authenticated WebView input is enabled |
71
75
  | `/input` | Authenticated, serialized WebView input |
package/index.js CHANGED
@@ -6,6 +6,8 @@ const telemetry = createPoiTelemetry()
6
6
  const controller = createBridgeController({
7
7
  getQuestList: telemetry.getQuestList,
8
8
  getQuestAction: telemetry.getQuestAction,
9
+ getEquipmentAction: telemetry.getEquipmentAction,
10
+ getFleetAction: telemetry.getFleetAction,
9
11
  getBattleTelemetry: telemetry.getBattleTelemetry,
10
12
  })
11
13
  const settingsClass = createSettingsClass(controller)
@@ -32,6 +32,8 @@ function createBridgeController(options = {}) {
32
32
  performInput: options.performInput,
33
33
  getQuestList: options.getQuestList,
34
34
  getQuestAction: options.getQuestAction,
35
+ getEquipmentAction: options.getEquipmentAction,
36
+ getFleetAction: options.getFleetAction,
35
37
  getBattleTelemetry: options.getBattleTelemetry,
36
38
  inputEnabled: settings.inputEnabled,
37
39
  inputToken,
@@ -15,18 +15,30 @@ const DEFAULT_PLANNER_FILE = path.join(
15
15
  'poi',
16
16
  'poi-plugin-ship-info.json',
17
17
  )
18
+ const DEFAULT_MASTER_FILE = path.join(
19
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
20
+ 'poi',
21
+ 'navy-album',
22
+ 'master.json',
23
+ )
18
24
  const JSONRPC_VERSION = '2.0'
19
25
  const MCP_PROTOCOL_VERSION = '2024-11-05'
20
26
  const INPUT_BODY_LIMIT = 64 * 1024
27
+ const MASTER_FILE_LIMIT = 16 * 1024 * 1024
21
28
 
22
29
  function createPoiDataBridge(options = {}) {
23
30
  const getStore = options.getStore || defaultGetStore
24
31
  const configuredPort = options.port == null ? DEFAULT_PORT : options.port
25
32
  const portFile = options.portFile || DEFAULT_PORT_FILE
26
33
  const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
34
+ const masterFile = options.masterFile || DEFAULT_MASTER_FILE
27
35
  const logger = options.logger || console
28
36
  const getQuestList = options.getQuestList || (() => ({ available: false, generation: 0 }))
29
37
  const getQuestAction = options.getQuestAction || (() => ({ available: false, generation: 0 }))
38
+ const getEquipmentAction = options.getEquipmentAction ||
39
+ (() => ({ available: false, generation: 0 }))
40
+ const getFleetAction = options.getFleetAction ||
41
+ (() => ({ available: false, generation: 0 }))
30
42
  const getBattleTelemetry = options.getBattleTelemetry ||
31
43
  (() => ({ available: false, generation: 0 }))
32
44
  const inputEnabled = options.inputEnabled === true
@@ -242,6 +254,16 @@ function createPoiDataBridge(options = {}) {
242
254
  return
243
255
  }
244
256
 
257
+ if (endpoint === '/equipment-action') {
258
+ sendJson(res, 200, getEquipmentAction())
259
+ return
260
+ }
261
+
262
+ if (endpoint === '/fleet-action') {
263
+ sendJson(res, 200, getFleetAction())
264
+ return
265
+ }
266
+
245
267
  const store = readStore()
246
268
  const info = store.info
247
269
 
@@ -274,7 +296,7 @@ function createPoiDataBridge(options = {}) {
274
296
  sendJson(res, 200, extractNames(store))
275
297
  break
276
298
  case '/master':
277
- sendJson(res, 200, extractMasterData(store))
299
+ sendJson(res, 200, extractMasterData(store, masterFile))
278
300
  break
279
301
  case '/event':
280
302
  sendJson(res, 200, extractEventData(store))
@@ -906,7 +928,7 @@ function collectSimpleNames(source, target) {
906
928
  }
907
929
  }
908
930
 
909
- function extractMasterData(store) {
931
+ function extractMasterData(store, masterFile = DEFAULT_MASTER_FILE) {
910
932
  const constants = store.const || {}
911
933
 
912
934
  return {
@@ -920,9 +942,52 @@ function extractMasterData(store) {
920
942
  constants.$slotItemTypes ||
921
943
  {},
922
944
  missions: constants.$missions || {},
945
+ equipmentRules: readEquipmentRules(masterFile),
923
946
  }
924
947
  }
925
948
 
949
+ function readEquipmentRules(masterFile) {
950
+ try {
951
+ const stat = fs.statSync(masterFile)
952
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MASTER_FILE_LIMIT) {
953
+ return { available: false, source: 'navy-album-master-cache' }
954
+ }
955
+ const master = JSON.parse(fs.readFileSync(masterFile, 'utf8'))
956
+ const equipmentShip = objectOrEmpty(master.api_mst_equip_ship)
957
+ const equipmentExslotTypes = Array.isArray(master.api_mst_equip_exslot)
958
+ ? master.api_mst_equip_exslot.filter(Number.isInteger)
959
+ : []
960
+ const equipmentExslotShip = objectOrEmpty(
961
+ master.api_mst_equip_exslot_ship,
962
+ )
963
+ const equipmentLimitExslot = objectOrEmpty(
964
+ master.api_mst_equip_limit_exslot,
965
+ )
966
+ if (
967
+ Object.keys(equipmentShip).length === 0 ||
968
+ equipmentExslotTypes.length === 0
969
+ ) {
970
+ return { available: false, source: 'navy-album-master-cache' }
971
+ }
972
+ return {
973
+ available: true,
974
+ source: 'navy-album-master-cache',
975
+ equipmentShip,
976
+ equipmentExslotTypes,
977
+ equipmentExslotShip,
978
+ equipmentLimitExslot,
979
+ }
980
+ } catch (_) {
981
+ return { available: false, source: 'navy-album-master-cache' }
982
+ }
983
+ }
984
+
985
+ function objectOrEmpty(value) {
986
+ return value && typeof value === 'object' && !Array.isArray(value)
987
+ ? value
988
+ : {}
989
+ }
990
+
926
991
  function extractEventData(store) {
927
992
  const tags = extractShipTags(store)
928
993
  const ships = {}
@@ -3,6 +3,13 @@ const QUEST_ACTION_PATHS = new Set([
3
3
  '/kcsapi/api_req_quest/start',
4
4
  '/kcsapi/api_req_quest/stop',
5
5
  ])
6
+ const EQUIPMENT_ACTION_PATHS = new Set([
7
+ '/kcsapi/api_req_kaisou/slotset',
8
+ '/kcsapi/api_req_kaisou/slotset_ex',
9
+ '/kcsapi/api_req_kaisou/slot_deprive',
10
+ '/kcsapi/api_req_kaisou/unsetslot_all',
11
+ ])
12
+ const FLEET_ACTION_PATH = '/kcsapi/api_req_hensei/change'
6
13
  const BATTLE_RESULT_PATHS = new Set([
7
14
  '/kcsapi/api_req_practice/battle_result',
8
15
  '/kcsapi/api_req_sortie/battleresult',
@@ -37,6 +44,10 @@ function createPoiTelemetry(options = {}) {
37
44
  let questList = null
38
45
  let questActionGeneration = 0
39
46
  let questAction = null
47
+ let equipmentActionGeneration = 0
48
+ let equipmentAction = null
49
+ let fleetActionGeneration = 0
50
+ let fleetAction = null
40
51
  let battleGeneration = 0
41
52
  let battleTelemetry = null
42
53
 
@@ -52,6 +63,14 @@ function createPoiTelemetry(options = {}) {
52
63
  captureQuestAction(detail)
53
64
  return
54
65
  }
66
+ if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
67
+ captureEquipmentAction(detail)
68
+ return
69
+ }
70
+ if (detail.path === FLEET_ACTION_PATH) {
71
+ captureFleetAction(detail)
72
+ return
73
+ }
55
74
  if (BATTLE_RESULT_PATHS.has(detail.path)) {
56
75
  captureBattleResult(detail)
57
76
  return
@@ -113,6 +132,40 @@ function createPoiTelemetry(options = {}) {
113
132
  }
114
133
  }
115
134
 
135
+ function captureEquipmentAction(detail) {
136
+ const apiResult = equipmentApiResult(detail)
137
+ if (apiResult === 0) return
138
+ const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
139
+ if (!postBody) return
140
+
141
+ equipmentActionGeneration += 1
142
+ equipmentAction = {
143
+ available: true,
144
+ generation: equipmentActionGeneration,
145
+ capturedAt: now().toISOString(),
146
+ path: detail.path,
147
+ apiResult,
148
+ postBody,
149
+ }
150
+ }
151
+
152
+ function captureFleetAction(detail) {
153
+ const apiResult = equipmentApiResult(detail)
154
+ if (apiResult === 0) return
155
+ const postBody = normalizeFleetPostBody(detail.postBody)
156
+ if (!postBody) return
157
+
158
+ fleetActionGeneration += 1
159
+ fleetAction = {
160
+ available: true,
161
+ generation: fleetActionGeneration,
162
+ capturedAt: now().toISOString(),
163
+ path: detail.path,
164
+ apiResult,
165
+ postBody,
166
+ }
167
+ }
168
+
116
169
  function captureBattlePacket(detail) {
117
170
  if (!battleTelemetry || battleTelemetry.status === 'settled') {
118
171
  battleGeneration += 1
@@ -175,12 +228,101 @@ function createPoiTelemetry(options = {}) {
175
228
  getQuestAction() {
176
229
  return questAction || { available: false, generation: 0 }
177
230
  },
231
+ getEquipmentAction() {
232
+ return equipmentAction || { available: false, generation: 0 }
233
+ },
234
+ getFleetAction() {
235
+ return fleetAction || { available: false, generation: 0 }
236
+ },
178
237
  getBattleTelemetry() {
179
238
  return battleTelemetry || { available: false, generation: 0 }
180
239
  },
181
240
  }
182
241
  }
183
242
 
243
+ function normalizeFleetPostBody(postBody) {
244
+ if (!postBody || typeof postBody !== 'object') return null
245
+ const fleetId = boundedInteger(postBody.api_id, 1, 4)
246
+ const shipIndex = boundedInteger(postBody.api_ship_idx, 0, 5)
247
+ const shipId = boundedInteger(postBody.api_ship_id, -1)
248
+ if (fleetId == null || shipIndex == null || shipId == null) return null
249
+ return {
250
+ api_id: fleetId,
251
+ api_ship_idx: shipIndex,
252
+ api_ship_id: shipId,
253
+ }
254
+ }
255
+
256
+ function normalizeEquipmentPostBody(path, postBody) {
257
+ if (!postBody || typeof postBody !== 'object') return null
258
+ if (path === '/kcsapi/api_req_kaisou/slotset') {
259
+ const shipId = positiveIntegerOrNull(postBody.api_id)
260
+ const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
261
+ const itemId = boundedInteger(postBody.api_item_id, -1)
262
+ if (shipId == null || slotIndex == null || itemId == null) return null
263
+ return {
264
+ api_id: shipId,
265
+ api_slot_idx: slotIndex,
266
+ api_item_id: itemId,
267
+ }
268
+ }
269
+ if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
270
+ const shipId = positiveIntegerOrNull(postBody.api_id)
271
+ const itemId = boundedInteger(postBody.api_item_id, -1)
272
+ if (shipId == null || itemId == null) return null
273
+ return {
274
+ api_id: shipId,
275
+ api_item_id: itemId,
276
+ }
277
+ }
278
+ if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
279
+ const setShip = positiveIntegerOrNull(postBody.api_set_ship)
280
+ const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
281
+ const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
282
+ const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
283
+ if (
284
+ setShip == null ||
285
+ unsetShip == null ||
286
+ setIndex == null ||
287
+ unsetIndex == null
288
+ ) {
289
+ return null
290
+ }
291
+ return {
292
+ api_set_ship: setShip,
293
+ api_unset_ship: unsetShip,
294
+ api_set_idx: setIndex,
295
+ api_unset_idx: unsetIndex,
296
+ }
297
+ }
298
+ if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
299
+ const shipId = positiveIntegerOrNull(postBody.api_id)
300
+ return shipId == null ? null : { api_id: shipId }
301
+ }
302
+ return null
303
+ }
304
+
305
+ function equipmentApiResult(detail) {
306
+ const candidates = [
307
+ detail.apiResult,
308
+ detail.api_result,
309
+ detail.result,
310
+ detail.body && detail.body.api_result,
311
+ ]
312
+ for (const candidate of candidates) {
313
+ const value = toInteger(candidate)
314
+ if (value != null) return value === 1 ? 1 : 0
315
+ }
316
+ return null
317
+ }
318
+
319
+ function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
320
+ const parsed = toInteger(value)
321
+ return parsed != null && parsed >= minimum && parsed <= maximum
322
+ ? parsed
323
+ : null
324
+ }
325
+
184
326
  function toInteger(value, fallback = null) {
185
327
  if (value == null || value === '') return fallback
186
328
  const parsed = Number(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Poi data, WebView capture, and opt-in authenticated input bridge for local KanColle tools.",
5
5
  "main": "index.js",
6
6
  "keywords": [