@zy_zhou/vps-mcp-server 1.0.0 → 1.0.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/dist/server.js CHANGED
@@ -5,280 +5,326 @@ 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
- const server = new Server({
11
- name: 'vps-sandbox',
12
- version: '1.0.0',
13
- }, {
14
- capabilities: {
15
- tools: {},
16
- },
17
- });
18
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
- tools: [
20
- {
21
- name: 'run_command',
22
- description: '在 VPS 沙盒中执行 Shell 命令(一次性)',
23
- inputSchema: {
24
- type: 'object',
25
- properties: {
26
- command: { type: 'string', description: '要执行的命令' },
27
- cwd: { type: 'string', description: '工作目录,默认 /tmp' },
28
- timeout: { type: 'number', description: '超时(ms),默认 30000' },
10
+ function registerHandlers(server) {
11
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
12
+ tools: [
13
+ {
14
+ name: 'run_command',
15
+ description: '在 VPS 沙盒中执行 Shell 命令(一次性)',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ command: { type: 'string', description: '要执行的命令' },
20
+ cwd: { type: 'string', description: '工作目录,默认 /tmp' },
21
+ timeout: { type: 'number', description: '超时(ms),默认 30000' },
22
+ },
23
+ required: ['command'],
29
24
  },
30
- required: ['command'],
31
25
  },
32
- },
33
- {
34
- name: 'run_script',
35
- description: '执行脚本代码 (python/bash/node)',
36
- inputSchema: {
37
- type: 'object',
38
- properties: {
39
- language: { type: 'string', enum: ['python', 'bash', 'node', 'sh'] },
40
- code: { type: 'string', description: '脚本源码' },
41
- timeout: { type: 'number', description: '超时(ms)' },
26
+ {
27
+ name: 'run_script',
28
+ description: '执行脚本代码 (python/bash/node)',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ language: { type: 'string', enum: ['python', 'bash', 'node', 'sh'] },
33
+ code: { type: 'string', description: '脚本源码' },
34
+ timeout: { type: 'number', description: '超时(ms)' },
35
+ },
36
+ required: ['language', 'code'],
42
37
  },
43
- required: ['language', 'code'],
44
38
  },
45
- },
46
- {
47
- name: 'file_read',
48
- description: '读取文件内容',
49
- inputSchema: {
50
- type: 'object',
51
- properties: {
52
- filepath: { type: 'string' },
39
+ {
40
+ name: 'file_read',
41
+ description: '读取文件内容',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ filepath: { type: 'string' },
46
+ },
47
+ required: ['filepath'],
53
48
  },
54
- required: ['filepath'],
55
49
  },
56
- },
57
- {
58
- name: 'file_write',
59
- description: '写入文件',
60
- inputSchema: {
61
- type: 'object',
62
- properties: {
63
- filepath: { type: 'string' },
64
- content: { type: 'string' },
50
+ {
51
+ name: 'file_write',
52
+ description: '写入文件',
53
+ inputSchema: {
54
+ type: 'object',
55
+ properties: {
56
+ filepath: { type: 'string' },
57
+ content: { type: 'string' },
58
+ },
59
+ required: ['filepath', 'content'],
65
60
  },
66
- required: ['filepath', 'content'],
67
61
  },
68
- },
69
- {
70
- name: 'file_patch',
71
- description: '增量编辑文件(搜索并替换)',
72
- inputSchema: {
73
- type: 'object',
74
- properties: {
75
- filepath: { type: 'string' },
76
- search: { type: 'string', description: '要搜索的原始文本' },
77
- replace: { type: 'string', description: '要替换成的新文本' },
62
+ {
63
+ name: 'file_patch',
64
+ description: '增量编辑文件(搜索并替换)',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ filepath: { type: 'string' },
69
+ search: { type: 'string', description: '要搜索的原始文本' },
70
+ replace: { type: 'string', description: '要替换成的新文本' },
71
+ },
72
+ required: ['filepath', 'search', 'replace'],
78
73
  },
79
- required: ['filepath', 'search', 'replace'],
80
74
  },
81
- },
82
- {
83
- name: 'list_files',
84
- description: '列出目录内容或递归获取目录树',
85
- inputSchema: {
86
- type: 'object',
87
- properties: {
88
- dirpath: { type: 'string', description: '目录路径' },
89
- recursive: { type: 'boolean', description: '是否递归列出 (树状结构)' },
90
- depth: { type: 'number', description: '递归深度' },
91
- exclude: { type: 'array', items: { type: 'string' }, description: '排除的目录' },
75
+ {
76
+ name: 'list_files',
77
+ description: '列出目录内容或递归获取目录树',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ dirpath: { type: 'string', description: '目录路径' },
82
+ recursive: { type: 'boolean', description: '是否递归列出 (树状结构)' },
83
+ depth: { type: 'number', description: '递归深度' },
84
+ exclude: { type: 'array', items: { type: 'string' }, description: '排除的目录' },
85
+ },
92
86
  },
93
87
  },
94
- },
95
- {
96
- name: 'file_delete',
97
- description: '删除文件或目录',
98
- inputSchema: {
99
- type: 'object',
100
- properties: {
101
- filepath: { type: 'string' },
88
+ {
89
+ name: 'file_delete',
90
+ description: '删除文件或目录',
91
+ inputSchema: {
92
+ type: 'object',
93
+ properties: {
94
+ filepath: { type: 'string' },
95
+ },
96
+ required: ['filepath'],
102
97
  },
103
- required: ['filepath'],
104
98
  },
105
- },
106
- {
107
- name: 'file_search',
108
- description: '搜索文件名或文件内容',
109
- inputSchema: {
110
- type: 'object',
111
- properties: {
112
- dirpath: { type: 'string', description: '起始目录' },
113
- query: { type: 'string', description: '搜索关键词' },
114
- type: { type: 'string', enum: ['name', 'content'], description: '搜索类型' },
115
- recursive: { type: 'boolean', description: '是否递归搜索' },
99
+ {
100
+ name: 'file_search',
101
+ description: '搜索文件名或文件内容',
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ dirpath: { type: 'string', description: '起始目录' },
106
+ query: { type: 'string', description: '搜索关键词' },
107
+ type: { type: 'string', enum: ['name', 'content'], description: '搜索类型' },
108
+ recursive: { type: 'boolean', description: '是否递归搜索' },
109
+ },
110
+ required: ['query'],
116
111
  },
117
- required: ['query'],
118
112
  },
119
- },
120
- {
121
- name: 'get_system_status',
122
- description: '获取 VPS 系统状态或进程列表',
123
- inputSchema: {
124
- type: 'object',
125
- properties: {
126
- type: { type: 'string', enum: ['summary', 'processes'], description: 'summary: 系统概览, processes: 进程列表' },
127
- sort: { type: 'string', enum: ['cpu', 'mem'], description: '进程排序方式' },
113
+ {
114
+ name: 'get_system_status',
115
+ description: '获取 VPS 系统状态或进程列表',
116
+ inputSchema: {
117
+ type: 'object',
118
+ properties: {
119
+ type: { type: 'string', enum: ['summary', 'processes'], description: 'summary: 系统概览, processes: 进程列表' },
120
+ sort: { type: 'string', enum: ['cpu', 'mem'], description: '进程排序方式' },
121
+ },
128
122
  },
129
123
  },
130
- },
131
- {
132
- name: 'kill_process',
133
- description: '终止指定 PID 的进程',
134
- inputSchema: {
135
- type: 'object',
136
- properties: {
137
- pid: { type: 'number' },
138
- force: { type: 'boolean' },
124
+ {
125
+ name: 'kill_process',
126
+ description: '终止指定 PID 的进程',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ pid: { type: 'number' },
131
+ force: { type: 'boolean' },
132
+ },
133
+ required: ['pid'],
139
134
  },
140
- required: ['pid'],
141
135
  },
142
- },
143
- {
144
- name: 'http_request',
145
- description: '发起 HTTP 请求或下载文件',
146
- inputSchema: {
147
- type: 'object',
148
- properties: {
149
- url: { type: 'string' },
150
- method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] },
151
- headers: { type: 'object' },
152
- body: { type: 'any' },
153
- savePath: { type: 'string', description: '如果提供,则将响应保存到此文件路径' },
136
+ {
137
+ name: 'http_request',
138
+ description: '发起 HTTP 请求或下载文件',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ url: { type: 'string' },
143
+ method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] },
144
+ headers: { type: 'object' },
145
+ body: { type: 'any' },
146
+ savePath: { type: 'string', description: '如果提供,则将响应保存到此文件路径' },
147
+ },
148
+ required: ['url'],
154
149
  },
155
- required: ['url'],
156
150
  },
157
- },
158
- {
159
- name: 'session_start',
160
- description: '启动一个持久 tmux 会话',
161
- inputSchema: {
162
- type: 'object',
163
- properties: {
164
- sessionId: { type: 'string' },
165
- command: { type: 'string' },
166
- cwd: { type: 'string' },
151
+ {
152
+ name: 'session_start',
153
+ description: '启动一个持久 tmux 会话',
154
+ inputSchema: {
155
+ type: 'object',
156
+ properties: {
157
+ sessionId: { type: 'string' },
158
+ command: { type: 'string' },
159
+ cwd: { type: 'string' },
160
+ },
161
+ required: ['sessionId'],
167
162
  },
168
- required: ['sessionId'],
169
163
  },
170
- },
171
- {
172
- name: 'session_exec',
173
- description: '向已有会话发送命令',
174
- inputSchema: {
175
- type: 'object',
176
- properties: {
177
- sessionId: { type: 'string' },
178
- command: { type: 'string' },
164
+ {
165
+ name: 'session_exec',
166
+ description: '向已有会话发送命令',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ sessionId: { type: 'string' },
171
+ command: { type: 'string' },
172
+ },
173
+ required: ['sessionId', 'command'],
179
174
  },
180
- required: ['sessionId', 'command'],
181
175
  },
182
- },
183
- {
184
- name: 'session_read',
185
- description: '读取会话的最近输出',
186
- inputSchema: {
187
- type: 'object',
188
- properties: {
189
- sessionId: { type: 'string' },
190
- maxChars: { type: 'number' },
176
+ {
177
+ name: 'session_read',
178
+ description: '读取会话的最近输出',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ sessionId: { type: 'string' },
183
+ maxChars: { type: 'number' },
184
+ },
185
+ required: ['sessionId'],
191
186
  },
192
- required: ['sessionId'],
193
187
  },
194
- },
195
- {
196
- name: 'session_stop',
197
- description: '停止并销毁会话',
198
- inputSchema: {
199
- type: 'object',
200
- properties: {
201
- sessionId: { type: 'string' },
188
+ {
189
+ name: 'session_stop',
190
+ description: '停止并销毁会话',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ sessionId: { type: 'string' },
195
+ },
196
+ required: ['sessionId'],
202
197
  },
203
- required: ['sessionId'],
204
198
  },
205
- },
206
- ],
207
- }));
208
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
209
- const { name, arguments: args } = request.params;
210
- try {
211
- let result;
212
- switch (name) {
213
- case 'run_command':
214
- result = await handleRunCommand(args);
215
- break;
216
- case 'run_script':
217
- result = await handleRunScript(args);
218
- break;
219
- case 'file_read':
220
- result = await handleFileRead(args);
221
- break;
222
- case 'file_write':
223
- result = await handleFileWrite(args);
224
- break;
225
- case 'file_patch':
226
- result = await handleFilePatch(args);
227
- break;
228
- case 'list_files':
229
- result = await handleListFiles(args);
230
- break;
231
- case 'file_delete':
232
- result = await handleFileDelete(args);
233
- break;
234
- case 'file_search':
235
- result = await handleFileSearch(args);
236
- break;
237
- case 'get_system_status':
238
- result = await handleGetSystemStatus(args);
239
- break;
240
- case 'kill_process':
241
- result = await handleKillProcess(args);
242
- break;
243
- case 'http_request':
244
- result = await handleHttpRequest(args);
245
- break;
246
- case 'session_start':
247
- result = await handleSessionStart(args);
248
- break;
249
- case 'session_exec':
250
- result = await handleSessionExec(args);
251
- break;
252
- case 'session_read':
253
- result = await handleSessionRead(args);
254
- break;
255
- case 'session_stop':
256
- result = await handleSessionStop(args);
257
- break;
258
- default: throw new Error(`Unknown tool: ${name}`);
199
+ {
200
+ name: 'agy_ask',
201
+ description: '向 agy 提问,自动维护上下文会话 ID。支持直接在普通沙盒中执行,或指定持久化的 tmux 会话执行并自动读取完毕后一次性返回。',
202
+ inputSchema: {
203
+ type: 'object',
204
+ properties: {
205
+ prompt: { type: 'string', description: '要提问的具体内容' },
206
+ sessionId: { type: 'string', description: '(可选)绑定的持久 tmux 会话 ID。如果提供,将在该会话中执行;否则在常规沙盒环境中执行。' },
207
+ clear: { type: 'boolean', description: '是否清空可见屏幕及历史滚动缓冲区,默认为 true' },
208
+ dangerouslySkipPermissions: { type: 'boolean', description: '是否自动跳过权限询问,默认为 true' },
209
+ model: { type: 'string', description: '透传给 agy 的模型参数' },
210
+ agent: { type: 'string', description: '透传给 agy 的代理参数' },
211
+ effort: { type: 'string', description: '透传给 agy 的思考深度参数' },
212
+ mode: { type: 'string', description: '透传给 agy 的模式参数' },
213
+ project: { type: 'string', description: '透传给 agy 的项目参数' },
214
+ addDir: { type: 'string', description: '透传给 agy 的添加目录参数' }
215
+ },
216
+ required: ['prompt']
217
+ }
218
+ },
219
+ ],
220
+ }));
221
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
222
+ const { name, arguments: args } = request.params;
223
+ try {
224
+ let result;
225
+ switch (name) {
226
+ case 'run_command':
227
+ result = await handleRunCommand(args);
228
+ break;
229
+ case 'run_script':
230
+ result = await handleRunScript(args);
231
+ break;
232
+ case 'file_read':
233
+ result = await handleFileRead(args);
234
+ break;
235
+ case 'file_write':
236
+ result = await handleFileWrite(args);
237
+ break;
238
+ case 'file_patch':
239
+ result = await handleFilePatch(args);
240
+ break;
241
+ case 'list_files':
242
+ result = await handleListFiles(args);
243
+ break;
244
+ case 'file_delete':
245
+ result = await handleFileDelete(args);
246
+ break;
247
+ case 'file_search':
248
+ result = await handleFileSearch(args);
249
+ break;
250
+ case 'get_system_status':
251
+ result = await handleGetSystemStatus(args);
252
+ break;
253
+ case 'kill_process':
254
+ result = await handleKillProcess(args);
255
+ break;
256
+ case 'http_request':
257
+ result = await handleHttpRequest(args);
258
+ break;
259
+ case 'session_start':
260
+ result = await handleSessionStart(args);
261
+ break;
262
+ case 'session_exec':
263
+ result = await handleSessionExec(args);
264
+ break;
265
+ case 'session_read':
266
+ result = await handleSessionRead(args);
267
+ break;
268
+ case 'session_stop':
269
+ result = await handleSessionStop(args);
270
+ break;
271
+ case 'agy_ask':
272
+ result = await handleAgyAsk(args);
273
+ break;
274
+ default: throw new Error(`Unknown tool: ${name}`);
275
+ }
276
+ return result;
277
+ }
278
+ catch (error) {
279
+ return {
280
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
281
+ isError: true,
282
+ };
259
283
  }
260
- return result;
261
- }
262
- catch (error) {
263
- return {
264
- content: [{ type: 'text', text: `Error: ${error.message}` }],
265
- isError: true,
266
- };
267
- }
268
- });
284
+ });
285
+ }
286
+ function createMcpServer() {
287
+ const s = new Server({
288
+ name: 'vps-sandbox',
289
+ version: '1.0.0',
290
+ }, {
291
+ capabilities: {
292
+ tools: {},
293
+ },
294
+ });
295
+ registerHandlers(s);
296
+ return s;
297
+ }
269
298
  const mode = process.env.MODE || 'stdio';
