import { afterEach, beforeEach, describe, expect, it } from "vitest";
import "./setup.js";
import {
createServer,
waitForServerReady,
createConfiguredServer,
startHttpServer
} from "../src/mcp/server.js";
import { startHttpBridge } from "../src/server/httpBridge.js";
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { loadRuntimeConfig, type HttpTransportConfig, type RuntimeConfig } from "../src/config/runtime.js";
import { fromEnv, toSessionConfig } from "../src/shared/config/schema.js";
import { createLogger } from "../src/shared/logging.js";
import { PROJECT_NAME } from "../src/config/constants.js";
import { PACKAGE_VERSION } from "../src/shared/version.js";
const ORIGINAL_ENV = { ...process.env };
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
process.env.CLICKUP_TOKEN = process.env.CLICKUP_TOKEN ?? "test-token";
process.env.CLICKUP_DEFAULT_TEAM_ID = process.env.CLICKUP_DEFAULT_TEAM_ID ?? "1";
process.env.MCP_TRANSPORT = "http";
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
describe("http bridge routing", () => {
it("serves streamable http GET requests", async () => {
const server = await createServer();
const http = await startHttpBridge(server, { port: 0 });
try {
await waitForServerReady(server);
const baseUrl = `http://127.0.0.1:${http.port}/mcp`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream"
} satisfies Record<string, string>;
const initialize = await fetch(baseUrl, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: "init",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "routing-test", version: "1.0.0" }
}
})
});
expect(initialize.status).toBe(200);
await initialize.text();
const stream = await fetch(baseUrl, {
method: "GET",
headers: { Accept: "text/event-stream" }
});
expect(stream.status).toBe(200);
expect(stream.headers.get("content-type") ?? "").toContain("text/event-stream");
await stream.body?.cancel();
} finally {
await http.close();
await server.close();
}
});
it("accepts post requests on the root path", async () => {
const server = await createServer();
const http = await startHttpBridge(server, { port: 0 });
try {
await waitForServerReady(server);
const baseUrl = `http://127.0.0.1:${http.port}/`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream"
} satisfies Record<string, string>;
const response = await fetch(baseUrl, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: "list",
method: "tools/list"
})
});
expect(response.status).toBe(200);
const payload = await response.json();
expect(Array.isArray(payload.result?.tools)).toBe(true);
} finally {
await http.close();
await server.close();
}
});
it("returns successful response when initialize is called without credentials", async () => {
delete process.env.CLICKUP_TOKEN;
delete process.env.CLICKUP_DEFAULT_TEAM_ID;
const server = await createServer();
const http = await startHttpBridge(server, { port: 0 });
try {
await waitForServerReady(server);
const baseUrl = `http://127.0.0.1:${http.port}/mcp`;
const headers = {
"Content-Type": "application/json",
Accept: "application/json"
} satisfies Record<string, string>;
const response = await fetch(baseUrl, {
method: "POST",
headers,
body: JSON.stringify({
jsonrpc: "2.0",
id: "init",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "routing-test", version: "1.0.0" }
}
})
});
expect(response.status).toBe(200);
const payload = await response.json();
expect(payload.error).toBeDefined();
expect(payload.error.code).toBe(ErrorCode.InvalidParams);
expect(payload.error.message).toContain("Missing configuration");
expect(payload.id).toBe("init");
} finally {
await http.close();
await server.close();
}
});
describe("primary http host", () => {
async function closeHttpServer(server: import("node:http").Server): Promise<void> {
await new Promise<void>((resolve, reject) => {
server.close(error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
function createNotifyingServer(server: Awaited<ReturnType<typeof createServer>>) {
const notifying = server as unknown as {
notify?: (method: string, params: unknown) => Promise<void>;
notification: (payload: { method: string; params: Record<string, unknown> }) => Promise<void>;
};
notifying.notify = async (method: string, params: unknown) => {
const normalized = method.startsWith("notifications/") ? method : `notifications/${method}`;
await notifying.notification({
method: normalized,
params: (params ?? {}) as Record<string, unknown>
});
};
return notifying as typeof server & { notify: (method: string, params: unknown) => Promise<void> };
}
async function startPrimaryHttpHost(): Promise<{
baseUrl: string;
close: () => Promise<void>;
server: Awaited<ReturnType<typeof createServer>>;
}> {
const runtime = loadRuntimeConfig();
const transportConfig: HttpTransportConfig = {
kind: "http",
host: "127.0.0.1",
port: 0,
corsAllowOrigin: "*",
corsAllowHeaders: "Content-Type, MCP-Session-Id, MCP-Protocol-Version",
corsAllowMethods: "GET,POST,DELETE,OPTIONS",
enableJsonResponse: true,
allowedHosts: undefined,
allowedOrigins: undefined,
enableDnsRebindingProtection: false,
initializeTimeoutMs: runtime.httpInitializeTimeoutMs
} satisfies HttpTransportConfig;
const httpRuntime: RuntimeConfig = { ...runtime, transport: transportConfig };
const envConfig = fromEnv();
const session = toSessionConfig(envConfig);
const { server, tools } = await createConfiguredServer(httpRuntime, session);
await waitForServerReady(server);
const notifyingServer = createNotifyingServer(server);
const logger = createLogger("test.http");
const httpServer = await startHttpServer(server, notifyingServer, tools, transportConfig, logger);
const address = httpServer.address();
if (!address || typeof address === "string") {
throw new Error("HTTP server did not expose address information");
}
const baseUrl = `http://127.0.0.1:${address.port}`;
return {
baseUrl,
server,
close: async () => {
await closeHttpServer(httpServer);
await server.close();
}
};
}
it("serves discovery and health endpoints", async () => {
const host = await startPrimaryHttpHost();
try {
const schemaResponse = await fetch(`${host.baseUrl}/.well-known/mcp-config`);
expect(schemaResponse.status).toBe(200);
expect(schemaResponse.headers.get("content-type")).toContain("application/schema+json");
const schema = await schemaResponse.json();
expect(schema.$id).toContain("/.well-known/mcp-config");
expect(schema.title).toBe("MCP Session Configuration");
const healthResponse = await fetch(`${host.baseUrl}/healthz`);
expect(healthResponse.status).toBe(200);
const health = await healthResponse.json();
expect(health).toMatchObject({
ok: true,
transport: "http",
name: PROJECT_NAME,
version: PACKAGE_VERSION
});
expect(Array.isArray(health.tools)).toBe(true);
expect(health.tools.length).toBeGreaterThan(0);
const missingResponse = await fetch(`${host.baseUrl}/unknown`);
expect(missingResponse.status).toBe(404);
const missing = await missingResponse.json();
expect(missing.error).toBe("Not Found");
} finally {
await host.close();
}
});
});
});