Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 201 additions & 42 deletions src/browser/components/Settings/sections/ProjectSettingsSection.tsx

Large diffs are not rendered by default.

123 changes: 102 additions & 21 deletions src/common/orpc/schemas/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,69 @@ export const WorkspaceMCPOverridesSchema = z.object({
toolAllowlist: z.record(z.string(), z.array(z.string())).optional(),
});

export const MCPAddParamsSchema = z.object({
projectPath: z.string(),
name: z.string(),
command: z.string(),
});
export const MCPTransportSchema = z.enum(["stdio", "http", "sse", "auto"]);

export const MCPHeaderValueSchema = z.union([z.string(), z.object({ secret: z.string() })]);
export const MCPHeadersSchema = z.record(z.string(), MCPHeaderValueSchema);

export const MCPServerInfoSchema = z.discriminatedUnion("transport", [
z.object({
transport: z.literal("stdio"),
command: z.string(),
disabled: z.boolean(),
toolAllowlist: z.array(z.string()).optional(),
}),
z.object({
transport: z.literal("http"),
url: z.string(),
headers: MCPHeadersSchema.optional(),
disabled: z.boolean(),
toolAllowlist: z.array(z.string()).optional(),
}),
z.object({
transport: z.literal("sse"),
url: z.string(),
headers: MCPHeadersSchema.optional(),
disabled: z.boolean(),
toolAllowlist: z.array(z.string()).optional(),
}),
z.object({
transport: z.literal("auto"),
url: z.string(),
headers: MCPHeadersSchema.optional(),
disabled: z.boolean(),
toolAllowlist: z.array(z.string()).optional(),
}),
]);

export const MCPServerMapSchema = z.record(z.string(), MCPServerInfoSchema);

export const MCPAddParamsSchema = z
.object({
projectPath: z.string(),
name: z.string(),

// Backward-compatible: if transport omitted, interpret as stdio.
transport: MCPTransportSchema.optional(),

command: z.string().optional(),
url: z.string().optional(),
headers: MCPHeadersSchema.optional(),
})
.superRefine((input, ctx) => {
const transport = input.transport ?? "stdio";

if (transport === "stdio") {
if (!input.command?.trim()) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "command is required for stdio" });
}
return;
}

if (!input.url?.trim()) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "url is required for http/sse/auto" });
}
});

export const MCPRemoveParamsSchema = z.object({
projectPath: z.string(),
Expand All @@ -41,15 +99,6 @@ export const MCPSetEnabledParamsSchema = z.object({
enabled: z.boolean(),
});

export const MCPServerMapSchema = z.record(
z.string(),
z.object({
command: z.string(),
disabled: z.boolean(),
toolAllowlist: z.array(z.string()).optional(),
})
);

export const MCPSetToolAllowlistParamsSchema = z.object({
projectPath: z.string(),
name: z.string(),
Expand All @@ -58,14 +107,46 @@ export const MCPSetToolAllowlistParamsSchema = z.object({
});

/**
* Unified test params - provide either name (to test configured server) or command (to test arbitrary command).
* At least one of name or command must be provided.
* Unified test params - provide either:
* - name (to test a configured server), OR
* - command (to test arbitrary stdio command), OR
* - url+transport (to test arbitrary http/sse/auto endpoint)
*/
export const MCPTestParamsSchema = z.object({
projectPath: z.string(),
name: z.string().optional(),
command: z.string().optional(),
});
export const MCPTestParamsSchema = z
.object({
projectPath: z.string(),
name: z.string().optional(),

transport: MCPTransportSchema.optional(),
command: z.string().optional(),
url: z.string().optional(),
headers: MCPHeadersSchema.optional(),
})
.superRefine((input, ctx) => {
if (input.name?.trim()) {
return;
}

if (input.command?.trim()) {
return;
}

if (input.url?.trim()) {
const transport = input.transport;
if (transport !== "http" && transport !== "sse" && transport !== "auto") {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "transport must be http|sse|auto when testing by url",
});
}
return;
}

ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Either name, command, or url is required",
});
});