270
299
  if (mode === 'sse') {
271
300
  const app = express();
272
301
  app.use(express.json());
273
- let transport = null;
302
+ // 记录每个客户端的独立服务器和传输通道
303
+ const transports = new Map();
274
304
  app.get('/sse', async (req, res) => {
275
305
  const auth = (req.headers.authorization?.replace('Bearer ', '') || req.query.token);
276
306
  if (auth !== TOKEN) {
277
307
  res.status(401).json({ error: 'Unauthorized' });
278
308
  return;
279
309
  }
280
- transport = new SSEServerTransport('/messages', res);
281
- await server.connect(transport);
310
+ const clientServer = createMcpServer(); // 为当前连接动态实例化独立的 Server
311
+ // 创建 SSE Transport,客户端接收消息的端点为 /messages
312
+ const transport = new SSEServerTransport('/messages', res);
313
+ const sessionId = transport.sessionId;
314
+ transports.set(sessionId, { server: clientServer, transport });
315
+ console.error(`Client connected. Registered SSE transport for sessionId: ${sessionId}`);
316
+ // 连接断开时的自动垃圾清理与资源释放,防范内存泄漏
317
+ req.on('close', async () => {
318
+ transports.delete(sessionId);
319
+ console.error(`Client disconnected. Cleaned up SSE transport for sessionId: ${sessionId}`);
320
+ try {
321
+ await clientServer.close();
322
+ }
323
+ catch (err) {
324
+ // 忽略关闭可能抛出的已断连异常
325
+ }
326
+ });
327
+ await clientServer.connect(transport);
282
328
  });
283
329
  app.post('/messages', async (req, res) => {
284
330
  const auth = req.headers.authorization?.replace('Bearer ', '');
@@ -286,11 +332,17 @@ if (mode === 'sse') {
286
332
  res.status(401).json({ error: 'Unauthorized' });
287
333
  return;
288
334
  }
289
- if (transport) {
290
- await transport.handlePostMessage(req, res, req.body);
335
+ const sessionId = req.query.sessionId;
336
+ if (!sessionId) {
337
+ res.status(400).json({ error: 'Missing sessionId query parameter' });
338
+ return;
339
+ }
340
+ const entry = transports.get(sessionId);
341
+ if (entry) {
342
+ await entry.transport.handlePostMessage(req, res, req.body);
291
343
  }
292
344
  else {
293
- res.status(400).json({ error: 'No active SSE transport' });
345
+ res.status(404).json({ error: `Active SSE transport not found for sessionId: ${sessionId}` });
294
346
  }
295
347
  });
296
348
  app.listen(process.env.PORT || 8080, () => {
@@ -298,7 +350,8 @@ if (mode === 'sse') {
298
350
  });
299
351
  }
300
352
  else {
353
+ const stdioServer = createMcpServer();
301
354
  const transport = new StdioServerTransport();
302
- server.connect(transport);
355
+ stdioServer.connect(transport);
303
356
  console.error('MCP stdio server running');
304
357
  }
@@ -0,0 +1,180 @@
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 隔离键,默认使用专属 sessionId 兜底以防并发碰撞
75
+ let ttyId = `tmux_${sessionId}`;
76
+ try {
77
+ const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
78
+ if (ttyCheck.success && ttyCheck.stdout && ttyCheck.stdout.trim()) {
79
+ ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
80
+ }
81
+ }
82
+ catch {
83
+ // 保持专属 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
+ if (err.code !== 'ENOENT') {
98
+ console.error(`Failed to read sidStore (${sidStore}):`, err.message);
99
+ }
100
+ }
101
+ if (agySid) {
102
+ extraArgs.push(`--conversation ${agySid}`);
103
+ }
104
+ else {
105
+ // 首次提问,强清除潜在的同名脏数据,避免污染
106
+ await fs.rm(sidStore, { force: true }).catch(() => { });
107
+ await fs.rm(logFile, { force: true }).catch(() => { });
108
+ extraArgs.push(`--log-file ${quoteShell(logFile)}`);
109
+ }
110
+ const argsStr = extraArgs.join(' ');
111
+ const markerId = Math.random().toString(36).slice(2, 8);
112
+ // 构造组合命令
113
+ let runCmd = '';
114
+ if (clear) {
115
+ runCmd += 'clear; ';
116
+ }
117
+ // 在 tmux 中执行
118
+ runCmd += `echo "===AGY_START_${markerId}==="; agy -p ${quoteShell(prompt)} ${argsStr}; echo "===AGY_DONE_${markerId}==="`;
119
+ await TmuxManager.execInSession(sessionId, runCmd);
120
+ // 轮询等待结束
121
+ let output = '';
122
+ const startTimeout = Date.now();
123
+ const timeoutLimit = 180000; // 3分钟超时
124
+ let isDone = false;
125
+ while (Date.now() - startTimeout < timeoutLimit) {
126
+ await new Promise(resolve => setTimeout(resolve, 500));
127
+ const screen = await TmuxManager.readSession(sessionId);
128
+ if (screen.includes(`===AGY_DONE_${markerId}===`)) {
129
+ // 截取 Marker 之间的部分
130
+ const startIdx = screen.indexOf(`===AGY_START_${markerId}===`);
131
+ const endIdx = screen.indexOf(`===AGY_DONE_${markerId}===`);
132
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
133
+ output = screen.substring(startIdx + `===AGY_START_${markerId}===`.length, endIdx).trim();
134
+ isDone = true;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ if (!isDone) {
140
+ // 超时兜底读取当前会话内容
141
+ const screen = await TmuxManager.readSession(sessionId);
142
+ const startIdx = screen.indexOf(`===AGY_START_${markerId}===`);
143
+ if (startIdx !== -1) {
144
+ output = screen.substring(startIdx + `===AGY_START_${markerId}===`.length).trim();
145
+ }
146
+ else {
147
+ output = screen;
148
+ }
149
+ output += '\n\n⚠️ [警告] 会话执行可能超时(超过 180 秒)。';
150
+ }
151
+ // 首次提问,从日志中提取并覆盖写入新分配的会话 ID
152
+ if (!agySid) {
153
+ try {
154
+ const logContent = await fs.readFile(logFile, 'utf-8');
155
+ const sidMatch = logContent.match(/Created conversation ([a-zA-Z0-9-]+)/);
156
+ if (sidMatch) {
157
+ const newSid = sidMatch[1];
158
+ await fs.writeFile(sidStore, `export agy_sid='${newSid}'`, 'utf-8');
159
+ }
160
+ }
161
+ catch (err) {
162
+ console.error('Failed to parse newly created conversation ID:', err);
163
+ }
164
+ }
165
+ return {
166
+ content: [{ type: 'text', text: output }]
167
+ };
168
+ }
169
+ else {
170
+ // 【场景 B:未传 sessionId,一次性命令执行,不做任何状态记录】
171
+ const argsStr = extraArgs.join(' ');
172
+ const command = `bash -c "agy -p ${quoteShell(prompt)} ${argsStr}"`;
173
+ const result = await executeCommand(command, { timeout: 120000 });
174
+ const output = result.stdout || result.stderr || (result.success ? '(无输出)' : '执行失败');
175
+ return {
176
+ content: [{ type: 'text', text: output }],
177
+ isError: !result.success
178
+ };
179
+ }
180
+ }
@@ -4,3 +4,4 @@ export * from './files.js';
4
4
  export * from './session.js';
5
5
  export * from './system.js';
6
6
  export * from './network.js';
7
+ export * from './agy.js';
@@ -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 = `tmux_${sessionId}`;
52
+ try {
53
+ const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
54
+ if (ttyCheck.success && ttyCheck.stdout && ttyCheck.stdout.trim()) {
55
+ ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
56
+ }
57
+ }
58
+ catch {
59
+ // 保持专属 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.0",
3
+ "version": "1.0.3",
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
+ }