Skip to content

Commit a6099d3

Browse files
authored
🤖 feat: add HTTP/SSE MCP servers and usage telemetry (#1202)
1 parent d12561c commit a6099d3

File tree

12 files changed

+1429
-216
lines changed

12 files changed

+1429
-216
lines changed

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

Lines changed: 201 additions & 42 deletions
Large diffs are not rendered by default.

src/common/orpc/schemas/mcp.ts

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,69 @@ export const WorkspaceMCPOverridesSchema = z.object({
2424
toolAllowlist: z.record(z.string(), z.array(z.string())).optional(),
2525
});
2626

27-
export const MCPAddParamsSchema = z.object({
28-
projectPath: z.string(),
29-
name: z.string(),
30-
command: z.string(),
31-
});
27+
export const MCPTransportSchema = z.enum(["stdio", "http", "sse", "auto"]);
28+
29+
export const MCPHeaderValueSchema = z.union([z.string(), z.object({ secret: z.string() })]);
30+
export const MCPHeadersSchema = z.record(z.string(), MCPHeaderValueSchema);
31+
32+
export const MCPServerInfoSchema = z.discriminatedUnion("transport", [
33+
z.object({
34+
transport: z.literal("stdio"),
35+
command: z.string(),
36+
disabled: z.boolean(),
37+
toolAllowlist: z.array(z.string()).optional(),
38+
}),
39+
z.object({
40+
transport: z.literal("http"),
41+
url: z.string(),
42+
headers: MCPHeadersSchema.optional(),
43+
disabled: z.boolean(),
44+
toolAllowlist: z.array(z.string()).optional(),
45+
}),
46+
z.object({
47+
transport: z.literal("sse"),
48+
url: z.string(),
49+
headers: MCPHeadersSchema.optional(),
50+
disabled: z.boolean(),
51+
toolAllowlist: z.array(z.string()).optional(),
52+
}),
53+
z.object({
54+
transport: z.literal("auto"),
55+
url: z.string(),
56+
headers: MCPHeadersSchema.optional(),
57+
disabled: z.boolean(),
58+
toolAllowlist: z.array(z.string()).optional(),
59+
}),
60+
]);
61+
62+
export const MCPServerMapSchema = z.record(z.string(), MCPServerInfoSchema);
63+
64+
export const MCPAddParamsSchema = z
65+
.object({
66+
projectPath: z.string(),
67+
name: z.string(),
68+
69+
// Backward-compatible: if transport omitted, interpret as stdio.
70+
transport: MCPTransportSchema.optional(),
71+
72+
command: z.string().optional(),
73+
url: z.string().optional(),
74+
headers: MCPHeadersSchema.optional(),
75+
})
76+
.superRefine((input, ctx) => {
77+
const transport = input.transport ?? "stdio";
78+
79+
if (transport === "stdio") {
80+
if (!input.command?.trim()) {
81+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "command is required for stdio" });
82+
}
83+
return;
84+
}
85+
86+
if (!input.url?.trim()) {
87+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "url is required for http/sse/auto" });
88+
}
89+
});
3290

