diff --git a/src/mcp.ts b/src/mcp.ts index bfba69e..d3dfae7 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,9 +1,11 @@ +import { randomUUID } from "node:crypto"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { loadConfig, saveConfig, isCacheValid, TOOLS_CACHE_VERSION, type ToolDef } from "./config.js"; import { VERSION } from "./version.js"; const MCP_URL = "https://mcp2.readwise.io/mcp"; +export const RUN_ID = randomUUID(); // Bound every MCP round-trip so a stalled connection (flaky DNS/IPv6, a proxy, // a half-open Cloudflare socket) surfaces as an error instead of hanging the @@ -12,13 +14,25 @@ const MCP_URL = "https://mcp2.readwise.io/mcp"; // replies. const MCP_TIMEOUT_MS = 30_000; +export function userAgent(): string { + return `readwise-cli/${VERSION} node/${process.versions.node} ${process.platform}/${process.arch}`; +} + +export function mcpRequestHeaders(authHeader: string, runId = RUN_ID): Record { + return { + Authorization: authHeader, + "User-Agent": userAgent(), + "X-Readwise-CLI-Version": VERSION, + "X-Readwise-CLI-Run-ID": runId, + "X-Correlation-ID": runId, + }; +} + function createTransport(token: string, authType: "oauth" | "token"): StreamableHTTPClientTransport { const authHeader = authType === "token" ? `Token ${token}` : `Bearer ${token}`; return new StreamableHTTPClientTransport(new URL(MCP_URL), { requestInit: { - headers: { - Authorization: authHeader, - }, + headers: mcpRequestHeaders(authHeader), }, }); } diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts new file mode 100644 index 0000000..9d005d4 --- /dev/null +++ b/tests/mcp.test.ts @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { mcpRequestHeaders, RUN_ID, userAgent } from "../src/mcp.js"; +import { VERSION } from "../src/version.js"; + +test("MCP headers include CLI version, run id, correlation id, and user agent", () => { + const headers = mcpRequestHeaders("Bearer test-token", "run-123"); + + assert.equal(headers.Authorization, "Bearer test-token"); + assert.equal(headers["X-Readwise-CLI-Version"], VERSION); + assert.equal(headers["X-Readwise-CLI-Run-ID"], "run-123"); + assert.equal(headers["X-Correlation-ID"], "run-123"); + assert.equal(headers["User-Agent"], userAgent()); +}); + +test("RUN_ID is generated once for the CLI process", () => { + assert.match(RUN_ID, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); + assert.equal(mcpRequestHeaders("Token test-token")["X-Readwise-CLI-Run-ID"], RUN_ID); +}); + +test("user agent identifies readwise-cli, Node, platform, and architecture", () => { + assert.equal(userAgent(), `readwise-cli/${VERSION} node/${process.versions.node} ${process.platform}/${process.arch}`); +});