Skip to content

Commit bee349b

Browse files
committed
fix(mcp-client): 增强协议版本校验和JSON-RPC批处理支持
- 在初始化时验证服务器返回的MCP协议版本,拒绝不支持的版本 - 支持处理JSON-RPC批量消息,逐条分发处理 - 抽离单条消息处理逻辑,统一处理通知和响应 - 防止通知处理器异常导致读取循环崩溃 fix(ui): 优化键盘交互提示与布局间距 - 移除Esc键作为取消操作,避免与Ctrl+C冲突 - 更新底部提示,明确Ctrl+C为关闭操作,Esc为返回操作 - 调整MessageView内容左边距,使间距更合理
1 parent 5a99628 commit bee349b

3 files changed

Lines changed: 53 additions & 23 deletions

File tree

src/mcp/mcp-client.ts

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,19 @@ export class McpClient {
178178
},
179179
timeoutMs
180180
)
181-
.then(() => {
181+
.then((result) => {
182+
// Validate protocol version from server response (per MCP spec §4.2.1.2)
183+
const initResult = result as { protocolVersion?: string } | undefined;
184+
const serverVersion = initResult?.protocolVersion;
185+
if (serverVersion && serverVersion !== "2025-03-26" && serverVersion !== "2024-11-05") {
186+
reject(
187+
new Error(
188+
`Unsupported MCP protocol version "${serverVersion}" from server "${this.serverName}". ` +
189+
`Client supports 2025-03-26 and 2024-11-05.`
190+
)
191+
);
192+
return;
193+
}
182194
// Send initialized notification
183195
this.sendNotification("notifications/initialized");
184196
resolve();
@@ -302,36 +314,54 @@ export class McpClient {
302314
try {
303315
const parsed: unknown = JSON.parse(line);
304316

305-
// Handle notifications (no id field — server-initiated)
306-
if (parsed && typeof parsed === "object" && !("id" in parsed)) {
307-
const notification = parsed as JsonRpcNotification;
308-
if (this.notificationHandler && typeof notification.method === "string") {
309-
try {
310-
this.notificationHandler(notification.method, notification.params);
311-
} catch {
312-
// Swallow handler errors to avoid crashing the reader loop
317+
// Handle JSON-RPC batch (array of requests/notifications/responses)
318+
// Per MCP 2025-03-26 §4.1.1.3: implementations MUST support receiving batches.
319+
if (Array.isArray(parsed)) {
320+
for (const item of parsed) {
321+
if (item && typeof item === "object") {
322+
this.handleSingleMessage(item);
313323
}
314324
}
315325
return;
316326
}
317327

318-
// Handle responses to our requests
319-
const message = parsed as JsonRpcResponse;
320-
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
321-
const pending = this.pendingRequests.get(message.id)!;
322-
this.pendingRequests.delete(message.id);
323-
clearTimeout(pending.timer);
324-
if (message.error) {
325-
pending.reject(this.withStderr(`MCP error: ${message.error.message}`));
326-
} else {
327-
pending.resolve(message.result);
328-
}
328+
// Handle single message
329+
if (parsed && typeof parsed === "object") {
330+
this.handleSingleMessage(parsed);
329331
}
330332
} catch {
331333
// Ignore unparseable lines
332334
}
333335
}
334336

337+
private handleSingleMessage(msg: object): void {
338+
// Handle notifications (no id field — server-initiated)
339+
if (!("id" in msg)) {
340+
const notification = msg as unknown as JsonRpcNotification;
341+
if (this.notificationHandler && typeof notification.method === "string") {
342+
try {
343+
this.notificationHandler(notification.method, notification.params);
344+
} catch {
345+
// Swallow handler errors to avoid crashing the reader loop
346+
}
347+
}
348+
return;
349+
}
350+
351+
// Handle responses to our requests
352+
const message = msg as unknown as JsonRpcResponse;
353+
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
354+
const pending = this.pendingRequests.get(message.id)!;
355+
this.pendingRequests.delete(message.id);
356+
clearTimeout(pending.timer);
357+
if (message.error) {
358+
pending.reject(this.withStderr(`MCP error: ${message.error.message}`));
359+
} else {
360+
pending.resolve(message.result);
361+
}
362+
}
363+
}
364+
335365
private withNpxYesArg(command: string, args: string[]): string[] {
336366
const executable = path
337367
.basename(command)

src/ui/McpStatusList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ function ServerDetailView({
356356
const visibleItems = allItems.slice(visibleStart, visibleStart + maxVisible);
357357

358358
useInput((input, key) => {
359-
if (key.escape || (key.ctrl && (input === "c" || input === "C"))) {
359+
if (key.ctrl && (input === "c" || input === "C")) {
360360
onCancel();
361361
return;
362362
}
@@ -466,7 +466,7 @@ function ServerDetailView({
466466
</Box>
467467
{/* Footer */}
468468
<Box paddingX={1}>
469-
<Text dimColor>↑/↓ scroll · Space/Enter back · Esc close</Text>
469+
<Text dimColor>↑/↓ scroll · Space/Enter back · Esc back · Ctrl+C close</Text>
470470
</Box>
471471
</Box>
472472
</Box>

src/ui/MessageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function MessageView({ message, collapsed, width = 80 }: Props): React.Re
4747
return (
4848
<Box marginLeft={1} flexDirection="column" marginY={0}>
4949
<StatusLine bulletColor="gray" name="Thinking" params={summary} />
50-
<Box flexDirection="column" marginLeft={4}>
50+
<Box flexDirection="column" marginLeft={2}>
5151
{content ? <Text dimColor>{renderMarkdown(content)}</Text> : null}
5252
</Box>
5353
</Box>

0 commit comments

Comments
 (0)