poi-plugin-mcp 0.2.0
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 +76 -0
- package/index.js +25 -0
- package/lib/bridge-controller.js +118 -0
- package/lib/poi-http-bridge.js +669 -0
- package/lib/settings-view.js +184 -0
- package/lib/settings.js +51 -0
- package/mcp-server.js +397 -0
- package/package.json +32 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
function loadReact() {
|
|
2
|
+
try {
|
|
3
|
+
return require('react')
|
|
4
|
+
} catch (_) {
|
|
5
|
+
if (typeof window !== 'undefined' && window.React) return window.React
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createSettingsClass(controller) {
|
|
11
|
+
return function PoiMcpSettings() {
|
|
12
|
+
const React = loadReact()
|
|
13
|
+
|
|
14
|
+
if (React && React.useState) {
|
|
15
|
+
return renderStatefulSettings(React, controller)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return renderStaticSettings(createFallbackElement, controller)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderStatefulSettings(React, controller) {
|
|
23
|
+
const e = React.createElement
|
|
24
|
+
const [status, setStatus] = React.useState(() => controller.getStatus())
|
|
25
|
+
const [portText, setPortText] = React.useState(() => String(status.port))
|
|
26
|
+
const [busy, setBusy] = React.useState(false)
|
|
27
|
+
const [message, setMessage] = React.useState('')
|
|
28
|
+
|
|
29
|
+
async function run(action, successMessage) {
|
|
30
|
+
setBusy(true)
|
|
31
|
+
setMessage('')
|
|
32
|
+
try {
|
|
33
|
+
await action()
|
|
34
|
+
const nextStatus = controller.getStatus()
|
|
35
|
+
setStatus(nextStatus)
|
|
36
|
+
setPortText(String(nextStatus.port))
|
|
37
|
+
setMessage(successMessage)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
setMessage(error && error.message ? error.message : String(error))
|
|
40
|
+
} finally {
|
|
41
|
+
setBusy(false)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const port = Number(portText)
|
|
46
|
+
const validPort = Number.isInteger(port) && port >= 1 && port <= 65535
|
|
47
|
+
|
|
48
|
+
return renderSettings(e, {
|
|
49
|
+
busy,
|
|
50
|
+
message,
|
|
51
|
+
port,
|
|
52
|
+
portText,
|
|
53
|
+
status,
|
|
54
|
+
validPort,
|
|
55
|
+
onPortChange: (event) => setPortText(event.target.value),
|
|
56
|
+
onApply: () => run(
|
|
57
|
+
() => controller.applySettings({ port, enabled: status.enabled }),
|
|
58
|
+
'Port saved',
|
|
59
|
+
),
|
|
60
|
+
onToggle: () => run(
|
|
61
|
+
() => (status.running ? controller.stopBridge() : controller.startBridge()),
|
|
62
|
+
status.running ? 'Stopped' : 'Started',
|
|
63
|
+
),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderStaticSettings(e, controller) {
|
|
68
|
+
const status = controller.getStatus()
|
|
69
|
+
const port = status.port
|
|
70
|
+
|
|
71
|
+
return renderSettings(e, {
|
|
72
|
+
busy: false,
|
|
73
|
+
message: '',
|
|
74
|
+
port,
|
|
75
|
+
portText: String(port),
|
|
76
|
+
status,
|
|
77
|
+
validPort: true,
|
|
78
|
+
onPortChange: null,
|
|
79
|
+
onApply: async () => {
|
|
80
|
+
const input = typeof document !== 'undefined' ? document.getElementById('poi-mcp-port') : null
|
|
81
|
+
const nextPort = input ? Number(input.value) : port
|
|
82
|
+
await controller.applySettings({ port: nextPort, enabled: status.enabled })
|
|
83
|
+
},
|
|
84
|
+
onToggle: () => (status.running ? controller.stopBridge() : controller.startBridge()),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderSettings(e, props) {
|
|
89
|
+
const statusText = props.status.running
|
|
90
|
+
? `Running on 127.0.0.1:${props.status.actualPort}`
|
|
91
|
+
: 'Stopped'
|
|
92
|
+
|
|
93
|
+
return e('div', { style: styles.root },
|
|
94
|
+
e('div', { style: styles.row },
|
|
95
|
+
e('label', { style: styles.label, htmlFor: 'poi-mcp-port' }, 'Port'),
|
|
96
|
+
e('input', {
|
|
97
|
+
id: 'poi-mcp-port',
|
|
98
|
+
type: 'number',
|
|
99
|
+
min: 1,
|
|
100
|
+
max: 65535,
|
|
101
|
+
value: props.onPortChange ? props.portText : undefined,
|
|
102
|
+
defaultValue: props.onPortChange ? undefined : props.portText,
|
|
103
|
+
disabled: props.busy,
|
|
104
|
+
style: styles.input,
|
|
105
|
+
onChange: props.onPortChange,
|
|
106
|
+
}),
|
|
107
|
+
e('button', {
|
|
108
|
+
type: 'button',
|
|
109
|
+
disabled: props.busy || !props.validPort,
|
|
110
|
+
style: styles.button,
|
|
111
|
+
onClick: props.onApply,
|
|
112
|
+
}, 'Apply'),
|
|
113
|
+
),
|
|
114
|
+
e('div', { style: styles.row },
|
|
115
|
+
e('span', { style: styles.label }, 'Service'),
|
|
116
|
+
e('button', {
|
|
117
|
+
type: 'button',
|
|
118
|
+
disabled: props.busy,
|
|
119
|
+
style: styles.button,
|
|
120
|
+
onClick: props.onToggle,
|
|
121
|
+
}, props.status.running ? 'Stop' : 'Start'),
|
|
122
|
+
e('span', { style: styles.status }, statusText),
|
|
123
|
+
),
|
|
124
|
+
e('div', { style: styles.note },
|
|
125
|
+
'Pi extension defaults to 127.0.0.1:17777; keep this port unless you also update Pi.',
|
|
126
|
+
),
|
|
127
|
+
props.message ? e('div', { style: styles.message }, props.message) : null,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createFallbackElement(type, props, ...children) {
|
|
132
|
+
const nextProps = { ...(props || {}) }
|
|
133
|
+
if (children.length === 1) nextProps.children = children[0]
|
|
134
|
+
if (children.length > 1) nextProps.children = children
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
$$typeof: Symbol.for('react.element'),
|
|
138
|
+
type,
|
|
139
|
+
key: nextProps.key == null ? null : String(nextProps.key),
|
|
140
|
+
ref: nextProps.ref == null ? null : nextProps.ref,
|
|
141
|
+
props: nextProps,
|
|
142
|
+
_owner: null,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const styles = {
|
|
147
|
+
root: {
|
|
148
|
+
display: 'flex',
|
|
149
|
+
flexDirection: 'column',
|
|
150
|
+
gap: 12,
|
|
151
|
+
maxWidth: 560,
|
|
152
|
+
},
|
|
153
|
+
row: {
|
|
154
|
+
alignItems: 'center',
|
|
155
|
+
display: 'flex',
|
|
156
|
+
gap: 8,
|
|
157
|
+
},
|
|
158
|
+
label: {
|
|
159
|
+
flex: '0 0 80px',
|
|
160
|
+
fontWeight: 600,
|
|
161
|
+
},
|
|
162
|
+
input: {
|
|
163
|
+
width: 120,
|
|
164
|
+
},
|
|
165
|
+
button: {
|
|
166
|
+
minWidth: 72,
|
|
167
|
+
},
|
|
168
|
+
status: {
|
|
169
|
+
color: '#59636e',
|
|
170
|
+
},
|
|
171
|
+
note: {
|
|
172
|
+
color: '#59636e',
|
|
173
|
+
fontSize: 12,
|
|
174
|
+
lineHeight: 1.4,
|
|
175
|
+
},
|
|
176
|
+
message: {
|
|
177
|
+
color: '#1f6feb',
|
|
178
|
+
fontSize: 12,
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
createSettingsClass,
|
|
184
|
+
}
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SETTINGS = Object.freeze({
|
|
6
|
+
port: 17777,
|
|
7
|
+
enabled: true,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SETTINGS_FILE = path.join(os.homedir(), '.poi-mcp', 'settings.json')
|
|
11
|
+
|
|
12
|
+
function normalizeSettings(input = {}) {
|
|
13
|
+
const port = normalizePort(input.port)
|
|
14
|
+
return {
|
|
15
|
+
port: port == null ? DEFAULT_SETTINGS.port : port,
|
|
16
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : DEFAULT_SETTINGS.enabled,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizePort(value) {
|
|
21
|
+
if (value == null || value === '') return null
|
|
22
|
+
|
|
23
|
+
const port = Number(value)
|
|
24
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return null
|
|
25
|
+
return port
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadSettings(settingsPath = DEFAULT_SETTINGS_FILE) {
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(settingsPath, 'utf8')
|
|
31
|
+
return normalizeSettings(JSON.parse(raw))
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return { ...DEFAULT_SETTINGS }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveSettings(settings, settingsPath = DEFAULT_SETTINGS_FILE) {
|
|
38
|
+
const normalized = normalizeSettings(settings)
|
|
39
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
40
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
|
41
|
+
return normalized
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
DEFAULT_SETTINGS,
|
|
46
|
+
DEFAULT_SETTINGS_FILE,
|
|
47
|
+
loadSettings,
|
|
48
|
+
normalizePort,
|
|
49
|
+
normalizeSettings,
|
|
50
|
+
saveSettings,
|
|
51
|
+
}
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// poi-mcp — MCP Server for KanColle game data
|
|
3
|
+
//
|
|
4
|
+
// 两种使用方式:
|
|
5
|
+
// 方式 A: 配合 POI DevTools 脚本 (推荐, 最稳定)
|
|
6
|
+
// 方式 B: 配合 POI 的 --remote-debugging-port
|
|
7
|
+
//
|
|
8
|
+
// ── 快速开始 ──
|
|
9
|
+
// 1. 启动 POI,进入游戏母港
|
|
10
|
+
// 2. POI 菜单 → 开发工具 → 切换开发工具 (F12)
|
|
11
|
+
// 3. 在 Console 中粘贴下面这段脚本:
|
|
12
|
+
//
|
|
13
|
+
// fetch('https://raw.githubusercontent.com/your/poi-plugin-mcp/main/inject.js')
|
|
14
|
+
// .then(r => r.text())
|
|
15
|
+
// .then(eval)
|
|
16
|
+
//
|
|
17
|
+
// 4. 脚本会自动启动 HTTP 服务并写入端口号到 ~/.poi-mcp/port
|
|
18
|
+
// 5. 然后运行: node mcp-server.js
|
|
19
|
+
//
|
|
20
|
+
// ── 备用方案: 粘贴下面脚本到 Console ──
|
|
21
|
+
// (function(){
|
|
22
|
+
// var port = 17777;
|
|
23
|
+
// var http = new XMLHttpRequest();
|
|
24
|
+
// http.open('GET', 'http://127.0.0.1:' + port + '/health', true);
|
|
25
|
+
// http.onload = function() {
|
|
26
|
+
// if (http.status === 200) console.log('[poi-mcp] Server already running on port', port);
|
|
27
|
+
// };
|
|
28
|
+
// http.send();
|
|
29
|
+
// var s = document.createElement('script');
|
|
30
|
+
// s.src = 'data:text/javascript,' + encodeURIComponent([
|
|
31
|
+
// 'var p='+port+';',
|
|
32
|
+
// 'var gs=function(){return window.getStore()};',
|
|
33
|
+
// 'var s=require("http").createServer(function(q,r){',
|
|
34
|
+
// ' r.setHeader("Access-Control-Allow-Origin","*");',
|
|
35
|
+
// ' r.setHeader("Content-Type","application/json");',
|
|
36
|
+
// ' var u=q.url;',
|
|
37
|
+
// ' if(u==="/health"){r.end('+JSON.stringify(JSON.stringify({status:"ok"}))+')}',
|
|
38
|
+
// ' else if(u==="/fleets"){r.end(JSON.stringify(gs().info.fleets))}',
|
|
39
|
+
// ' else if(u==="/ships"){r.end(JSON.stringify(gs().info.ships))}',
|
|
40
|
+
// ' else if(u==="/equipment"){r.end(JSON.stringify(gs().info.equips))}',
|
|
41
|
+
// ' else if(u==="/resources"){r.end(JSON.stringify(gs().info.resources))}',
|
|
42
|
+
// ' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:gs().info.quests.activeQuests,records:gs().info.quests.records}))}',
|
|
43
|
+
// ' else if(u==="/airbase"){r.end(JSON.stringify(gs().info.airbase))}',
|
|
44
|
+
// ' else if(u==="/basic"){r.end(JSON.stringify(gs().info.basic))}',
|
|
45
|
+
// ' else if(u==="/all"){r.end(JSON.stringify(gs().info))}',
|
|
46
|
+
// ' else{r.writeHead(404);r.end("Not found")}',
|
|
47
|
+
// '});',
|
|
48
|
+
// 's.listen(p,"127.0.0.1",function(){',
|
|
49
|
+
// ' require("fs").writeFileSync("'+require('path').join(os.homedir(),'.poi-mcp','port').replace(/\\/g,'/')+'",String(p),"utf8");',
|
|
50
|
+
// ' console.log("[poi-mcp] API running on http://127.0.0.1:"+p);',
|
|
51
|
+
// '});'
|
|
52
|
+
// ].join(''));
|
|
53
|
+
// document.head.appendChild(s);
|
|
54
|
+
// })();
|
|
55
|
+
|
|
56
|
+
const os = require('os')
|
|
57
|
+
const path = require('path')
|
|
58
|
+
const fs = require('fs')
|
|
59
|
+
const http = require('http')
|
|
60
|
+
|
|
61
|
+
const PORT_FILE = path.join(os.homedir(), '.poi-mcp', 'port')
|
|
62
|
+
|
|
63
|
+
// ─── POI HTTP API Client ─────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function getPoiPort() {
|
|
66
|
+
try {
|
|
67
|
+
return parseInt(fs.readFileSync(PORT_FILE, 'utf8').trim(), 10)
|
|
68
|
+
} catch (_) {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fetchFromPoi(endpoint) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const port = getPoiPort()
|
|
76
|
+
if (!port) {
|
|
77
|
+
return reject(new Error(
|
|
78
|
+
'POI data API not found.\n\n' +
|
|
79
|
+
'Please:\n' +
|
|
80
|
+
' 1. Open POI → F12 (DevTools) → Console tab\n' +
|
|
81
|
+
' 2. Paste this script and press Enter:\n\n' +
|
|
82
|
+
'─── PASTE THIS INTO POI CONSOLE ───\n' +
|
|
83
|
+
getInjectScript() +
|
|
84
|
+
'\n─── END ───\n\n' +
|
|
85
|
+
' 3. Then run this MCP server again.'
|
|
86
|
+
))
|
|
87
|
+
}
|
|
88
|
+
http.get(`http://127.0.0.1:${port}${endpoint}`, (res) => {
|
|
89
|
+
let data = ''
|
|
90
|
+
res.on('data', chunk => data += chunk)
|
|
91
|
+
res.on('end', () => {
|
|
92
|
+
try { resolve(JSON.parse(data)) } catch (e) { reject(e) }
|
|
93
|
+
})
|
|
94
|
+
}).on('error', reject).setTimeout(10000, function() {
|
|
95
|
+
this.destroy()
|
|
96
|
+
reject(new Error('Request timed out'))
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getInjectScript() {
|
|
102
|
+
return [
|
|
103
|
+
'(function(){',
|
|
104
|
+
'var p=17777;',
|
|
105
|
+
'var s=require("http").createServer(function(q,r){',
|
|
106
|
+
' r.setHeader("Access-Control-Allow-Origin","*");',
|
|
107
|
+
' r.setHeader("Content-Type","application/json");',
|
|
108
|
+
' try{',
|
|
109
|
+
' var st=window.getStore();',
|
|
110
|
+
' if(!st||!st.info)throw new Error("Store not ready");',
|
|
111
|
+
' var u=q.url;',
|
|
112
|
+
' if(u==="/health"){r.end(JSON.stringify({status:"ok"}))}',
|
|
113
|
+
' else if(u==="/fleets"){r.end(JSON.stringify(st.info.fleets||[]))}',
|
|
114
|
+
' else if(u==="/ships"){r.end(JSON.stringify(st.info.ships||{}))}',
|
|
115
|
+
' else if(u==="/equipment"){r.end(JSON.stringify(st.info.equips||{}))}',
|
|
116
|
+
' else if(u==="/resources"){r.end(JSON.stringify(st.info.resources||[]))}',
|
|
117
|
+
' else if(u==="/quests"){r.end(JSON.stringify({activeQuests:st.info.quests?.activeQuests||{},records:st.info.quests?.records||{}}))}',
|
|
118
|
+
' else if(u==="/airbase"){r.end(JSON.stringify(st.info.airbase||[]))}',
|
|
119
|
+
' else if(u==="/basic"){r.end(JSON.stringify(st.info.basic||{}))}',
|
|
120
|
+
' else if(u==="/all"){r.end(JSON.stringify({',
|
|
121
|
+
' basic:st.info.basic,',
|
|
122
|
+
' fleets:st.info.fleets,',
|
|
123
|
+
' ships:st.info.ships,',
|
|
124
|
+
' equipment:st.info.equips,',
|
|
125
|
+
' resources:st.info.resources,',
|
|
126
|
+
' quests:{activeQuests:st.info.quests?.activeQuests,records:st.info.quests?.records},',
|
|
127
|
+
' airbase:st.info.airbase',
|
|
128
|
+
' }))}',
|
|
129
|
+
' else{r.writeHead(404);r.end("Not found")}',
|
|
130
|
+
' }catch(e){r.writeHead(500);r.end(e.message)}',
|
|
131
|
+
'});',
|
|
132
|
+
's.listen(p,"127.0.0.1",function(){',
|
|
133
|
+
' var d=require("path").join(require("os").homedir(),".poi-mcp");',
|
|
134
|
+
' try{require("fs").mkdirSync(d,{recursive:true})}catch(e){}',
|
|
135
|
+
' require("fs").writeFileSync(require("path").join(d,"port"),String(p),"utf8");',
|
|
136
|
+
' console.log("[poi-mcp] API: http://127.0.0.1:"+p+" (/fleets /ships /equipment /resources /quests /airbase /basic /all)");',
|
|
137
|
+
'});',
|
|
138
|
+
'})()'
|
|
139
|
+
].join('\n')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── MCP Protocol ────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
// Minimum viable MCP stdio server — no external dependencies
|
|
145
|
+
|
|
146
|
+
const JSONRPC_VERSION = '2.0'
|
|
147
|
+
let reqId = 0
|
|
148
|
+
|
|
149
|
+
function send(id, result, error) {
|
|
150
|
+
const msg = { jsonrpc: JSONRPC_VERSION }
|
|
151
|
+
if (id != null) msg.id = id
|
|
152
|
+
if (error) msg.error = { code: error.code || -32603, message: error.message }
|
|
153
|
+
else msg.result = result
|
|
154
|
+
process.stdout.write(JSON.stringify(msg) + '\n')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sendLog(text) {
|
|
158
|
+
console.error('[poi-mcp] ' + text)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async function main() {
|
|
164
|
+
sendLog('POI MCP Server v0.1.0')
|
|
165
|
+
sendLog('Checking POI data API...')
|
|
166
|
+
|
|
167
|
+
const port = getPoiPort()
|
|
168
|
+
if (!port) {
|
|
169
|
+
sendLog('NOT CONNECTED — POI DevTools script not running')
|
|
170
|
+
sendLog('')
|
|
171
|
+
sendLog('=== 请在 POI 中执行以下步骤 ===')
|
|
172
|
+
sendLog('1. 启动 POI,进入游戏母港')
|
|
173
|
+
sendLog('2. 按 F12 打开 DevTools → Console 标签')
|
|
174
|
+
sendLog('3. 粘贴下面一整段脚本,按回车:')
|
|
175
|
+
sendLog('')
|
|
176
|
+
console.error(getInjectScript())
|
|
177
|
+
sendLog('')
|
|
178
|
+
sendLog('4. 关闭 DevTools,重新运行本命令')
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Verify the API is responding
|
|
183
|
+
try {
|
|
184
|
+
const health = await fetchFromPoi('/health')
|
|
185
|
+
sendLog(`Connected to POI API on port ${port}: ${health.status}`)
|
|
186
|
+
} catch (err) {
|
|
187
|
+
sendLog(`ERROR: POI API on port ${port} is not responding: ${err.message}`)
|
|
188
|
+
sendLog('Make sure POI is running and the script was pasted into DevTools Console.')
|
|
189
|
+
process.exit(1)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── MCP Request Handler ──────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
const toolHandlers = {
|
|
195
|
+
get_fleet_status: async (args) => {
|
|
196
|
+
const fleets = await fetchFromPoi('/fleets')
|
|
197
|
+
if (!Array.isArray(fleets)) return { error: 'No fleet data' }
|
|
198
|
+
const fleet = fleets[args.fleetId - 1]
|
|
199
|
+
if (!fleet) return { error: `Fleet #${args.fleetId} not found` }
|
|
200
|
+
|
|
201
|
+
// Enrich with ship names and equipment
|
|
202
|
+
const ships = await fetchFromPoi('/ships')
|
|
203
|
+
const equips = await fetchFromPoi('/equipment')
|
|
204
|
+
return {
|
|
205
|
+
id: fleet.api_id,
|
|
206
|
+
name: fleet.api_name,
|
|
207
|
+
mission: fleet.api_mission,
|
|
208
|
+
ships: (fleet.api_ship || []).filter(id => id > 0).map(sid => {
|
|
209
|
+
const s = ships[sid]
|
|
210
|
+
if (!s) return { id: sid }
|
|
211
|
+
return {
|
|
212
|
+
id: s.api_id,
|
|
213
|
+
shipId: s.api_ship_id,
|
|
214
|
+
level: s.api_lv,
|
|
215
|
+
hp: `${s.api_nowhp}/${s.api_maxhp}`,
|
|
216
|
+
morale: s.api_cond,
|
|
217
|
+
locked: s.api_locked,
|
|
218
|
+
slotItems: (s.api_slot || []).filter(eid => eid > 0).map(eid => {
|
|
219
|
+
const e = equips[eid]
|
|
220
|
+
return e ? { equipId: e.api_slotitem_id, level: e.api_level || 0, prof: e.api_alv || 0 } : null
|
|
221
|
+
}).filter(Boolean)
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
search_ships: async (args) => {
|
|
228
|
+
const ships = await fetchFromPoi('/ships')
|
|
229
|
+
const results = Object.values(ships).filter(s => {
|
|
230
|
+
if (!s) return false
|
|
231
|
+
if (args.minLevel != null && s.api_lv < args.minLevel) return false
|
|
232
|
+
if (args.maxLevel != null && s.api_lv > args.maxLevel) return false
|
|
233
|
+
if (args.minMorale != null && s.api_cond < args.minMorale) return false
|
|
234
|
+
return true
|
|
235
|
+
})
|
|
236
|
+
return { total: results.length, ships: results }
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
search_equipment: async (args) => {
|
|
240
|
+
const equips = await fetchFromPoi('/equipment')
|
|
241
|
+
const results = Object.values(equips).filter(e => {
|
|
242
|
+
if (!e) return false
|
|
243
|
+
if (args.minLevel != null && (e.api_level || 0) < args.minLevel) return false
|
|
244
|
+
return true
|
|
245
|
+
})
|
|
246
|
+
return { total: results.length, equipment: results }
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
get_resources: async () => {
|
|
250
|
+
return await fetchFromPoi('/resources')
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
get_all: async () => {
|
|
254
|
+
return await fetchFromPoi('/all')
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const resourceUris = [
|
|
259
|
+
'poi://fleets', 'poi://ships', 'poi://equipment',
|
|
260
|
+
'poi://resources', 'poi://quests', 'poi://airbase', 'poi://basic', 'poi://all'
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
// ── JSON-RPC over stdio ──────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
let buffer = ''
|
|
266
|
+
process.stdin.setEncoding('utf8')
|
|
267
|
+
process.stdin.on('data', async (chunk) => {
|
|
268
|
+
buffer += chunk
|
|
269
|
+
const lines = buffer.split('\n')
|
|
270
|
+
buffer = lines.pop() || ''
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
if (!line.trim()) continue
|
|
273
|
+
try {
|
|
274
|
+
const req = JSON.parse(line)
|
|
275
|
+
const { id, method, params } = req
|
|
276
|
+
|
|
277
|
+
switch (method) {
|
|
278
|
+
case 'initialize':
|
|
279
|
+
send(id, {
|
|
280
|
+
protocolVersion: '2024-11-05',
|
|
281
|
+
capabilities: {
|
|
282
|
+
resources: { subscribe: false },
|
|
283
|
+
tools: {}
|
|
284
|
+
},
|
|
285
|
+
serverInfo: { name: 'poi-mcp', version: '0.1.0' }
|
|
286
|
+
})
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
case 'notifications/initialized':
|
|
290
|
+
case 'notifications/cancelled':
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
case 'ping':
|
|
294
|
+
send(id, {})
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
case 'resources/list':
|
|
298
|
+
send(id, {
|
|
299
|
+
resources: resourceUris.map(uri => ({
|
|
300
|
+
uri, name: uri.replace('poi://', ''), mimeType: 'application/json'
|
|
301
|
+
}))
|
|
302
|
+
})
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
case 'resources/read': {
|
|
306
|
+
const uri = params?.uri
|
|
307
|
+
const endpoint = '/' + uri.replace('poi://', '')
|
|
308
|
+
const data = await fetchFromPoi(endpoint)
|
|
309
|
+
send(id, {
|
|
310
|
+
contents: [{
|
|
311
|
+
uri,
|
|
312
|
+
mimeType: 'application/json',
|
|
313
|
+
text: JSON.stringify(data, null, 2)
|
|
314
|
+
}]
|
|
315
|
+
})
|
|
316
|
+
break
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
case 'tools/list':
|
|
320
|
+
send(id, {
|
|
321
|
+
tools: [
|
|
322
|
+
{
|
|
323
|
+
name: 'get_fleet_status',
|
|
324
|
+
description: '获取舰队详细编成',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: { fleetId: { type: 'number', description: '舰队编号 1-4' } },
|
|
328
|
+
required: ['fleetId']
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'search_ships',
|
|
333
|
+
description: '搜索舰娘',
|
|
334
|
+
inputSchema: {
|
|
335
|
+
type: 'object',
|
|
336
|
+
properties: {
|
|
337
|
+
minLevel: { type: 'number' },
|
|
338
|
+
maxLevel: { type: 'number' },
|
|
339
|
+
minMorale: { type: 'number', description: '最低士气(闪)' }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'search_equipment',
|
|
345
|
+
description: '搜索装备',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
minLevel: { type: 'number', description: '最低改修★' }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'get_resources',
|
|
355
|
+
description: '获取资源概况',
|
|
356
|
+
inputSchema: { type: 'object', properties: {} }
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'get_all',
|
|
360
|
+
description: '获取所有数据(舰队/舰娘/装备/资源/任务/陆航)',
|
|
361
|
+
inputSchema: { type: 'object', properties: {} }
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
})
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
case 'tools/call': {
|
|
368
|
+
const toolName = params?.name
|
|
369
|
+
const toolArgs = params?.arguments || {}
|
|
370
|
+
const handler = toolHandlers[toolName]
|
|
371
|
+
if (handler) {
|
|
372
|
+
const result = await handler(toolArgs)
|
|
373
|
+
send(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] })
|
|
374
|
+
} else {
|
|
375
|
+
send(id, null, { code: -32602, message: `Unknown tool: ${toolName}` })
|
|
376
|
+
}
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
default:
|
|
381
|
+
send(id, null, { code: -32601, message: `Unknown method: ${method}` })
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
// Malformed JSON — ignore
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
process.stdin.on('end', () => process.exit(0))
|
|
390
|
+
process.on('SIGINT', () => process.exit(0))
|
|
391
|
+
process.on('SIGTERM', () => process.exit(0))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
main().catch(err => {
|
|
395
|
+
console.error('[poi-mcp] Fatal:', err.message)
|
|
396
|
+
process.exit(1)
|
|
397
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "poi-plugin-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Poi data bridge for local KanColle inventory tools.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"poi-plugin",
|
|
8
|
+
"kancolle",
|
|
9
|
+
"mcp"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"lib",
|
|
14
|
+
"mcp-server.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"bin": {
|
|
18
|
+
"poi-mcp": "mcp-server.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node mcp-server.js",
|
|
22
|
+
"test": "node --test test/*.test.js"
|
|
23
|
+
},
|
|
24
|
+
"poiPlugin": {
|
|
25
|
+
"title": "MCP 数据桥",
|
|
26
|
+
"id": "mcp_data_bridge",
|
|
27
|
+
"description": "Expose Poi fleet, equipment, master, event tag, and planner data on local HTTP for CLI/MCP agents.",
|
|
28
|
+
"icon": "fa/database",
|
|
29
|
+
"priority": 53
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|