288 lines
8.4 KiB
TypeScript
288 lines
8.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
import { handleMcpRequest } from "../src/protocol/handler";
|
|
import { tools } from "../src/protocol/tools";
|
|
|
|
const originalFetch = global.fetch;
|
|
|
|
describe("MCP protocol cookie inputs", () => {
|
|
beforeEach(() => {
|
|
global.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify([]), { status: 200 })),
|
|
) as unknown as typeof fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
test("search tools should not expose cookie inputs", () => {
|
|
const toolNames = ["search_kijiji", "search_facebook", "search_ebay"];
|
|
for (const toolName of toolNames) {
|
|
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
expect(tool?.inputSchema.properties).not.toHaveProperty("cookies");
|
|
expect(tool?.inputSchema.properties).not.toHaveProperty("cookiesSource");
|
|
}
|
|
});
|
|
|
|
test("search_facebook should not forward cookies query parameters", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_facebook",
|
|
arguments: {
|
|
query: "laptop",
|
|
cookiesSource: "c_user=1",
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("/facebook?q=laptop");
|
|
expect(String(calledUrl)).not.toContain("cookies=");
|
|
});
|
|
|
|
test("search_kijiji should not forward cookies query parameters", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_kijiji",
|
|
arguments: {
|
|
query: "laptop",
|
|
cookies: "s=1",
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("/kijiji?q=laptop");
|
|
expect(String(calledUrl)).not.toContain("cookies=");
|
|
});
|
|
});
|
|
|
|
describe("MCP protocol unstableFilter", () => {
|
|
beforeEach(() => {
|
|
global.fetch = mock(() =>
|
|
Promise.resolve(new Response(JSON.stringify([]), { status: 200 })),
|
|
) as unknown as typeof fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
test("all search tools should document the unstableFilter property", () => {
|
|
const toolNames = ["search_kijiji", "search_facebook", "search_ebay"];
|
|
for (const toolName of toolNames) {
|
|
const tool = tools.find((t) => t.name === toolName);
|
|
expect(tool).toBeDefined();
|
|
expect(tool?.inputSchema.properties).toHaveProperty("unstableFilter");
|
|
const prop = tool?.inputSchema.properties.unstableFilter as {
|
|
type: string;
|
|
description: string;
|
|
};
|
|
expect(prop.type).toBe("boolean");
|
|
expect(prop.description).toContain("optional");
|
|
expect(prop.description).toContain("20%");
|
|
expect(prop.description).toContain("median");
|
|
expect(prop.description).toContain("unstableResults");
|
|
}
|
|
});
|
|
|
|
test("handler should forward unstableFilter=true for search_kijiji", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_kijiji",
|
|
arguments: {
|
|
query: "laptop",
|
|
unstableFilter: true,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("unstableFilter=true");
|
|
});
|
|
|
|
test("search_kijiji should document price filters as dollars", () => {
|
|
const tool = tools.find((candidate) => candidate.name === "search_kijiji");
|
|
|
|
const priceMin = tool?.inputSchema.properties.priceMin as {
|
|
description: string;
|
|
};
|
|
const priceMax = tool?.inputSchema.properties.priceMax as {
|
|
description: string;
|
|
};
|
|
|
|
expect(priceMin.description).toContain("dollars");
|
|
expect(priceMax.description).toContain("dollars");
|
|
});
|
|
|
|
test("handler should forward Kijiji dollar price filters to API", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_kijiji",
|
|
arguments: {
|
|
query: "macbook",
|
|
priceMin: 999.99,
|
|
priceMax: 1000,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("priceMin=999.99");
|
|
expect(String(calledUrl)).toContain("priceMax=1000");
|
|
});
|
|
|
|
test("handler should forward unstableFilter=true for search_facebook", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_facebook",
|
|
arguments: {
|
|
query: "laptop",
|
|
unstableFilter: true,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("unstableFilter=true");
|
|
});
|
|
|
|
test("tools/call returns API JSON as text content", async () => {
|
|
global.fetch = mock(() =>
|
|
Promise.resolve(
|
|
new Response(JSON.stringify([{ title: "item" }]), { status: 200 }),
|
|
),
|
|
) as unknown as typeof fetch;
|
|
|
|
const response = await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_facebook",
|
|
arguments: { query: "laptop" },
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const body = await response.json();
|
|
expect(body.result.content[0].type).toBe("text");
|
|
expect(JSON.parse(body.result.content[0].text)).toEqual([
|
|
{ title: "item" },
|
|
]);
|
|
});
|
|
|
|
test("handler should forward unstableFilter=true for search_ebay", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_ebay",
|
|
arguments: {
|
|
query: "laptop",
|
|
unstableFilter: true,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("unstableFilter=true");
|
|
});
|
|
|
|
test("search_ebay should document price filters as dollars", () => {
|
|
const tool = tools.find((candidate) => candidate.name === "search_ebay");
|
|
|
|
const minPrice = tool?.inputSchema.properties.minPrice as {
|
|
description: string;
|
|
};
|
|
const maxPrice = tool?.inputSchema.properties.maxPrice as {
|
|
description: string;
|
|
};
|
|
|
|
expect(minPrice.description).toContain("dollars");
|
|
expect(maxPrice.description).toContain("dollars");
|
|
});
|
|
|
|
test("handler should forward eBay dollar price filters to API", async () => {
|
|
await handleMcpRequest(
|
|
new Request("http://localhost", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/call",
|
|
params: {
|
|
name: "search_ebay",
|
|
arguments: {
|
|
query: "macbook",
|
|
minPrice: 999.99,
|
|
maxPrice: 1000,
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const calledUrl = (global.fetch as unknown as ReturnType<typeof mock>).mock
|
|
.calls[0]?.[0];
|
|
expect(String(calledUrl)).toContain("minPrice=999.99");
|
|
expect(String(calledUrl)).toContain("maxPrice=1000");
|
|
});
|
|
});
|