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 +7 -0
- package/lib/bridge-controller.js +1 -0
- package/lib/poi-http-bridge.js +44 -5
- package/lib/poi-screenshot.js +104 -0
- package/package.json +3 -3
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:
|
package/lib/bridge-controller.js
CHANGED
package/lib/poi-http-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "Poi data bridge for local KanColle
|
|
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
|
|
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
|
},
|