Skip to content

Commit d8dd733

Browse files
committed
refactor: DRY and consolidate MCP types
- Extract runServerTest() helper to deduplicate testServer/testCommand - Move MCPTestResult type to common/types/mcp.ts (single source of truth) - Remove redundant nullish coalescing where getConfig() guarantees non-null - Net reduction of 29 lines
1 parent 9336c80 commit d8dd733

File tree

4 files changed

+68
-97
lines changed

4 files changed

+68
-97
lines changed

src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {
1515
X,
1616
} from "lucide-react";
1717
import { createEditKeyHandler } from "@/browser/utils/ui/keybinds";
18-
19-
type TestResult = { success: true; tools: string[] } | { success: false; error: string };
18+
import type { MCPTestResult } from "@/common/types/mcp";
2019

2120
export const ProjectSettingsSection: React.FC = () => {
2221
const { api } = useAPI();
@@ -28,14 +27,14 @@ export const ProjectSettingsSection: React.FC = () => {
2827
const [loading, setLoading] = useState(false);
2928
const [error, setError] = useState<string | null>(null);
3029
const [testingServer, setTestingServer] = useState<string | null>(null);
31-
const [testResults, setTestResults] = useState<Map<string, TestResult>>(new Map());
30+
const [testResults, setTestResults] = useState<Map<string, MCPTestResult>>(new Map());
3231

3332
// Add server form state
3433
const [newServerName, setNewServerName] = useState("");
3534
const [newServerCommand, setNewServerCommand] = useState("");
3635
const [addingServer, setAddingServer] = useState(false);
3736
const [testingNewCommand, setTestingNewCommand] = useState(false);
38-
const [newCommandTestResult, setNewCommandTestResult] = useState<TestResult | null>(null);
37+
const [newCommandTestResult, setNewCommandTestResult] = useState<MCPTestResult | null>(null);
3938

4039
// Edit server state
4140
const [editingServer, setEditingServer] = useState<string | null>(null);

src/common/types/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export interface MCPConfig {
33
}
44

55
export type MCPServerMap = Record<string, string>;
6+
7+
/** Result of testing an MCP server connection */
8+
export type MCPTestResult = { success: true; tools: string[] } | { success: false; error: string };

src/node/services/mcpConfigService.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class MCPConfigService {
4040
if (!parsed || typeof parsed !== "object" || !parsed.servers) {
4141
return { servers: {} };
4242
}
43-
return { servers: parsed.servers ?? {} };
43+
return { servers: parsed.servers };
4444
} catch (error) {
4545
log.error("Failed to read MCP config", { projectPath, error });
4646
return { servers: {} };
@@ -55,7 +55,7 @@ export class MCPConfigService {
5555

5656
async listServers(projectPath: string): Promise<MCPServerMap> {
5757
const cfg = await this.getConfig(projectPath);
58-
return cfg.servers ?? {};
58+
return cfg.servers;
5959
}
6060

6161
async addServer(projectPath: string, name: string, command: string): Promise<Result<void>> {
@@ -67,7 +67,6 @@ export class MCPConfigService {
6767
}
6868

6969
const cfg = await this.getConfig(projectPath);
70-
cfg.servers = cfg.servers ?? {};
7170
cfg.servers[name] = command;
7271

7372
try {

src/node/services/mcpServerManager.ts

Lines changed: 60 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { experimental_createMCPClient, type MCPTransport } from "@ai-sdk/mcp";
22
import type { Tool } from "ai";
33
import { log } from "@/node/services/log";
44
import { MCPStdioTransport } from "@/node/services/mcpStdioTransport";
5-
import type { MCPServerMap } from "@/common/types/mcp";
5+
import type { MCPServerMap, MCPTestResult } from "@/common/types/mcp";
66
import type { Runtime } from "@/node/runtime/Runtime";
77
import type { MCPConfigService } from "@/node/services/mcpConfigService";
88
import { createRuntime } from "@/node/runtime/runtimeFactory";
@@ -120,7 +120,56 @@ function wrapMCPTools(tools: Record<string, Tool>): Record<string, Tool> {
120120
return wrapped;
121121
}
122122

123-
export type MCPTestResult = { success: true; tools: string[] } | { success: false; error: string };
123+
export type { MCPTestResult } from "@/common/types/mcp";
124+
125+
/**
126+
* Run a test connection to an MCP server command.
127+
* Spawns the process, connects, fetches tools, then closes.
128+
*/
129+
async function runServerTest(
130+
command: string,
131+
projectPath: string,
132+
logContext: string
133+
): Promise<MCPTestResult> {
134+
const runtime = createRuntime({ type: "local", srcBaseDir: projectPath });
135+
const timeoutPromise = new Promise<MCPTestResult>((resolve) =>
136+
setTimeout(() => resolve({ success: false, error: "Connection timed out" }), TEST_TIMEOUT_MS)
137+
);
138+
139+
const testPromise = (async (): Promise<MCPTestResult> => {
140+
let transport: MCPStdioTransport | null = null;
141+
try {
142+
log.debug(`[MCP] Testing ${logContext}`, { command });
143+
const execStream = await runtime.exec(command, {
144+
cwd: projectPath,
145+
timeout: TEST_TIMEOUT_MS / 1000,
146+
});
147+
148+
transport = new MCPStdioTransport(execStream);
149+
await transport.start();
150+
const client = await experimental_createMCPClient({ transport });
151+
const tools = await client.tools();
152+
const toolNames = Object.keys(tools);
153+
await client.close();
154+
await transport.close();
155+
log.info(`[MCP] ${logContext} test successful`, { tools: toolNames });
156+
return { success: true, tools: toolNames };
157+
} catch (error) {
158+
const message = error instanceof Error ? error.message : String(error);
159+
log.warn(`[MCP] ${logContext} test failed`, { error: message });
160+
if (transport) {
161+
try {
162+
await transport.close();
163+
} catch {
164+
// ignore cleanup errors
165+
}
166+
}
167+
return { success: false, error: message };
168+
}
169+
})();
170+
171+
return Promise.race([testPromise, timeoutPromise]);
172+
}
124173

125174
interface MCPServerInstance {
126175
name: string;
@@ -155,21 +204,18 @@ export class MCPServerManager {
155204
}): Promise<Record<string, Tool>> {
156205
const { workspaceId, projectPath, runtime, workspacePath } = options;
157206
const servers = await this.configService.listServers(projectPath);
158-
const signature = JSON.stringify(servers ?? {});
159-
const serverCount = Object.keys(servers ?? {}).length;
207+
const signature = JSON.stringify(servers);
208+
const serverNames = Object.keys(servers);
160209

161210
const existing = this.workspaceServers.get(workspaceId);
162211
if (existing?.configSignature === signature) {
163-
log.debug("[MCP] Using cached servers", { workspaceId, serverCount });
212+
log.debug("[MCP] Using cached servers", { workspaceId, serverCount: serverNames.length });
164213
return this.collectTools(existing.instances);
165214
}
166215

167216
// Config changed or not started yet -> restart
168-
if (serverCount > 0) {
169-
log.info("[MCP] Starting servers", {
170-
workspaceId,
171-
servers: Object.keys(servers ?? {}),
172-
});
217+
if (serverNames.length > 0) {
218+
log.info("[MCP] Starting servers", { workspaceId, servers: serverNames });
173219
}
174220
await this.stopServers(workspaceId);
175221
const instances = await this.startServers(servers, runtime, workspacePath);
@@ -201,49 +247,11 @@ export class MCPServerManager {
201247
*/
202248
async testServer(projectPath: string, name: string): Promise<MCPTestResult> {
203249
const servers = await this.configService.listServers(projectPath);
204-
const command = servers?.[name];
250+
const command = servers[name];
205251
if (!command) {
206252
return { success: false, error: `Server "${name}" not found in configuration` };
207253
}
208-
209-
const runtime = createRuntime({ type: "local", srcBaseDir: projectPath });
210-
const timeoutPromise = new Promise<MCPTestResult>((resolve) =>
211-
setTimeout(() => resolve({ success: false, error: "Connection timed out" }), TEST_TIMEOUT_MS)
212-
);
213-
214-
const testPromise = (async (): Promise<MCPTestResult> => {
215-
let transport: MCPStdioTransport | null = null;
216-
try {
217-
log.debug("[MCP] Testing server", { name, command });
218-
const execStream = await runtime.exec(command, {
219-
cwd: projectPath,
220-
timeout: TEST_TIMEOUT_MS / 1000,
221-
});
222-
223-
transport = new MCPStdioTransport(execStream);
224-
await transport.start();
225-
const client = await experimental_createMCPClient({ transport });
226-
const tools = await client.tools();
227-
const toolNames = Object.keys(tools);
228-
await client.close();
229-
await transport.close();
230-
log.info("[MCP] Test successful", { name, tools: toolNames });
231-
return { success: true, tools: toolNames };
232-
} catch (error) {
233-
const message = error instanceof Error ? error.message : String(error);
234-
log.warn("[MCP] Test failed", { name, error: message });
235-
if (transport) {
236-
try {
237-
await transport.close();
238-
} catch {
239-
// ignore cleanup errors
240-
}
241-
}
242-
return { success: false, error: message };
243-
}
244-
})();
245-
246-
return Promise.race([testPromise, timeoutPromise]);
254+
return runServerTest(command, projectPath, `server "${name}"`);
247255
}
248256

249257
/**
@@ -254,45 +262,7 @@ export class MCPServerManager {
254262
if (!command.trim()) {
255263
return { success: false, error: "Command is required" };
256264
}
257-
258-
const runtime = createRuntime({ type: "local", srcBaseDir: projectPath });
259-
const timeoutPromise = new Promise<MCPTestResult>((resolve) =>
260-
setTimeout(() => resolve({ success: false, error: "Connection timed out" }), TEST_TIMEOUT_MS)
261-
);
262-
263-
const testPromise = (async (): Promise<MCPTestResult> => {
264-
let transport: MCPStdioTransport | null = null;
265-
try {
266-
log.debug("[MCP] Testing command", { command });
267-
const execStream = await runtime.exec(command, {
268-
cwd: projectPath,
269-
timeout: TEST_TIMEOUT_MS / 1000,
270-
});
271-
272-
transport = new MCPStdioTransport(execStream);
273-
await transport.start();
274-
const client = await experimental_createMCPClient({ transport });
275-
const tools = await client.tools();
276-
const toolNames = Object.keys(tools);
277-
await client.close();
278-
await transport.close();
279-
log.info("[MCP] Command test successful", { command, tools: toolNames });
280-
return { success: true, tools: toolNames };
281-
} catch (error) {
282-
const message = error instanceof Error ? error.message : String(error);
283-
log.warn("[MCP] Command test failed", { command, error: message });
284-
if (transport) {
285-
try {
286-
await transport.close();
287-
} catch {
288-
// ignore cleanup errors
289-
}
290-
}
291-
return { success: false, error: message };
292-
}
293-
})();
294-
295-
return Promise.race([testPromise, timeoutPromise]);
265+
return runServerTest(command, projectPath, "command");
296266
}
297267

298268
private collectTools(instances: Map<string, MCPServerInstance>): Record<string, Tool> {
@@ -309,7 +279,7 @@ export class MCPServerManager {
309279
workspacePath: string
310280
): Promise<Map<string, MCPServerInstance>> {
311281
const result = new Map<string, MCPServerInstance>();
312-
const entries = Object.entries(servers ?? {});
282+
const entries = Object.entries(servers);
313283
for (const [name, command] of entries) {
314284
try {
315285
const instance = await this.startSingleServer(name, command, runtime, workspacePath);

0 commit comments

Comments
 (0)