From 2d70c4fe992a4979568a66d838d75934e1627fc5 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 29 Apr 2025 14:08:35 +0100 Subject: [PATCH 01/15] simple sse/streamable http connectivity, backwards compatible --- packages/mcp-client/cli.ts | 28 ++++++++++++------- packages/mcp-client/src/Agent.ts | 5 ++-- packages/mcp-client/src/McpClient.ts | 42 ++++++++++++++++++++++++---- packages/mcp-client/src/types.ts | 31 ++++++++++++++++++++ 4 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 packages/mcp-client/src/types.ts diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index 713de3b236..723ee3bbd7 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -4,6 +4,7 @@ import { stdin, stdout } from "node:process"; import { join } from "node:path"; import { homedir } from "node:os"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; +import type { ServerConfig } from "./src/types"; import type { InferenceProvider } from "@huggingface/inference"; import { ANSI } from "./src/utils"; import { Agent } from "./src"; @@ -12,17 +13,24 @@ import { version as packageVersion } from "./package.json"; const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct"; const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius"; -const SERVERS: StdioServerParameters[] = [ +const SERVERS: (ServerConfig|StdioServerParameters)[] = [ + // { + // // Filesystem "official" mcp-server with access to your Desktop + // command: "npx", + // args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")], + // }, + // { + // // Playwright MCP + // command: "npx", + // args: ["@playwright/mcp@latest"], + // }, { - // Filesystem "official" mcp-server with access to your Desktop - command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")], - }, - { - // Playwright MCP - command: "npx", - args: ["@playwright/mcp@latest"], - }, + type:"sse", + config: + { + url: "https://abidlabs-mcp-tools.hf.space/gradio_api/mcp/sse", + } + } ]; if (process.env.EXPERIMENTAL_HF_MCP_SERVER) { diff --git a/packages/mcp-client/src/Agent.ts b/packages/mcp-client/src/Agent.ts index be4aeb3bba..791c0e77d7 100644 --- a/packages/mcp-client/src/Agent.ts +++ b/packages/mcp-client/src/Agent.ts @@ -5,6 +5,7 @@ import type { ChatCompletionInputMessage, ChatCompletionStreamOutput } from "@hu import type { ChatCompletionInputTool } from "@huggingface/tasks/src/tasks/chat-completion/inference"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio"; import { debug } from "./utils"; +import { ServerConfig } from "./types"; const DEFAULT_SYSTEM_PROMPT = ` You are an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved, or if you need more info from the user to solve the problem. @@ -44,7 +45,7 @@ const askQuestionTool: ChatCompletionInputTool = { const exitLoopTools = [taskCompletionTool, askQuestionTool]; export class Agent extends McpClient { - private readonly servers: StdioServerParameters[]; + private readonly servers: (ServerConfig|StdioServerParameters)[]; protected messages: ChatCompletionInputMessage[]; constructor({ @@ -57,7 +58,7 @@ export class Agent extends McpClient { provider: InferenceProvider; model: string; apiKey: string; - servers: StdioServerParameters[]; + servers: (ServerConfig|StdioServerParameters)[]; prompt?: string; }) { super({ provider, model, apiKey }); diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index 93f9511949..2b570042e2 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -11,6 +11,10 @@ import type { } from "@huggingface/tasks/src/tasks/chat-completion/inference"; import { version as packageVersion } from "../package.json"; import { debug } from "./utils"; +import { ServerConfig } from "./types"; +import { Transport } from "@modelcontextprotocol/sdk/shared/transport"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; type ToolName = string; @@ -34,15 +38,41 @@ export class McpClient { this.model = model; } - async addMcpServers(servers: StdioServerParameters[]): Promise { + + + + async addMcpServers(servers: (ServerConfig | StdioServerParameters)[]): Promise { await Promise.all(servers.map((s) => this.addMcpServer(s))); } - async addMcpServer(server: StdioServerParameters): Promise { - const transport = new StdioClientTransport({ - ...server, - env: { ...server.env, PATH: process.env.PATH ?? "" }, - }); + async addMcpServer(server: ServerConfig | StdioServerParameters): Promise { + let transport: Transport; + const asUrl = (url: string | URL): URL => { + return typeof url === 'string' ? new URL(url) : url; + }; + + if(!("type" in server)){ + transport = new StdioClientTransport({ + ...server, + env: { ...server.env, PATH: process.env.PATH ?? "" }, + }); + } else { + switch (server.type) { + case 'stdio': + transport = new StdioClientTransport({ + ...server.config, + env: { ...server.config.env, PATH: process.env.PATH ?? "" }, + }); + break; + case 'sse': + transport = new SSEClientTransport(asUrl(server.config.url), server.config.options); + break; + case 'streamableHttp': + transport = new StreamableHTTPClientTransport(asUrl(server.config.url), server.config.options); + break; + } + + } const mcp = new Client({ name: "@huggingface/mcp-client", version: packageVersion }); await mcp.connect(transport); diff --git a/packages/mcp-client/src/types.ts b/packages/mcp-client/src/types.ts new file mode 100644 index 0000000000..cb5bae2282 --- /dev/null +++ b/packages/mcp-client/src/types.ts @@ -0,0 +1,31 @@ +// src/types.ts +import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; +import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js"; +import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; + +/** StdioServerParameters is usable as-is */ + +/** + * Configuration for an SSE MCP server + */ +export interface SSEServerConfig { + url: string | URL; + options?: SSEClientTransportOptions; +} + +/** + * Configuration for a StreamableHTTP MCP server + */ +export interface StreamableHTTPServerConfig { + url: string | URL; + options?: StreamableHTTPClientTransportOptions; +} + + +/** + * Discriminated union type for different MCP server types + */ +export type ServerConfig = + | { type: 'stdio'; config: StdioServerParameters } + | { type: 'sse'; config: SSEServerConfig } + | { type: 'streamableHttp'; config: StreamableHTTPServerConfig } From 98dd695b879a16b64b63c9e3480897b409b96a41 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 29 Apr 2025 18:06:11 +0100 Subject: [PATCH 02/15] handle multipart responses --- packages/mcp-client/cli.ts | 4 + packages/mcp-client/package.json | 3 +- packages/mcp-client/src/McpClient.ts | 6 +- packages/mcp-client/src/ResultFormat.spec.ts | 101 +++++++++++++++++++ packages/mcp-client/src/ResultFormatter.ts | 78 ++++++++++++++ 5 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 packages/mcp-client/src/ResultFormat.spec.ts create mode 100644 packages/mcp-client/src/ResultFormatter.ts diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index 723ee3bbd7..778b17ef07 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -30,6 +30,10 @@ const SERVERS: (ServerConfig|StdioServerParameters)[] = [ { url: "https://abidlabs-mcp-tools.hf.space/gradio_api/mcp/sse", } + }, + { + command: "npx", + args: ["-y","@llmindset/mcp-hfspace","--work-dir",join(homedir(),"temp/hfspace/")] } ]; diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json index 3775a9bf39..d8df1ff179 100644 --- a/packages/mcp-client/package.json +++ b/packages/mcp-client/package.json @@ -55,6 +55,7 @@ "dependencies": { "@huggingface/inference": "workspace:^", "@huggingface/tasks": "workspace:^", - "@modelcontextprotocol/sdk": "^1.9.0" + "@modelcontextprotocol/sdk": "^1.9.0", + "vitest": "^3.1.2" } } diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index 2b570042e2..81da9841d2 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -15,6 +15,7 @@ import { ServerConfig } from "./types"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { ResultFormatter } from "./ResultFormatter.js"; type ToolName = string; @@ -38,9 +39,6 @@ export class McpClient { this.model = model; } - - - async addMcpServers(servers: (ServerConfig | StdioServerParameters)[]): Promise { await Promise.all(servers.map((s) => this.addMcpServer(s))); } @@ -182,7 +180,7 @@ export class McpClient { const client = this.clients.get(toolName); if (client) { const result = await client.callTool({ name: toolName, arguments: toolArgs, signal: opts.abortSignal }); - toolMessage.content = (result.content as Array<{ text: string }>)[0].text; + toolMessage.content = (ResultFormatter.format(result)); } else { toolMessage.content = `Error: No session found for tool: ${toolName}`; } diff --git a/packages/mcp-client/src/ResultFormat.spec.ts b/packages/mcp-client/src/ResultFormat.spec.ts new file mode 100644 index 0000000000..c3de6b9189 --- /dev/null +++ b/packages/mcp-client/src/ResultFormat.spec.ts @@ -0,0 +1,101 @@ +// test/CallToolResultFormatter.test.ts +import { describe, expect, it } from "vitest"; +import { ResultFormatter } from "./ResultFormatter.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; + +describe("CallToolResultFormatter", () => { + it("should handle empty content", () => { + const result: CallToolResult = { + content: [] + }; + expect(ResultFormatter.format(result)).toBe("[No content]"); + }); + + it("should format text content", () => { + const result: CallToolResult = { + content: [ + { + type: "text", + text: "Hello, world!" + }, + { + type: "text", + text: "This is a test." + } + ] + }; + expect(ResultFormatter.format(result)).toBe("Hello, world!\nThis is a test."); + }); + + it("should format binary content with summaries", () => { + const result: CallToolResult = { + content: [ + { + type: "image", + mimeType: "image/png", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + }, + { + type: "audio", + mimeType: "audio/mp3", + data: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQwAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8" + } + ] + }; + + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("[Binary Content: Image image/png"); + expect(formatted).toContain("[Binary Content: Audio audio/mp3"); + expect(formatted).toContain("bytes]"); + }); + + it("should format resource content correctly", () => { + const result: CallToolResult = { + content: [ + { + type: "resource", + resource: { + uri: "https://example.com/text.txt", + text: "This is text from a resource." + } + }, + { + type: "resource", + resource: { + uri: "https://example.com/image.png", + mimeType: "image/png", + blob: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" + } + } + ] + }; + + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("This is text from a resource."); + expect(formatted).toContain("[Binary Content (https://example.com/image.png): image/png"); + expect(formatted).toContain("bytes]"); + }); + + it("should handle mixed content types", () => { + const result: CallToolResult = { + content: [ + { + type: "text", + text: "Here's a file I found:" + }, + { + type: "resource", + resource: { + uri: "https://example.com/document.pdf", + mimeType: "application/pdf", + blob: "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAEtjTsKgDAUBHt" + } + } + ] + }; + + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("Here's a file I found:"); + expect(formatted).toContain("[Binary Content (https://example.com/document.pdf): application/pdf"); + }); +}); diff --git a/packages/mcp-client/src/ResultFormatter.ts b/packages/mcp-client/src/ResultFormatter.ts new file mode 100644 index 0000000000..cdf9b56b80 --- /dev/null +++ b/packages/mcp-client/src/ResultFormatter.ts @@ -0,0 +1,78 @@ +import type { + CallToolResult, + TextResourceContents, + BlobResourceContents +} from "@modelcontextprotocol/sdk/types"; + +/** + * A utility class for formatting CallToolResult contents into human-readable text. + * Processes different content types, extracts text, and summarizes binary data. + */ +export class ResultFormatter { + /** + * Formats a CallToolResult's contents into a single string. + * - Text content is included directly + * - Binary content (images, audio, blobs) is summarized + * + * @param result The CallToolResult to format + * @returns A human-readable string representation of the result contents + */ + static format(result: CallToolResult): string { + if (!result.content || !Array.isArray(result.content) || result.content.length === 0) { + return "[No content]"; + } + + const formattedParts: string[] = []; + + for (const item of result.content) { + switch (item.type) { + case "text": + // Extract text content directly + formattedParts.push(item.text); + break; + + case "image": + // Summarize image content + const imageSize = this.getBase64Size(item.data); + formattedParts.push(`[Binary Content: Image ${item.mimeType}, ${imageSize} bytes]\nThe task is complete and the content accessible to the User` ); + break; + + case "audio": + // Summarize audio content + const audioSize = this.getBase64Size(item.data); + formattedParts.push(`[Binary Content: Audio ${item.mimeType}, ${audioSize} bytes]\nThe task is complete and the content accessible to the User`); + break; + + case "resource": + // Handle embedded resources - explicitly type the resource + if ('text' in item.resource) { + // It's a text resource with a text property + const textResource = item.resource as TextResourceContents; + formattedParts.push(textResource.text); + } else if ('blob' in item.resource) { + // It's a binary resource with a blob property + const blobResource = item.resource as BlobResourceContents; + const blobSize = this.getBase64Size(blobResource.blob); + const uri = blobResource.uri ? ` (${blobResource.uri})` : ''; + const mimeType = blobResource.mimeType ? blobResource.mimeType : 'unknown type'; + formattedParts.push(`[Binary Content${uri}: ${mimeType}, ${blobSize} bytes]\nThe task is complete and the content accessible to the User`); + } + break; + } + } + + return formattedParts.join("\n"); + } + + /** + * Calculates the approximate size in bytes of base64-encoded data + */ + private static getBase64Size(base64: string): number { + // Remove base64 header if present (e.g., data:image/png;base64,) + const cleanBase64 = base64.includes(',') ? base64.split(',')[1] : base64; + + // Calculate size: Base64 encodes 3 bytes into 4 characters + const padding = cleanBase64.endsWith('==') ? 2 : cleanBase64.endsWith('=') ? 1 : 0; + return Math.floor((cleanBase64.length * 3) / 4 - padding); + } +} From bf0cee2352704664f69ad49454015b4624f558b1 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Thu, 1 May 2025 17:40:44 +0100 Subject: [PATCH 03/15] workaround for passing HF_TOKEN with the current TS SDK --- packages/mcp-client/cli.ts | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index c70f9f3f56..8cf214a7d7 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -14,7 +14,7 @@ const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct"; const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius"; const BASE_URL = process.env.BASE_URL; -const SERVERS: (ServerConfig|StdioServerParameters)[] = [ +const SERVERS: (ServerConfig | StdioServerParameters)[] = [ // { // // Filesystem "official" mcp-server with access to your Desktop // command: "npx", @@ -26,16 +26,33 @@ const SERVERS: (ServerConfig|StdioServerParameters)[] = [ // args: ["@playwright/mcp@latest"], // }, { - type:"sse", - config: - { - url: "https://abidlabs-mcp-tools.hf.space/gradio_api/mcp/sse", - } + type: "sse", + config: { + url: "https://evalstate-flux1-schnell.hf.space/gradio_api/mcp/sse", + options: { + requestInit: { + headers: { + "Authorization": `Bearer ${process.env.HF_TOKEN || ""}` + } + }, + // workaround for https://github.com/modelcontextprotocol/typescript-sdk/issues/436 + eventSourceInit: { + fetch: (url, init) => { + const headers = new Headers(init?.headers || {}); + headers.set("Authorization", `Bearer ${process.env.HF_TOKEN || ""}`); + return fetch(url, { + ...init, + headers + }); + } + }, + }, + }, }, - { - command: "npx", - args: ["-y","@llmindset/mcp-hfspace","--work-dir",join(homedir(),"temp/hfspace/")] - } + // { + // command: "npx", + // args: ["-y","@llmindset/mcp-hfspace","--work-dir",join(homedir(),"temp/hfspace/")] + // } ]; if (process.env.EXPERIMENTAL_HF_MCP_SERVER) { @@ -68,13 +85,13 @@ async function main() { model: MODEL_ID, apiKey: process.env.HF_TOKEN, servers: SERVERS, - } + } : { provider: PROVIDER, model: MODEL_ID, apiKey: process.env.HF_TOKEN, servers: SERVERS, - } + } ); const rl = readline.createInterface({ input: stdin, output: stdout }); From 061ec225e695287c852396b8fa531fdb4351ceb1 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 12 May 2025 09:42:47 +0100 Subject: [PATCH 04/15] Update to InferenceProvider type, coercion for MCP type --- packages/mcp-client/package.json | 2 +- packages/mcp-client/src/McpClient.ts | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json index 72384d2e3a..f7f36679c9 100644 --- a/packages/mcp-client/package.json +++ b/packages/mcp-client/package.json @@ -55,7 +55,7 @@ "dependencies": { "@huggingface/inference": "workspace:^", "@huggingface/tasks": "workspace:^", - "@modelcontextprotocol/sdk": "^1.9.0", + "@modelcontextprotocol/sdk": "~=1.10.2", "vitest": "^3.1.2" } } diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index e9d5a30ac4..3286796c11 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -2,7 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { InferenceClient } from "@huggingface/inference"; -import type { InferenceProviderOrPolicy } from "@huggingface/inference"; +import type { InferenceProvider, Options } from "@huggingface/inference"; import type { ChatCompletionInputMessage, ChatCompletionInputTool, @@ -16,6 +16,7 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ResultFormatter } from "./ResultFormatter.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types"; type ToolName = string; @@ -28,7 +29,7 @@ export interface ChatCompletionInputMessageTool extends ChatCompletionInputMessa export class McpClient { protected client: InferenceClient; - protected provider: InferenceProviderOrPolicy | undefined; + protected provider: InferenceProvider | undefined; protected model: string; private clients: Map = new Map(); @@ -41,7 +42,7 @@ export class McpClient { apiKey, }: ( | { - provider: InferenceProviderOrPolicy; + provider: InferenceProvider; endpointUrl?: undefined; } | { @@ -52,11 +53,11 @@ export class McpClient { model: string; apiKey: string; }) { - this.client = endpointUrl ? new InferenceClient(apiKey, { endpointUrl: endpointUrl }) : new InferenceClient(apiKey); + const clientOptions = endpointUrl ? {endpointUrl} as Options & {endpointUrl: string} : undefined; + this.client = endpointUrl ? new InferenceClient(apiKey, clientOptions) : new InferenceClient(apiKey); this.provider = provider; this.model = model; } - async addMcpServers(servers: (ServerConfig | StdioServerParameters)[]): Promise { await Promise.all(servers.map((s) => this.addMcpServer(s))); } @@ -198,7 +199,7 @@ export class McpClient { const client = this.clients.get(toolName); if (client) { const result = await client.callTool({ name: toolName, arguments: toolArgs, signal: opts.abortSignal }); - toolMessage.content = (ResultFormatter.format(result)); + toolMessage.content = ResultFormatter.format(result as CallToolResult); } else { toolMessage.content = `Error: No session found for tool: ${toolName}`; } From 6c47cd67a7f7545044688d2ed640f5b6762efcac Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 12 May 2025 09:49:00 +0100 Subject: [PATCH 05/15] move test to correct directory --- .../{src/ResultFormat.spec.ts => test/ResultFormatter.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/mcp-client/{src/ResultFormat.spec.ts => test/ResultFormatter.spec.ts} (98%) diff --git a/packages/mcp-client/src/ResultFormat.spec.ts b/packages/mcp-client/test/ResultFormatter.spec.ts similarity index 98% rename from packages/mcp-client/src/ResultFormat.spec.ts rename to packages/mcp-client/test/ResultFormatter.spec.ts index c3de6b9189..506a0d0220 100644 --- a/packages/mcp-client/src/ResultFormat.spec.ts +++ b/packages/mcp-client/test/ResultFormatter.spec.ts @@ -1,6 +1,6 @@ // test/CallToolResultFormatter.test.ts import { describe, expect, it } from "vitest"; -import { ResultFormatter } from "./ResultFormatter.js"; +import { ResultFormatter } from "../src/ResultFormatter"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; describe("CallToolResultFormatter", () => { From bc5aba70f0b675696e6fb5bbf6dc526cf83fe435 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 12 May 2025 11:18:04 +0100 Subject: [PATCH 06/15] allow specification of (remote mcp) url on command line - include HF_TOKEN for HF domains (or local for testing) --- packages/mcp-client/cli.ts | 49 ++++---- packages/mcp-client/package.json | 2 +- packages/mcp-client/src/utils.ts | 75 ++++++++++++ .../mcp-client/test/UrlConversion.spec.ts | 111 ++++++++++++++++++ 4 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 packages/mcp-client/test/UrlConversion.spec.ts diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index be28d3d6d8..0245f01e10 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -6,7 +6,7 @@ import { homedir } from "node:os"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; import type { ServerConfig } from "./src/types"; import type { InferenceProvider } from "@huggingface/inference"; -import { ANSI } from "./src/utils"; +import { ANSI, urlToServerConfig } from "./src/utils"; import { Agent } from "./src"; import { version as packageVersion } from "./package.json"; @@ -20,41 +20,32 @@ const SERVERS: (ServerConfig | StdioServerParameters)[] = [ // command: "npx", // args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")], // }, + // { // // Playwright MCP // command: "npx", // args: ["@playwright/mcp@latest"], // }, - { - type: "sse", - config: { - url: "https://evalstate-flux1-schnell.hf.space/gradio_api/mcp/sse", - options: { - requestInit: { - headers: { - "Authorization": `Bearer ${process.env.HF_TOKEN || ""}` - } - }, - // workaround for https://github.com/modelcontextprotocol/typescript-sdk/issues/436 - eventSourceInit: { - fetch: (url, init) => { - const headers = new Headers(init?.headers || {}); - headers.set("Authorization", `Bearer ${process.env.HF_TOKEN || ""}`); - return fetch(url, { - ...init, - headers - }); - } - }, - }, - }, - }, - // { - // command: "npx", - // args: ["-y","@llmindset/mcp-hfspace","--work-dir",join(homedir(),"temp/hfspace/")] - // } + + // now using "--url https://evalstate-flux1-schnell.hf.space/gradio_api/mcp/sse" ]; + // Handle --url parameter: parse comma-separated URLs into ServerConfig objects + const urlIndex = process.argv.indexOf("--url"); + if (urlIndex !== -1 && urlIndex + 1 < process.argv.length) { + const urlsArg = process.argv[urlIndex + 1]; + const urls = urlsArg.split(",").map(url => url.trim()).filter(url => url.length > 0); + + for (const url of urls) { + try { + SERVERS.push(urlToServerConfig(url)); + } catch (error) { + console.error(`Error adding server from URL "${url}": ${error.message}`); + } + } + } + + if (process.env.EXPERIMENTAL_HF_MCP_SERVER) { SERVERS.push({ // Early version of a HF-MCP server diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json index f7f36679c9..db2b0b8897 100644 --- a/packages/mcp-client/package.json +++ b/packages/mcp-client/package.json @@ -55,7 +55,7 @@ "dependencies": { "@huggingface/inference": "workspace:^", "@huggingface/tasks": "workspace:^", - "@modelcontextprotocol/sdk": "~=1.10.2", + "@modelcontextprotocol/sdk": ">=1.11.2", "vitest": "^3.1.2" } } diff --git a/packages/mcp-client/src/utils.ts b/packages/mcp-client/src/utils.ts index 2440cfe8de..ae3931ab51 100644 --- a/packages/mcp-client/src/utils.ts +++ b/packages/mcp-client/src/utils.ts @@ -1,4 +1,5 @@ import { inspect } from "util"; +import { ServerConfig } from "./types"; export function debug(...args: unknown[]): void { if (process.env.DEBUG) { @@ -13,3 +14,77 @@ export const ANSI = { RED: "\x1b[31m", RESET: "\x1b[0m", }; + +export function urlToServerConfig(urlStr: string, token?: string): ServerConfig { + if (!urlStr.startsWith("http:") && !urlStr.startsWith("https:")) { + throw new Error(`Unsupported URL format: ${urlStr}. Use http:// or https:// prefix.`); + } + + const url = new URL(urlStr); + const hostname = url.hostname; + const path = url.pathname; + + let type: "streamableHttp" | "sse"; + if (path.endsWith("/sse")) { + type = "sse"; + } else if (path.endsWith("/mcp")) { + type = "streamableHttp"; + } else { + throw new Error(`Unsupported endpoint: ${urlStr}. URL must end with /sse or /mcp`); + } + + // Check if we should include the token + const authToken = token || process.env.HF_TOKEN; + const shouldIncludeToken = + !!authToken && + (hostname.endsWith(".hf.space") || + hostname.endsWith("huggingface.co") || + hostname === "localhost" || + hostname === "127.0.0.1"); + + // Create appropriate config based on type and authorization requirements + if (type === "streamableHttp") { + return { + type: "streamableHttp", + config: { + url: urlStr, + options: shouldIncludeToken + ? { + requestInit: { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + } + : undefined, + }, + }; + } else { + return { + type: "sse", + config: { + url: urlStr, + options: shouldIncludeToken + ? { + requestInit: { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + // workaround for https://github.com/modelcontextprotocol/typescript-sdk/issues/436 + eventSourceInit: { + fetch: (url, init) => { + const reqHeaders = new Headers(init?.headers || {}); + reqHeaders.set("Authorization", `Bearer ${authToken}`); + return fetch(url, { + ...init, + headers: reqHeaders, + }); + }, + }, + } + : undefined, + }, + }; + } +} diff --git a/packages/mcp-client/test/UrlConversion.spec.ts b/packages/mcp-client/test/UrlConversion.spec.ts new file mode 100644 index 0000000000..3dd124f6e5 --- /dev/null +++ b/packages/mcp-client/test/UrlConversion.spec.ts @@ -0,0 +1,111 @@ +// test/UrlConversion.spec.ts +import { describe, expect, it } from "vitest"; +import { urlToServerConfig } from "../src/utils"; +import { SSEServerConfig, StreamableHTTPServerConfig } from "../src/types"; + +describe("urlToServerConfig", () => { + const TOKEN = "test-token"; + + describe("Transport Type Selection", () => { + it("should create streamableHttp config for URLs ending with /mcp", () => { + const urls = ["https://example.com/api/mcp", "https://test.hf.space/mcp", "http://localhost:3000/mcp"]; + + for (const url of urls) { + const config = urlToServerConfig(url, TOKEN); + expect(config.type).toBe("streamableHttp"); + if (config.type === "streamableHttp") { + expect(config.config.url).toBe(url); + } + } + }); + + it("should create sse config for URLs ending with /sse", () => { + const urls = ["https://example.com/api/sse", "https://test.hf.space/sse", "http://localhost:3000/sse"]; + + for (const url of urls) { + const config = urlToServerConfig(url, TOKEN); + expect(config.type).toBe("sse"); + if (config.type === "sse") { + expect(config.config.url).toBe(url); + } + } + }); + + it("should throw for URLs not ending with /mcp or /sse", () => { + expect(() => urlToServerConfig("https://example.com/api")).toThrow("Unsupported endpoint"); + }); + }); + + describe("Authorization Token Inclusion", () => { + it("should include token for Hugging Face domains regardless of transport type", () => { + const urls = [ + "https://test.hf.space/api/mcp", // StreamableHTTP + "https://api.huggingface.co/models/mcp", // StreamableHTTP + "https://test.hf.space/api/sse", // SSE + "https://api.huggingface.co/models/sse", // SSE + ]; + + for (const url of urls) { + const config = urlToServerConfig(url, TOKEN); + if (config.type === "streamableHttp") { + const authHeader = config.config.options?.requestInit?.headers as Record | undefined; + expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); + } else if (config.type === "sse") { + const authHeader = config.config.options?.requestInit?.headers as Record | undefined; + expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); + } + } + }); + + it("should include token for localhost domains regardless of transport type", () => { + const urls = [ + "http://localhost:3000/mcp", // StreamableHTTP + "http://127.0.0.1:8000/api/mcp", // StreamableHTTP + "http://localhost:3000/sse", // SSE + "http://127.0.0.1:8000/api/sse", // SSE + ]; + + for (const url of urls) { + const config = urlToServerConfig(url, TOKEN); + if (config.type === "streamableHttp") { + const authHeader = config.config.options?.requestInit?.headers as Record | undefined; + expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); + } else if (config.type === "sse") { + const authHeader = config.config.options?.requestInit?.headers as Record | undefined; + expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); + } + } + }); + + it("should NOT include token for non-HF/non-local domains regardless of transport type", () => { + const urls = [ + "https://example.com/api/mcp", // StreamableHTTP + "https://someother.org/mcp", // StreamableHTTP + "https://example.com/api/sse", // SSE + "https://someother.org/sse", // SSE + ]; + + for (const url of urls) { + const config = urlToServerConfig(url, TOKEN); + if (config.type === "streamableHttp" || config.type === "sse") { + expect(config.config.options).toBeUndefined(); + } + } + }); + + it("should not include token when not provided even for HF domains", () => { + const urls = ["https://test.hf.space/api/mcp", "https://test.hf.space/api/sse"]; + + for (const url of urls) { + const config = urlToServerConfig(url); // No token provided + if (config.type === "streamableHttp" || config.type === "sse") { + expect(config.config.options).toBeUndefined(); + } + } + }); + }); + + it("should throw for unsupported URL formats", () => { + expect(() => urlToServerConfig("ftp://example.com/mcp")).toThrow("Unsupported URL format"); + }); +}); \ No newline at end of file From 6e6e684492b483d8ab19ce9b5d9d0133898412c0 Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Mon, 12 May 2025 13:48:24 +0100 Subject: [PATCH 07/15] linter, recheck main, pin package. --- packages/mcp-client/cli.ts | 36 ++-- packages/mcp-client/package.json | 2 +- packages/mcp-client/src/Agent.ts | 6 +- packages/mcp-client/src/McpClient.ts | 23 ++- packages/mcp-client/src/ResultFormatter.ts | 138 +++++++------- packages/mcp-client/src/types.ts | 17 +- packages/mcp-client/src/utils.ts | 2 +- .../mcp-client/test/ResultFormatter.spec.ts | 180 +++++++++--------- .../mcp-client/test/UrlConversion.spec.ts | 3 +- 9 files changed, 204 insertions(+), 203 deletions(-) diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index 0245f01e10..1544e9a8bc 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -20,31 +20,31 @@ const SERVERS: (ServerConfig | StdioServerParameters)[] = [ // command: "npx", // args: ["-y", "@modelcontextprotocol/server-filesystem", join(homedir(), "Desktop")], // }, - // { // // Playwright MCP // command: "npx", // args: ["@playwright/mcp@latest"], // }, - // now using "--url https://evalstate-flux1-schnell.hf.space/gradio_api/mcp/sse" ]; - // Handle --url parameter: parse comma-separated URLs into ServerConfig objects - const urlIndex = process.argv.indexOf("--url"); - if (urlIndex !== -1 && urlIndex + 1 < process.argv.length) { - const urlsArg = process.argv[urlIndex + 1]; - const urls = urlsArg.split(",").map(url => url.trim()).filter(url => url.length > 0); - - for (const url of urls) { - try { - SERVERS.push(urlToServerConfig(url)); - } catch (error) { - console.error(`Error adding server from URL "${url}": ${error.message}`); - } - } - } +// Handle --url parameter: parse comma-separated URLs into ServerConfig objects +const urlIndex = process.argv.indexOf("--url"); +if (urlIndex !== -1 && urlIndex + 1 < process.argv.length) { + const urlsArg = process.argv[urlIndex + 1]; + const urls = urlsArg + .split(",") + .map((url) => url.trim()) + .filter((url) => url.length > 0); + for (const url of urls) { + try { + SERVERS.push(urlToServerConfig(url)); + } catch (error) { + console.error(`Error adding server from URL "${url}": ${error.message}`); + } + } +} if (process.env.EXPERIMENTAL_HF_MCP_SERVER) { SERVERS.push({ @@ -76,13 +76,13 @@ async function main() { model: MODEL_ID, apiKey: process.env.HF_TOKEN, servers: SERVERS, - } + } : { provider: PROVIDER, model: MODEL_ID, apiKey: process.env.HF_TOKEN, servers: SERVERS, - } + } ); const rl = readline.createInterface({ input: stdin, output: stdout }); diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json index db2b0b8897..dee09da2dc 100644 --- a/packages/mcp-client/package.json +++ b/packages/mcp-client/package.json @@ -55,7 +55,7 @@ "dependencies": { "@huggingface/inference": "workspace:^", "@huggingface/tasks": "workspace:^", - "@modelcontextprotocol/sdk": ">=1.11.2", + "@modelcontextprotocol/sdk": "^1.11.2", "vitest": "^3.1.2" } } diff --git a/packages/mcp-client/src/Agent.ts b/packages/mcp-client/src/Agent.ts index 2bca23746f..4947380cab 100644 --- a/packages/mcp-client/src/Agent.ts +++ b/packages/mcp-client/src/Agent.ts @@ -5,7 +5,7 @@ import type { ChatCompletionInputMessage, ChatCompletionStreamOutput } from "@hu import type { ChatCompletionInputTool } from "@huggingface/tasks/src/tasks/chat-completion/inference"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio"; import { debug } from "./utils"; -import { ServerConfig } from "./types"; +import type { ServerConfig } from "./types"; const DEFAULT_SYSTEM_PROMPT = ` You are an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved, or if you need more info from the user to solve the problem. @@ -45,7 +45,7 @@ const askQuestionTool: ChatCompletionInputTool = { const exitLoopTools = [taskCompletionTool, askQuestionTool]; export class Agent extends McpClient { - private readonly servers: (ServerConfig|StdioServerParameters)[]; + private readonly servers: (ServerConfig | StdioServerParameters)[]; protected messages: ChatCompletionInputMessage[]; constructor({ @@ -67,7 +67,7 @@ export class Agent extends McpClient { ) & { model: string; apiKey: string; - servers: (ServerConfig|StdioServerParameters)[]; + servers: (ServerConfig | StdioServerParameters)[]; prompt?: string; }) { super(provider ? { provider, endpointUrl, model, apiKey } : { provider, endpointUrl, model, apiKey }); diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index 3286796c11..ccbadc3a91 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -11,12 +11,12 @@ import type { } from "@huggingface/tasks/src/tasks/chat-completion/inference"; import { version as packageVersion } from "../package.json"; import { debug } from "./utils"; -import { ServerConfig } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport"; +import type { ServerConfig } from "./types"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ResultFormatter } from "./ResultFormatter.js"; -import { CallToolResult } from "@modelcontextprotocol/sdk/types"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; type ToolName = string; @@ -53,7 +53,7 @@ export class McpClient { model: string; apiKey: string; }) { - const clientOptions = endpointUrl ? {endpointUrl} as Options & {endpointUrl: string} : undefined; + const clientOptions = endpointUrl ? ({ endpointUrl } as Options & { endpointUrl: string }) : undefined; this.client = endpointUrl ? new InferenceClient(apiKey, clientOptions) : new InferenceClient(apiKey); this.provider = provider; this.model = model; @@ -65,30 +65,29 @@ export class McpClient { async addMcpServer(server: ServerConfig | StdioServerParameters): Promise { let transport: Transport; const asUrl = (url: string | URL): URL => { - return typeof url === 'string' ? new URL(url) : url; - }; + return typeof url === "string" ? new URL(url) : url; + }; - if(!("type" in server)){ + if (!("type" in server)) { transport = new StdioClientTransport({ ...server, env: { ...server.env, PATH: process.env.PATH ?? "" }, }); } else { switch (server.type) { - case 'stdio': + case "stdio": transport = new StdioClientTransport({ ...server.config, env: { ...server.config.env, PATH: process.env.PATH ?? "" }, }); break; - case 'sse': + case "sse": transport = new SSEClientTransport(asUrl(server.config.url), server.config.options); break; - case 'streamableHttp': + case "streamableHttp": transport = new StreamableHTTPClientTransport(asUrl(server.config.url), server.config.options); break; - } - + } } const mcp = new Client({ name: "@huggingface/mcp-client", version: packageVersion }); await mcp.connect(transport); diff --git a/packages/mcp-client/src/ResultFormatter.ts b/packages/mcp-client/src/ResultFormatter.ts index cdf9b56b80..c67c893731 100644 --- a/packages/mcp-client/src/ResultFormatter.ts +++ b/packages/mcp-client/src/ResultFormatter.ts @@ -1,78 +1,82 @@ -import type { - CallToolResult, - TextResourceContents, - BlobResourceContents -} from "@modelcontextprotocol/sdk/types"; +import type { CallToolResult, TextResourceContents, BlobResourceContents } from "@modelcontextprotocol/sdk/types"; /** * A utility class for formatting CallToolResult contents into human-readable text. * Processes different content types, extracts text, and summarizes binary data. */ export class ResultFormatter { - /** - * Formats a CallToolResult's contents into a single string. - * - Text content is included directly - * - Binary content (images, audio, blobs) is summarized - * - * @param result The CallToolResult to format - * @returns A human-readable string representation of the result contents - */ - static format(result: CallToolResult): string { - if (!result.content || !Array.isArray(result.content) || result.content.length === 0) { - return "[No content]"; - } + /** + * Formats a CallToolResult's contents into a single string. + * - Text content is included directly + * - Binary content (images, audio, blobs) is summarized + * + * @param result The CallToolResult to format + * @returns A human-readable string representation of the result contents + */ + static format(result: CallToolResult): string { + if (!result.content || !Array.isArray(result.content) || result.content.length === 0) { + return "[No content]"; + } - const formattedParts: string[] = []; + const formattedParts: string[] = []; - for (const item of result.content) { - switch (item.type) { - case "text": - // Extract text content directly - formattedParts.push(item.text); - break; - - case "image": - // Summarize image content - const imageSize = this.getBase64Size(item.data); - formattedParts.push(`[Binary Content: Image ${item.mimeType}, ${imageSize} bytes]\nThe task is complete and the content accessible to the User` ); - break; - - case "audio": - // Summarize audio content - const audioSize = this.getBase64Size(item.data); - formattedParts.push(`[Binary Content: Audio ${item.mimeType}, ${audioSize} bytes]\nThe task is complete and the content accessible to the User`); - break; - - case "resource": - // Handle embedded resources - explicitly type the resource - if ('text' in item.resource) { - // It's a text resource with a text property - const textResource = item.resource as TextResourceContents; - formattedParts.push(textResource.text); - } else if ('blob' in item.resource) { - // It's a binary resource with a blob property - const blobResource = item.resource as BlobResourceContents; - const blobSize = this.getBase64Size(blobResource.blob); - const uri = blobResource.uri ? ` (${blobResource.uri})` : ''; - const mimeType = blobResource.mimeType ? blobResource.mimeType : 'unknown type'; - formattedParts.push(`[Binary Content${uri}: ${mimeType}, ${blobSize} bytes]\nThe task is complete and the content accessible to the User`); - } - break; - } - } + for (const item of result.content) { + switch (item.type) { + case "text": + // Extract text content directly + formattedParts.push(item.text); + break; - return formattedParts.join("\n"); - } + case "image": { + // Summarize image content + const imageSize = this.getBase64Size(item.data); + formattedParts.push( + `[Binary Content: Image ${item.mimeType}, ${imageSize} bytes]\nThe task is complete and the content accessible to the User` + ); + break; + } - /** - * Calculates the approximate size in bytes of base64-encoded data - */ - private static getBase64Size(base64: string): number { - // Remove base64 header if present (e.g., data:image/png;base64,) - const cleanBase64 = base64.includes(',') ? base64.split(',')[1] : base64; - - // Calculate size: Base64 encodes 3 bytes into 4 characters - const padding = cleanBase64.endsWith('==') ? 2 : cleanBase64.endsWith('=') ? 1 : 0; - return Math.floor((cleanBase64.length * 3) / 4 - padding); - } + case "audio": { + // Summarize audio content + const audioSize = this.getBase64Size(item.data); + formattedParts.push( + `[Binary Content: Audio ${item.mimeType}, ${audioSize} bytes]\nThe task is complete and the content accessible to the User` + ); + break; + } + + case "resource": + // Handle embedded resources - explicitly type the resource + if ("text" in item.resource) { + // It's a text resource with a text property + const textResource = item.resource as TextResourceContents; + formattedParts.push(textResource.text); + } else if ("blob" in item.resource) { + // It's a binary resource with a blob property + const blobResource = item.resource as BlobResourceContents; + const blobSize = this.getBase64Size(blobResource.blob); + const uri = blobResource.uri ? ` (${blobResource.uri})` : ""; + const mimeType = blobResource.mimeType ? blobResource.mimeType : "unknown type"; + formattedParts.push( + `[Binary Content${uri}: ${mimeType}, ${blobSize} bytes]\nThe task is complete and the content accessible to the User` + ); + } + break; + } + } + + return formattedParts.join("\n"); + } + + /** + * Calculates the approximate size in bytes of base64-encoded data + */ + private static getBase64Size(base64: string): number { + // Remove base64 header if present (e.g., data:image/png;base64,) + const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64; + + // Calculate size: Base64 encodes 3 bytes into 4 characters + const padding = cleanBase64.endsWith("==") ? 2 : cleanBase64.endsWith("=") ? 1 : 0; + return Math.floor((cleanBase64.length * 3) / 4 - padding); + } } diff --git a/packages/mcp-client/src/types.ts b/packages/mcp-client/src/types.ts index cb5bae2282..19dc0cf949 100644 --- a/packages/mcp-client/src/types.ts +++ b/packages/mcp-client/src/types.ts @@ -9,23 +9,22 @@ import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol * Configuration for an SSE MCP server */ export interface SSEServerConfig { - url: string | URL; - options?: SSEClientTransportOptions; + url: string | URL; + options?: SSEClientTransportOptions; } /** * Configuration for a StreamableHTTP MCP server */ export interface StreamableHTTPServerConfig { - url: string | URL; - options?: StreamableHTTPClientTransportOptions; + url: string | URL; + options?: StreamableHTTPClientTransportOptions; } - /** * Discriminated union type for different MCP server types */ -export type ServerConfig = - | { type: 'stdio'; config: StdioServerParameters } - | { type: 'sse'; config: SSEServerConfig } - | { type: 'streamableHttp'; config: StreamableHTTPServerConfig } +export type ServerConfig = + | { type: "stdio"; config: StdioServerParameters } + | { type: "sse"; config: SSEServerConfig } + | { type: "streamableHttp"; config: StreamableHTTPServerConfig }; diff --git a/packages/mcp-client/src/utils.ts b/packages/mcp-client/src/utils.ts index ae3931ab51..a06ac8eb12 100644 --- a/packages/mcp-client/src/utils.ts +++ b/packages/mcp-client/src/utils.ts @@ -1,5 +1,5 @@ import { inspect } from "util"; -import { ServerConfig } from "./types"; +import type { ServerConfig } from "./types"; export function debug(...args: unknown[]): void { if (process.env.DEBUG) { diff --git a/packages/mcp-client/test/ResultFormatter.spec.ts b/packages/mcp-client/test/ResultFormatter.spec.ts index 506a0d0220..4826e26580 100644 --- a/packages/mcp-client/test/ResultFormatter.spec.ts +++ b/packages/mcp-client/test/ResultFormatter.spec.ts @@ -4,98 +4,98 @@ import { ResultFormatter } from "../src/ResultFormatter"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; describe("CallToolResultFormatter", () => { - it("should handle empty content", () => { - const result: CallToolResult = { - content: [] - }; - expect(ResultFormatter.format(result)).toBe("[No content]"); - }); + it("should handle empty content", () => { + const result: CallToolResult = { + content: [], + }; + expect(ResultFormatter.format(result)).toBe("[No content]"); + }); - it("should format text content", () => { - const result: CallToolResult = { - content: [ - { - type: "text", - text: "Hello, world!" - }, - { - type: "text", - text: "This is a test." - } - ] - }; - expect(ResultFormatter.format(result)).toBe("Hello, world!\nThis is a test."); - }); + it("should format text content", () => { + const result: CallToolResult = { + content: [ + { + type: "text", + text: "Hello, world!", + }, + { + type: "text", + text: "This is a test.", + }, + ], + }; + expect(ResultFormatter.format(result)).toBe("Hello, world!\nThis is a test."); + }); - it("should format binary content with summaries", () => { - const result: CallToolResult = { - content: [ - { - type: "image", - mimeType: "image/png", - data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" - }, - { - type: "audio", - mimeType: "audio/mp3", - data: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQwAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8" - } - ] - }; - - const formatted = ResultFormatter.format(result); - expect(formatted).toContain("[Binary Content: Image image/png"); - expect(formatted).toContain("[Binary Content: Audio audio/mp3"); - expect(formatted).toContain("bytes]"); - }); + it("should format binary content with summaries", () => { + const result: CallToolResult = { + content: [ + { + type: "image", + mimeType: "image/png", + data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + }, + { + type: "audio", + mimeType: "audio/mp3", + data: "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQwAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8", + }, + ], + }; - it("should format resource content correctly", () => { - const result: CallToolResult = { - content: [ - { - type: "resource", - resource: { - uri: "https://example.com/text.txt", - text: "This is text from a resource." - } - }, - { - type: "resource", - resource: { - uri: "https://example.com/image.png", - mimeType: "image/png", - blob: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" - } - } - ] - }; - - const formatted = ResultFormatter.format(result); - expect(formatted).toContain("This is text from a resource."); - expect(formatted).toContain("[Binary Content (https://example.com/image.png): image/png"); - expect(formatted).toContain("bytes]"); - }); + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("[Binary Content: Image image/png"); + expect(formatted).toContain("[Binary Content: Audio audio/mp3"); + expect(formatted).toContain("bytes]"); + }); - it("should handle mixed content types", () => { - const result: CallToolResult = { - content: [ - { - type: "text", - text: "Here's a file I found:" - }, - { - type: "resource", - resource: { - uri: "https://example.com/document.pdf", - mimeType: "application/pdf", - blob: "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAEtjTsKgDAUBHt" - } - } - ] - }; - - const formatted = ResultFormatter.format(result); - expect(formatted).toContain("Here's a file I found:"); - expect(formatted).toContain("[Binary Content (https://example.com/document.pdf): application/pdf"); - }); + it("should format resource content correctly", () => { + const result: CallToolResult = { + content: [ + { + type: "resource", + resource: { + uri: "https://example.com/text.txt", + text: "This is text from a resource.", + }, + }, + { + type: "resource", + resource: { + uri: "https://example.com/image.png", + mimeType: "image/png", + blob: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + }, + }, + ], + }; + + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("This is text from a resource."); + expect(formatted).toContain("[Binary Content (https://example.com/image.png): image/png"); + expect(formatted).toContain("bytes]"); + }); + + it("should handle mixed content types", () => { + const result: CallToolResult = { + content: [ + { + type: "text", + text: "Here's a file I found:", + }, + { + type: "resource", + resource: { + uri: "https://example.com/document.pdf", + mimeType: "application/pdf", + blob: "JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeAEtjTsKgDAUBHt", + }, + }, + ], + }; + + const formatted = ResultFormatter.format(result); + expect(formatted).toContain("Here's a file I found:"); + expect(formatted).toContain("[Binary Content (https://example.com/document.pdf): application/pdf"); + }); }); diff --git a/packages/mcp-client/test/UrlConversion.spec.ts b/packages/mcp-client/test/UrlConversion.spec.ts index 3dd124f6e5..0537a60dc3 100644 --- a/packages/mcp-client/test/UrlConversion.spec.ts +++ b/packages/mcp-client/test/UrlConversion.spec.ts @@ -1,7 +1,6 @@ // test/UrlConversion.spec.ts import { describe, expect, it } from "vitest"; import { urlToServerConfig } from "../src/utils"; -import { SSEServerConfig, StreamableHTTPServerConfig } from "../src/types"; describe("urlToServerConfig", () => { const TOKEN = "test-token"; @@ -108,4 +107,4 @@ describe("urlToServerConfig", () => { it("should throw for unsupported URL formats", () => { expect(() => urlToServerConfig("ftp://example.com/mcp")).toThrow("Unsupported URL format"); }); -}); \ No newline at end of file +}); From 8d287af5e52756b4c47dc8592ff3e7fb6b69d4d1 Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Mon, 12 May 2025 19:50:53 +0200 Subject: [PATCH 08/15] tweaks --- packages/mcp-client/cli.ts | 45 +++++++++++++++----------------- packages/mcp-client/package.json | 3 +-- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index 9accbe6baa..21bd799a12 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -9,17 +9,21 @@ import type { InferenceProvider } from "@huggingface/inference"; import { ANSI, urlToServerConfig } from "./src/utils"; import { Agent } from "./src"; import { version as packageVersion } from "./package.json"; +import { parseArgs } from "node:util"; const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct"; const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius"; const ENDPOINT_URL = process.env.ENDPOINT_URL ?? process.env.BASE_URL; -const MCP_EXAMPLER_LOCAL_FOLDER = process.platform === "darwin" ? join(homedir(), "Desktop") : homedir(); const SERVERS: (ServerConfig | StdioServerParameters)[] = [ { // Filesystem "official" mcp-server with access to your Desktop command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem", MCP_EXAMPLER_LOCAL_FOLDER], + args: [ + "-y", + "@modelcontextprotocol/server-filesystem", + process.platform === "darwin" ? join(homedir(), "Desktop") : homedir(), + ], }, { // Playwright MCP @@ -28,15 +32,21 @@ const SERVERS: (ServerConfig | StdioServerParameters)[] = [ }, ]; -// Handle --url parameter: parse comma-separated URLs into ServerConfig objects -const urlIndex = process.argv.indexOf("--url"); -if (urlIndex !== -1 && urlIndex + 1 < process.argv.length) { - const urlsArg = process.argv[urlIndex + 1]; - const urls = urlsArg - .split(",") - .map((url) => url.trim()) - .filter((url) => url.length > 0); - +// Handle --url parameters: each URL will be parsed into a ServerConfig object +const { + values: { url: urls }, +} = parseArgs({ + options: { + url: { + type: "string", + multiple: true, + }, + }, +}); +if (urls?.length) { + while (SERVERS.length) { + SERVERS.pop(); + } for (const url of urls) { try { SERVERS.push(urlToServerConfig(url)); @@ -46,19 +56,6 @@ if (urlIndex !== -1 && urlIndex + 1 < process.argv.length) { } } -if (process.env.EXPERIMENTAL_HF_MCP_SERVER) { - SERVERS.push({ - // Early version of a HF-MCP server - // you can download it from gist.github.com/julien-c/0500ba922e1b38f2dc30447fb81f7dc6 - // and replace the local path below - command: "node", - args: ["--disable-warning=ExperimentalWarning", join(homedir(), "Desktop/hf-mcp/index.ts")], - env: { - HF_TOKEN: process.env.HF_TOKEN ?? "", - }, - }); -} - async function main() { if (process.argv.includes("--version")) { console.log(packageVersion); diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json index dee09da2dc..7cd4915deb 100644 --- a/packages/mcp-client/package.json +++ b/packages/mcp-client/package.json @@ -55,7 +55,6 @@ "dependencies": { "@huggingface/inference": "workspace:^", "@huggingface/tasks": "workspace:^", - "@modelcontextprotocol/sdk": "^1.11.2", - "vitest": "^3.1.2" + "@modelcontextprotocol/sdk": "^1.11.2" } } From f562ae84be626abeee018686966986eee7633c59 Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Mon, 12 May 2025 19:52:03 +0200 Subject: [PATCH 09/15] Update pnpm-lock.yaml --- packages/mcp-client/pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/mcp-client/pnpm-lock.yaml b/packages/mcp-client/pnpm-lock.yaml index 1d86bc378c..3ac7dd666a 100644 --- a/packages/mcp-client/pnpm-lock.yaml +++ b/packages/mcp-client/pnpm-lock.yaml @@ -15,13 +15,13 @@ importers: specifier: workspace:^ version: link:../tasks '@modelcontextprotocol/sdk': - specifier: ^1.9.0 - version: 1.10.1 + specifier: ^1.11.2 + version: 1.11.2 packages: - '@modelcontextprotocol/sdk@1.10.1': - resolution: {integrity: sha512-xNYdFdkJqEfIaTVP1gPKoEvluACHZsHZegIoICX8DM1o6Qf3G5u2BQJHmgd0n4YgRPqqK/u1ujQvrgAxxSJT9w==} + '@modelcontextprotocol/sdk@1.11.2': + resolution: {integrity: sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==} engines: {node: '>=18'} accepts@2.0.0: @@ -342,7 +342,7 @@ packages: snapshots: - '@modelcontextprotocol/sdk@1.10.1': + '@modelcontextprotocol/sdk@1.11.2': dependencies: content-type: 1.0.5 cors: 2.8.5 From 59c4b5b7d87f7ea0a18cc9e8e990671d550a1c3e Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Mon, 12 May 2025 19:54:28 +0200 Subject: [PATCH 10/15] Update packages/mcp-client/cli.ts --- packages/mcp-client/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index 21bd799a12..add92c2ef3 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -32,7 +32,7 @@ const SERVERS: (ServerConfig | StdioServerParameters)[] = [ }, ]; -// Handle --url parameters: each URL will be parsed into a ServerConfig object +// Handle --url parameters from command line: each URL will be parsed into a ServerConfig object const { values: { url: urls }, } = parseArgs({ From 383ffc756c34ca0c7020a776eb9942636fbfeea7 Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Mon, 12 May 2025 19:59:39 +0200 Subject: [PATCH 11/15] `InferenceProvider` => `InferenceProviderOrPolicy` --- packages/mcp-client/cli.ts | 4 ++-- packages/mcp-client/src/Agent.ts | 4 ++-- packages/mcp-client/src/McpClient.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/mcp-client/cli.ts b/packages/mcp-client/cli.ts index add92c2ef3..fd11e353f7 100644 --- a/packages/mcp-client/cli.ts +++ b/packages/mcp-client/cli.ts @@ -5,14 +5,14 @@ import { join } from "node:path"; import { homedir } from "node:os"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; import type { ServerConfig } from "./src/types"; -import type { InferenceProvider } from "@huggingface/inference"; +import type { InferenceProviderOrPolicy } from "@huggingface/inference"; import { ANSI, urlToServerConfig } from "./src/utils"; import { Agent } from "./src"; import { version as packageVersion } from "./package.json"; import { parseArgs } from "node:util"; const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct"; -const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius"; +const PROVIDER = (process.env.PROVIDER as InferenceProviderOrPolicy) ?? "nebius"; const ENDPOINT_URL = process.env.ENDPOINT_URL ?? process.env.BASE_URL; const SERVERS: (ServerConfig | StdioServerParameters)[] = [ diff --git a/packages/mcp-client/src/Agent.ts b/packages/mcp-client/src/Agent.ts index 4947380cab..e1a3a84d2e 100644 --- a/packages/mcp-client/src/Agent.ts +++ b/packages/mcp-client/src/Agent.ts @@ -1,4 +1,4 @@ -import type { InferenceProvider } from "@huggingface/inference"; +import type { InferenceProviderOrPolicy } from "@huggingface/inference"; import type { ChatCompletionInputMessageTool } from "./McpClient"; import { McpClient } from "./McpClient"; import type { ChatCompletionInputMessage, ChatCompletionStreamOutput } from "@huggingface/tasks"; @@ -57,7 +57,7 @@ export class Agent extends McpClient { prompt, }: ( | { - provider: InferenceProvider; + provider: InferenceProviderOrPolicy; endpointUrl?: undefined; } | { diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index ccbadc3a91..a25498d853 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -2,7 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { InferenceClient } from "@huggingface/inference"; -import type { InferenceProvider, Options } from "@huggingface/inference"; +import type { InferenceProviderOrPolicy } from "@huggingface/inference"; import type { ChatCompletionInputMessage, ChatCompletionInputTool, @@ -29,7 +29,7 @@ export interface ChatCompletionInputMessageTool extends ChatCompletionInputMessa export class McpClient { protected client: InferenceClient; - protected provider: InferenceProvider | undefined; + protected provider: InferenceProviderOrPolicy | undefined; protected model: string; private clients: Map = new Map(); @@ -42,7 +42,7 @@ export class McpClient { apiKey, }: ( | { - provider: InferenceProvider; + provider: InferenceProviderOrPolicy; endpointUrl?: undefined; } | { @@ -53,11 +53,11 @@ export class McpClient { model: string; apiKey: string; }) { - const clientOptions = endpointUrl ? ({ endpointUrl } as Options & { endpointUrl: string }) : undefined; - this.client = endpointUrl ? new InferenceClient(apiKey, clientOptions) : new InferenceClient(apiKey); + this.client = endpointUrl ? new InferenceClient(apiKey, { endpointUrl: endpointUrl }) : new InferenceClient(apiKey); this.provider = provider; this.model = model; } + async addMcpServers(servers: (ServerConfig | StdioServerParameters)[]): Promise { await Promise.all(servers.map((s) => this.addMcpServer(s))); } From 86085f6415cf1dcbb29ef8bf8dd0f79c4e50442b Mon Sep 17 00:00:00 2001 From: Julien Chaumond Date: Tue, 13 May 2025 12:32:37 +0200 Subject: [PATCH 12/15] "streamableHttp" => "http" --- packages/mcp-client/src/McpClient.ts | 2 +- packages/mcp-client/src/types.ts | 2 +- packages/mcp-client/src/utils.ts | 8 ++++---- packages/mcp-client/test/UrlConversion.spec.ts | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index a25498d853..d07f89c6cf 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -84,7 +84,7 @@ export class McpClient { case "sse": transport = new SSEClientTransport(asUrl(server.config.url), server.config.options); break; - case "streamableHttp": + case "http": transport = new StreamableHTTPClientTransport(asUrl(server.config.url), server.config.options); break; } diff --git a/packages/mcp-client/src/types.ts b/packages/mcp-client/src/types.ts index 19dc0cf949..5c9077ceb5 100644 --- a/packages/mcp-client/src/types.ts +++ b/packages/mcp-client/src/types.ts @@ -27,4 +27,4 @@ export interface StreamableHTTPServerConfig { export type ServerConfig = | { type: "stdio"; config: StdioServerParameters } | { type: "sse"; config: SSEServerConfig } - | { type: "streamableHttp"; config: StreamableHTTPServerConfig }; + | { type: "http"; config: StreamableHTTPServerConfig }; diff --git a/packages/mcp-client/src/utils.ts b/packages/mcp-client/src/utils.ts index a06ac8eb12..973880f25a 100644 --- a/packages/mcp-client/src/utils.ts +++ b/packages/mcp-client/src/utils.ts @@ -24,11 +24,11 @@ export function urlToServerConfig(urlStr: string, token?: string): ServerConfig const hostname = url.hostname; const path = url.pathname; - let type: "streamableHttp" | "sse"; + let type: "http" | "sse"; if (path.endsWith("/sse")) { type = "sse"; } else if (path.endsWith("/mcp")) { - type = "streamableHttp"; + type = "http"; } else { throw new Error(`Unsupported endpoint: ${urlStr}. URL must end with /sse or /mcp`); } @@ -43,9 +43,9 @@ export function urlToServerConfig(urlStr: string, token?: string): ServerConfig hostname === "127.0.0.1"); // Create appropriate config based on type and authorization requirements - if (type === "streamableHttp") { + if (type === "http") { return { - type: "streamableHttp", + type: "http", config: { url: urlStr, options: shouldIncludeToken diff --git a/packages/mcp-client/test/UrlConversion.spec.ts b/packages/mcp-client/test/UrlConversion.spec.ts index 0537a60dc3..c89e48d486 100644 --- a/packages/mcp-client/test/UrlConversion.spec.ts +++ b/packages/mcp-client/test/UrlConversion.spec.ts @@ -11,8 +11,8 @@ describe("urlToServerConfig", () => { for (const url of urls) { const config = urlToServerConfig(url, TOKEN); - expect(config.type).toBe("streamableHttp"); - if (config.type === "streamableHttp") { + expect(config.type).toBe("http"); + if (config.type === "http") { expect(config.config.url).toBe(url); } } @@ -46,7 +46,7 @@ describe("urlToServerConfig", () => { for (const url of urls) { const config = urlToServerConfig(url, TOKEN); - if (config.type === "streamableHttp") { + if (config.type === "http") { const authHeader = config.config.options?.requestInit?.headers as Record | undefined; expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); } else if (config.type === "sse") { @@ -66,7 +66,7 @@ describe("urlToServerConfig", () => { for (const url of urls) { const config = urlToServerConfig(url, TOKEN); - if (config.type === "streamableHttp") { + if (config.type === "http") { const authHeader = config.config.options?.requestInit?.headers as Record | undefined; expect(authHeader?.Authorization).toBe(`Bearer ${TOKEN}`); } else if (config.type === "sse") { @@ -86,7 +86,7 @@ describe("urlToServerConfig", () => { for (const url of urls) { const config = urlToServerConfig(url, TOKEN); - if (config.type === "streamableHttp" || config.type === "sse") { + if (config.type === "http" || config.type === "sse") { expect(config.config.options).toBeUndefined(); } } @@ -97,7 +97,7 @@ describe("urlToServerConfig", () => { for (const url of urls) { const config = urlToServerConfig(url); // No token provided - if (config.type === "streamableHttp" || config.type === "sse") { + if (config.type === "http" || config.type === "sse") { expect(config.config.options).toBeUndefined(); } } From bb352be66abee8c6611ca689ac72e8726df82bdf Mon Sep 17 00:00:00 2001 From: evalstate <1936278+evalstate@users.noreply.github.com> Date: Tue, 13 May 2025 11:55:45 +0100 Subject: [PATCH 13/15] use compatibility type rather than type assertion --- packages/mcp-client/src/McpClient.ts | 3 +-- packages/mcp-client/src/ResultFormatter.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mcp-client/src/McpClient.ts b/packages/mcp-client/src/McpClient.ts index d07f89c6cf..0b8dbe2815 100644 --- a/packages/mcp-client/src/McpClient.ts +++ b/packages/mcp-client/src/McpClient.ts @@ -16,7 +16,6 @@ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { ResultFormatter } from "./ResultFormatter.js"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types"; type ToolName = string; @@ -198,7 +197,7 @@ export class McpClient { const client = this.clients.get(toolName); if (client) { const result = await client.callTool({ name: toolName, arguments: toolArgs, signal: opts.abortSignal }); - toolMessage.content = ResultFormatter.format(result as CallToolResult); + toolMessage.content = ResultFormatter.format(result); } else { toolMessage.content = `Error: No session found for tool: ${toolName}`; } diff --git a/packages/mcp-client/src/ResultFormatter.ts b/packages/mcp-client/src/ResultFormatter.ts index c67c893731..04c93e9e7f 100644 --- a/packages/mcp-client/src/ResultFormatter.ts +++ b/packages/mcp-client/src/ResultFormatter.ts @@ -1,4 +1,8 @@ -import type { CallToolResult, TextResourceContents, BlobResourceContents } from "@modelcontextprotocol/sdk/types"; +import type { + TextResourceContents, + BlobResourceContents, + CompatibilityCallToolResult, +} from "@modelcontextprotocol/sdk/types"; /** * A utility class for formatting CallToolResult contents into human-readable text. @@ -13,7 +17,7 @@ export class ResultFormatter { * @param result The CallToolResult to format * @returns A human-readable string representation of the result contents */ - static format(result: CallToolResult): string { + static format(result: CompatibilityCallToolResult): string { if (!result.content || !Array.isArray(result.content) || result.content.length === 0) { return "[No content]"; } From 0ac2586ecab1d2408b821d0e00f5c52a0ca29bdf Mon Sep 17 00:00:00 2001 From: shaun smith <1936278+evalstate@users.noreply.github.com> Date: Tue, 13 May 2025 11:56:42 +0100 Subject: [PATCH 14/15] Update packages/mcp-client/src/utils.ts Co-authored-by: Julien Chaumond --- packages/mcp-client/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-client/src/utils.ts b/packages/mcp-client/src/utils.ts index 973880f25a..40ac04f839 100644 --- a/packages/mcp-client/src/utils.ts +++ b/packages/mcp-client/src/utils.ts @@ -34,7 +34,7 @@ export function urlToServerConfig(urlStr: string, token?: string): ServerConfig } // Check if we should include the token - const authToken = token || process.env.HF_TOKEN; + const authToken = token ?? process.env.HF_TOKEN; const shouldIncludeToken = !!authToken && (hostname.endsWith(".hf.space") || From 542cfa8ad34bcf68e5fd71dd12ae39b666b3f566 Mon Sep 17 00:00:00 2001 From: shaun smith <1936278+evalstate@users.noreply.github.com> Date: Tue, 13 May 2025 11:56:52 +0100 Subject: [PATCH 15/15] Update packages/mcp-client/src/utils.ts Co-authored-by: Julien Chaumond --- packages/mcp-client/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-client/src/utils.ts b/packages/mcp-client/src/utils.ts index 40ac04f839..05f29f9033 100644 --- a/packages/mcp-client/src/utils.ts +++ b/packages/mcp-client/src/utils.ts @@ -74,7 +74,7 @@ export function urlToServerConfig(urlStr: string, token?: string): ServerConfig // workaround for https://github.com/modelcontextprotocol/typescript-sdk/issues/436 eventSourceInit: { fetch: (url, init) => { - const reqHeaders = new Headers(init?.headers || {}); + const reqHeaders = new Headers(init?.headers ?? {}); reqHeaders.set("Authorization", `Bearer ${authToken}`); return fetch(url, { ...init,