3391
export const MCPRemoveParamsSchema = z.object({
3492
projectPath: z.string(),
@@ -41,15 +99,6 @@ export const MCPSetEnabledParamsSchema = z.object({
4199
enabled: z.boolean(),
42100
});
43101

44-
export const MCPServerMapSchema = z.record(
45-
z.string(),
46-
z.object({
47-
command: z.string(),
48-
disabled: z.boolean(),
49-
toolAllowlist: z.array(z.string()).optional(),
50-
})
51-
);
52-
53102
export const MCPSetToolAllowlistParamsSchema = z.object({
54103
projectPath: z.string(),
55104
name: z.string(),
@@ -58,14 +107,46 @@ export const MCPSetToolAllowlistParamsSchema = z.object({
58107
});
59108

60109
/**
61-
* Unified test params - provide either name (to test configured server) or command (to test arbitrary command).
62-
* At least one of name or command must be provided.
110+
* Unified test params - provide either:
111+
* - name (to test a configured server), OR
112+
* - command (to test arbitrary stdio command), OR
113+
* - url+transport (to test arbitrary http/sse/auto endpoint)
63114
*/
64-
export const MCPTestParamsSchema = z.object({
65-
projectPath: z.string(),
66-
name: z.string().optional(),
67-
command: z.string().optional(),
68-
});
115+
export const MCPTestParamsSchema = z
116+
.object({
117+
projectPath: z.string(),
118+
name: z.string().optional(),
119+
120+
transport: MCPTransportSchema.optional(),
121+
command: z.string().optional(),
122+
url: z.string().optional(),
123+
headers: MCPHeadersSchema.optional(),
124+
})
125+
.superRefine((input, ctx) => {
126+
if (input.name?.trim()) {
127+
return;
128+
}
129+
130+
if (input.command?.trim()) {
131+
return;
132+
}
133+
134+
if (input.url?.trim()) {
135+
const transport = input.transport;
136+
if (transport !== "http" && transport !== "sse" && transport !== "auto") {
137+
ctx.addIssue({
138+
code: z.ZodIssueCode.custom,
139+
message: "transport must be http|sse|auto when testing by url",
140+
});
141+
}
142+
return;
143+
}
144+
145+
ctx.addIssue({
146+
code: z.ZodIssueCode.custom,
147+
message: "Either name, command, or url is required",
148+
});
149+
});
69150

70151
export const MCPTestResultSchema = z.discriminatedUnion("success", [
71152
z.object({ success: z.literal(true), tools: z.array(z.string()) }),

src/common/orpc/schemas/telemetry.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,70 @@ const MessageSentPropertiesSchema = z.object({
7171
thinkingLevel: TelemetryThinkingLevelSchema,
7272
});
7373

74+
// MCP transport mode enum (matches payload.ts TelemetryMCPTransportMode)
75+
const TelemetryMCPTransportModeSchema = z.enum([
76+
"none",
77+
"stdio_only",
78+
"http_only",
79+
"sse_only",
80+
"mixed",
81+
]);
82+
83+
const MCPContextInjectedPropertiesSchema = z.object({
84+
workspaceId: z.string(),
85+
model: z.string(),
86+
mode: z.string(),
87+
runtimeType: TelemetryRuntimeTypeSchema,
88+
89+
mcp_server_enabled_count: z.number(),
90+
mcp_server_started_count: z.number(),
91+
mcp_server_failed_count: z.number(),
92+
93+
mcp_tool_count: z.number(),
94+
total_tool_count: z.number(),
95+
builtin_tool_count: z.number(),
96+
97+
mcp_transport_mode: TelemetryMCPTransportModeSchema,
98+
mcp_has_http: z.boolean(),
99+
mcp_has_sse: z.boolean(),
100+
mcp_has_stdio: z.boolean(),
101+
mcp_auto_fallback_count: z.number(),
102+
mcp_setup_duration_ms_b2: z.number(),
103+
});
104+
105+
const TelemetryMCPServerTransportSchema = z.enum(["stdio", "http", "sse", "auto"]);
106+
const TelemetryMCPTestErrorCategorySchema = z.enum([
107+
"timeout",
108+
"connect",
109+
"http_status",
110+
"unknown",
111+
]);
112+
113+
const MCPServerTestedPropertiesSchema = z.object({
114+
transport: TelemetryMCPServerTransportSchema,
115+
success: z.boolean(),
116+
duration_ms_b2: z.number(),
117+
error_category: TelemetryMCPTestErrorCategorySchema.optional(),
118+
});
119+
120+
const TelemetryMCPServerConfigActionSchema = z.enum([
121+
"add",
122+
"edit",
123+
"remove",
124+
"enable",
125+
"disable",
126+
"set_tool_allowlist",
127+
"set_headers",
128+
]);
129+
130+
const MCPServerConfigChangedPropertiesSchema = z.object({
131+
action: TelemetryMCPServerConfigActionSchema,
132+
transport: TelemetryMCPServerTransportSchema,
133+
has_headers: z.boolean(),
134+
uses_secret_headers: z.boolean(),
135+
tool_allowlist_size_b2: z.number().optional(),
136+
});
137+
74138
const StreamCompletedPropertiesSchema = z.object({
75139
model: z.string(),
76140
wasInterrupted: z.boolean(),
@@ -125,6 +189,18 @@ export const TelemetryEventSchema = z.discriminatedUnion("event", [
125189
event: z.literal("workspace_switched"),
126190
properties: WorkspaceSwitchedPropertiesSchema,
127191
}),
192+
z.object({
193+
event: z.literal("mcp_context_injected"),
194+
properties: MCPContextInjectedPropertiesSchema,
195+
}),
196+
z.object({
197+
event: z.literal("mcp_server_tested"),
198+
properties: MCPServerTestedPropertiesSchema,
199+
}),
200+
z.object({
201+
event: z.literal("mcp_server_config_changed"),
202+
properties: MCPServerConfigChangedPropertiesSchema,
203+
}),
128204
z.object({
129205
event: z.literal("message_sent"),
130206
properties: MessageSentPropertiesSchema,

src/common/telemetry/payload.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,79 @@ export interface MessageSentPayload {
108108
thinkingLevel: TelemetryThinkingLevel;
109109
}
110110

111+
/**
112+
* MCP usage events
113+
*/
114+
export type TelemetryMCPTransportMode = "none" | "stdio_only" | "http_only" | "sse_only" | "mixed";
115+
116+
export interface MCPContextInjectedPayload {
117+
/** Workspace ID (randomly generated, safe to send) */
118+
workspaceId: string;
119+
/** Full model identifier */
120+
model: string;
121+
/** UI mode */
122+
mode: string;
123+
/** Runtime type for the workspace */
124+
runtimeType: TelemetryRuntimeType;
125+
126+
/** How many servers are enabled for this workspace message */
127+
mcp_server_enabled_count: number;
128+
/** How many servers successfully started (client created + tools fetched) */
129+
mcp_server_started_count: number;
130+
/** How many enabled servers failed to start */
131+
mcp_server_failed_count: number;
132+
133+
/** MCP tools injected into the model request */
134+
mcp_tool_count: number;
135+
/** Total tools injected into the model request (built-in + MCP) */
136+
total_tool_count: number;
137+
/** Built-in tool count injected into the model request */
138+
builtin_tool_count: number;
139+
140+
/** Effective transport mix for *started* servers (auto transport is resolved to http/sse) */
141+
mcp_transport_mode: TelemetryMCPTransportMode;
142+
/** Whether any started server uses HTTP (auto transport resolves to http/sse at runtime) */
143+
mcp_has_http: boolean;
144+
/** Whether any started server uses legacy SSE */
145+
mcp_has_sse: boolean;
146+
/** Whether any started server uses stdio */
147+
mcp_has_stdio: boolean;
148+
149+
/** Number of servers that required auto-fallback from HTTP to SSE */
150+
mcp_auto_fallback_count: number;
151+
152+
/** Time spent preparing MCP servers/tools (ms, rounded to nearest power of 2) */
153+
mcp_setup_duration_ms_b2: number;
154+
}
155+
156+
export type TelemetryMCPServerTransport = "stdio" | "http" | "sse" | "auto";
157+
export type TelemetryMCPTestErrorCategory = "timeout" | "connect" | "http_status" | "unknown";
158+
159+
export interface MCPServerTestedPayload {
160+
transport: TelemetryMCPServerTransport;
161+
success: boolean;
162+
duration_ms_b2: number;
163+
/** Error category when success=false (no raw error messages for privacy) */
164+
error_category?: TelemetryMCPTestErrorCategory;
165+
}
166+
167+
export type TelemetryMCPServerConfigAction =
168+
| "add"
169+
| "edit"
170+
| "remove"
171+
| "enable"
172+
| "disable"
173+
| "set_tool_allowlist"
174+
| "set_headers";
175+
176+
export interface MCPServerConfigChangedPayload {
177+
action: TelemetryMCPServerConfigAction;
178+
transport: TelemetryMCPServerTransport;
179+
has_headers: boolean;
180+
uses_secret_headers: boolean;
181+
/** Only set when action=set_tool_allowlist */
182+
tool_allowlist_size_b2?: number;
183+
}
111184
/**
112185
* Stream completion event - tracks when AI responses finish
113186
*/
@@ -226,6 +299,9 @@ export type TelemetryEventPayload =
226299
| { event: "workspace_created"; properties: WorkspaceCreatedPayload }
227300
| { event: "workspace_switched"; properties: WorkspaceSwitchedPayload }
228301
| { event: "message_sent"; properties: MessageSentPayload }
302+
| { event: "mcp_context_injected"; properties: MCPContextInjectedPayload }
303+
| { event: "mcp_server_tested"; properties: MCPServerTestedPayload }
304+
| { event: "mcp_server_config_changed"; properties: MCPServerConfigChangedPayload }
229305
| { event: "stream_completed"; properties: StreamCompletedPayload }
230306
| { event: "compaction_completed"; properties: CompactionCompletedPayload }
231307
| { event: "provider_configured"; properties: ProviderConfiguredPayload }

src/common/types/mcp.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
/** Normalized server info (always has disabled field) */
2-
export interface MCPServerInfo {
3-
command: string;
1+
/** Supported MCP server transports. */
2+
export type MCPServerTransport = "stdio" | "http" | "sse" | "auto";
3+
4+
export type MCPHeaderValue = string | { secret: string };
5+
6+
export interface MCPServerBaseInfo {
7+
transport: MCPServerTransport;
48
disabled: boolean;
59
/**
610
* Optional tool allowlist at project level.
@@ -10,12 +14,32 @@ export interface MCPServerInfo {
1014
toolAllowlist?: string[];
1115
}
1216

17+
/** stdio server definition (local process). */
18+
export interface MCPStdioServerInfo extends MCPServerBaseInfo {
19+
transport: "stdio";
20+
command: string;
21+
}
22+
23+
/** HTTP-based server definition. */
24+
export interface MCPHttpServerInfo extends MCPServerBaseInfo {
25+
transport: "http" | "sse" | "auto";
26+
url: string;
27+
/** Optional headers (string literal or reference to a project secret key). */
28+
headers?: Record<string, MCPHeaderValue>;
29+
}
30+
31+
/** Normalized server info (always has disabled field). */
32+
export type MCPServerInfo = MCPStdioServerInfo | MCPHttpServerInfo;
33+
1334
export interface MCPConfig {
1435
servers: Record<string, MCPServerInfo>;
1536
}
1637

17-
/** Internal map of server name → command string (used after filtering disabled) */
18-
export type MCPServerMap = Record<string, string>;
38+
/**
39+
* Internal map of server name → server info (used after filtering disabled).
40+
* Values are not shown to the model; only server names are exposed.
41+
*/
42+
export type MCPServerMap = Record<string, MCPServerInfo>;
1943

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

0 commit comments

Comments
 (0)