Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
14 changes: 14 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
Comment on lines +14 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Let's add cross-env as devDependency to support Windows!?

"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",
Expand Down
45 changes: 45 additions & 0 deletions test/server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down
Loading