@zy_zhou/vps-mcp-server 1.0.2 → 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 +287 -257
- package/dist/tools/agy.js +7 -5
- package/dist/tools/session.js +3 -3
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -7,301 +7,324 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprot
|
|
|
7
7
|
import express from 'express';
|
|
8
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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}`);
|
|
224
275
|
}
|
|
225
|
-
|
|
226
|
-
],
|
|
227
|
-
}));
|
|
228
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
229
|
-
const { name, arguments: args } = request.params;
|
|
230
|
-
try {
|
|
231
|
-
let result;
|
|
232
|
-
switch (name) {
|
|
233
|
-
case 'run_command':
|
|
234
|
-
result = await handleRunCommand(args);
|
|
235
|
-
break;
|
|
236
|
-
case 'run_script':
|
|
237
|
-
result = await handleRunScript(args);
|
|
238
|
-
break;
|
|
239
|
-
case 'file_read':
|
|
240
|
-
result = await handleFileRead(args);
|
|
241
|
-
break;
|
|
242
|
-
case 'file_write':
|
|
243
|
-
result = await handleFileWrite(args);
|
|
244
|
-
break;
|
|
245
|
-
case 'file_patch':
|
|
246
|
-
result = await handleFilePatch(args);
|
|
247
|
-
break;
|
|
248
|
-
case 'list_files':
|
|
249
|
-
result = await handleListFiles(args);
|
|
250
|
-
break;
|
|
251
|
-
case 'file_delete':
|
|
252
|
-
result = await handleFileDelete(args);
|
|
253
|
-
break;
|
|
254
|
-
case 'file_search':
|
|
255
|
-
result = await handleFileSearch(args);
|
|
256
|
-
break;
|
|
257
|
-
case 'get_system_status':
|
|
258
|
-
result = await handleGetSystemStatus(args);
|
|
259
|
-
break;
|
|
260
|
-
case 'kill_process':
|
|
261
|
-
result = await handleKillProcess(args);
|
|
262
|
-
break;
|
|
263
|
-
case 'http_request':
|
|
264
|
-
result = await handleHttpRequest(args);
|
|
265
|
-
break;
|
|
266
|
-
case 'session_start':
|
|
267
|
-
result = await handleSessionStart(args);
|
|
268
|
-
break;
|
|
269
|
-
case 'session_exec':
|
|
270
|
-
result = await handleSessionExec(args);
|
|
271
|
-
break;
|
|
272
|
-
case 'session_read':
|
|
273
|
-
result = await handleSessionRead(args);
|
|
274
|
-
break;
|
|
275
|
-
case 'session_stop':
|
|
276
|
-
result = await handleSessionStop(args);
|
|
277
|
-
break;
|
|
278
|
-
case 'agy_ask':
|
|
279
|
-
result = await handleAgyAsk(args);
|
|
280
|
-
break;
|
|
281
|
-
default: throw new Error(`Unknown tool: ${name}`);
|
|
276
|
+
return result;
|
|
282
277
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
278
|
+
catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
281
|
+
isError: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
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
|
+
}
|
|
292
298
|
const mode = process.env.MODE || 'stdio';
|
|
293
299
|
if (mode === 'sse') {
|
|
294
300
|
const app = express();
|
|
295
301
|
app.use(express.json());
|
|
296
|
-
|
|
302
|
+
// 记录每个客户端的独立服务器和传输通道
|
|
303
|
+
const transports = new Map();
|
|
297
304
|
app.get('/sse', async (req, res) => {
|
|
298
305
|
const auth = (req.headers.authorization?.replace('Bearer ', '') || req.query.token);
|
|
299
306
|
if (auth !== TOKEN) {
|
|
300
307
|
res.status(401).json({ error: 'Unauthorized' });
|
|
301
308
|
return;
|
|
302
309
|
}
|
|
303
|
-
|
|
304
|
-
|
|
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);
|
|
305
328
|
});
|
|
306
329
|
app.post('/messages', async (req, res) => {
|
|
307
330
|
const auth = req.headers.authorization?.replace('Bearer ', '');
|
|
@@ -309,11 +332,17 @@ if (mode === 'sse') {
|
|
|
309
332
|
res.status(401).json({ error: 'Unauthorized' });
|
|
310
333
|
return;
|
|
311
334
|
}
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
314
343
|
}
|
|
315
344
|
else {
|
|
316
|
-
res.status(
|
|
345
|
+
res.status(404).json({ error: `Active SSE transport not found for sessionId: ${sessionId}` });
|
|
317
346
|
}
|
|
318
347
|
});
|
|
319
348
|
app.listen(process.env.PORT || 8080, () => {
|
|
@@ -321,7 +350,8 @@ if (mode === 'sse') {
|
|
|
321
350
|
});
|
|
322
351
|
}
|
|
323
352
|
else {
|
|
353
|
+
const stdioServer = createMcpServer();
|
|
324
354
|
const transport = new StdioServerTransport();
|
|
325
|
-
|
|
355
|
+
stdioServer.connect(transport);
|
|
326
356
|
console.error('MCP stdio server running');
|
|
327
357
|
}
|
package/dist/tools/agy.js
CHANGED
|
@@ -71,16 +71,16 @@ export async function handleAgyAsk(args) {
|
|
|
71
71
|
// 4. 分情况执行
|
|
72
72
|
if (sessionId) {
|
|
73
73
|
// 【场景 A:传了 sessionId,需要实现上下文持续】
|
|
74
|
-
// 获取 TTY
|
|
75
|
-
let ttyId =
|
|
74
|
+
// 获取 TTY 隔离键,默认使用专属 sessionId 兜底以防并发碰撞
|
|
75
|
+
let ttyId = `tmux_${sessionId}`;
|
|
76
76
|
try {
|
|
77
77
|
const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
|
|
78
|
-
if (ttyCheck.success && ttyCheck.stdout) {
|
|
78
|
+
if (ttyCheck.success && ttyCheck.stdout && ttyCheck.stdout.trim()) {
|
|
79
79
|
ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
catch {
|
|
83
|
-
|
|
83
|
+
// 保持专属 sessionId 兜底
|
|
84
84
|
}
|
|
85
85
|
const logFile = `${TMP_DIR}/agy_conv_${ttyId}.log`;
|
|
86
86
|
const sidStore = `${TMP_DIR}/agy_sid_${ttyId}.env`;
|
|
@@ -94,7 +94,9 @@ export async function handleAgyAsk(args) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
catch (err) {
|
|
97
|
-
|
|
97
|
+
if (err.code !== 'ENOENT') {
|
|
98
|
+
console.error(`Failed to read sidStore (${sidStore}):`, err.message);
|
|
99
|
+
}
|
|
98
100
|
}
|
|
99
101
|
if (agySid) {
|
|
100
102
|
extraArgs.push(`--conversation ${agySid}`);
|
package/dist/tools/session.js
CHANGED
|
@@ -48,15 +48,15 @@ export async function handleSessionStop(args) {
|
|
|
48
48
|
if (!sessionId)
|
|
49
49
|
throw new Error('sessionId required');
|
|
50
50
|
// 1. 获取 TTY 隔离键以删除关联的 agy 缓存临时文件
|
|
51
|
-
let ttyId =
|
|
51
|
+
let ttyId = `tmux_${sessionId}`;
|
|
52
52
|
try {
|
|
53
53
|
const ttyCheck = await executeCommand(`tmux display-message -p -t mcp-${sessionId} '#{pane_tty}'`);
|
|
54
|
-
if (ttyCheck.success && ttyCheck.stdout) {
|
|
54
|
+
if (ttyCheck.success && ttyCheck.stdout && ttyCheck.stdout.trim()) {
|
|
55
55
|
ttyId = ttyCheck.stdout.trim().replace(/^\/dev\//, '').replace(/\//g, '_');
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
|
-
|
|
59
|
+
// 保持专属 sessionId 兜底
|
|
60
60
|
}
|
|
61
61
|
// 删除 TTY 关联和兜底命名的缓存与日志
|
|
62
62
|
const filesToRemove = [
|
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.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",
|