Skip to main content
Glama

MCP Specification Server

by MCPJam
23
6
  • Apple
FastMCP.test.ts72.4 kB
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { CreateMessageRequestSchema, ErrorCode, ListRootsRequestSchema, LoggingMessageNotificationSchema, McpError, PingRequestSchema, Root, } from "@modelcontextprotocol/sdk/types.js"; import { createEventSource, EventSourceClient } from "eventsource-client"; import { getRandomPort } from "get-port-please"; import { setTimeout as delay } from "timers/promises"; import { fetch } from "undici"; import { expect, test, vi } from "vitest"; import { z } from "zod"; import { z as z4 } from "zod/v4"; import { audioContent, type ContentResult, FastMCP, FastMCPSession, imageContent, type TextContent, UserError, } from "./FastMCP.js"; const runWithTestServer = async ({ client: createClient, run, server: createServer, }: { client?: () => Promise<Client>; run: ({ client, server, }: { client: Client; server: FastMCP; session: FastMCPSession; }) => Promise<void>; server?: () => Promise<FastMCP>; }) => { const port = await getRandomPort(); const server = createServer ? await createServer() : new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); try { const client = createClient ? await createClient() : new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); const session = await new Promise<FastMCPSession>((resolve) => { server.on("connect", async (event) => { // Wait for session to be fully ready before resolving await event.session.waitForReady(); resolve(event.session); }); client.connect(transport); }); await run({ client, server, session }); } finally { await server.stop(); } return port; }; test("adds tools", async () => { await runWithTestServer({ run: async ({ client }) => { expect(await client.listTools()).toEqual({ tools: [ { description: "Add two numbers", inputSchema: { $schema: "http://json-schema.org/draft-07/schema#", additionalProperties: false, properties: { a: { type: "number" }, b: { type: "number" }, }, required: ["a", "b"], type: "object", }, name: "add", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("adds tools with Zod v4 schema", async () => { await runWithTestServer({ run: async ({ client }) => { expect(await client.listTools()).toEqual({ tools: [ { description: "Add two numbers (using Zod v4 schema)", inputSchema: { $schema: "https://json-schema.org/draft/2020-12/schema", additionalProperties: false, properties: { a: { type: "number" }, b: { type: "number" }, }, required: ["a", "b"], type: "object", }, name: "add-zod-v4", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); const AddParamsZod4 = z4.object({ a: z4.number(), b: z4.number(), }); server.addTool({ description: "Add two numbers (using Zod v4 schema)", execute: async (args) => { return String(args.a + args.b); }, name: "add-zod-v4", parameters: AddParamsZod4, }); return server; }, }); }); test("health endpoint returns ok", async () => { const port = await getRandomPort(); const server = new FastMCP({ health: { message: "healthy", path: "/healthz" }, name: "Test", version: "1.0.0", }); await server.start({ httpStream: { port }, transportType: "httpStream", }); try { const response = await fetch(`http://localhost:${port}/healthz`); expect(response.status).toBe(200); expect(await response.text()).toBe("healthy"); } finally { await server.stop(); } }); test("calls a tool", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "3", type: "text" }], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("returns a list", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [ { text: "a", type: "text" }, { text: "b", type: "text" }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async () => { return { content: [ { text: "a", type: "text" }, { text: "b", type: "text" }, ], }; }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("returns an image", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [ { data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "image/png", type: "image", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async () => { return imageContent({ buffer: Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64", ), }); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("returns an audio", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [ { data: "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA", mimeType: "audio/wav", type: "audio", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async () => { return audioContent({ buffer: Buffer.from( "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA", "base64", ), }); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("handles UserError errors", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "Something went wrong", type: "text" }], isError: true, }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async () => { throw new UserError("Something went wrong"); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("calling an unknown tool throws McpError with MethodNotFound code", async () => { await runWithTestServer({ run: async ({ client }) => { try { await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.MethodNotFound); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); return server; }, }); }); test("tracks tool progress", async () => { await runWithTestServer({ run: async ({ client }) => { const onProgress = vi.fn(); await client.callTool( { arguments: { a: 1, b: 2, }, name: "add", }, undefined, { onprogress: onProgress, }, ); expect(onProgress).toHaveBeenCalledTimes(1); expect(onProgress).toHaveBeenCalledWith({ progress: 0, total: 10, }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args, { reportProgress }) => { reportProgress({ progress: 0, total: 10, }); await delay(100); return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test( "reports multiple progress updates without buffering", { // Earlier this test was flaky because the last progress update was not reported. // We are now running it 10 times to make sure that future updates do not regress. repeats: 10, }, async () => { await runWithTestServer({ run: async ({ client }) => { const progressCalls: Array<{ progress: number; total: number }> = []; const onProgress = vi.fn((data) => { progressCalls.push(data); }); await client.callTool( { arguments: { steps: 3, }, name: "progress-test", }, undefined, { onprogress: onProgress, }, ); expect(onProgress).toHaveBeenCalledTimes(4); expect(progressCalls).toEqual([ { progress: 0, total: 100 }, { progress: 50, total: 100 }, { progress: 90, total: 100 }, { progress: 100, total: 100 }, // This was previously lost due to buffering ]); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Test tool for progress buffering fix", execute: async (args, { reportProgress }) => { const { steps } = args; // Initial await reportProgress({ progress: 0, total: 100 }); for (let i = 1; i <= steps; i++) { await delay(50); // Small delay to simulate work if (i === 1) { await reportProgress({ progress: 50, total: 100 }); } else if (i === 2) { await reportProgress({ progress: 90, total: 100 }); } } // This was the critical test case that failed before the fix // because there's no await after it, causing it to be buffered await reportProgress({ progress: 100, total: 100 }); return "Progress test completed"; }, name: "progress-test", parameters: z.object({ steps: z.number(), }), }); return server; }, }); }, ); test("sets logging levels", async () => { await runWithTestServer({ run: async ({ client, session }) => { await client.setLoggingLevel("debug"); expect(session.loggingLevel).toBe("debug"); await client.setLoggingLevel("info"); expect(session.loggingLevel).toBe("info"); }, }); }); test("handles tool timeout", async () => { await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ arguments: { a: 1500, b: 2, }, name: "add", }); expect(result.isError).toBe(true); const result_typed = result as ContentResult; expect(Array.isArray(result_typed.content)).toBe(true); expect(result_typed.content.length).toBe(1); const firstItem = result_typed.content[0] as TextContent; expect(firstItem.type).toBe("text"); expect(firstItem.text).toBeDefined(); expect(firstItem.text).toContain("timed out"); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers with potential timeout", execute: async (args) => { console.log(`Adding ${args.a} and ${args.b}`); if (args.a > 1000 || args.b > 1000) { await new Promise((resolve) => setTimeout(resolve, 3000)); } return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), timeoutMs: 1000, }); return server; }, }); }); test("sends logging messages to the client", async () => { await runWithTestServer({ run: async ({ client }) => { const onLog = vi.fn(); client.setNotificationHandler( LoggingMessageNotificationSchema, (message) => { if (message.method === "notifications/message") { onLog({ level: message.params.level, ...(message.params.data ?? {}), }); } }, ); await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }); expect(onLog).toHaveBeenCalledTimes(4); expect(onLog).toHaveBeenNthCalledWith(1, { context: { foo: "bar", }, level: "debug", message: "debug message", }); expect(onLog).toHaveBeenNthCalledWith(2, { level: "error", message: "error message", }); expect(onLog).toHaveBeenNthCalledWith(3, { level: "info", message: "info message", }); expect(onLog).toHaveBeenNthCalledWith(4, { level: "warning", message: "warn message", }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args, { log }) => { log.debug("debug message", { foo: "bar", }); log.error("error message"); log.info("info message"); log.warn("warn message"); return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("adds resources", async () => { await runWithTestServer({ run: async ({ client }) => { expect(await client.listResources()).toEqual({ resources: [ { mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ async load() { return { text: "Example log content", }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", }); return server; }, }); }); test("clients reads a resource", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { mimeType: "text/plain", name: "Application Logs", text: "Example log content", uri: "file:///logs/app.log", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ async load() { return { text: "Example log content", }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", }); return server; }, }); }); test("clients reads a resource that returns multiple resources", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { mimeType: "text/plain", name: "Application Logs", text: "a", uri: "file:///logs/app.log", }, { mimeType: "text/plain", name: "Application Logs", text: "b", uri: "file:///logs/app.log", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ async load() { return [ { text: "a", }, { text: "b", }, ]; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", }); return server; }, }); }); test("embedded resources work in tools", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: { userId: "123", }, name: "get_user_profile", }), ).toEqual({ content: [ { resource: { mimeType: "application/json", text: '{"id":"123","name":"User","email":"user@example.com"}', uri: "user://profile/123", }, type: "resource", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ arguments: [ { name: "userId", required: true, }, ], async load(args) { return { text: `{"id":"${args.userId}","name":"User","email":"user@example.com"}`, }; }, mimeType: "application/json", name: "User Profile", uriTemplate: "user://profile/{userId}", }); server.addTool({ description: "Get user profile data", execute: async (args) => { return { content: [ { resource: await server.embedded( `user://profile/${args.userId}`, ), type: "resource", }, ], }; }, name: "get_user_profile", parameters: z.object({ userId: z.string(), }), }); return server; }, }); }); test("embedded resources work with direct resources", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.callTool({ arguments: {}, name: "get_logs", }), ).toEqual({ content: [ { resource: { mimeType: "text/plain", text: "Example log content", uri: "file:///logs/app.log", }, type: "resource", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResource({ async load() { return { text: "Example log content", }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", }); server.addTool({ description: "Get application logs", execute: async () => { return { content: [ { resource: await server.embedded("file:///logs/app.log"), type: "resource", }, ], }; }, name: "get_logs", parameters: z.object({}), }); return server; }, }); }); test("adds prompts", async () => { await runWithTestServer({ run: async ({ client }) => { expect( await client.getPrompt({ arguments: { changes: "foo", }, name: "git-commit", }), ).toEqual({ description: "Generate a Git commit message", messages: [ { content: { text: "Generate a concise but descriptive commit message for these changes:\n\nfoo", type: "text", }, role: "user", }, ], }); expect(await client.listPrompts()).toEqual({ prompts: [ { arguments: [ { description: "Git diff or description of changes", name: "changes", required: true, }, ], description: "Generate a Git commit message", name: "git-commit", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ arguments: [ { description: "Git diff or description of changes", name: "changes", required: true, }, ], description: "Generate a Git commit message", load: async (args) => { return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, name: "git-commit", }); return server; }, }); }); test("uses events to notify server of client connect/disconnect", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); const onConnect = vi.fn().mockResolvedValue(undefined); const onDisconnect = vi.fn().mockResolvedValue(undefined); server.on("connect", onConnect); server.on("disconnect", onDisconnect); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client.connect(transport); await delay(100); expect(onConnect).toHaveBeenCalledTimes(1); expect(onDisconnect).toHaveBeenCalledTimes(0); expect(server.sessions).toEqual([expect.any(FastMCPSession)]); await client.close(); await delay(100); expect(onConnect).toHaveBeenCalledTimes(1); expect(onDisconnect).toHaveBeenCalledTimes(1); await server.stop(); }); test("handles multiple clients", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client1 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport1 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client1.connect(transport1); const client2 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport2 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client2.connect(transport2); await delay(100); expect(server.sessions).toEqual([ expect.any(FastMCPSession), expect.any(FastMCPSession), ]); await server.stop(); }); test("session knows about client capabilities", async () => { await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: [ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, ], }; }); return client; }, run: async ({ session }) => { expect(session.clientCapabilities).toEqual({ roots: { listChanged: true, }, }); }, }); }); test("session knows about roots", async () => { await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: [ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, ], }; }); return client; }, run: async ({ session }) => { expect(session.roots).toEqual([ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, ]); }, }); }); test("session listens to roots changes", async () => { const clientRoots: Root[] = [ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, ]; await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: clientRoots, }; }); return client; }, run: async ({ client, session }) => { expect(session.roots).toEqual([ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, ]); clientRoots.push({ name: "Backend Repository", uri: "file:///home/user/projects/backend", }); await client.sendRootsListChanged(); const onRootsChanged = vi.fn(); session.on("rootsChanged", onRootsChanged); await delay(100); expect(session.roots).toEqual([ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, { name: "Backend Repository", uri: "file:///home/user/projects/backend", }, ]); expect(onRootsChanged).toHaveBeenCalledTimes(1); expect(onRootsChanged).toHaveBeenCalledWith({ roots: [ { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, { name: "Backend Repository", uri: "file:///home/user/projects/backend", }, ], }); }, }); }); test("session sends pings to the client", async () => { await runWithTestServer({ run: async ({ client }) => { const onPing = vi.fn().mockReturnValue({}); client.setRequestHandler(PingRequestSchema, onPing); await delay(2000); expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1); expect(onPing.mock.calls.length).toBeLessThanOrEqual(3); }, server: async () => { const server = new FastMCP({ name: "Test", ping: { enabled: true, intervalMs: 1000, }, version: "1.0.0", }); return server; }, }); }); test("completes prompt arguments", async () => { await runWithTestServer({ run: async ({ client }) => { const response = await client.complete({ argument: { name: "name", value: "Germ", }, ref: { name: "countryPoem", type: "ref/prompt", }, }); expect(response).toEqual({ completion: { values: ["Germany"], }, }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ arguments: [ { complete: async (value) => { if (value === "Germ") { return { values: ["Germany"], }; } return { values: [], }; }, description: "Name of the country", name: "name", required: true, }, ], description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, name: "countryPoem", }); return server; }, }); }); test("adds automatic prompt argument completion when enum is provided", async () => { await runWithTestServer({ run: async ({ client }) => { const response = await client.complete({ argument: { name: "name", value: "Germ", }, ref: { name: "countryPoem", type: "ref/prompt", }, }); expect(response).toEqual({ completion: { total: 1, values: ["Germany"], }, }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addPrompt({ arguments: [ { description: "Name of the country", enum: ["Germany", "France", "Italy"], name: "name", required: true, }, ], description: "Writes a poem about a country", load: async ({ name }) => { return `Hello, ${name}!`; }, name: "countryPoem", }); return server; }, }); }); test("completes template resource arguments", async () => { await runWithTestServer({ run: async ({ client }) => { const response = await client.complete({ argument: { name: "issueId", value: "123", }, ref: { type: "ref/resource", uri: "issue:///{issueId}", }, }); expect(response).toEqual({ completion: { values: ["123456"], }, }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ arguments: [ { complete: async (value) => { if (value === "123") { return { values: ["123456"], }; } return { values: [], }; }, description: "ID of the issue", name: "issueId", }, ], load: async ({ issueId }) => { return { text: `Issue ${issueId}`, }; }, mimeType: "text/plain", name: "Issue", uriTemplate: "issue:///{issueId}", }); return server; }, }); }); test("lists resource templates", async () => { await runWithTestServer({ run: async ({ client }) => { expect(await client.listResourceTemplates()).toEqual({ resourceTemplates: [ { mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", }, ], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ arguments: [ { description: "Name of the log", name: "name", required: true, }, ], load: async ({ name }) => { return { text: `Example log content for ${name}`, }; }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", }); return server; }, }); }); test( "HTTP Stream: custom endpoint works with /another-mcp", { timeout: 20000 }, async () => { const port = await getRandomPort(); // Create server with custom endpoint const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); await server.start({ httpStream: { endpoint: "/another-mcp", port, }, transportType: "httpStream", }); try { // Create client const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new StreamableHTTPClientTransport( new URL(`http://localhost:${port}/another-mcp`), ); // Connect client to server and wait for session to be ready const sessionPromise = new Promise<FastMCPSession>((resolve) => { server.on("connect", async (event) => { await event.session.waitForReady(); resolve(event.session); }); }); await client.connect(transport); await sessionPromise; // Call tool const result = await client.callTool({ arguments: { a: 5, b: 7, }, name: "add", }); // Check result expect(result).toEqual({ content: [{ text: "12", type: "text" }], }); // Clean up connection await transport.terminateSession(); await client.close(); } finally { await server.stop(); } }, ); test("clients reads a resource accessed via a resource template", async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const loadSpy = vi.fn((_args) => { return { text: "Example log content", }; }); await runWithTestServer({ run: async ({ client }) => { expect( await client.readResource({ uri: "file:///logs/app.log", }), ).toEqual({ contents: [ { mimeType: "text/plain", name: "Application Logs", text: "Example log content", uri: "file:///logs/app.log", }, ], }); expect(loadSpy).toHaveBeenCalledWith({ name: "app", }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addResourceTemplate({ arguments: [ { description: "Name of the log", name: "name", }, ], async load(args) { return loadSpy(args); }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", }); return server; }, }); }); test("makes a sampling request", async () => { const onMessageRequest = vi.fn(() => { return { content: { text: "The files are in the current directory.", type: "text", }, model: "gpt-3.5-turbo", role: "assistant", }; }); await runWithTestServer({ client: async () => { const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: { sampling: {}, }, }, ); return client; }, run: async ({ client, session }) => { client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest); const response = await session.requestSampling({ includeContext: "thisServer", maxTokens: 100, messages: [ { content: { text: "What files are in the current directory?", type: "text", }, role: "user", }, ], systemPrompt: "You are a helpful file system assistant.", }); expect(response).toEqual({ content: { text: "The files are in the current directory.", type: "text", }, model: "gpt-3.5-turbo", role: "assistant", }); expect(onMessageRequest).toHaveBeenCalledTimes(1); }, }); }); test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => { await runWithTestServer({ run: async ({ client }) => { try { await client.callTool({ arguments: { a: 1, b: "invalid", }, name: "add", }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", ); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("server remains usable after InvalidParams error", async () => { await runWithTestServer({ run: async ({ client }) => { try { await client.callTool({ arguments: { a: 1, b: "invalid", }, name: "add", }); } catch (error) { expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", ); } expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "3", type: "text" }], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); return server; }, }); }); test("allows new clients to connect after a client disconnects", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client1 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport1 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client1.connect(transport1); expect( await client1.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "3", type: "text" }], }); await client1.close(); const client2 = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport2 = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); await client2.connect(transport2); expect( await client2.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "3", type: "text" }], }); await client2.close(); await server.stop(); }); test("able to close server immediately after starting it", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); // We were previously not waiting for the server to start. // Therefore, this would have caused error 'Server is not running.'. await server.stop(); }); test("closing event source does not produce error", async () => { const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const eventSource = await new Promise<EventSourceClient>((onMessage) => { const eventSource = createEventSource({ onConnect: () => { console.info("connected"); }, onDisconnect: () => { console.info("disconnected"); }, onMessage: () => { onMessage(eventSource); }, url: `http://127.0.0.1:${port}/sse`, }); }); expect(eventSource.readyState).toBe("open"); eventSource.close(); // We were getting unhandled error 'Not connected' // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52 await delay(1000); await server.stop(); }); test("provides auth to tools", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { id: 1, }; }); const server = new FastMCP<{ id: number }>({ authenticate, name: "Test", version: "1.0.0", }); const execute = vi.fn(async (args) => { return String(args.a + args.b); }); server.addTool({ description: "Add two numbers", execute, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); expect( authenticate, "authenticate should have been called", ).toHaveBeenCalledTimes(1); expect( await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }), ).toEqual({ content: [{ text: "3", type: "text" }], }); expect(execute, "execute should have been called").toHaveBeenCalledTimes(1); expect(execute).toHaveBeenCalledWith( { a: 1, b: 2, }, { log: { debug: expect.any(Function), error: expect.any(Function), info: expect.any(Function), warn: expect.any(Function), }, reportProgress: expect.any(Function), session: { id: 1 }, streamContent: expect.any(Function), }, ); }); test("provides auth to resources", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { role: "admin", userId: 42, }; }); const server = new FastMCP<{ role: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", }); const resourceLoad = vi.fn(async (auth) => { return { text: `User ${auth?.userId} with role ${auth?.role} loaded this resource`, }; }); server.addResource({ load: resourceLoad, mimeType: "text/plain", name: "Auth Resource", uri: "auth://resource", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const result = await client.readResource({ uri: "auth://resource", }); expect(resourceLoad).toHaveBeenCalledTimes(1); expect(resourceLoad).toHaveBeenCalledWith({ role: "admin", userId: 42, }); expect(result).toEqual({ contents: [ { mimeType: "text/plain", name: "Auth Resource", text: "User 42 with role admin loaded this resource", uri: "auth://resource", }, ], }); }); test("provides auth to resource templates", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { permissions: ["read", "write"], userId: 99, }; }); const server = new FastMCP<{ permissions: string[]; userId: number }>({ authenticate, name: "Test", version: "1.0.0", }); const templateLoad = vi.fn(async (args, auth) => { return { text: `Resource ${args.resourceId} accessed by user ${auth?.userId} with permissions: ${auth?.permissions?.join(", ")}`, }; }); server.addResourceTemplate({ arguments: [ { name: "resourceId", required: true, }, ], load: templateLoad, mimeType: "text/plain", name: "Auth Template", uriTemplate: "auth://template/{resourceId}", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const result = await client.readResource({ uri: "auth://template/resource-123", }); expect(templateLoad).toHaveBeenCalledTimes(1); expect(templateLoad).toHaveBeenCalledWith( { resourceId: "resource-123" }, { permissions: ["read", "write"], userId: 99 }, ); expect(result).toEqual({ contents: [ { mimeType: "text/plain", name: "Auth Template", text: "Resource resource-123 accessed by user 99 with permissions: read, write", uri: "auth://template/resource-123", }, ], }); }); test("provides auth to resource templates returning arrays", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { accessLevel: 3, teamId: "team-alpha", }; }); const server = new FastMCP<{ accessLevel: number; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", }); const templateLoad = vi.fn(async (args, auth) => { return [ { text: `Document 1 for ${args.category} - Team: ${auth?.teamId}`, }, { text: `Document 2 for ${args.category} - Access Level: ${auth?.accessLevel}`, }, ]; }); server.addResourceTemplate({ arguments: [ { name: "category", required: true, }, ], load: templateLoad, mimeType: "text/plain", name: "Multi Doc Template", uriTemplate: "docs://category/{category}", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const result = await client.readResource({ uri: "docs://category/reports", }); expect(templateLoad).toHaveBeenCalledTimes(1); expect(templateLoad).toHaveBeenCalledWith( { category: "reports" }, { accessLevel: 3, teamId: "team-alpha" }, ); expect(result).toEqual({ contents: [ { mimeType: "text/plain", name: "Multi Doc Template", text: "Document 1 for reports - Team: team-alpha", uri: "docs://category/reports", }, { mimeType: "text/plain", name: "Multi Doc Template", text: "Document 2 for reports - Access Level: 3", uri: "docs://category/reports", }, ], }); }); test("provides auth to prompt argument completion", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { department: "engineering", userId: 100, }; }); const server = new FastMCP<{ department: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", }); const promptCompleter = vi.fn(async (value: string, auth) => { return { values: [ `${value}_user${auth?.userId}`, `${value}_dept${auth?.department}`, ], }; }); server.addPrompt({ arguments: [ { complete: promptCompleter, description: "Project name", name: "project", required: true, }, ], async load(args) { return `Loading project: ${args.project}`; }, name: "load-project", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const completionResult = await client.complete({ argument: { name: "project", value: "test", }, ref: { name: "load-project", type: "ref/prompt", }, }); expect(promptCompleter).toHaveBeenCalledTimes(1); expect(promptCompleter).toHaveBeenCalledWith("test", { department: "engineering", userId: 100, }); expect(completionResult).toEqual({ completion: { values: ["test_user100", "test_deptengineering"], }, }); }); test("provides auth to prompt load function", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { level: "admin", username: "testuser", }; }); const server = new FastMCP<{ level: string; username: string }>({ authenticate, name: "Test", version: "1.0.0", }); const promptLoad = vi.fn(async (args, auth) => { return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}`; }); server.addPrompt({ arguments: [ { description: "Option to select", name: "option", required: true, }, ], load: promptLoad, name: "auth-prompt", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const result = await client.getPrompt({ arguments: { option: "dashboard" }, name: "auth-prompt", }); expect(promptLoad).toHaveBeenCalledTimes(1); expect(promptLoad).toHaveBeenCalledWith( { option: "dashboard" }, { level: "admin", username: "testuser" }, ); expect(result).toEqual({ messages: [ { content: { text: "Welcome testuser (admin): You selected dashboard", type: "text", }, role: "user", }, ], }); }); test("provides auth to resource template argument completion", async () => { const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { region: "us-west", teamId: "alpha", }; }); const server = new FastMCP<{ region: string; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", }); const resourceCompleter = vi.fn(async (value: string, auth) => { return { values: [`${value}_${auth?.region}`, `${value}_team_${auth?.teamId}`], }; }); server.addResourceTemplate({ arguments: [ { complete: resourceCompleter, description: "Service ID", name: "serviceId", required: true, }, ], async load(args) { return { text: `Service ${args.serviceId} data`, }; }, mimeType: "text/plain", name: "Service Resource", uriTemplate: "service://{serviceId}", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), { eventSourceInit: { fetch: async (url, init) => { return fetch(url, { ...init, headers: { ...init?.headers, "x-api-key": "123", }, }); }, }, }, ); await client.connect(transport); const completionResult = await client.complete({ argument: { name: "serviceId", value: "api", }, ref: { type: "ref/resource", uri: "service://{serviceId}", }, }); expect(resourceCompleter).toHaveBeenCalledTimes(1); expect(resourceCompleter).toHaveBeenCalledWith("api", { region: "us-west", teamId: "alpha", }); expect(completionResult).toEqual({ completion: { values: ["api_us-west", "api_team_alpha"], }, }); }); test("supports streaming output from tools", async () => { let streamResult: { content: Array<{ text: string; type: string }> }; await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ arguments: {}, name: "streaming-void-tool", }); expect(result).toEqual({ content: [], }); streamResult = (await client.callTool({ arguments: {}, name: "streaming-with-result", })) as { content: Array<{ text: string; type: string }> }; expect(streamResult).toEqual({ content: [{ text: "Final result after streaming", type: "text" }], }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ annotations: { streamingHint: true, }, description: "A streaming tool that returns void", execute: async (_args, context) => { await context.streamContent({ text: "Streaming content 1", type: "text", }); await context.streamContent({ text: "Streaming content 2", type: "text", }); // Return void return; }, name: "streaming-void-tool", parameters: z.object({}), }); server.addTool({ annotations: { streamingHint: true, }, description: "A streaming tool that returns a result.", execute: async (_args, context) => { await context.streamContent({ text: "Streaming content 1", type: "text", }); await context.streamContent({ text: "Streaming content 2", type: "text", }); return "Final result after streaming"; }, name: "streaming-with-result", parameters: z.object({}), }); return server; }, }); }); test("blocks unauthorized requests", async () => { const port = await getRandomPort(); const server = new FastMCP<{ id: number }>({ authenticate: async () => { throw new Response(null, { status: 401, statusText: "Unauthorized", }); }, name: "Test", version: "1.0.0", }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport( new URL(`http://localhost:${port}/sse`), ); expect(async () => { await client.connect(transport); }).rejects.toThrow("SSE error: Non-200 status code (401)"); }); // We now use a direct approach for testing HTTP Stream functionality // rather than a helper function // Set longer timeout for HTTP Stream tests test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { console.log("Starting HTTP Stream test..."); const port = await getRandomPort(); // Create server directly (don't use helper function) const server = new FastMCP({ name: "Test", version: "1.0.0", }); server.addTool({ description: "Add two numbers", execute: async (args) => { return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), }); await server.start({ httpStream: { port, }, transportType: "httpStream", }); try { // Create client const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); // IMPORTANT: Don't provide sessionId manually with HTTP streaming // The server will generate a session ID automatically const transport = new StreamableHTTPClientTransport( new URL(`http://localhost:${port}/mcp`), ); // Connect client to server and wait for session to be ready const sessionPromise = new Promise<FastMCPSession>((resolve) => { server.on("connect", async (event) => { await event.session.waitForReady(); resolve(event.session); }); }); await client.connect(transport); await sessionPromise; // Call tool const result = await client.callTool({ arguments: { a: 1, b: 2, }, name: "add", }); // Check result expect(result).toEqual({ content: [{ text: "3", type: "text" }], }); // Clean up connection await transport.terminateSession(); await client.close(); } finally { await server.stop(); } });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MCPJam/mcp-spec'

If you have feedback or need assistance with the MCP directory API, please join our Discord server