From 287c123c41e3fb9bf2a9c1db326a2917fa2af9b6 Mon Sep 17 00:00:00 2001 From: Leo McArdle Date: Mon, 5 Oct 2026 17:01:44 +0100 Subject: [PATCH 1/2] test: run tests with NODE_ENV=test Stops Express logging stack traces for errors the tests trigger on purpose. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cf18bba..d2f3a3e 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "dev:server": "nodemon index.js", "dev:inspector": "mcp-inspector --config mcp.json", "tsc": "tsc", - "test": "node --test --experimental-test-coverage --test-coverage-lines 100 --test-coverage-exclude 'test/**/*' 'test/**/*.test.js'", - "test:watch": "node --test --watch 'test/**/*.test.js'", + "test": "NODE_ENV=test node --test --experimental-test-coverage --test-coverage-lines 100 --test-coverage-exclude 'test/**/*' 'test/**/*.test.js'", + "test:watch": "NODE_ENV=test node --test --watch 'test/**/*.test.js'", "glean:generate": "glean translate glean/metrics.yaml -f javascript -o glean/generated/ && glean translate glean/metrics.yaml -f typescript -o glean/generated/ && sed -i 's/ DO NOT COMMIT\\.//g' glean/generated/*" }, "packageManager": "npm@11.16.0", From 24f006af5d9b7e598e0f29b8b1666f6f914e2ec9 Mon Sep 17 00:00:00 2001 From: Leo McArdle Date: Mon, 5 Oct 2026 14:31:32 +0100 Subject: [PATCH 2/2] fix(server): allow direct browser MCP connections with CORS --- README.md | 2 ++ index.js | 14 ++++++++++++++ test/server.test.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/README.md b/README.md index 414be92..5440016 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ Add the remote server to your tool of choice. For example, in Claude Code: claude mcp add --transport http mdn https://mcp.mdn.mozilla.net/ ``` +Browser clients can connect directly using Streamable HTTP, without a CORS proxy. + ## Using locally - Install dependencies: `npm install` diff --git a/index.js b/index.js index c3fd340..a0382d3 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,20 @@ import express from "express"; import handleRequest from "./transport.js"; const app = express(); + +app.all(["/", "/mcp"], (req, res, next) => { + res.set("Access-Control-Allow-Origin", "*"); + if (req.method === "OPTIONS") { + res.set( + "Access-Control-Allow-Headers", + "Content-Type, MCP-Protocol-Version, X-Moz-1st-Party-Data-Opt-Out", + ); + res.sendStatus(204); + return; + } + next(); +}); + app.use(express.json()); app.post(["/", "/mcp"], handleRequest); diff --git a/test/server.test.js b/test/server.test.js index 146413b..cd586e5 100644 --- a/test/server.test.js +++ b/test/server.test.js @@ -55,10 +55,55 @@ describe("server", () => { assert.equal(res.status, 405); assert.equal(res.headers.get("allow"), "POST"); assert.equal(res.headers.get("location"), null); + assert.equal(res.headers.get("access-control-allow-origin"), "*"); } } }); + it("should allow browser MCP preflights", async () => { + const res = await fetch(`http://localhost:${server.port}/`, { + method: "OPTIONS", + headers: { + Origin: "https://example.com", + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": + "content-type,mcp-protocol-version,x-moz-1st-party-data-opt-out", + }, + }); + assert.equal(res.status, 204); + assert.equal(res.headers.get("access-control-allow-origin"), "*"); + assert.equal( + res.headers.get("access-control-allow-headers"), + "Content-Type, MCP-Protocol-Version, X-Moz-1st-Party-Data-Opt-Out", + ); + }); + + it("should allow browsers to read MCP responses", async () => { + const res = await fetch(`http://localhost:${server.port}/`, { + method: "POST", + headers: { + Origin: "https://example.com", + "Content-Type": "application/json", + Accept: "application/json, text/event-stream", + "MCP-Protocol-Version": "2025-11-25", + }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "ping" }), + }); + assert.equal(res.status, 200); + assert.equal(res.headers.get("access-control-allow-origin"), "*"); + assert.deepEqual(await res.json(), { jsonrpc: "2.0", id: 1, result: {} }); + }); + + it("should include CORS headers on JSON parsing errors", async () => { + const res = await fetch(`http://localhost:${server.port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{", + }); + assert.equal(res.status, 400); + assert.equal(res.headers.get("access-control-allow-origin"), "*"); + }); + after(() => { server.listener.close(); });