import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import createServer from "../src/server/smithery.js";
import { getServerContext } from "../src/mcp/server.js";
const ORIGINAL_ENV = { ...process.env };
async function invokeToolCall(server: Server, toolName: string, args: Record<string, unknown>): Promise<unknown> {
const handlers = (server as unknown as {
_requestHandlers?: Map<string, unknown>;
})._requestHandlers;
const handler = handlers?.get("tools/call");
if (typeof handler !== "function") {
throw new Error("CallTool handler not registered");
}
const request = CallToolRequestSchema.parse({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: {
name: toolName,
arguments: args
}
});
const abortController = new AbortController();
return (handler as (
request: typeof request,
extra: {
signal: AbortSignal;
requestId: typeof request.id;
sendNotification: (notification: unknown) => Promise<void>;
sendRequest: (request: unknown) => Promise<unknown>;
}
) => Promise<unknown>)(request, {
signal: abortController.signal,
requestId: request.id,
sendNotification: async () => {},
sendRequest: async () => {
throw new Error("sendRequest is not supported in tests");
}
});
}
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
delete process.env.CLICKUP_TOKEN;
delete process.env.CLICKUP_DEFAULT_TEAM_ID;
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
describe("config fallback behavior", () => {
it("uses full config from Smithery when all fields are provided", async () => {
const server = await createServer({
config: {
apiToken: "smithery-token",
defaultTeamId: 999,
baseUrl: "https://custom.api.com",
primaryLanguage: "en-US"
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("smithery-token");
expect(context.session.defaultTeamId).toBe(999);
expect(context.session.baseUrl).toContain("custom.api.com");
await server.close();
});
it("falls back to environment variables when Smithery config is partial", async () => {
process.env.CLICKUP_TOKEN = "env-token";
process.env.CLICKUP_DEFAULT_TEAM_ID = "777";
const server = await createServer({
config: {
primaryLanguage: "fr-FR"
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("env-token");
expect(context.session.defaultTeamId).toBe(777);
await server.close();
});
it("uses environment variables when no Smithery config is provided", async () => {
process.env.CLICKUP_TOKEN = "env-token-only";
process.env.CLICKUP_DEFAULT_TEAM_ID = "555";
const server = await createServer({});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("env-token-only");
expect(context.session.defaultTeamId).toBe(555);
await server.close();
});
it("uses defaults when neither config nor environment provide values", async () => {
const server = await createServer({});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("");
expect(context.session.defaultTeamId).toBeUndefined();
expect(context.session.baseUrl).toBe("https://api.clickup.com/api/v2");
await server.close();
});
it("prefers Smithery config over environment variables", async () => {
process.env.CLICKUP_TOKEN = "env-token";
process.env.CLICKUP_DEFAULT_TEAM_ID = "111";
const server = await createServer({
config: {
apiToken: "smithery-token",
defaultTeamId: 222
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("smithery-token");
expect(context.session.defaultTeamId).toBe(222);
await server.close();
});
it("server initializes successfully without any credentials", async () => {
const server = await createServer({});
const context = getServerContext(server);
expect(context.session).toBeDefined();
expect(context.tools.length).toBeGreaterThan(0);
await server.close();
});
it("server initializes successfully with only apiToken", async () => {
const server = await createServer({
config: {
apiToken: "partial-token"
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("partial-token");
expect(context.session.defaultTeamId).toBeUndefined();
await server.close();
});
it("server initializes successfully with only defaultTeamId", async () => {
const server = await createServer({
config: {
defaultTeamId: 888
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("");
expect(context.session.defaultTeamId).toBe(888);
await server.close();
});
it("tool calls requiring auth fail gracefully when credentials are missing", async () => {
const server = await createServer({});
// Try to call a tool that requires authentication
const response = await invokeToolCall(server, "clickup_list_spaces", { teamId: 123 });
expect(response).toHaveProperty("structuredContent");
const content = (response as { structuredContent: unknown }).structuredContent;
expect(content).toHaveProperty("isError", true);
expect(content).toHaveProperty("code", "INVALID_PARAMETER");
await server.close();
});
it("merges auth context with environment variables", async () => {
process.env.CLICKUP_DEFAULT_TEAM_ID = "333";
const server = await createServer({
auth: {
CLICKUP_TOKEN: "auth-token"
}
});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("auth-token");
expect(context.session.defaultTeamId).toBe(333);
await server.close();
});
it("handles invalid defaultTeamId in environment gracefully", async () => {
process.env.CLICKUP_TOKEN = "valid-token";
process.env.CLICKUP_DEFAULT_TEAM_ID = "not-a-number";
const server = await createServer({});
const context = getServerContext(server);
expect(context.session.apiToken).toBe("valid-token");
expect(context.session.defaultTeamId).toBeUndefined();
await server.close();
});
it("uses custom baseUrl from config over environment", async () => {
process.env.CLICKUP_BASE_URL = "https://env.api.com";
const server = await createServer({
config: {
apiToken: "token",
defaultTeamId: 100,
baseUrl: "https://config.api.com"
}
});
const context = getServerContext(server);
expect(context.session.baseUrl).toContain("config.api.com");
await server.close();
});
it("falls back to environment baseUrl when not in config", async () => {
process.env.CLICKUP_TOKEN = "token";
process.env.CLICKUP_BASE_URL = "https://env-only.api.com";
const server = await createServer({
config: {
defaultTeamId: 100
}
});
const context = getServerContext(server);
expect(context.session.baseUrl).toContain("env-only.api.com");
await server.close();
});
});