Skip to content

Commit a6d45ea

Browse files
fix(local-mcp): match the sandbox's null-id notification check, reuse sanitizeHeaders
The desktop relay executor only treated payload.id === undefined as a fire-and-forget notification, but the sandbox's McpRelayServer treats undefined or null as a notification. An id: null message ran as a real request against the user's local server and its response was rejected as an orphaned mcp_response. Aligned both checks. Also dropped sanitizeStringRecord, a byte-for-byte copy of mcp-config.ts's sanitizeHeaders that this file already imports from — exported and reused it instead of maintaining two copies of the same header/env sanitizer.
1 parent c90fcca commit a6d45ea

3 files changed

Lines changed: 24 additions & 15 deletions

File tree

packages/agent/src/adapters/claude/session/mcp-config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ export function loadUserClaudeJsonMcpServers(
8585
return servers;
8686
}
8787

88-
function sanitizeHeaders(headers: unknown): Record<string, string> | undefined {
88+
export function sanitizeHeaders(
89+
headers: unknown,
90+
): Record<string, string> | undefined {
8991
if (!headers || typeof headers !== "object") return undefined;
9092
const entries = Object.entries(headers as Record<string, unknown>).filter(
9193
(entry): entry is [string, string] => typeof entry[1] === "string",

packages/workspace-server/src/services/mcp-relay/mcp-relay.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ describe("McpRelayServiceImpl", () => {
181181
]);
182182
});
183183

184+
it("treats an explicit null id the same as a missing id (fire-and-forget)", async () => {
185+
const service = makeService("srv");
186+
187+
const execution = await service.execute("run-1", "srv", {
188+
jsonrpc: "2.0",
189+
id: null,
190+
method: "notifications/initialized",
191+
});
192+
193+
expect(execution).toEqual({});
194+
expect(service.transports[0].sent).toEqual([
195+
{ jsonrpc: "2.0", id: null, method: "notifications/initialized" },
196+
]);
197+
});
198+
184199
it("replaces a response over 256 KB with a -32003 error", async () => {
185200
const service = makeService("srv");
186201

packages/workspace-server/src/services/mcp-relay/mcp-relay.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
99
import {
1010
type ClaudeJsonMcpServerEntry,
1111
loadUserClaudeJsonMcpServerEntries,
12+
sanitizeHeaders,
1213
} from "@posthog/agent/adapters/claude/session/mcp-config";
1314
import {
1415
ROOT_LOGGER,
@@ -39,16 +40,6 @@ function errorMessage(err: unknown): string {
3940
return err instanceof Error ? err.message : String(err);
4041
}
4142

42-
function sanitizeStringRecord(
43-
value: unknown,
44-
): Record<string, string> | undefined {
45-
if (!value || typeof value !== "object") return undefined;
46-
const entries = Object.entries(value as Record<string, unknown>).filter(
47-
(entry): entry is [string, string] => typeof entry[1] === "string",
48-
);
49-
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
50-
}
51-
5243
/**
5344
* Desktop-side executor for the cloud MCP relay (docs/cloud-mcp-relay.md).
5445
*
@@ -120,8 +111,9 @@ export class McpRelayServiceImpl implements McpRelayService {
120111
return { error: { code: -32000, message: errorMessage(err) } };
121112
}
122113

123-
if (payload.id === undefined) {
114+
if (payload.id === undefined || payload.id === null) {
124115
// JSON-RPC notification: fire-and-forget, no response correlation.
116+
// Matches the sandbox's own notification check (mcp-relay-server.ts).
125117
try {
126118
await connection.transport.send(payload as JSONRPCMessage);
127119
} catch (err) {
@@ -161,7 +153,7 @@ export class McpRelayServiceImpl implements McpRelayService {
161153
const type = typeof raw.type === "string" ? raw.type : undefined;
162154
const url = typeof raw.url === "string" ? raw.url : undefined;
163155
const command = typeof raw.command === "string" ? raw.command : undefined;
164-
const headers = sanitizeStringRecord(raw.headers);
156+
const headers = sanitizeHeaders(raw.headers);
165157

166158
if ((type === undefined || type === "stdio") && command) {
167159
const args = Array.isArray(raw.args)
@@ -170,7 +162,7 @@ export class McpRelayServiceImpl implements McpRelayService {
170162
return new StdioClientTransport({
171163
command,
172164
args,
173-
env: { ...getDefaultEnvironment(), ...sanitizeStringRecord(raw.env) },
165+
env: { ...getDefaultEnvironment(), ...sanitizeHeaders(raw.env) },
174166
});
175167
}
176168
if (type === "sse" && url) {
@@ -293,7 +285,7 @@ export class McpRelayServiceImpl implements McpRelayService {
293285
pending.resolve({
294286
error: {
295287
code: -32003,
296-
message: "Relayed MCP response exceeds 256 KB",
288+
message: `Relayed MCP response exceeds ${MAX_RESPONSE_BYTES / 1000} KB`,
297289
},
298290
});
299291
return;

0 commit comments

Comments
 (0)