Skip to content

Commit fb38560

Browse files
committed
feat(mcp): 集成并展示MCP服务器状态和资源列表
- 新增McpStatusList组件,实现MCP服务器状态、工具、提示和资源的可视化展示 - 修改App组件,支持切换视图显示MCP状态列表 - MCP客户端增加对prompts和resources的分页列表及读取功能 - MCP管理器扩展,支持发现和管理prompts及resources,及工具列表变化的事件通知 - 优化MCP客户端,支持JSON-RPC通知的处理,完善请求超时控制 - 在session中添加MCP工具列表变化监听,实时更新工具定义数据 - 提供键盘操作支持,实现MCP状态列表的上下翻页和快速导航 - 美化MCP状态列表界面,显示服务器状态及能力的详细信息
1 parent 80d81b2 commit fb38560

5 files changed

Lines changed: 537 additions & 42 deletions

File tree

src/mcp/mcp-client.ts

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ type JsonRpcResponse = {
1717
error?: { code: number; message: string; data?: unknown };
1818
};
1919

20+
type JsonRpcNotification = {
21+
jsonrpc: "2.0";
22+
method: string;
23+
params?: Record<string, unknown>;
24+
};
25+
2026
export type McpToolDefinition = {
2127
name: string;
2228
description?: string;
2329
inputSchema: {
2430
type: "object";
2531
properties: Record<string, unknown>;
2632
required?: string[];
33+
additionalProperties?: boolean;
2734
};
2835
};
2936

@@ -37,6 +44,58 @@ type CallToolResult = {
3744
isError?: boolean;
3845
};
3946

47+
export type McpPromptArgument = {
48+
name: string;
49+
description?: string;
50+
required?: boolean;
51+
};
52+
53+
export type McpPromptDefinition = {
54+
name: string;
55+
description?: string;
56+
arguments?: McpPromptArgument[];
57+
};
58+
59+
type ListPromptsResult = {
60+
prompts: McpPromptDefinition[];
61+
nextCursor?: string;
62+
};
63+
64+
export type McpPromptMessage = {
65+
role: "user" | "assistant";
66+
content: { type: string; text?: string };
67+
};
68+
69+
type GetPromptResult = {
70+
description?: string;
71+
messages: McpPromptMessage[];
72+
};
73+
74+
export type McpResourceDefinition = {
75+
uri: string;
76+
name: string;
77+
description?: string;
78+
mimeType?: string;
79+
};
80+
81+
type ListResourcesResult = {
82+
resources: McpResourceDefinition[];
83+
nextCursor?: string;
84+
};
85+
86+
export type McpResourceContent = {
87+
uri: string;
88+
mimeType?: string;
89+
text?: string;
90+
blob?: string;
91+
};
92+
93+
type ReadResourceResult = {
94+
contents: McpResourceContent[];
95+
};
96+
97+
export type McpNotificationHandler = (method: string, params?: Record<string, unknown>) => void;
98+
4099
export class McpClient {
41100
private process: ChildProcess | null = null;
42101
private reader: Interface | null = null;
@@ -46,13 +105,17 @@ export class McpClient {
46105
{ resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }
47106
>();
48107
private stderrBuffer = "";
108+
private notificationHandler: McpNotificationHandler | null = null;
49109

50110
constructor(
51111
private readonly serverName: string,
52112
private readonly command: string,
53113
private readonly args: string[] = [],
54-
private readonly env?: Record<string, string>
55-
) {}
114+
private readonly env?: Record<string, string>,
115+
onNotification?: McpNotificationHandler
116+
) {
117+
this.notificationHandler = onNotification ?? null;
118+
}
56119

57120
async connect(timeoutMs: number): Promise<void> {
58121
return new Promise((resolve, reject) => {
@@ -109,7 +172,7 @@ export class McpClient {
109172
this.sendRequest(
110173
"initialize",
111174
{
112-
protocolVersion: "2024-11-05",
175+
protocolVersion: "2025-03-26",
113176
capabilities: {},
114177
clientInfo: { name: "deepcode-cli", version: "0.1.0" },
115178
},
@@ -141,8 +204,50 @@ export class McpClient {
141204
throw this.withStderr(`MCP server "${this.serverName}" returned too many tools/list pages`);
142205
}
143206

144-
async callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult> {
145-
return (await this.sendRequest("tools/call", { name, arguments: args })) as CallToolResult;
207+
async callTool(name: string, args: Record<string, unknown>, timeoutMs = 60_000): Promise<CallToolResult> {
208+
return (await this.sendRequest("tools/call", { name, arguments: args }, timeoutMs)) as CallToolResult;
209+
}
210+
211+
async listPrompts(timeoutMs: number): Promise<McpPromptDefinition[]> {
212+
const prompts: McpPromptDefinition[] = [];
213+
let cursor: string | undefined;
214+
215+
for (let page = 0; page < 100; page++) {
216+
const params = cursor ? { cursor } : {};
217+
const result = (await this.sendRequest("prompts/list", params, timeoutMs)) as ListPromptsResult;
218+
prompts.push(...(result.prompts ?? []));
219+
cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
220+
if (!cursor) {
221+
return prompts;
222+
}
223+
}
224+
225+
throw this.withStderr(`MCP server "${this.serverName}" returned too many prompts/list pages`);
226+
}
227+
228+
async getPrompt(name: string, args: Record<string, unknown>, timeoutMs = 30_000): Promise<GetPromptResult> {
229+
return (await this.sendRequest("prompts/get", { name, arguments: args }, timeoutMs)) as GetPromptResult;
230+
}
231+
232+
async listResources(timeoutMs: number): Promise<McpResourceDefinition[]> {
233+
const resources: McpResourceDefinition[] = [];
234+
let cursor: string | undefined;
235+
236+
for (let page = 0; page < 100; page++) {
237+
const params = cursor ? { cursor } : {};
238+
const result = (await this.sendRequest("resources/list", params, timeoutMs)) as ListResourcesResult;
239+
resources.push(...(result.resources ?? []));
240+
cursor = typeof result.nextCursor === "string" && result.nextCursor ? result.nextCursor : undefined;
241+
if (!cursor) {
242+
return resources;
243+
}
244+
}
245+
246+
throw this.withStderr(`MCP server "${this.serverName}" returned too many resources/list pages`);
247+
}
248+
249+
async readResource(uri: string, timeoutMs = 30_000): Promise<ReadResourceResult> {
250+
return (await this.sendRequest("resources/read", { uri }, timeoutMs)) as ReadResourceResult;
146251
}
147252

148253
disconnect(): void {
@@ -195,7 +300,23 @@ export class McpClient {
195300

196301
private handleLine(line: string): void {
197302
try {
198-
const message = JSON.parse(line) as JsonRpcResponse;
303+
const parsed: unknown = JSON.parse(line);
304+
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
313+
}
314+
}
315+
return;
316+
}
317+
318+
// Handle responses to our requests
319+
const message = parsed as JsonRpcResponse;
199320
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
200321
const pending = this.pendingRequests.get(message.id)!;
201322
this.pendingRequests.delete(message.id);

0 commit comments

Comments
 (0)