You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
517 lines
78 KiB
517 lines
78 KiB
|
2 weeks ago
|
"use strict";
|
||
|
|
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
||
|
|
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
||
|
|
var m = o[Symbol.asyncIterator], i;
|
||
|
|
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
||
|
|
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
||
|
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
||
|
|
};
|
||
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
|
|
};
|
||
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
|
exports.McpHttpServer = exports.MCP_DEFAULT_PORT = void 0;
|
||
|
|
const http_1 = __importDefault(require("http"));
|
||
|
|
const zod_1 = require("zod");
|
||
|
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
||
|
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
||
|
|
/** MCP 服务默认端口(HTTP Streamable)。 */
|
||
|
|
exports.MCP_DEFAULT_PORT = 33010;
|
||
|
|
const text = (s, isError = false) => ({
|
||
|
|
content: [{ type: 'text', text: s }],
|
||
|
|
isError,
|
||
|
|
});
|
||
|
|
const json = (v, isError = false) => ({
|
||
|
|
content: [{ type: 'text', text: JSON.stringify(v, null, 2) }],
|
||
|
|
isError,
|
||
|
|
});
|
||
|
|
/** 统计节点树总节点数。 */
|
||
|
|
function countNodes(roots) {
|
||
|
|
let n = 0;
|
||
|
|
const walk = (list) => {
|
||
|
|
for (const r of list) {
|
||
|
|
n++;
|
||
|
|
if (Array.isArray(r.children)) {
|
||
|
|
walk(r.children);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
walk(roots !== null && roots !== void 0 ? roots : []);
|
||
|
|
return n;
|
||
|
|
}
|
||
|
|
/** 把节点树裁剪到指定深度(在序列化结果上处理,无需改桥接脚本)。 */
|
||
|
|
function pruneTree(roots, maxDepth) {
|
||
|
|
const walk = (list, depth) => {
|
||
|
|
return (list !== null && list !== void 0 ? list : []).map((r) => {
|
||
|
|
const out = {
|
||
|
|
id: r.id,
|
||
|
|
name: r.name,
|
||
|
|
active: r.active,
|
||
|
|
layer: r.layer,
|
||
|
|
comps: r.comps,
|
||
|
|
};
|
||
|
|
if (Array.isArray(r.children) && depth < maxDepth) {
|
||
|
|
out.children = walk(r.children, depth + 1);
|
||
|
|
}
|
||
|
|
else if (Array.isArray(r.children) && r.children.length) {
|
||
|
|
out.children = `...${r.children.length} 个子节点(超出 max_depth)`;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
});
|
||
|
|
};
|
||
|
|
return walk(roots !== null && roots !== void 0 ? roots : [], 0);
|
||
|
|
}
|
||
|
|
function fmtT(ts) {
|
||
|
|
const d = new Date(ts);
|
||
|
|
const p = (n) => String(n).padStart(2, '0');
|
||
|
|
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* 内嵌在主进程的 MCP 服务(Streamable HTTP)。
|
||
|
|
* 工具直接代理到游戏侧桥接脚本(query-tree / get-inspector / set-property / capture-frame 等),
|
||
|
|
* 供 Codex 等 AI Agent 连接并观察、操作正在运行的游戏。
|
||
|
|
*/
|
||
|
|
class McpHttpServer {
|
||
|
|
constructor() {
|
||
|
|
this.srv = null;
|
||
|
|
this.port = 0;
|
||
|
|
this.token = '';
|
||
|
|
this.deps = null;
|
||
|
|
/** 每个会话持有独立的 McpServer 实例(SDK 限制一个 Protocol 只能连接一个 Transport)。 */
|
||
|
|
this.sessions = new Map();
|
||
|
|
}
|
||
|
|
get isRunning() {
|
||
|
|
return !!this.srv;
|
||
|
|
}
|
||
|
|
get boundPort() {
|
||
|
|
return this.port;
|
||
|
|
}
|
||
|
|
checkAuth(req) {
|
||
|
|
if (!this.token) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return req.headers.authorization === `Bearer ${this.token}`;
|
||
|
|
}
|
||
|
|
async readBody(req, maxBytes = 1024 * 1024) {
|
||
|
|
var _a, e_1, _b, _c;
|
||
|
|
const chunks = [];
|
||
|
|
let size = 0;
|
||
|
|
try {
|
||
|
|
for (var _d = true, req_1 = __asyncValues(req), req_1_1; req_1_1 = await req_1.next(), _a = req_1_1.done, !_a; _d = true) {
|
||
|
|
_c = req_1_1.value;
|
||
|
|
_d = false;
|
||
|
|
const chunk = _c;
|
||
|
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
||
|
|
size += buf.length;
|
||
|
|
if (size > maxBytes) {
|
||
|
|
throw new Error('body too large');
|
||
|
|
}
|
||
|
|
chunks.push(buf);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
|
|
finally {
|
||
|
|
try {
|
||
|
|
if (!_d && !_a && (_b = req_1.return)) await _b.call(req_1);
|
||
|
|
}
|
||
|
|
finally { if (e_1) throw e_1.error; }
|
||
|
|
}
|
||
|
|
const raw = Buffer.concat(chunks).toString('utf-8');
|
||
|
|
if (!raw) {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return JSON.parse(raw);
|
||
|
|
}
|
||
|
|
catch (_e) {
|
||
|
|
return raw;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async handle(req, res) {
|
||
|
|
var _a;
|
||
|
|
if (!this.checkAuth(req)) {
|
||
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||
|
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const url = (_a = req.url) !== null && _a !== void 0 ? _a : '';
|
||
|
|
if (!url.startsWith('/mcp')) {
|
||
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
|
|
res.end(JSON.stringify({ error: 'not found' }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const sessionId = req.headers['mcp-session-id'];
|
||
|
|
let session = sessionId ? this.sessions.get(sessionId) : undefined;
|
||
|
|
if (sessionId && !session) {
|
||
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
|
|
res.end(JSON.stringify({ error: 'invalid session id' }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
if (!session) {
|
||
|
|
// 新会话:为每个会话创建独立的 McpServer 实例,避免 Protocol 重复连接报错
|
||
|
|
const deps = this.deps;
|
||
|
|
if (!deps) {
|
||
|
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||
|
|
res.end(JSON.stringify({ error: 'server not ready' }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
||
|
|
sessionIdGenerator: undefined,
|
||
|
|
});
|
||
|
|
const sid = transport.sessionId;
|
||
|
|
const mcp = this.buildServer(deps);
|
||
|
|
session = { transport, mcp };
|
||
|
|
this.sessions.set(sid, session);
|
||
|
|
transport.onclose = () => {
|
||
|
|
if (this.sessions.get(sid) === session) {
|
||
|
|
this.sessions.delete(sid);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
await mcp.connect(transport);
|
||
|
|
}
|
||
|
|
const parsedBody = req.method === 'POST' ? await this.readBody(req) : undefined;
|
||
|
|
await session.transport.handleRequest(req, res, parsedBody);
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
try {
|
||
|
|
if (!res.headersSent) {
|
||
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
|
|
}
|
||
|
|
res.end(JSON.stringify({ error: String(err) }));
|
||
|
|
}
|
||
|
|
catch (_b) {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/** 注册工具集。 */
|
||
|
|
buildServer(deps) {
|
||
|
|
const mcp = new mcp_js_1.McpServer({ name: 'yyka-runtime-inspector', version: '1.0.0' });
|
||
|
|
const noClient = () => {
|
||
|
|
const st = deps.getServerState();
|
||
|
|
return !st.running || st.clients === 0;
|
||
|
|
};
|
||
|
|
mcp.registerTool('get_runtime_info', {
|
||
|
|
title: '获取运行时信息',
|
||
|
|
description: '获取当前连接的游戏实例信息:连接状态、场景名、节点数量、FPS、DrawCall、三角形数、内存占用、物理系统状态、桥接版本。游戏未连接时返回错误。',
|
||
|
|
inputSchema: zod_1.z.object({}),
|
||
|
|
}, async () => {
|
||
|
|
var _a;
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接:请先启动 WS 服务、运行游戏并确认桥接脚本已连接。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const [stats, tree] = await Promise.all([
|
||
|
|
deps.callGame('get-stats', undefined, 4000),
|
||
|
|
deps.callGame('query-tree', {}, 4000),
|
||
|
|
]);
|
||
|
|
const st = deps.getServerState();
|
||
|
|
return json({
|
||
|
|
connected: true,
|
||
|
|
clients: st.clients,
|
||
|
|
wsPort: st.port,
|
||
|
|
scene: (_a = tree === null || tree === void 0 ? void 0 : tree.scene) !== null && _a !== void 0 ? _a : '',
|
||
|
|
nodeCount: countNodes(tree === null || tree === void 0 ? void 0 : tree.roots),
|
||
|
|
stats: stats !== null && stats !== void 0 ? stats : {},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`获取运行时信息失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('get_node_tree', {
|
||
|
|
title: '获取节点树',
|
||
|
|
description: '获取当前场景的运行时节点树(名称/激活/层级/组件数)。可用 filter 按节点名筛选;max_depth 限制展开深度避免输出过大。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
filter: zod_1.z.string().optional().describe('按节点名关键字筛选(可选)'),
|
||
|
|
max_depth: zod_1.z.number().optional().describe('最大展开深度,默认 20(可选)'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
var _a, _b;
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const tree = await deps.callGame('query-tree', { filter: (_a = args === null || args === void 0 ? void 0 : args.filter) !== null && _a !== void 0 ? _a : '' }, 8000);
|
||
|
|
if (tree && Array.isArray(tree.roots)) {
|
||
|
|
const depth = typeof (args === null || args === void 0 ? void 0 : args.max_depth) === 'number' ? Math.max(1, Math.min(50, args.max_depth)) : 20;
|
||
|
|
return json({ scene: (_b = tree.scene) !== null && _b !== void 0 ? _b : '', roots: pruneTree(tree.roots, depth) });
|
||
|
|
}
|
||
|
|
return text('获取节点树失败。', true);
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`获取节点树失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('get_node_info', {
|
||
|
|
title: '获取节点信息',
|
||
|
|
description: '按节点 id 获取节点详情:位置/旋转/缩放/激活/层级等 Transform 属性,以及所有组件及其可编辑属性(含枚举、数组、向量、颜色、引用等)。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
id: zod_1.z.string().describe('节点 id(来自 get_node_tree)'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
if (!(args === null || args === void 0 ? void 0 : args.id)) {
|
||
|
|
return text('缺少参数 id。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const r = await deps.callGame('get-inspector', { id: args.id }, 8000);
|
||
|
|
if (r && r.error) {
|
||
|
|
return text(String(r.error), true);
|
||
|
|
}
|
||
|
|
return json(r !== null && r !== void 0 ? r : {});
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`获取节点信息失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('set_node_property', {
|
||
|
|
title: '修改运行时属性',
|
||
|
|
description: '修改运行中节点的属性。节点级:field 取 position/euler/scale(value 为 {x,y,z})、active(value 为 boolean)、layer(value 为 number)。组件级:传 comp 组件索引(0 起)与 key 属性名,value 为属性值(向量/颜色传对象,枚举传数字)。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
id: zod_1.z.string().describe('节点 id(来自 get_node_tree)'),
|
||
|
|
field: zod_1.z.string().optional().describe('节点级字段:position | euler | scale | active | layer'),
|
||
|
|
value: zod_1.z.any().optional().describe('要写入的值'),
|
||
|
|
comp: zod_1.z.number().optional().describe('组件索引(0 起,与 get_node_info 的 components 顺序一致)'),
|
||
|
|
key: zod_1.z.string().optional().describe('组件属性名'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
if (!(args === null || args === void 0 ? void 0 : args.id)) {
|
||
|
|
return text('缺少参数 id。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const payload = { id: args.id };
|
||
|
|
if (typeof args.field === 'string') {
|
||
|
|
payload.field = args.field;
|
||
|
|
payload.value = args.value;
|
||
|
|
}
|
||
|
|
if (typeof args.comp === 'number') {
|
||
|
|
payload.comp = args.comp;
|
||
|
|
payload.key = args.key;
|
||
|
|
payload.value = args.value;
|
||
|
|
}
|
||
|
|
const r = await deps.callGame('set-property', payload, 8000);
|
||
|
|
if (r && r.error) {
|
||
|
|
return text(String(r.error), true);
|
||
|
|
}
|
||
|
|
return json(r !== null && r !== void 0 ? r : { ok: true });
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`修改属性失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('get_screenshot', {
|
||
|
|
title: '获取游戏截图',
|
||
|
|
description: '抓取当前游戏画面并返回图片(JPEG),用于视觉验证游戏状态。',
|
||
|
|
inputSchema: zod_1.z.object({}),
|
||
|
|
}, async () => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const r = await deps.callGame('capture-frame', {}, 6000);
|
||
|
|
const dataUrl = r && typeof r.data === 'string' ? r.data : '';
|
||
|
|
const base64 = dataUrl.includes(',') ? dataUrl.split(',')[1] : dataUrl;
|
||
|
|
if (!base64) {
|
||
|
|
return text('截图失败:游戏侧未能捕获画面。', true);
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
content: [{ type: 'image', data: base64, mimeType: 'image/jpeg' }],
|
||
|
|
};
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`截图失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('get_logs', {
|
||
|
|
title: '获取运行时日志',
|
||
|
|
description: '获取游戏侧最近的控制台日志(console.log/warn/error)。level 可选 log/warn/error,默认全部。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
limit: zod_1.z.number().optional().describe('返回条数,默认 50'),
|
||
|
|
level: zod_1.z.string().optional().describe('过滤级别:all | log | warn | error'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
const limit = typeof (args === null || args === void 0 ? void 0 : args.limit) === 'number' ? Math.max(1, Math.min(200, args.limit)) : 50;
|
||
|
|
const level = typeof (args === null || args === void 0 ? void 0 : args.level) === 'string' ? args.level : 'all';
|
||
|
|
const items = deps.getLogs(limit, level);
|
||
|
|
if (!items.length) {
|
||
|
|
return text('(暂无日志)');
|
||
|
|
}
|
||
|
|
const lines = items.map((it) => {
|
||
|
|
var _a, _b;
|
||
|
|
if (it && it.evt === 'console') {
|
||
|
|
const tag = it.level === 'error' ? 'ERROR' : it.level === 'warn' ? 'WARN ' : 'LOG ';
|
||
|
|
return `${fmtT(it.t)} [${tag}] ${it.text}`;
|
||
|
|
}
|
||
|
|
if (it && it.evt === 'sys') {
|
||
|
|
return `${fmtT((_a = it.t) !== null && _a !== void 0 ? _a : Date.now())} [SYS] ${(_b = it.msg) !== null && _b !== void 0 ? _b : ''}`;
|
||
|
|
}
|
||
|
|
return JSON.stringify(it);
|
||
|
|
});
|
||
|
|
return text(lines.join('\n'));
|
||
|
|
});
|
||
|
|
mcp.registerTool('wait', {
|
||
|
|
title: '等待',
|
||
|
|
description: '等待指定毫秒(1-60000),常用于等待游戏状态变化后再查询。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
ms: zod_1.z.number().describe('毫秒数,1-60000'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
const ms = Math.max(1, Math.min(60000, Number(args === null || args === void 0 ? void 0 : args.ms) || 0));
|
||
|
|
await new Promise((r) => setTimeout(r, ms));
|
||
|
|
return text(`已等待 ${ms}ms`);
|
||
|
|
});
|
||
|
|
mcp.registerTool('execute_command', {
|
||
|
|
title: '执行游戏命令',
|
||
|
|
description: '向游戏侧桥接脚本发送任意命令。常用:get-stats、get-resource-stats、query-physics、set-preview({enabled,fps})、set-mute({muted})、set-physics-debug({enabled})。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
cmd: zod_1.z.string().describe('命令名'),
|
||
|
|
payload: zod_1.z.any().optional().describe('命令参数(可选)'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
if (!(args === null || args === void 0 ? void 0 : args.cmd)) {
|
||
|
|
return text('缺少参数 cmd。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const r = await deps.callGame(String(args.cmd), args.payload, 8000);
|
||
|
|
return json(r !== null && r !== void 0 ? r : {});
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`命令执行失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('click', {
|
||
|
|
title: '模拟点击',
|
||
|
|
description: '模拟一次触摸点击。传 id 则点击该节点(取其 UI 中心点);传 x/y 则点击屏幕(UI 坐标)上命中的最上层节点。常用于触发按钮、开始游戏等操作。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
id: zod_1.z.string().optional().describe('节点 id(来自 get_node_tree)'),
|
||
|
|
x: zod_1.z.number().optional().describe('UI 横坐标(与 id 二选一)'),
|
||
|
|
y: zod_1.z.number().optional().describe('UI 纵坐标(与 id 二选一)'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
if (!(args === null || args === void 0 ? void 0 : args.id) && (typeof (args === null || args === void 0 ? void 0 : args.x) !== 'number' || typeof (args === null || args === void 0 ? void 0 : args.y) !== 'number')) {
|
||
|
|
return text('需要参数 id(节点 id)或 x/y(UI 坐标)。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const payload = {};
|
||
|
|
if (typeof args.id === 'string' && args.id) {
|
||
|
|
payload.id = args.id;
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
payload.x = args.x;
|
||
|
|
payload.y = args.y;
|
||
|
|
}
|
||
|
|
const r = await deps.callGame('simulate-click', payload, 8000);
|
||
|
|
if (r && r.error) {
|
||
|
|
return text(String(r.error), true);
|
||
|
|
}
|
||
|
|
return json(r !== null && r !== void 0 ? r : { ok: true });
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`模拟点击失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('list_scenes', {
|
||
|
|
title: '列出场景',
|
||
|
|
description: '列出当前运行场景及已加载场景名,用于确认跳转目标。',
|
||
|
|
inputSchema: zod_1.z.object({}),
|
||
|
|
}, async () => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const r = await deps.callGame('list-scenes', {}, 6000);
|
||
|
|
return json(r !== null && r !== void 0 ? r : { current: '', scenes: [] });
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`列出场景失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
mcp.registerTool('load_scene', {
|
||
|
|
title: '跳转场景',
|
||
|
|
description: '运行时加载指定名称的场景(场景跳转)。场景名可先通过 list_scenes 查看。',
|
||
|
|
inputSchema: zod_1.z.object({
|
||
|
|
name: zod_1.z.string().describe('场景名'),
|
||
|
|
}),
|
||
|
|
}, async (args) => {
|
||
|
|
if (noClient()) {
|
||
|
|
return text('游戏未连接。', true);
|
||
|
|
}
|
||
|
|
if (!(args === null || args === void 0 ? void 0 : args.name)) {
|
||
|
|
return text('缺少参数 name。', true);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const r = await deps.callGame('load-scene', { name: String(args.name) }, 6000);
|
||
|
|
if (r && r.error) {
|
||
|
|
return text(String(r.error), true);
|
||
|
|
}
|
||
|
|
return json(r !== null && r !== void 0 ? r : { ok: true });
|
||
|
|
}
|
||
|
|
catch (err) {
|
||
|
|
return text(`跳转场景失败: ${String(err)}`, true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return mcp;
|
||
|
|
}
|
||
|
|
/** 启动 HTTP 服务。port 被占用时抛错。 */
|
||
|
|
async start(port, token, deps) {
|
||
|
|
if (this.srv) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.token = token;
|
||
|
|
this.deps = deps;
|
||
|
|
const srv = http_1.default.createServer((req, res) => {
|
||
|
|
this.handle(req, res).catch(() => {
|
||
|
|
/* handled inside */
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await new Promise((resolve, reject) => {
|
||
|
|
const onError = (err) => {
|
||
|
|
srv.removeListener('listening', onListening);
|
||
|
|
reject(err);
|
||
|
|
};
|
||
|
|
const onListening = () => {
|
||
|
|
srv.removeListener('error', onError);
|
||
|
|
resolve();
|
||
|
|
};
|
||
|
|
srv.once('error', onError);
|
||
|
|
srv.once('listening', onListening);
|
||
|
|
srv.listen(port, '127.0.0.1');
|
||
|
|
});
|
||
|
|
this.srv = srv;
|
||
|
|
this.port = port;
|
||
|
|
}
|
||
|
|
/** 停止服务并关闭所有会话。 */
|
||
|
|
async stop() {
|
||
|
|
const srv = this.srv;
|
||
|
|
this.srv = null;
|
||
|
|
this.port = 0;
|
||
|
|
this.deps = null;
|
||
|
|
for (const { transport } of this.sessions.values()) {
|
||
|
|
try {
|
||
|
|
await transport.close();
|
||
|
|
}
|
||
|
|
catch (_a) {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.sessions.clear();
|
||
|
|
if (srv) {
|
||
|
|
await new Promise((resolve) => srv.close(() => resolve()));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
exports.McpHttpServer = McpHttpServer;
|
||
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWNwLXNlcnZlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NvdXJjZS9tY3Atc2VydmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7QUFBQSxnREFBd0I7QUFDeEIsNkJBQXdCO0FBQ3hCLG9FQUFvRTtBQUNwRSwwRkFBbUc7QUFFbkcsbUNBQW1DO0FBQ3RCLFFBQUEsZ0JBQWdCLEdBQUcsS0FBSyxDQUFDO0FBb0J0QyxNQUFNLElBQUksR0FBRyxDQUFDLENBQVMsRUFBRSxPQUFPLEdBQUcsS0FBSyxFQUFjLEVBQUUsQ0FBQyxDQUFDO0lBQ3RELE9BQU8sRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUM7SUFDcEMsT0FBTztDQUNWLENBQUMsQ0FBQztBQUVILE1BQU0sSUFBSSxHQUFHLENBQUMsQ0FBVSxFQUFFLE9BQU8sR0FBRyxLQUFLLEVBQWMsRUFBRSxDQUFDLENBQUM7SUFDdkQsT0FBTyxFQUFFLENBQUMsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQztJQUM3RCxPQUFPO0NBQ1YsQ0FBQyxDQUFDO0FBRUgsaUJBQWlCO0FBQ2pCLFNBQVMsVUFBVSxDQUFDLEtBQVk7SUFDNUIsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ1YsTUFBTSxJQUFJLEdBQUcsQ0FBQyxJQUFXLEVBQUUsRUFBRTtRQUN6QixLQUFLLE1BQU0sQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDO1lBQ25CLENBQUMsRUFBRSxDQUFDO1lBQ0osSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDO2dCQUM1QixJQUFJLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1lBQ3JCLENBQUM7UUFDTCxDQUFDO0lB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
|