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
15 changes: 15 additions & 0 deletions src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ const MCP_URL = "https://mcp2.readwise.io/mcp";
// replies.
const MCP_TIMEOUT_MS = 30_000;

export function createMcpFetch(baseFetch: typeof fetch = fetch): typeof fetch {
return (url, init) => {
const method = (init?.method ?? (url instanceof Request ? url.method : "GET")).toUpperCase();
if (method === "GET") {
// The CLI never consumes server-initiated MCP messages; opting out avoids
// Node 26/undici stalls when the SDK keeps this optional SSE stream open.
return Promise.resolve(new Response(null, { status: 405, statusText: "Method Not Allowed" }));
}
return baseFetch(url, init);
};
}

export const mcpFetch = createMcpFetch();

function createTransport(token: string, authType: "oauth" | "token"): StreamableHTTPClientTransport {
const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`;
return new StreamableHTTPClientTransport(new URL(MCP_URL), {
Expand All @@ -20,6 +34,7 @@ function createTransport(token: string, authType: "oauth" | "token"): Streamable
Authorization: authHeader,
},
},
fetch: mcpFetch,
});
}

Expand Down
46 changes: 46 additions & 0 deletions tests/mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createMcpFetch } from "../src/mcp.js";

test("mcp fetch opts out of the optional GET SSE stream", async () => {
let delegated = false;
const wrappedFetch = createMcpFetch(async () => {
delegated = true;
return new Response(null, { status: 500 });
});

const response = await wrappedFetch("https://mcp2.readwise.io/mcp", { method: "GET" });

assert.equal(response.status, 405);
assert.equal(response.statusText, "Method Not Allowed");
assert.equal(delegated, false);
});

test("mcp fetch delegates non-GET requests", async () => {
let seenUrl = "";
let seenMethod = "";
const wrappedFetch = createMcpFetch(async (url, init) => {
seenUrl = String(url);
seenMethod = init?.method ?? "";
return new Response("{}", { status: 200 });
});

const response = await wrappedFetch("https://mcp2.readwise.io/mcp", { method: "POST", body: "{}" });

assert.equal(response.status, 200);
assert.equal(seenUrl, "https://mcp2.readwise.io/mcp");
assert.equal(seenMethod, "POST");
});

test("mcp fetch reads the method from Request inputs", async () => {
let delegated = false;
const wrappedFetch = createMcpFetch(async () => {
delegated = true;
return new Response("{}", { status: 200 });
});

const response = await wrappedFetch(new Request("https://mcp2.readwise.io/mcp", { method: "POST" }));

assert.equal(response.status, 200);
assert.equal(delegated, true);
});
Loading