Skip to content

Commit 5f4b160

Browse files
evalstatejulien-c
andauthored
SSE and Streaming Support POC (huggingface#1422)
Update to enable: - Connection via SSE (e.g. Gradio endpoints) - Connection via HTTP Streaming - Maintains compatibility with original STDIO config format - Concatenates TextContent/TextResourceContents from Results; describes ImageContent/BlobResourceContents - SSE Server definition contains workaround for modelcontextprotocol/typescript-sdk#436 to pass HF_TOKEN - Ability to connect to "Streaming HTTP" endpoints from the command line. use `pnpm agent --url https://<host>/mcp`. You can specify multiple endpoints with `--url <url1> --url <url2>` ![image](https://github.com/user-attachments/assets/13243a4d-8441-4e38-9465-f4bbb2d4aa89) --------- Co-authored-by: Julien Chaumond <[email protected]>
1 parent f60a851 commit 5f4b160

File tree

10 files changed

+479
-33
lines changed

10 files changed

+479
-33
lines changed

packages/mcp-client/cli.ts

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,26 @@ import { stdin, stdout } from "node:process";
44
import { join } from "node:path";
55
import { homedir } from "node:os";
66
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
7-
import type { InferenceProvider } from "@huggingface/inference";
8-
import { ANSI } from "./src/utils";
7+
import type { ServerConfig } from "./src/types";
8+
import type { InferenceProviderOrPolicy } from "@huggingface/inference";
9+
import { ANSI, urlToServerConfig } from "./src/utils";
910
import { Agent } from "./src";
1011
import { version as packageVersion } from "./package.json";
12+
import { parseArgs } from "node:util";
1113

1214
const MODEL_ID = process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct";
13-
const PROVIDER = (process.env.PROVIDER as InferenceProvider) ?? "nebius";
15+
const PROVIDER = (process.env.PROVIDER as InferenceProviderOrPolicy) ?? "nebius";
1416
const ENDPOINT_URL = process.env.ENDPOINT_URL ?? process.env.BASE_URL;
15-
const MCP_EXAMPLER_LOCAL_FOLDER = process.platform === "darwin" ? join(homedir(), "Desktop") : homedir();
1617

17-
const SERVERS: StdioServerParameters[] = [
18+
const SERVERS: (ServerConfig | StdioServerParameters)[] = [
1819
{
1920
// Filesystem "official" mcp-server with access to your Desktop
2021
command: "npx",
21-
args: ["-y", "@modelcontextprotocol/server-filesystem", MCP_EXAMPLER_LOCAL_FOLDER],
22+
args: [
23+
"-y",
24+
"@modelcontextprotocol/server-filesystem",
25+
process.platform === "darwin" ? join(homedir(), "Desktop") : homedir(),
26+
],
2227
},
2328
{
2429
// Playwright MCP
@@ -27,17 +32,28 @@ const SERVERS: StdioServerParameters[] = [
2732
},
2833
];
2934

30-
if (process.env.EXPERIMENTAL_HF_MCP_SERVER) {
31-
SERVERS.push({
32-
// Early version of a HF-MCP server
33-
// you can download it from gist.github.com/julien-c/0500ba922e1b38f2dc30447fb81f7dc6
34-
// and replace the local path below
35-
command: "node",
36-
args: ["--disable-warning=ExperimentalWarning", join(homedir(), "Desktop/hf-mcp/index.ts")],
37-
env: {
38-
HF_TOKEN: process.env.HF_TOKEN ?? "",
35+
// Handle --url parameters from command line: each URL will be parsed into a ServerConfig object
36+
const {
37+
values: { url: urls },
38+
} = parseArgs({
39+
options: {
40+
url: {
41+
type: "string",
42+
multiple: true,
3943
},
40-
});
44+
},
45+
});
46+
if (urls?.length) {
47+
while (SERVERS.length) {
48+
SERVERS.pop();
49+
}
50+
for (const url of urls) {
51+
try {
52+
SERVERS.push(urlToServerConfig(url));
53+
} catch (error) {
54+
console.error(`Error adding server from URL "${url}": ${error.message}`);
55+
}
56+
}
4157
}
4258

4359
async function main() {

packages/mcp-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@
5555
"dependencies": {
5656
"@huggingface/inference": "workspace:^",
5757
"@huggingface/tasks": "workspace:^",
58-
"@modelcontextprotocol/sdk": "^1.9.0"
58+
"@modelcontextprotocol/sdk": "^1.11.2"
5959
}
6060
}

packages/mcp-client/pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp-client/src/Agent.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { InferenceProvider } from "@huggingface/inference";
1+
import type { InferenceProviderOrPolicy } from "@huggingface/inference";
22
import type { ChatCompletionInputMessageTool } from "./McpClient";
33
import { McpClient } from "./McpClient";
44
import type { ChatCompletionInputMessage, ChatCompletionStreamOutput } from "@huggingface/tasks";
55
import type { ChatCompletionInputTool } from "@huggingface/tasks/src/tasks/chat-completion/inference";
66
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio";
77
import { debug } from "./utils";
8+
import type { ServerConfig } from "./types";
89

910
const DEFAULT_SYSTEM_PROMPT = `
1011
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 = {
4445
const exitLoopTools = [taskCompletionTool, askQuestionTool];
4546

4647
export class Agent extends McpClient {
47-
private readonly servers: StdioServerParameters[];
48+
private readonly servers: (ServerConfig | StdioServerParameters)[];
4849
protected messages: ChatCompletionInputMessage[];
4950

5051
constructor({
@@ -56,7 +57,7 @@ export class Agent extends McpClient {
5657
prompt,
5758
}: (
5859
| {
59-
provider: InferenceProvider;
60+
provider: InferenceProviderOrPolicy;
6061
endpointUrl?: undefined;
6162
}
6263
| {
@@ -66,7 +67,7 @@ export class Agent extends McpClient {
6667
) & {
6768
model: string;
6869
apiKey: string;
69-
servers: StdioServerParameters[];
70+
servers: (ServerConfig | StdioServerParameters)[];
7071
prompt?: string;
7172
}) {
7273
super(provider ? { provider, endpointUrl, model, apiKey } : { provider, endpointUrl, model, apiKey });

packages/mcp-client/src/McpClient.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import type {
1111
} from "@huggingface/tasks/src/tasks/chat-completion/inference";
1212
import { version as packageVersion } from "../package.json";
1313
import { debug } from "./utils";
14+
import type { ServerConfig } from "./types";
15+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport";
16+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
17+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
18+
import { ResultFormatter } from "./ResultFormatter.js";
1419

1520
type ToolName = string;
1621

@@ -52,15 +57,37 @@ export class McpClient {
5257
this.model = model;
5358
}
5459

55-
async addMcpServers(servers: StdioServerParameters[]): Promise<void> {
60+
async addMcpServers(servers: (ServerConfig | StdioServerParameters)[]): Promise<void> {
5661
await Promise.all(servers.map((s) => this.addMcpServer(s)));
5762
}
5863

59-
async addMcpServer(server: StdioServerParameters): Promise<void> {
60-
const transport = new StdioClientTransport({
61-
...server,
62-
env: { ...server.env, PATH: process.env.PATH ?? "" },
63-
});
64+
async addMcpServer(server: ServerConfig | StdioServerParameters): Promise<void> {
65+
let transport: Transport;
66+
const asUrl = (url: string | URL): URL => {
67+
return typeof url === "string" ? new URL(url) : url;
68+
};
69+
70+
if (!("type" in server)) {
71+
transport = new StdioClientTransport({
72+
...server,
73+
env: { ...server.env, PATH: process.env.PATH ?? "" },
74+
});
75+
} else {
76+
switch (server.type) {
77+
case "stdio":
78+
transport = new StdioClientTransport({
79+
...server.config,
80+
env: { ...server.config.env, PATH: process.env.PATH ?? "" },
81+
});
82+
break;
83+
case "sse":
84+
transport = new SSEClientTransport(asUrl(server.config.url), server.config.options);
85+
break;
86+
case "http":
87+
transport = new StreamableHTTPClientTransport(asUrl(server.config.url), server.config.options);
88+
break;
89+
}
90+
}
6491
const mcp = new Client({ name: "@huggingface/mcp-client", version: packageVersion });
6592
await mcp.connect(transport);
6693

@@ -170,7 +197,7 @@ export class McpClient {
170197
const client = this.clients.get(toolName);
171198
if (client) {
172199
const result = await client.callTool({ name: toolName, arguments: toolArgs, signal: opts.abortSignal });
173-
toolMessage.content = (result.content as Array<{ text: string }>)[0].text;
200+
toolMessage.content = ResultFormatter.format(result);
174201
} else {
175202
toolMessage.content = `Error: No session found for tool: ${toolName}`;
176203
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type {
2+
TextResourceContents,
3+
BlobResourceContents,
4+
CompatibilityCallToolResult,
5+
} from "@modelcontextprotocol/sdk/types";
6+
7+
/**
8+
* A utility class for formatting CallToolResult contents into human-readable text.
9+
* Processes different content types, extracts text, and summarizes binary data.
10+
*/
11+
export class ResultFormatter {
12+
/**
13+
* Formats a CallToolResult's contents into a single string.
14+
* - Text content is included directly
15+
* - Binary content (images, audio, blobs) is summarized
16+
*
17+
* @param result The CallToolResult to format
18+
* @returns A human-readable string representation of the result contents
19+
*/
20+
static format(result: CompatibilityCallToolResult): string {
21+
if (!result.content || !Array.isArray(result.content) || result.content.length === 0) {
22+
return "[No content]";
23+
}
24+
25+
const formattedParts: string[] = [];
26+
27+
for (const item of result.content) {
28+
switch (item.type) {
29+
case "text":
30+
// Extract text content directly
31+
formattedParts.push(item.text);
32+
break;
33+
34+
case "image": {
35+
// Summarize image content
36+
const imageSize = this.getBase64Size(item.data);
37+
formattedParts.push(
38+
`[Binary Content: Image ${item.mimeType}, ${imageSize} bytes]\nThe task is complete and the content accessible to the User`
39+
);
40+
break;
41+
}
42+
43+
case "audio": {
44+
// Summarize audio content
45+
const audioSize = this.getBase64Size(item.data);
46+
formattedParts.push(
47+
`[Binary Content: Audio ${item.mimeType}, ${audioSize} bytes]\nThe task is complete and the content accessible to the User`
48+
);
49+
break;
50+
}
51+
52+
case "resource":
53+
// Handle embedded resources - explicitly type the resource
54+
if ("text" in item.resource) {
55+
// It's a text resource with a text property
56+
const textResource = item.resource as TextResourceContents;
57+
formattedParts.push(textResource.text);
58+
} else if ("blob" in item.resource) {
59+
// It's a binary resource with a blob property
60+
const blobResource = item.resource as BlobResourceContents;
61+
const blobSize = this.getBase64Size(blobResource.blob);
62+
const uri = blobResource.uri ? ` (${blobResource.uri})` : "";
63+
const mimeType = blobResource.mimeType ? blobResource.mimeType : "unknown type";
64+
formattedParts.push(
65+
`[Binary Content${uri}: ${mimeType}, ${blobSize} bytes]\nThe task is complete and the content accessible to the User`
66+
);
67+
}
68+
break;
69+
}
70+
}
71+
72+
return formattedParts.join("\n");
73+
}
74+
75+
/**
76+
* Calculates the approximate size in bytes of base64-encoded data
77+
*/
78+
private static getBase64Size(base64: string): number {
79+
// Remove base64 header if present (e.g., data:image/png;base64,)
80+
const cleanBase64 = base64.includes(",") ? base64.split(",")[1] : base64;
81+
82+
// Calculate size: Base64 encodes 3 bytes into 4 characters
83+
const padding = cleanBase64.endsWith("==") ? 2 : cleanBase64.endsWith("=") ? 1 : 0;
84+
return Math.floor((cleanBase64.length * 3) / 4 - padding);
85+
}
86+
}

packages/mcp-client/src/types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// src/types.ts
2+
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js";
3+
import type { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
4+
import type { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5+
6+
/** StdioServerParameters is usable as-is */
7+
8+
/**
9+
* Configuration for an SSE MCP server
10+
*/
11+
export interface SSEServerConfig {
12+
url: string | URL;
13+
options?: SSEClientTransportOptions;
14+
}
15+
16+
/**
17+
* Configuration for a StreamableHTTP MCP server
18+
*/
19+
export interface StreamableHTTPServerConfig {
20+
url: string | URL;
21+
options?: StreamableHTTPClientTransportOptions;
22+
}
23+
24+
/**
25+
* Discriminated union type for different MCP server types
26+
*/
27+
export type ServerConfig =
28+
| { type: "stdio"; config: StdioServerParameters }
29+
| { type: "sse"; config: SSEServerConfig }
30+
| { type: "http"; config: StreamableHTTPServerConfig };

0 commit comments

Comments
 (0)