poi-plugin-mcp 0.2.2 → 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,
@@ -3,6 +3,7 @@ const http = require('http')
3
3
  const os = require('os')
4
4
  const path = require('path')
5
5
  const packageJson = require('../package.json')
6
+ const { createPoiScreenshotProvider } = require('./poi-screenshot')
6
7
 
7
8
  const DEFAULT_PORT = 17777
8
9
  const DEFAULT_PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
@@ -20,6 +21,7 @@ function createPoiDataBridge(options = {}) {
20
21
  const portFile = options.portFile || DEFAULT_PORT_FILE
21
22
  const plannerFile = options.plannerFile || DEFAULT_PLANNER_FILE
22
23
  const logger = options.logger || console
24
+ let captureScreenshot = options.captureScreenshot || null
23
25
 
24
26
  let server = null
25
27
  let actualPort = 0
@@ -32,11 +34,17 @@ function createPoiDataBridge(options = {}) {
32
34
  return store
33
35
  }
34
36
 
35
- function sendJson(res, statusCode, data) {
36
- res.writeHead(statusCode, {
37
- 'Access-Control-Allow-Origin': '*',
37
+ function sendJson(res, statusCode, data, options = {}) {
38
+ const headers = {
38
39
  'Content-Type': 'application/json',
39
- })
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)
40
48
  res.end(JSON.stringify(data))
41
49
  }
42
50
 
@@ -67,7 +75,7 @@ function createPoiDataBridge(options = {}) {
67
75
  })
68
76
  }
69
77
 
70
- function handleRequest(req, res) {
78
+ async function handleRequest(req, res) {
71
79
  try {
72
80
  if (req.url === '/shutdown') {
73
81
  sendJson(res, 200, { status: 'shutting down' })
@@ -87,6 +95,37 @@ function createPoiDataBridge(options = {}) {
87
95
  return
88
96
  }
89
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
+
90
129
  const store = readStore()
91
130
  const info = store.info
92
131
 
@@ -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poi-plugin-mcp",
3
- "version": "0.2.2",
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
  },