Skip to content

Commit 0eeedab

Browse files
committed
refactor: address review feedback
- Consolidate ProjectSettingsSection state (18 useState → grouped objects + custom hook) - Extract parseMCPNameCommand helper to dedupe add/edit parsing in registry.ts - Unify MCP test API (testServer + testCommand → single test endpoint) - Add localStorage caching for test results with CachedMCPTestResult type - Show test age in Settings UI (formatRelativeTime) - Cache successful test results when adding servers
1 parent c71a6cb commit 0eeedab

File tree

10 files changed

+203
-148
lines changed

10 files changed

+203
-148
lines changed

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

Lines changed: 142 additions & 90 deletions
Large diffs are not rendered by default.

src/browser/utils/slashCommands/registry.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,25 @@ const newCommandDefinition: SlashCommandDefinition = {
585585
},
586586
};
587587

588+
/**
589+
* Parse MCP subcommand that takes name + command (add/edit).
590+
* Returns { name, command } or null if invalid.
591+
*/
592+
function parseMCPNameCommand(
593+
subcommand: string,
594+
tokens: string[],
595+
rawInput: string
596+
): { name: string; command: string } | null {
597+
const name = tokens[1];
598+
// Extract command text after "subcommand name"
599+
const command = rawInput
600+
.trim()
601+
.replace(new RegExp(`^${subcommand}\\s+[^\\s]+\\s*`, "i"), "")
602+
.trim();
603+
if (!name || !command) return null;
604+
return { name, command };
605+
}
606+
588607
const mcpCommandDefinition: SlashCommandDefinition = {
589608
key: "mcp",
590609
description: "Manage MCP servers for this project",
@@ -594,28 +613,13 @@ const mcpCommandDefinition: SlashCommandDefinition = {
594613
}
595614

596615
const sub = cleanRemainingTokens[0];
597-
if (sub === "add") {
598-
const name = cleanRemainingTokens[1];
599-
const commandText = rawInput
600-
.trim()
601-
.replace(/^add\s+[^\s]+\s*/i, "")
602-
.trim();
603-
if (!name || !commandText) {
604-
return { type: "unknown-command", command: "mcp", subcommand: "add" };
605-
}
606-
return { type: "mcp-add", name, command: commandText };
607-
}
608616

609-
if (sub === "edit") {
610-
const name = cleanRemainingTokens[1];
611-
const commandText = rawInput
612-
.trim()
613-
.replace(/^edit\s+[^\s]+\s*/i, "")
614-
.trim();
615-
if (!name || !commandText) {
616-
return { type: "unknown-command", command: "mcp", subcommand: "edit" };
617+
if (sub === "add" || sub === "edit") {
618+
const parsed = parseMCPNameCommand(sub, cleanRemainingTokens, rawInput);
619+
if (!parsed) {
620+
return { type: "unknown-command", command: "mcp", subcommand: sub };
617621
}
618-
return { type: "mcp-edit", name, command: commandText };
622+
return { type: sub === "add" ? "mcp-add" : "mcp-edit", ...parsed };
619623
}
620624

621625
if (sub === "remove") {

src/common/constants/storage.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ export const SELECTED_WORKSPACE_KEY = "selectedWorkspace";
4848
*/
4949
export const EXPANDED_PROJECTS_KEY = "expandedProjects";
5050

51+
/**
52+
* Get the localStorage key for cached MCP server test results (per project)
53+
* Format: "mcpTestResults:{projectPath}"
54+
* Stores: Record<serverName, CachedMCPTestResult>
55+
*/
56+
export function getMCPTestResultsKey(projectPath: string): string {
57+
return `mcpTestResults:${projectPath}`;
58+
}
59+
5160
/**
5261
* Helper to create a thinking level storage key for a workspace
5362
* Format: "thinkingLevel:{workspaceId}"

src/common/orpc/schemas.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ export {
4444
MCPAddParamsSchema,
4545
MCPRemoveParamsSchema,
4646
MCPTestParamsSchema,
47-
MCPTestCommandParamsSchema,
4847
MCPTestResultSchema,
4948
} from "./schemas/mcp";
5049

src/common/orpc/schemas/api.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
MCPRemoveParamsSchema,
2121
MCPServerMapSchema,
2222
MCPTestParamsSchema,
23-
MCPTestCommandParamsSchema,
2423
MCPTestResultSchema,
2524
} from "./mcp";
2625

@@ -141,10 +140,6 @@ export const projects = {
141140
input: MCPTestParamsSchema,
142141
output: MCPTestResultSchema,
143142
},
144-
testCommand: {
145-
input: MCPTestCommandParamsSchema,
146-
output: MCPTestResultSchema,
147-
},
148143
},
149144
secrets: {
150145
get: {

src/common/orpc/schemas/mcp.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export const MCPRemoveParamsSchema = z.object({
1313
name: z.string(),
1414
});
1515

16+
/**
17+
* Unified test params - provide either name (to test configured server) or command (to test arbitrary command).
18+
* At least one of name or command must be provided.
19+
*/
1620
export const MCPTestParamsSchema = z.object({
1721
projectPath: z.string(),
18-
name: z.string(),
19-
});
20-
21-
export const MCPTestCommandParamsSchema = z.object({
22-
projectPath: z.string(),
23-
command: z.string(),
22+
name: z.string().optional(),
23+
command: z.string().optional(),
2424
});
2525

2626
export const MCPTestResultSchema = z.discriminatedUnion("success", [

src/common/types/mcp.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ export type MCPServerMap = Record<string, string>;
66

77
/** Result of testing an MCP server connection */
88
export type MCPTestResult = { success: true; tools: string[] } | { success: false; error: string };
9+
10+
/** Cached test result with timestamp for age display */
11+
export interface CachedMCPTestResult {
12+
result: MCPTestResult;
13+
testedAt: number; // Unix timestamp ms
14+
}

src/node/orpc/router.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,7 @@ export const router = (authToken?: string) => {
189189
.input(schemas.projects.mcp.test.input)
190190
.output(schemas.projects.mcp.test.output)
191191
.handler(({ context, input }) =>
192-
context.mcpServerManager.testServer(input.projectPath, input.name)
193-
),
194-
testCommand: t
195-
.input(schemas.projects.mcp.testCommand.input)
196-
.output(schemas.projects.mcp.testCommand.output)
197-
.handler(({ context, input }) =>
198-
context.mcpServerManager.testCommand(input.projectPath, input.command)
192+
context.mcpServerManager.test(input.projectPath, input.name, input.command)
199193
),
200194
},
201195
},

src/node/services/mcpConfigService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class MCPConfigService {
8080

8181
async removeServer(projectPath: string, name: string): Promise<Result<void>> {
8282
const cfg = await this.getConfig(projectPath);
83-
if (!cfg.servers?.[name]) {
83+
if (!cfg.servers[name]) {
8484
return Err(`Server ${name} not found`);
8585
}
8686
delete cfg.servers[name];

src/node/services/mcpServerManager.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -242,27 +242,23 @@ export class MCPServerManager {
242242
}
243243

244244
/**
245-
* Test an MCP server configuration by spawning it, fetching tools, then closing.
246-
* Used by the Settings UI to verify a server works before relying on it.
245+
* Test an MCP server. Provide either:
246+
* - `name` to test a configured server by looking up its command
247+
* - `command` to test an arbitrary command directly
247248
*/
248-
async testServer(projectPath: string, name: string): Promise<MCPTestResult> {
249-
const servers = await this.configService.listServers(projectPath);
250-
const command = servers[name];
251-
if (!command) {
252-
return { success: false, error: `Server "${name}" not found in configuration` };
249+
async test(projectPath: string, name?: string, command?: string): Promise<MCPTestResult> {
250+
if (name) {
251+
const servers = await this.configService.listServers(projectPath);
252+
const serverCommand = servers[name];
253+
if (!serverCommand) {
254+
return { success: false, error: `Server "${name}" not found in configuration` };
255+
}
256+
return runServerTest(serverCommand, projectPath, `server "${name}"`);
253257
}
254-
return runServerTest(command, projectPath, `server "${name}"`);
255-
}
256-
257-
/**
258-
* Test an MCP command directly without requiring it to be in config.
259-
* Used by Settings UI to validate before adding.
260-
*/
261-
async testCommand(projectPath: string, command: string): Promise<MCPTestResult> {
262-
if (!command.trim()) {
263-
return { success: false, error: "Command is required" };
258+
if (command?.trim()) {
259+
return runServerTest(command, projectPath, "command");
264260
}
265-
return runServerTest(command, projectPath, "command");
261+
return { success: false, error: "Either name or command is required" };
266262
}
267263

268264
private collectTools(instances: Map<string, MCPServerInstance>): Record<string, Tool> {

0 commit comments

Comments
 (0)