poi-plugin-mcp 0.2.13 → 0.2.15

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
@@ -1,151 +1,201 @@
1
1
  # poi-plugin-mcp
2
2
 
3
- Poi plugin that starts a loopback HTTP/MCP bridge inside Poi so local tools can
4
- read current KanColle data, capture the game WebView, and use explicitly enabled
5
- authenticated input.
3
+ `poi-plugin-mcp` 是运行在 Poi 内的本地数据桥。它把当前舰队、舰娘、装备、
4
+ 资源、任务、陆航和相关主数据通过本机 HTTP MCP 提供给本机工具。
6
5
 
7
- ## Install
6
+ 默认监听:
8
7
 
9
- Install it from Poi's plugin manager as:
8
+ ```none
9
+ http://127.0.0.1:17777
10
+ ```
11
+
12
+ 服务只绑定本机回环地址 `127.0.0.1`,不对局域网或公网监听。本文介绍用于
13
+ 读取 Poi 状态的 MCP 工具、MCP 资源和 HTTP 数据端点。
14
+
15
+ ## 安装与重启
10
16
 
11
- ```text
17
+ 在 Poi 插件管理器中安装 npm 包:
18
+
19
+ ```none
12
20
  poi-plugin-mcp
13
21
  ```
14
22
 
15
- Or install it into Poi's plugin directory with npm:
23
+ 也可以完全退出 Poi 后,在 Poi 插件目录中手动安装:
16
24
 
17
25
  ```powershell
18
26
  cd "$env:APPDATA\poi\plugins"
19
27
  npm install poi-plugin-mcp
20
28
  ```
21
29
 
22
- Then restart Poi and enable `MCP Data Bridge` if needed.
23
-
24
- ## Development Install
30
+ 安装或更新后完整退出并重新启动 Poi。在插件设置中确认“`MCP 数据桥`”已启用,
31
+ 然后进入游戏;需要读取账号数据的端点要等 Poi store 初始化完成。
25
32
 
26
- When working from this repository, verify the package from the repo root:
33
+ 如果 MCP 客户端要使用 stdio 方式,可另外全局安装同一个 npm 包,使
34
+ `poi-mcp` 命令进入 `PATH`:
27
35
 
28
36
  ```powershell
