poi-plugin-mcp 0.2.9 → 0.2.11

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,7 @@ const telemetry = createPoiTelemetry()
6
6
  const controller = createBridgeController({
7
7
  getQuestList: telemetry.getQuestList,
8
8
  getQuestAction: telemetry.getQuestAction,
9
+ getEquipmentAction: telemetry.getEquipmentAction,
9
10
  getBattleTelemetry: telemetry.getBattleTelemetry,
10
11
  })
11
12
  const settingsClass = createSettingsClass(controller)
@@ -32,6 +32,7 @@ function createBridgeController(options = {}) {
32
32
  performInput: options.performInput,
33
33
  getQuestList: options.getQuestList,
34
34
  getQuestAction: options.getQuestAction,
35
+ getEquipmentAction: options.getEquipmentAction,
35
36
  getBattleTelemetry: options.getBattleTelemetry,
36
37
  inputEnabled: settings.inputEnabled,
37
38
  inputToken,
@@ -15,18 +15,28 @@ 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 }))
30
40
  const getBattleTelemetry = options.getBattleTelemetry ||
31
41
  (() => ({ available: false, generation: 0 }))
32
42
  const inputEnabled = options.inputEnabled === true
@@ -242,6 +252,11 @@ function createPoiDataBridge(options = {}) {
242
252
  return
243
253
  }
244
254
 
255
+ if (endpoint === '/equipment-action') {
256
+ sendJson(res, 200, getEquipmentAction())
257
+ return
258
+ }
259
+
245
260
  const store = readStore()
246
261
  const info = store.info
247
262
 
@@ -274,7 +289,7 @@ function createPoiDataBridge(options = {}) {
274
289
  sendJson(res, 200, extractNames(store))
275
290
  break
276
291
  case '/master':
277
- sendJson(res, 200, extractMasterData(store))
292
+ sendJson(res, 200, extractMasterData(store, masterFile))
278
293
  break
279
294
  case '/event':
280
295
  sendJson(res, 200, extractEventData(store))
@@ -906,7 +921,7 @@ function collectSimpleNames(source, target) {
906
921
  }
907
922
  }
908
923
 
