Skip to content

Commit 5e9043d

Browse files
committed
Validate MCP app CSP metadata
Signed-off-by: Andrew Harvard <aharvard@squareup.com>
1 parent 0a91482 commit 5e9043d

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from "vitest";
2+
import { extractRenderableMcpAppDocument } from "../mcpAppPayload";
3+
import type { McpAppPayload } from "@/shared/types/messages";
4+
5+
function createPayload(csp: unknown): McpAppPayload {
6+
return {
7+
sessionId: "session-1",
8+
gooseSessionId: null,
9+
toolCallId: "tool-1",
10+
toolCallTitle: "inspect app",
11+
source: "toolCallUpdateMeta",
12+
tool: {
13+
name: "inspect-app",
14+
extensionName: "mcpappbench",
15+
resourceUri: "ui://inspect-app",
16+
},
17+
resource: {
18+
result: {
19+
contents: [
20+
{
21+
uri: "ui://inspect-app",
22+
mimeType: "text/html;profile=mcp-app",
23+
text: "<div>App</div>",
24+
_meta: {
25+
ui: {
26+
csp,
27+
},
28+
},
29+
},
30+
],
31+
},
32+
},
33+
};
34+
}
35+
36+
describe("extractRenderableMcpAppDocument", () => {
37+
it("normalizes MCP app CSP metadata to string arrays", () => {
38+
const document = extractRenderableMcpAppDocument(
39+
createPayload({
40+
connectDomains: "https://api.example.com",
41+
resourceDomains: ["https://cdn.example.com", 42],
42+
frameDomains: ["https://frame.example.com"],
43+
baseUriDomains: { origin: "https://base.example.com" },
44+
scriptDomains: ["https://scripts.example.com"],
45+
}),
46+
);
47+
48+
expect(document?.csp).toEqual({
49+
resourceDomains: ["https://cdn.example.com"],
50+
frameDomains: ["https://frame.example.com"],
51+
scriptDomains: ["https://scripts.example.com"],
52+
});
53+
});
54+
55+
it("drops malformed MCP app CSP metadata", () => {
56+
const document = extractRenderableMcpAppDocument(
57+
createPayload({
58+
connectDomains: "https://api.example.com",
59+
resourceDomains: [],
60+
frameDomains: { origin: "https://frame.example.com" },
61+
}),
62+
);
63+
64+
expect(document?.csp).toBeNull();
65+
});
66+
});

ui/goose2/src/features/chat/ui/mcpAppPayload.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ type TextContentWithMeta = GooseTextResourceContents & {
2626
};
2727

2828
const MCP_APP_RESOURCE_MIME_TYPE = UI_EXTENSION_CONFIG.mimeTypes[0];
29+
const MCP_APP_CSP_FIELDS = [
30+
"connectDomains",
31+
"resourceDomains",
32+
"frameDomains",
33+
"baseUriDomains",
34+
"scriptDomains",
35+
] satisfies readonly (keyof McpAppResourceCsp)[];
2936

3037
function isRecord(value: unknown): value is Record<string, unknown> {
3138
return typeof value === "object" && value !== null;
@@ -59,6 +66,33 @@ function getContentPriority(content: TextContentWithMeta): number {
5966
return 2;
6067
}
6168

69+
function normalizeCspDomains(value: unknown): string[] | undefined {
70+
if (!Array.isArray(value)) {
71+
return undefined;
72+
}
73+
74+
const domains = value.filter(
75+
(domain): domain is string => typeof domain === "string",
76+
);
77+
return domains.length > 0 ? domains : undefined;
78+
}
79+
80+
function normalizeResourceCsp(csp: unknown): McpAppResourceCsp | null {
81+
if (!isRecord(csp)) {
82+
return null;
83+
}
84+
85+
const normalized: McpAppResourceCsp = {};
86+
for (const field of MCP_APP_CSP_FIELDS) {
87+
const domains = normalizeCspDomains(csp[field]);
88+
if (domains) {
89+
normalized[field] = domains;
90+
}
91+
}
92+
93+
return Object.keys(normalized).length > 0 ? normalized : null;
94+
}
95+
6296
export function extractRenderableMcpAppDocument(
6397
payload: McpAppPayload,
6498
): RenderableMcpAppDocument | null {
@@ -84,7 +118,7 @@ export function extractRenderableMcpAppDocument(
84118
return {
85119
html: bestContent.text,
86120
resourceUri: bestContent.uri ?? payload.tool.resourceUri,
87-
csp: isRecord(csp) ? (csp as McpAppResourceCsp) : null,
121+
csp: normalizeResourceCsp(csp),
88122
prefersBorder: metadata?.prefersBorder ?? true,
89123
};
90124
}

0 commit comments

Comments
 (0)