export const MCPTestResultSchema = z.discriminatedUnion("success", [
z.object({ success: z.literal(true), tools: z.array(z.string()) }),
Expand Down
76 changes: 76 additions & 0 deletions src/common/orpc/schemas/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,70 @@ const MessageSentPropertiesSchema = z.object({
thinkingLevel: TelemetryThinkingLevelSchema,
});

// MCP transport mode enum (matches payload.ts TelemetryMCPTransportMode)
const TelemetryMCPTransportModeSchema = z.enum([
"none",
"stdio_only",
"http_only",
"sse_only",
"mixed",
]);

const MCPContextInjectedPropertiesSchema = z.object({
workspaceId: z.string(),
model: z.string(),
mode: z.string(),
runtimeType: TelemetryRuntimeTypeSchema,

mcp_server_enabled_count: z.number(),
mcp_server_started_count: z.number(),
mcp_server_failed_count: z.number(),

mcp_tool_count: z.number(),
total_tool_count: z.number(),
builtin_tool_count: z.number(),

mcp_transport_mode: TelemetryMCPTransportModeSchema,
mcp_has_http: z.boolean(),
mcp_has_sse: z.boolean(),
mcp_has_stdio: z.boolean(),
mcp_auto_fallback_count: z.number(),
mcp_setup_duration_ms_b2: z.number(),
});

const TelemetryMCPServerTransportSchema = z.enum(["stdio", "http", "sse", "auto"]);
const TelemetryMCPTestErrorCategorySchema = z.enum([
"timeout",
"connect",
"http_status",
"unknown",
]);

const MCPServerTestedPropertiesSchema = z.object({
transport: TelemetryMCPServerTransportSchema,
success: z.boolean(),
duration_ms_b2: z.number(),
error_category: TelemetryMCPTestErrorCategorySchema.optional(),
});

const TelemetryMCPServerConfigActionSchema = z.enum([
"add",
"edit",
"remove",
"enable",
"disable",
"set_tool_allowlist",
"set_headers",
]);

const MCPServerConfigChangedPropertiesSchema = z.object({
action: TelemetryMCPServerConfigActionSchema,
transport: TelemetryMCPServerTransportSchema,
has_headers: z.boolean(),
uses_secret_headers: z.boolean(),
tool_allowlist_size_b2: z.number().optional(),
});

const StreamCompletedPropertiesSchema = z.object({
model: z.string(),
wasInterrupted: z.boolean(),
Expand Down Expand Up @@ -125,6 +189,18 @@ export const TelemetryEventSchema = z.discriminatedUnion("event", [
event: z.literal("workspace_switched"),
properties: WorkspaceSwitchedPropertiesSchema,
}),
z.object({
event: z.literal("mcp_context_injected"),
properties: MCPContextInjectedPropertiesSchema,
}),
z.object({
event: z.literal("mcp_server_tested"),
properties: MCPServerTestedPropertiesSchema,
}),
z.object({
event: z.literal("mcp_server_config_changed"),
properties: MCPServerConfigChangedPropertiesSchema,
}),
z.object({
event: z.literal("message_sent"),
properties: MessageSentPropertiesSchema,
Expand Down
76 changes: 76 additions & 0 deletions src/common/telemetry/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,79 @@ export interface MessageSentPayload {
thinkingLevel: TelemetryThinkingLevel;
}

/**
* MCP usage events
*/
export type TelemetryMCPTransportMode = "none" | "stdio_only" | "http_only" | "sse_only" | "mixed";

export interface MCPContextInjectedPayload {
/** Workspace ID (randomly generated, safe to send) */
workspaceId: string;
/** Full model identifier */
model: string;
/** UI mode */
mode: string;
/** Runtime type for the workspace */
runtimeType: TelemetryRuntimeType;

/** How many servers are enabled for this workspace message */
mcp_server_enabled_count: number;
/** How many servers successfully started (client created + tools fetched) */
mcp_server_started_count: number;
/** How many enabled servers failed to start */
mcp_server_failed_count: number;

/** MCP tools injected into the model request */
mcp_tool_count: number;
/** Total tools injected into the model request (built-in + MCP) */
total_tool_count: number;
/** Built-in tool count injected into the model request */
builtin_tool_count: number;

/** Effective transport mix for *started* servers (auto transport is resolved to http/sse) */
mcp_transport_mode: TelemetryMCPTransportMode;
/** Whether any started server uses HTTP (auto transport resolves to http/sse at runtime) */
mcp_has_http: boolean;
/** Whether any started server uses legacy SSE */
mcp_has_sse: boolean;
/** Whether any started server uses stdio */
mcp_has_stdio: boolean;

/** Number of servers that required auto-fallback from HTTP to SSE */
mcp_auto_fallback_count: number;

/** Time spent preparing MCP servers/tools (ms, rounded to nearest power of 2) */
mcp_setup_duration_ms_b2: number;
}

export type TelemetryMCPServerTransport = "stdio" | "http" | "sse" | "auto";
export type TelemetryMCPTestErrorCategory = "timeout" | "connect" | "http_status" | "unknown";

export interface MCPServerTestedPayload {
transport: TelemetryMCPServerTransport;
success: boolean;
duration_ms_b2: number;
/** Error category when success=false (no raw error messages for privacy) */
error_category?: TelemetryMCPTestErrorCategory;
}

export type TelemetryMCPServerConfigAction =
| "add"
| "edit"
| "remove"
| "enable"
| "disable"
| "set_tool_allowlist"
| "set_headers";

export interface MCPServerConfigChangedPayload {
action: TelemetryMCPServerConfigAction;
transport: TelemetryMCPServerTransport;
has_headers: boolean;
uses_secret_headers: boolean;
/** Only set when action=set_tool_allowlist */
tool_allowlist_size_b2?: number;
}
/**
* Stream completion event - tracks when AI responses finish
*/
Expand Down Expand Up @@ -226,6 +299,9 @@ export type TelemetryEventPayload =
| { event: "workspace_created"; properties: WorkspaceCreatedPayload }
| { event: "workspace_switched"; properties: WorkspaceSwitchedPayload }
| { event: "message_sent"; properties: MessageSentPayload }
| { event: "mcp_context_injected"; properties: MCPContextInjectedPayload }
| { event: "mcp_server_tested"; properties: MCPServerTestedPayload }
| { event: "mcp_server_config_changed"; properties: MCPServerConfigChangedPayload }
| { event: "stream_completed"; properties: StreamCompletedPayload }
| { event: "compaction_completed"; properties: CompactionCompletedPayload }
| { event: "provider_configured"; properties: ProviderConfiguredPayload }
Expand Down
34 changes: 29 additions & 5 deletions src/common/types/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/** Normalized server info (always has disabled field) */
export interface MCPServerInfo {
command: string;
/** Supported MCP server transports. */
export type MCPServerTransport = "stdio" | "http" | "sse" | "auto";

export type MCPHeaderValue = string | { secret: string };

export interface MCPServerBaseInfo {
transport: MCPServerTransport;
disabled: boolean;
/**
* Optional tool allowlist at project level.
Expand All @@ -10,12 +14,32 @@ export interface MCPServerInfo {
toolAllowlist?: string[];
}

/** stdio server definition (local process). */
export interface MCPStdioServerInfo extends MCPServerBaseInfo {
transport: "stdio";
command: string;
}

/** HTTP-based server definition. */
export interface MCPHttpServerInfo extends MCPServerBaseInfo {
transport: "http" | "sse" | "auto";
url: string;
/** Optional headers (string literal or reference to a project secret key). */
headers?: Record<string, MCPHeaderValue>;
}

/** Normalized server info (always has disabled field). */
export type MCPServerInfo = MCPStdioServerInfo | MCPHttpServerInfo;

export interface MCPConfig {
servers: Record<string, MCPServerInfo>;
}

/** Internal map of server name → command string (used after filtering disabled) */
export type MCPServerMap = Record<string, string>;
/**
* Internal map of server name → server info (used after filtering disabled).
* Values are not shown to the model; only server names are exposed.
*/
export type MCPServerMap = Record<string, MCPServerInfo>;

/** Result of testing an MCP server connection */
export type MCPTestResult = { success: true; tools: string[] } | { success: false; error: string };
Expand Down
Loading