@zy_zhou/vps-mcp-server 1.0.0 → 1.0.2
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/dist/server.js +24 -1
- package/dist/tools/agy.js +178 -0
- package/dist/tools/index.js +1 -0
- package/dist/tools/session.js +26 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
5
5
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
6
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
7
|
import express from 'express';
|
|
8
|
-
import { handleRunCommand, handleRunScript, handleFileRead, handleFileWrite, handleListFiles, handleFileDelete, handleFileSearch, handleFilePatch, handleSessionStart, handleSessionExec, handleSessionRead, handleSessionStop, handleGetSystemStatus, handleKillProcess, handleHttpRequest, } from './tools/index.js';
|
|
8
|
+
import { handleRunCommand, handleRunScript, handleFileRead, handleFileWrite, handleListFiles, handleFileDelete, handleFileSearch, handleFilePatch, handleSessionStart, handleSessionExec, handleSessionRead, handleSessionStop, handleGetSystemStatus, handleKillProcess, handleHttpRequest, handleAgyAsk, } from './tools/index.js';
|
|
9
9
|
const TOKEN = process.env.MCP_TOKEN || 'change-me';
|
|
10
10
|
const server = new Server({
|
|
11
11
|
name: 'vps-sandbox',
|
|
@@ -203,6 +203,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
203
203
|
required: ['sessionId'],
|
|
204
204
|
},
|
|
205
205
|
},
|
|
206
|
+
{
|
|
207
|
+
name: 'agy_ask',
|
|
208
|
+
description: '向 agy 提问,自动维护上下文会话 ID。支持直接在普通沙盒中执行,或指定持久化的 tmux 会话执行并自动读取完毕后一次性返回。',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
prompt: { type: 'string', description: '要提问的具体内容' },
|
|
213
|
+
sessionId: { type: 'string', description: '(可选)绑定的持久 tmux 会话 ID。如果提供,将在该会话中执行;否则在常规沙盒环境中执行。' },
|
|
214
|
+
clear: { type: 'boolean', description: '是否清空可见屏幕及历史滚动缓冲区,默认为 true' },
|
|
215
|
+
dangerouslySkipPermissions: { type: 'boolean', description: '是否自动跳过权限询问,默认为 true' },
|
|
216
|
+
model: { type: 'string', description: '透传给 agy 的模型参数' },
|
|
217
|
+
agent: { type: 'string', description: '透传给 agy 的代理参数' },
|
|
218
|
+
effort: { type: 'string', description: '透传给 agy 的思考深度参数' },
|
|
219
|
+
mode: { type: 'string', description: '透传给 agy 的模式参数' },
|
|
220
|
+
project: { type: 'string', description: '透传给 agy 的项目参数' },
|
|
221
|
+
addDir: { type: 'string', description: '透传给 agy 的添加目录参数' }
|
|
222
|
+
},
|
|
223
|
+
required: ['prompt']
|
|
224
|
+
}
|
|
225
|
+
},
|
|
206
226
|
],
|
|
207
227
|
}));
|
|
208
228
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -255,6 +275,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
255
275
|
case 'session_stop':
|
|
256
276
|
result = await handleSessionStop(args);
|
|
257
277
|
break;
|
|
278
|
+
case 'agy_ask':
|
|
279
|
+
result = await handleAgyAsk(args);
|
|
280
|
+
break;
|
|
258
281
|
default: throw new Error(`Unknown tool: ${name}`);
|
|
259
282
|
}
|
|
260
283
|
return result;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { executeCommand } from '../sandbox/executor.js';
|
|
2
|
+
import { TmuxManager } from '../sandbox/tmux-manager.js';
|
|
3
|
+
import { quoteShell } from '../utils.js';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
// 跨平台临时目录规范
|
|
6
|
+
const TMP_DIR = process.platform === 'win32' ? 'd:/vps-mcp-server/tmp' : '/tmp';
|
|
7
|
+
// 平台无关的工具存在性检查方法
|
|
8
|
+
async function checkCommand(cmd) {
|
|
9
|
+
try {
|
|
10
|
+
const check = await executeCommand(`command -v ${cmd} || which ${cmd} || where ${cmd}`);
|
|
11
|
+
return check.success;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function handleAgyAsk(args) {
|
|
18
|
+
const { prompt, sessionId, clear = true, dangerouslySkipPermissions = true, model, agent, effort, mode, project, addDir, } = args;
|
|
19
|
+
if (!prompt) {
|
|
20
|
+
throw new Error('prompt required');
|
|
21
|
+
}
|
|
22
|
+
// 确保临时目录存在
|
|
23
|
+
await fs.mkdir(TMP_DIR, { recursive: true }).catch(() => { });
|
|
24
|
+
// 1. 检查是否存在 agy 命令
|
|
25
|
+
const hasAgy = await checkCommand('agy');
|
|
26
|
+
if (!hasAgy) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: '❌ 系统中未检测到 agy 命令行工具,请确保 Antigravity 运行环境已在远程沙盒中正确配置。'
|
|
31
|
+
}]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// 2. 检查会话是否存在(如果传了 sessionId)
|
|
35
|
+
if (sessionId) {
|
|
36
|
+
const hasTmux = await checkCommand('tmux');
|
|
37
|
+
if (!hasTmux) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{
|
|
40
|
+
type: 'text',
|
|
41
|
+
text: '❌ 系统未安装 tmux,无法在会话中执行。'
|
|
42
|
+
}]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const sessionExists = await TmuxManager.sessionExists(sessionId);
|
|
46
|
+
if (!sessionExists) {
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `❌ 找不到指定的 tmux 会话 sessionId: ${sessionId}。请先使用 session_start 启动该会话。`
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 3. 构造参数串
|
|
56
|
+
const extraArgs = [];
|
|
57
|
+
if (model)
|
|
58
|
+
extraArgs.push(`--model ${quoteShell(model)}`);
|
|
59
|
+
if (agent)
|
|
60
|
+
extraArgs.push(`--agent ${quoteShell(agent)}`);
|
|
61
|
+
if (effort)
|
|
62
|
+
extraArgs.push(`--effort ${quoteShell(effort)}`);
|
|
63
|
+
if (mode)
|
|
64
|
+
extraArgs.push(`--mode ${quoteShell(mode)}`);
|
|
65
|
+
if (project)
|
|
66
|
+
extraArgs.push(`--project ${quoteShell(project)}`);
|
|
67
|
+
if (addDir)
|
|
68
|
+
extraArgs.push(`--add-dir ${quoteShell(addDir)}`);
|
|
69
|
+
if (dangerouslySkipPermissions)
|
|
70
|
+
extraArgs.push('--dangerously-skip-permissions');
|
|
71
|
+
// 4. 分情况执行
|
|
72
|
+
if (sessionId) {
|
|
73
|
+
// 【场景 A:传了 sessionId,需要实现上下文持续】
|
|
74
|
+
// 获取 TTY 隔离键
|
|
75
|
+
let ttyId = 'default';
|
|
76
|
+
try {
|
|
77
|
+
const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
|
|
78
|
+
if (ttyCheck.success && ttyCheck.stdout) {
|
|
79
|
+
ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
ttyId = `tmux_${sessionId}`;
|
|
84
|
+
}
|
|
85
|
+
const logFile = `${TMP_DIR}/agy_conv_${ttyId}.log`;
|
|
86
|
+
const sidStore = `${TMP_DIR}/agy_sid_${ttyId}.env`;
|
|
87
|
+
// 读入已有的 agy_sid
|
|
88
|
+
let agySid = null;
|
|
89
|
+
try {
|
|
90
|
+
const storeContent = await fs.readFile(sidStore, 'utf-8');
|
|
91
|
+
const match = storeContent.match(/export agy_sid='([a-zA-Z0-9-]+)'/);
|
|
92
|
+
if (match) {
|
|
93
|
+
agySid = match[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error(`Failed to read sidStore (${sidStore}):`, err.message);
|
|
98
|
+
}
|
|
99
|
+
if (agySid) {
|
|
100
|
+
extraArgs.push(`--conversation ${agySid}`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// 首次提问,强清除潜在的同名脏数据,避免污染
|
|
104
|
+
await fs.rm(sidStore, { force: true }).catch(() => { });
|
|
105
|
+
await fs.rm(logFile, { force: true }).catch(() => { });
|
|
106
|
+
extraArgs.push(`--log-file ${quoteShell(logFile)}`);
|
|
107
|
+
}
|
|
108
|
+
const argsStr = extraArgs.join(' ');
|
|
109
|
+
const markerId = Math.random().toString(36).slice(2, 8);
|
|
110
|
+
// 构造组合命令
|
|
111
|
+
let runCmd = '';
|
|
112
|
+
if (clear) {
|
|
113
|
+
runCmd += 'clear; ';
|
|
114
|
+
}
|
|
115
|
+
// 在 tmux 中执行
|
|
116
|
+
runCmd += `echo "===AGY_START_${markerId}==="; agy -p ${quoteShell(prompt)} ${argsStr}; echo "===AGY_DONE_${markerId}==="`;
|
|
117
|
+
await TmuxManager.execInSession(sessionId, runCmd);
|
|
118
|
+
// 轮询等待结束
|
|
119
|
+
let output = '';
|
|
120
|
+
const startTimeout = Date.now();
|
|
121
|
+
const timeoutLimit = 180000; // 3分钟超时
|
|
122
|
+
let isDone = false;
|
|
123
|
+
while (Date.now() - startTimeout < timeoutLimit) {
|
|
124
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
125
|
+
const screen = await TmuxManager.readSession(sessionId);
|
|
126
|
+
if (screen.includes(`===AGY_DONE_${markerId}===`)) {
|
|
127
|
+
// 截取 Marker 之间的部分
|
|
128
|
+
const startIdx = screen.indexOf(`===AGY_START_${markerId}===`);
|
|
129
|
+
const endIdx = screen.indexOf(`===AGY_DONE_${markerId}===`);
|
|
130
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
131
|
+
output = screen.substring(startIdx + `===AGY_START_${markerId}===`.length, endIdx).trim();
|
|
132
|
+
isDone = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!isDone) {
|
|
138
|
+
// 超时兜底读取当前会话内容
|
|
139
|
+
const screen = await TmuxManager.readSession(sessionId);
|
|
140
|
+
const startIdx = screen.indexOf(`===AGY_START_${markerId}===`);
|
|
141
|
+
if (startIdx !== -1) {
|
|
142
|
+
output = screen.substring(startIdx + `===AGY_START_${markerId}===`.length).trim();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
output = screen;
|
|
146
|
+
}
|
|
147
|
+
output += '\n\n⚠️ [警告] 会话执行可能超时(超过 180 秒)。';
|
|
148
|
+
}
|
|
149
|
+
// 首次提问,从日志中提取并覆盖写入新分配的会话 ID
|
|
150
|
+
if (!agySid) {
|
|
151
|
+
try {
|
|
152
|
+
const logContent = await fs.readFile(logFile, 'utf-8');
|
|
153
|
+
const sidMatch = logContent.match(/Created conversation ([a-zA-Z0-9-]+)/);
|
|
154
|
+
if (sidMatch) {
|
|
155
|
+
const newSid = sidMatch[1];
|
|
156
|
+
await fs.writeFile(sidStore, `export agy_sid='${newSid}'`, 'utf-8');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.error('Failed to parse newly created conversation ID:', err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: 'text', text: output }]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// 【场景 B:未传 sessionId,一次性命令执行,不做任何状态记录】
|
|
169
|
+
const argsStr = extraArgs.join(' ');
|
|
170
|
+
const command = `bash -c "agy -p ${quoteShell(prompt)} ${argsStr}"`;
|
|
171
|
+
const result = await executeCommand(command, { timeout: 120000 });
|
|
172
|
+
const output = result.stdout || result.stderr || (result.success ? '(无输出)' : '执行失败');
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: 'text', text: output }],
|
|
175
|
+
isError: !result.success
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
package/dist/tools/index.js
CHANGED
package/dist/tools/session.js
CHANGED
|
@@ -40,13 +40,38 @@ export async function handleSessionRead(args) {
|
|
|
40
40
|
text: truncated
|
|
41
41
|
}] };
|
|
42
42
|
}
|
|
43
|
+
import fs from 'fs/promises';
|
|
44
|
+
import { executeCommand } from '../sandbox/executor.js';
|
|
45
|
+
const TMP_DIR = process.platform === 'win32' ? 'd:/vps-mcp-server/tmp' : '/tmp';
|
|
43
46
|
export async function handleSessionStop(args) {
|
|
44
47
|
const { sessionId } = args;
|
|
45
48
|
if (!sessionId)
|
|
46
49
|
throw new Error('sessionId required');
|
|
50
|
+
// 1. 获取 TTY 隔离键以删除关联的 agy 缓存临时文件
|
|
51
|
+
let ttyId = 'default';
|
|
52
|
+
try {
|
|
53
|
+
const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
|
|
54
|
+
if (ttyCheck.success && ttyCheck.stdout) {
|
|
55
|
+
ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
ttyId = `tmux_${sessionId}`;
|
|
60
|
+
}
|
|
61
|
+
// 删除 TTY 关联和兜底命名的缓存与日志
|
|
62
|
+
const filesToRemove = [
|
|
63
|
+
`${TMP_DIR}/agy_sid_${ttyId}.env`,
|
|
64
|
+
`${TMP_DIR}/agy_conv_${ttyId}.log`,
|
|
65
|
+
`${TMP_DIR}/agy_sid_tmux_${sessionId}.env`,
|
|
66
|
+
`${TMP_DIR}/agy_conv_tmux_${sessionId}.log`
|
|
67
|
+
];
|
|
68
|
+
for (const file of filesToRemove) {
|
|
69
|
+
await fs.rm(file, { force: true }).catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
// 2. 停止 tmux 会话
|
|
47
72
|
await TmuxManager.stopSession(sessionId);
|
|
48
73
|
return { content: [{
|
|
49
74
|
type: 'text',
|
|
50
|
-
text: `Session ${sessionId} stopped
|
|
75
|
+
text: `Session ${sessionId} stopped and associated temporary context files cleaned up.`
|
|
51
76
|
}] };
|
|
52
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zy_zhou/vps-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "MCP Server for VPS sandbox execution — run commands, scripts, and manage sessions remotely via MCP protocol. Supports stdio and SSE transports.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
@@ -28,4 +28,4 @@
|
|
|
28
28
|
"tsx": "^4.19.0",
|
|
29
29
|
"typescript": "^5.5.3"
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
}
|