909
- function extractMasterData(store) {
924
+ function extractMasterData(store, masterFile = DEFAULT_MASTER_FILE) {
910
925
  const constants = store.const || {}
911
926
 
912
927
  return {
@@ -920,9 +935,52 @@ function extractMasterData(store) {
920
935
  constants.$slotItemTypes ||
921
936
  {},
922
937
  missions: constants.$missions || {},
938
+ equipmentRules: readEquipmentRules(masterFile),
923
939
  }
924
940
  }
925
941
 
942
+ function readEquipmentRules(masterFile) {
943
+ try {
944
+ const stat = fs.statSync(masterFile)
945
+ if (!stat.isFile() || stat.size <= 0 || stat.size > MASTER_FILE_LIMIT) {
946
+ return { available: false, source: 'navy-album-master-cache' }
947
+ }
948
+ const master = JSON.parse(fs.readFileSync(masterFile, 'utf8'))
949
+ const equipmentShip = objectOrEmpty(master.api_mst_equip_ship)
950
+ const equipmentExslotTypes = Array.isArray(master.api_mst_equip_exslot)
951
+ ? master.api_mst_equip_exslot.filter(Number.isInteger)
952
+ : []
953
+ const equipmentExslotShip = objectOrEmpty(
954
+ master.api_mst_equip_exslot_ship,
955
+ )
956
+ const equipmentLimitExslot = objectOrEmpty(
957
+ master.api_mst_equip_limit_exslot,
958
+ )
959
+ if (
960
+ Object.keys(equipmentShip).length === 0 ||
961
+ equipmentExslotTypes.length === 0
962
+ ) {
963
+ return { available: false, source: 'navy-album-master-cache' }
964
+ }
965
+ return {
966
+ available: true,
967
+ source: 'navy-album-master-cache',
968
+ equipmentShip,
969
+ equipmentExslotTypes,
970
+ equipmentExslotShip,
971
+ equipmentLimitExslot,
972
+ }
973
+ } catch (_) {
974
+ return { available: false, source: 'navy-album-master-cache' }
975
+ }
976
+ }
977
+
978
+ function objectOrEmpty(value) {
979
+ return value && typeof value === 'object' && !Array.isArray(value)
980
+ ? value
981
+ : {}
982
+ }
983
+
926
984
  function extractEventData(store) {
927
985
  const tags = extractShipTags(store)
928
986
  const ships = {}
@@ -3,6 +3,12 @@ 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
+ ])
6
12
  const BATTLE_RESULT_PATHS = new Set([
7
13
  '/kcsapi/api_req_practice/battle_result',
8
14
  '/kcsapi/api_req_sortie/battleresult',
@@ -37,6 +43,8 @@ function createPoiTelemetry(options = {}) {
37
43
  let questList = null
38
44
  let questActionGeneration = 0
39
45
  let questAction = null
46
+ let equipmentActionGeneration = 0
47
+ let equipmentAction = null
40
48
  let battleGeneration = 0
41
49
  let battleTelemetry = null
42
50
 
@@ -52,6 +60,10 @@ function createPoiTelemetry(options = {}) {
52
60
  captureQuestAction(detail)
53
61
  return
54
62
  }
63
+ if (EQUIPMENT_ACTION_PATHS.has(detail.path)) {
64
+ captureEquipmentAction(detail)
65
+ return
66
+ }
55
67
  if (BATTLE_RESULT_PATHS.has(detail.path)) {
56
68
  captureBattleResult(detail)
57
69
  return
@@ -79,7 +91,7 @@ function createPoiTelemetry(options = {}) {
79
91
  if (!quest || typeof quest !== 'object' || !Number.isInteger(quest.api_no)) {
80
92
  return []
81
93
  }
82
- return [{ ...quest, pageRow: index + 1 }]
94
+ return [{ ...quest, listIndex: index + 1 }]
83
95
  })
84
96
 
85
97
  questGeneration += 1
@@ -113,6 +125,23 @@ function createPoiTelemetry(options = {}) {
113
125
  }
114
126
  }
115
127
 
128
+ function captureEquipmentAction(detail) {
129
+ const apiResult = equipmentApiResult(detail)
130
+ if (apiResult === 0) return
131
+ const postBody = normalizeEquipmentPostBody(detail.path, detail.postBody)
132
+ if (!postBody) return
133
+
134
+ equipmentActionGeneration += 1
135
+ equipmentAction = {
136
+ available: true,
137
+ generation: equipmentActionGeneration,
138
+ capturedAt: now().toISOString(),
139
+ path: detail.path,
140
+ apiResult,
141
+ postBody,
142
+ }
143
+ }
144
+
116
145
  function captureBattlePacket(detail) {
117
146
  if (!battleTelemetry || battleTelemetry.status === 'settled') {
118
147
  battleGeneration += 1
@@ -175,12 +204,85 @@ function createPoiTelemetry(options = {}) {
175
204
  getQuestAction() {
176
205
  return questAction || { available: false, generation: 0 }
177
206
  },
207
+ getEquipmentAction() {
208
+ return equipmentAction || { available: false, generation: 0 }
209
+ },
178
210
  getBattleTelemetry() {
179
211
  return battleTelemetry || { available: false, generation: 0 }
180
212
  },
181
213
  }
182
214
  }
183
215
 
216
+ function normalizeEquipmentPostBody(path, postBody) {
217
+ if (!postBody || typeof postBody !== 'object') return null
218
+ if (path === '/kcsapi/api_req_kaisou/slotset') {
219
+ const shipId = positiveIntegerOrNull(postBody.api_id)
220
+ const slotIndex = boundedInteger(postBody.api_slot_idx, 0, 4)
221
+ const itemId = boundedInteger(postBody.api_item_id, -1)
222
+ if (shipId == null || slotIndex == null || itemId == null) return null
223
+ return {
224
+ api_id: shipId,
225
+ api_slot_idx: slotIndex,
226
+ api_item_id: itemId,
227
+ }
228
+ }
229
+ if (path === '/kcsapi/api_req_kaisou/slotset_ex') {
230
+ const shipId = positiveIntegerOrNull(postBody.api_id)
231
+ const itemId = boundedInteger(postBody.api_item_id, -1)
232
+ if (shipId == null || itemId == null) return null
233
+ return {
234
+ api_id: shipId,
235
+ api_item_id: itemId,
236
+ }
237
+ }
238
+ if (path === '/kcsapi/api_req_kaisou/slot_deprive') {
239
+ const setShip = positiveIntegerOrNull(postBody.api_set_ship)
240
+ const unsetShip = positiveIntegerOrNull(postBody.api_unset_ship)
241
+ const setIndex = boundedInteger(postBody.api_set_idx, -1, 4)
242
+ const unsetIndex = boundedInteger(postBody.api_unset_idx, -1, 4)
243
+ if (
244
+ setShip == null ||
245
+ unsetShip == null ||
246
+ setIndex == null ||
247
+ unsetIndex == null
248
+ ) {
249
+ return null
250
+ }
251
+ return {
252
+ api_set_ship: setShip,
253
+ api_unset_ship: unsetShip,
254
+ api_set_idx: setIndex,
255
+ api_unset_idx: unsetIndex,
256
+ }
257
+ }
258
+ if (path === '/kcsapi/api_req_kaisou/unsetslot_all') {
259
+ const shipId = positiveIntegerOrNull(postBody.api_id)
260
+ return shipId == null ? null : { api_id: shipId }
261
+ }
262
+ return null
263
+ }
264
+
265
+ function equipmentApiResult(detail) {
266
+ const candidates = [
267
+ detail.apiResult,
268
+ detail.api_result,
269
+ detail.result,
270
+ detail.body && detail.body.api_result,
271
+ ]
272
+ for (const candidate of candidates) {
273
+ const value = toInteger(candidate)
274
+ if (value != null) return value === 1 ? 1 : 0
275
+ }
276
+ return null
277
+ }
278
+
279
+ function boundedInteger(value, minimum, maximum = Number.MAX_SAFE_INTEGER) {
280
+ const parsed = toInteger(value)
281
+ return parsed != null && parsed >= minimum && parsed <= maximum
282
+ ? parsed
283
+ : null
284
+ }
285
+
184
286
  function toInteger(value, fallback = null) {
185
287
  if (value == null || value === '') return fallback
186
288
  const parsed = Number(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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": [