29
- npm install
30
- npm test
31
- npm pack --dry-run --ignore-scripts
37
+ npm install -g poi-plugin-mcp
38
+ Get-Command poi-mcp
32
39
  ```
33
40
 
34
- Publish or install the generated package through Poi's plugin manager. Fully
35
- exit Poi before a manual npm installation into its plugin directory.
41
+ ## 健康检查与端口发现
36
42
 
37
- ## Settings
43
+ 默认端口的检查命令:
38
44
 
39
- The Poi plugin settings panel supports:
45
+ ```powershell
46
+ Invoke-RestMethod http://127.0.0.1:17777/health
47
+ ```
40
48
 
41
- - changing the HTTP port, saved in `~/.poi-mcp/settings.json`
42
- - manually starting and stopping the local bridge
43
- - enabling WebView input, which defaults to off
49
+ 正常响应:
44
50
 
45
- The default port is `17777`. A random 32-byte hex input token is generated at
46
- `~/.poi-mcp/input-token` with restrictive best-effort file permissions. The
47
- token is never returned by an HTTP endpoint.
51
+ ```json
52
+ {"status":"ok"}
53
+ ```
48
54
 
49
- ## HTTP Endpoints
55
+ 插件启动后会把实际端口写入:
50
56
 
51
- | Endpoint | Data |
52
- |---|---|
53
- | `/health` | Bridge status |
54
- | `/basic` | Admiral profile |
55
- | `/fleets` | Raw Poi fleet data |
56
- | `/ships` | Raw owned ship instance data |
57
- | `/equipment` | Raw owned equipment instance data |
58
- | `/resources` | Resource array |
59
- | `/quests` | Active quests and quest records |
60
- | `/quest-list` | Latest complete quest-page telemetry |
61
- | `/quest-action` | Latest successful quest start or stop request |
62
- | `/equipment-action` | Latest successful equipment mutation request |
63
- | `/fleet-action` | Latest successful fleet-position mutation request |
64
- | `/action-events` | Bounded generic KanColle API action-event stream |
65
- | `/airbase` | Land base air squadron data |
66
- | `/names` | Ship, equipment, and mission name maps |
67
- | `/master` | Master ship, equipment, ship type, equipment type, mission, and bounded equipment compatibility data |
68
- | `/event` | Event ship tag definitions plus owned ships' current sally area |
69
- | `/planner` | Ship Info deck planner areas and ship assignments |
70
- | `/battle` | Observed battle packets, official settlement, and compact Prophet state |
71
- | `/screenshot` | In-memory PNG capture of the game WebView |
72
- | `/input/status` | Whether authenticated WebView input is enabled |
73
- | `/input/lease/*` | Acquire, renew, release, inspect, or revoke the one input lease |
74
- | `/input` | Authenticated, serialized WebView input |
75
- | `/all` | Combined basic runtime data |
76
-
77
- `/screenshot` accepts `GET` only. It uses Poi's existing
78
- `screenshot::get` WebContents capture path and returns PNG base64 in JSON. It
79
- does not save a file, write the clipboard, capture the desktop, or appear in
80
- MCP resources and tools. Its response disables CORS and uses
81
- `Cache-Control: no-store`.
82
-
83
- ### WebView Input
84
-
85
- `GET /input/status` returns only `{"enabled":true|false}`. Input requires both
86
- `Authorization: Bearer <token>` and one active expiring lease. A static token
87
- alone cannot authorize `POST /input`. Requests accept at most 64 KiB of JSON
88
- and work only while WebView input is enabled in the plugin settings. Input
89
- routes disable CORS and return `Cache-Control: no-store`.
90
-
91
- Each POST accepts exactly one operation:
57
+ ```none
58
+ %USERPROFILE%\.poi-mcp\port
59
+ ```
92
60
 
93
- ```json
94
- {"operation":"click","x":600,"y":360,"button":"left"}
61
+ 修改过端口时,应以该文件为准:
62
+
63
+ ```powershell
64
+ $port = (Get-Content "$HOME\.poi-mcp\port" -Raw).Trim()
65
+ Invoke-RestMethod "http://127.0.0.1:$port/health"
95
66
  ```
96
67
 
97
- Click coordinates use a canonical 1200x720 layout and are scaled to Poi's
98
- current game WebView. Supported buttons are `left`, `middle`, and `right`.
68
+ 根路径 `/` 返回 404 不代表服务故障,请使用 `/health`。
99
69
 
100
- ```json
101
- {"operation":"drag","fromX":247,"fromY":178,"toX":202,"toY":178,"durationMs":400,"button":"left"}
70
+ ## MCP 接入
71
+
72
+ 插件提供两种 MCP 连接方式:
73
+
74
+ - **HTTP MCP**:客户端直接连接 Poi 内的 `POST /mcp`。
75
+ - **stdio MCP**:客户端启动 npm 安装的 `poi-mcp` 命令;该命令读取端口文件,
76
+ 再访问 Poi 的本地数据 API。
77
+
78
+ 两种方式都要求 Poi 正在运行且插件已加载。
79
+
80
+ ### HTTP MCP
81
+
82
+ MCP URL:
83
+
84
+ ```none
85
+ http://127.0.0.1:17777/mcp
102
86
  ```
103
87
 
104
- Drag endpoints use the same canonical layout. Duration must be from 50 through
105
- 2000 milliseconds.
88
+ 这是 JSON-RPC over HTTP 端点,请由 MCP 客户端向它发送 `POST` 请求。Codex
89
+ 配置示例:
106
90
 
107
- ```json
108
- {"operation":"key","event":"keyDown","key":"Enter"}
91
+ ```toml
92
+ [mcp_servers.poi]
93
+ url = "http://127.0.0.1:17777/mcp"
109
94
  ```
110
95
 
111
- Key events are `keyDown` or `keyUp`. Supported keys are `Backspace`, `Delete`,
112
- `End`, `Enter`, `Escape`, `Home`, `PageDown`, `PageUp`, `Space`, `Tab`, and the
113
- four arrow keys.
96
+ 使用 JSON 配置的 MCP 客户端可采用:
114
97
 
115
98
  ```json
116
- {"operation":"text","text":"literal text"}
99
+ {
100
+ "mcpServers": {
101
+ "poi": {
102
+ "url": "http://127.0.0.1:17777/mcp"
103
+ }
104
+ }
105
+ }
117
106
  ```
118
107
 
119
- Literal text must contain 1-256 printable characters. Successful requests
120
- return a bounded response such as
121
- `{"ok":true,"operation":"text","sequence":1}`. Complete operations execute in
122
- sequence, so concurrent requests cannot interleave their WebView events.
108
+ 若修改过端口,请把 URL 中的 `17777` 替换为端口文件中的值。
109
+
110
+ ### stdio CLI
123
111
 
124
- ## MCP Endpoint
112
+ `poi-mcp` MCP stdio 服务命令,不是交互式终端程序。MCP 客户端应负责启动
113
+ 并通过标准输入输出与它通信。
125
114
 
126
- The same local server also exposes a JSON-RPC MCP endpoint:
115
+ Codex 配置示例:
127
116
 
128
- ```text
129
- POST http://127.0.0.1:17777/mcp
117
+ ```toml
118
+ [mcp_servers.poi]
119
+ command = "poi-mcp"
120
+ args = []
130
121
  ```
131
122
 
132
- It supports `initialize`, `ping`, `resources/list`, `resources/read`,
133
- `tools/list`, and `tools/call`. Resource URIs mirror the HTTP endpoints,
134
- including `poi://ships`, `poi://equipment`, `poi://resources`, `poi://master`,
135
- `poi://event`, and `poi://planner`.
123
+ 通用 JSON 配置示例:
136
124
 
137
- Available tools:
125
+ ```json
126
+ {
127
+ "mcpServers": {
128
+ "poi": {
129
+ "command": "poi-mcp",
130
+ "args": []
131
+ }
132
+ }
133
+ }
134
+ ```
138
135
 
139
- - `get_fleet_status`
140
- - `search_ships`
141
- - `search_equipment`
142
- - `get_resources`
143
- - `get_all`
136
+ 如果桌面客户端找不到 npm 的全局命令,请先运行 `Get-Command poi-mcp`,再把
137
+ 配置中的 `command` 改为返回的完整可执行文件路径。
144
138
 
145
- ## Verification
139
+ ## MCP 工具
146
140
 
147
- From the repo root:
141
+ | 工具 | 用途 |
142
+ |---|---|
143
+ | `get_fleet_status` | 按 `fleetId`(1-4)读取一支舰队及舰娘、装备实例 |
144
+ | `search_ships` | 按可选的 `minLevel`、`maxLevel`、`minMorale` 筛选持有舰娘 |
145
+ | `search_equipment` | 按可选的 `minLevel` 筛选持有装备 |
146
+ | `get_resources` | 读取当前资源数组 |
147
+ | `get_all` | 读取基础汇总;`include` 可选 `master`、`event`、`planner` |
148
+
149
+ ## MCP 资源
150
+
151
+ 可通过 `resources/list` 和 `resources/read` 读取:
152
+
153
+ ```none
154
+ poi://basic
155
+ poi://fleets
156
+ poi://ships
157
+ poi://equipment
158
+ poi://resources
159
+ poi://quests
160
+ poi://airbase
161
+ poi://names
162
+ poi://master
163
+ poi://event
164
+ poi://planner
165
+ poi://all
166
+ ```
167
+
168
+ ## 数据端点
169
+
170
+ 主要的本地 HTTP 数据查询端点:
171
+
172
+ | 端点 | 内容 |
173
+ |---|---|
174
+ | `GET /health` | bridge 健康状态 |
175
+ | `GET /basic` | 提督基础信息 |
176
+ | `GET /fleets` | 舰队数据 |
177
+ | `GET /ships` | 持有舰娘实例 |
178
+ | `GET /equipment` | 持有装备实例 |
179
+ | `GET /resources` | 当前资源 |
180
+ | `GET /quests` | 当前任务与任务记录 |
181
+ | `GET /airbase` | 基地航空队 |
182
+ | `GET /names` | 舰娘、装备和远征名称映射 |
183
+ | `GET /master` | 舰娘、装备、类型、远征等主数据 |
184
+ | `GET /event` | 活动标签定义与持有舰娘出击标签 |
185
+ | `GET /planner` | Ship Info 配装规划数据 |
186
+ | `GET /battle` | 已观测战斗数据、结算与简化战斗状态 |
187
+ | `GET /all` | 基础运行数据汇总 |
188
+ | `POST /mcp` | HTTP MCP JSON-RPC 入口 |
189
+
190
+ ## 开发验证
191
+
192
+ 在源码目录执行:
148
193
 
149
194
  ```powershell
150
- npm run test:plugin
195
+ npm install
196
+ npm test
197
+ npm pack --dry-run --ignore-scripts
151
198
  ```
199
+
200
+ `npm test` 应运行 `node --test test/*.test.js`;打包预览应包含
201
+ `index.js`、`lib`、`mcp-server.js` 和本 README。
package/index.js CHANGED
@@ -7,6 +7,7 @@ const controller = createBridgeController({
7
7
  getQuestList: telemetry.getQuestList,
8
8
  getQuestAction: telemetry.getQuestAction,
9
9
  getEquipmentAction: telemetry.getEquipmentAction,
10
+ getUnsetSlot: telemetry.getUnsetSlot,
10
11
  getFleetAction: telemetry.getFleetAction,
11
12
  getActionEvents: telemetry.getActionEvents,
12
13
  getBattleTelemetry: telemetry.getBattleTelemetry,
@@ -33,6 +33,7 @@ function createBridgeController(options = {}) {
33
33
  getQuestList: options.getQuestList,
34
34
  getQuestAction: options.getQuestAction,
35
35
  getEquipmentAction: options.getEquipmentAction,
36
+ getUnsetSlot: options.getUnsetSlot,
36
37
  getFleetAction: options.getFleetAction,
37
38
  getActionEvents: options.getActionEvents,
38
39
  getBattleTelemetry: options.getBattleTelemetry,
@@ -22,6 +22,7 @@ const EXACT_PATHS = new Set([
22
22
  '/kcsapi/api_get_member/ship2',
23
23
  '/kcsapi/api_get_member/ship3',
24
24
  '/kcsapi/api_get_member/slot_item',
25
+ '/kcsapi/api_get_member/unsetslot',
25
26
  '/kcsapi/api_get_member/useitem',
26
27
  '/kcsapi/api_req_air_corps/set_action',
27
28
  '/kcsapi/api_req_air_corps/set_plane',
@@ -33,6 +34,7 @@ const EXACT_PATHS = new Set([
33
34
  '/kcsapi/api_req_hensei/preset_select',
34
35
  '/kcsapi/api_req_hokyu/charge',
35
36
  '/kcsapi/api_req_kaisou/powerup',
37
+ '/kcsapi/api_req_kaisou/slot_select',
36
38
  '/kcsapi/api_req_kaisou/slot_deprive',
37
39
  '/kcsapi/api_req_kaisou/slotset',
38
40
  '/kcsapi/api_req_kaisou/slotset_ex',
@@ -67,6 +69,7 @@ const ALLOWED_PREFIXES = [
67
69
 
68
70
  const SHARED_TELEMETRY_TIMESTAMP_PATHS = new Set([
69
71
  '/kcsapi/api_get_member/questlist',
72
+ '/kcsapi/api_get_member/unsetslot',
70
73
  '/kcsapi/api_req_quest/start',
71
74
  '/kcsapi/api_req_quest/stop',
72
75
  '/kcsapi/api_req_kaisou/slotset',
@@ -40,6 +40,8 @@ function createPoiDataBridge(options = {}) {
40
40
  const getQuestAction = options.getQuestAction || (() => ({ available: false, generation: 0 }))
41
41
  const getEquipmentAction = options.getEquipmentAction ||
42
42
  (() => ({ available: false, generation: 0 }))
43
+ const getUnsetSlot = options.getUnsetSlot ||
44
+ (() => ({ available: false, generation: 0 }))
43
45
  const getFleetAction = options.getFleetAction ||
44
46
  (() => ({ available: false, generation: 0 }))
45
47
  const getActionEvents = options.getActionEvents ||
@@ -367,6 +369,11 @@ function createPoiDataBridge(options = {}) {
367
369
  return
368
370
  }
369
371
 
372
+ if (endpoint === '/unsetslot') {
373
+ sendJson(res, 200, getUnsetSlot())
374
+ return
375
+ }
376
+
370
377
  if (endpoint === '/fleet-action') {
371
378
  sendJson(res, 200, getFleetAction())
372
379
  return
@@ -1,6 +1,7 @@
1
1
  const { createPoiActionEvents } = require('./poi-action-events')
2
2
 
3
3
  const QUEST_LIST_PATH = '/kcsapi/api_get_member/questlist'
4
+ const UNSET_SLOT_PATH = '/kcsapi/api_get_member/unsetslot'
4
5
  const QUEST_ACTION_PATHS = new Set([
5
6
  '/kcsapi/api_req_quest/start',
6
7
  '/kcsapi/api_req_quest/stop',
@@ -49,6 +50,8 @@ function createPoiTelemetry(options = {}) {
49
50
  let questAction = null
50
51
  let equipmentActionGeneration = 0
51
52
  let equipmentAction = null
53
+ let unsetSlotGeneration = 0
54
+ let unsetSlot = null
52
55
  let fleetActionGeneration = 0
53
56
  let fleetAction = null
54
57
  let battleGeneration = 0
@@ -63,6 +66,10 @@ function createPoiTelemetry(options = {}) {
63
66
  captureQuestList(detail, actionEvent)
64
67
  return
65
68
  }
69
+ if (detail.path === UNSET_SLOT_PATH) {
70
+ captureUnsetSlot(detail, actionEvent)
71
+ return
72
+ }
66
73
  if (QUEST_ACTION_PATHS.has(detail.path)) {
67
74
  captureQuestAction(detail, actionEvent)
68
75
  return
@@ -166,6 +173,24 @@ function createPoiTelemetry(options = {}) {
166
173
  }
167
174
  }
168
175
 
176
+ function captureUnsetSlot(detail, actionEvent) {
177
+ const bySlotType = normalizeUnsetSlotBody(detail.body)
178
+ if (bySlotType == null) return
179
+
180
+ unsetSlotGeneration += 1
181
+ unsetSlot = {
182
+ available: true,
183
+ generation: unsetSlotGeneration,
184
+ capturedAt: actionEvent
185
+ ? actionEvent.capturedAt
186
+ : now().toISOString(),
187
+ bySlotType,
188
+ equipmentIds: Object.values(bySlotType)
189
+ .flatMap((groups) => Object.values(groups))
190
+ .flat(),
191
+ }
192
+ }
193
+
169
194
  function captureFleetAction(detail, actionEvent) {
170
195
  const apiResult = actionEvent
171
196
  ? actionEvent.apiResult
@@ -252,6 +277,9 @@ function createPoiTelemetry(options = {}) {
252
277
  getEquipmentAction() {
253
278
  return equipmentAction || { available: false, generation: 0 }
254
279
  },
280
+ getUnsetSlot() {
281
+ return unsetSlot || { available: false, generation: 0 }
282
+ },
255
283
  getFleetAction() {
256
284
  return fleetAction || { available: false, generation: 0 }
257
285
  },
@@ -264,6 +292,36 @@ function createPoiTelemetry(options = {}) {
264
292
  }
265
293
  }
266
294
 
295
+ function normalizeUnsetSlotBody(body) {
296
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null
297
+ const source = body.api_data &&
298
+ typeof body.api_data === 'object' &&
299
+ !Array.isArray(body.api_data)
300
+ ? body.api_data
301
+ : body
302
+ const output = {}
303
+ let totalIds = 0
304
+
305
+ for (const [slotType, groups] of Object.entries(source)) {
306
+ if (!/^api_slottype\d+$/.test(slotType)) continue
307
+ if (!groups || typeof groups !== 'object' || Array.isArray(groups)) continue
308
+ const normalizedGroups = {}
309
+ for (const [groupId, ids] of Object.entries(groups)) {
310
+ if (!/^\d+$/.test(groupId) || !Array.isArray(ids)) continue
311
+ const normalizedIds = ids
312
+ .map(positiveIntegerOrNull)
313
+ .filter((id) => id != null)
314
+ totalIds += normalizedIds.length
315
+ if (totalIds > 10000) return null
316
+ normalizedGroups[groupId] = normalizedIds
317
+ }
318
+ if (Object.keys(normalizedGroups).length > 0) {
319
+ output[slotType] = normalizedGroups
320
+ }
321
+ }
322
+ return Object.keys(output).length > 0 ? output : null
323
+ }
324
+
267
325
  function normalizeFleetPostBody(postBody) {
268
326
  if (!postBody || typeof postBody !== 'object') return null
269
327
  const fleetId = boundedInteger(postBody.api_id, 1, 4)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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": [