Allow SurfMind Chrome extension MCP origin (#513)

This commit is contained in:
Ben Senescu 2026-08-21 15:41:44 -04:00 committed by Ben Senescu
parent 47883b5d5a
commit 6470b875ad
3 changed files with 65 additions and 2 deletions

View File

@ -9,13 +9,18 @@ const ctx: ExecutionContext = {
props: {}, props: {},
}; };
function request(method: string, body?: unknown) { function request(
method: string,
body?: unknown,
headers?: Record<string, string>,
) {
return new Request("https://open-seo.test/mcp", { return new Request("https://open-seo.test/mcp", {
method, method,
headers: { headers: {
Host: "open-seo.test", Host: "open-seo.test",
Accept: "application/json, text/event-stream", Accept: "application/json, text/event-stream",
"Content-Type": "application/json", "Content-Type": "application/json",
...headers,
}, },
body: body === undefined ? undefined : JSON.stringify(body), body: body === undefined ? undefined : JSON.stringify(body),
}); });
@ -97,4 +102,38 @@ describe("Agents SDK v2 MCP transport", () => {
expect(responseText).toContain('\\"scopes\\":[\\"mcp\\"]'); expect(responseText).toContain('\\"scopes\\":[\\"mcp\\"]');
expect(responseText).toContain('\\"organizationId\\":\\"org-1\\"'); expect(responseText).toContain('\\"organizationId\\":\\"org-1\\"');
}); });
it("accepts the SurfMind extension origin and rejects other browser origins", async () => {
const handler = createMcpHandler(
() => new McpServer({ name: "test", version: "1.0.0" }),
{
route: "/mcp",
allowedOriginHostnames: [
"open-seo.test",
"pghallcbnfabbgfijhbcldaapmgidnaa",
],
},
);
const body = {
jsonrpc: "2.0",
id: 1,
method: "tools/list",
};
const surfMindResponse = await handler(
request("POST", body, {
Origin: "chrome-extension://pghallcbnfabbgfijhbcldaapmgidnaa",
}),
{},
ctx,
);
const unrelatedOriginResponse = await handler(
request("POST", body, { Origin: "https://evil.com" }),
{},
ctx,
);
expect(surfMindResponse.status).toBe(200);
expect(unrelatedOriginResponse.status).toBe(403);
});
}); });

View File

@ -215,7 +215,10 @@ describe("handleAuthenticatedOpenSeoMcpRequest", () => {
expect(response.headers.get("connection")).not.toBe("keep-alive"); expect(response.headers.get("connection")).not.toBe("keep-alive");
expect(selfHostedAuthMocks.createMcpHandler).toHaveBeenCalledWith( expect(selfHostedAuthMocks.createMcpHandler).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
allowedOriginHostnames: ["open-seo.test"], allowedOriginHostnames: [
"open-seo.test",
"pghallcbnfabbgfijhbcldaapmgidnaa",
],
legacy: "reject", legacy: "reject",
}), }),
); );
@ -255,6 +258,24 @@ describe("handleAuthenticatedOpenSeoMcpRequest", () => {
expect(selfHostedAuthMocks.createOpenSeoMcpServer).not.toHaveBeenCalled(); expect(selfHostedAuthMocks.createOpenSeoMcpServer).not.toHaveBeenCalled();
}); });
it("accepts a legacy request from the SurfMind Chrome extension", async () => {
const props = hostedProps();
const response = await handleAuthenticatedOpenSeoMcpRequest(
createMcpRequest({
Origin: "chrome-extension://pghallcbnfabbgfijhbcldaapmgidnaa",
}),
props,
{},
{ ...ctx, props },
);
expect(response.status).toBe(200);
expect(selfHostedAuthMocks.createOpenSeoMcpServer).toHaveBeenCalledWith(
props,
);
});
it("rejects provider props missing the OAuth client identity", async () => { it("rejects provider props missing the OAuth client identity", async () => {
// Hosted tokens always carry clientId/scopes; a token without them must // Hosted tokens always carry clientId/scopes; a token without them must
// fail closed rather than skip scope enforcement. // fail closed rather than skip scope enforcement.

View File

@ -32,6 +32,8 @@ const MCP_CORS_HEADERS = {
"Access-Control-Max-Age": "86400", "Access-Control-Max-Age": "86400",
} as const; } as const;
const SURFMIND_CHROME_EXTENSION_HOSTNAME = "pghallcbnfabbgfijhbcldaapmgidnaa";
function withMcpCors(response: Response) { function withMcpCors(response: Response) {
const headers = new Headers(response.headers); const headers = new Headers(response.headers);
for (const [name, value] of Object.entries(MCP_CORS_HEADERS)) { for (const [name, value] of Object.entries(MCP_CORS_HEADERS)) {
@ -158,6 +160,7 @@ export async function handleAuthenticatedOpenSeoMcpRequest(
return createRequestHandler(result.data, [ return createRequestHandler(result.data, [
new URL(getHostedBaseUrl()).hostname, new URL(getHostedBaseUrl()).hostname,
SURFMIND_CHROME_EXTENSION_HOSTNAME,
])(request, env, ctx); ])(request, env, ctx);
} }