@llmindset/hf-mcp 0.2.53 → 0.2.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export type ParsedSchemaFormat = 'array' | 'object';
3
+ export interface ParsedGradioSchema {
4
+ format: ParsedSchemaFormat;
5
+ tools: Array<{
6
+ name: string;
7
+ description?: string;
8
+ inputSchema: unknown;
9
+ }>;
10
+ }
11
+ export declare function parseGradioSchemaResponse(schemaResponse: unknown): ParsedGradioSchema;
12
+ export declare function normalizeParsedTools(parsed: ParsedGradioSchema): Tool[];
13
+ //# sourceMappingURL=gradio-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradio-schema.d.ts","sourceRoot":"","sources":["../../../src/space/utils/gradio-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC3E;AAUD,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,OAAO,GAAG,kBAAkB,CA2CrF;AAKD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,EAAE,CAQvE"}
@@ -0,0 +1,44 @@
1
+ export function parseGradioSchemaResponse(schemaResponse) {
2
+ if (Array.isArray(schemaResponse)) {
3
+ const tools = schemaResponse.filter((tool) => {
4
+ return (typeof tool === 'object' &&
5
+ tool !== null &&
6
+ 'name' in tool &&
7
+ typeof tool.name === 'string' &&
8
+ 'inputSchema' in tool);
9
+ });
10
+ if (tools.length === 0) {
11
+ throw new Error('Invalid schema: no tools found in array schema');
12
+ }
13
+ return {
14
+ format: 'array',
15
+ tools,
16
+ };
17
+ }
18
+ if (typeof schemaResponse === 'object' && schemaResponse !== null) {
19
+ const entries = Object.entries(schemaResponse);
20
+ const tools = entries.map(([name, toolSchema]) => ({
21
+ name,
22
+ description: typeof toolSchema.description === 'string' ? toolSchema.description : undefined,
23
+ inputSchema: toolSchema,
24
+ }));
25
+ if (tools.length === 0) {
26
+ throw new Error('Invalid schema: no tools found in object schema');
27
+ }
28
+ return {
29
+ format: 'object',
30
+ tools,
31
+ };
32
+ }
33
+ throw new Error('Invalid schema format: expected array or object');
34
+ }
35
+ export function normalizeParsedTools(parsed) {
36
+ return parsed.tools
37
+ .filter((t) => !t.name.toLowerCase().includes('<lambda'))
38
+ .map((parsedTool) => ({
39
+ name: parsedTool.name,
40
+ description: parsedTool.description || `${parsedTool.name} tool`,
41
+ inputSchema: parsedTool.inputSchema,
42
+ }));
43
+ }
44
+ //# sourceMappingURL=gradio-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gradio-schema.js","sourceRoot":"","sources":["../../../src/space/utils/gradio-schema.ts"],"names":[],"mappings":"AAiBA,MAAM,UAAU,yBAAyB,CAAC,cAAuB;IAEhE,IAAI,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;QACnC,MAAM,KAAK,GAAI,cAAiC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAwE,EAAE;YACtI,OAAO,CACN,OAAO,IAAI,KAAK,QAAQ;gBACxB,IAAI,KAAK,IAAI;gBACb,MAAM,IAAI,IAAI;gBACd,OAAQ,IAA2B,CAAC,IAAI,KAAK,QAAQ;gBACrD,aAAa,IAAI,IAAI,CACrB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACnE,CAAC;QAED,OAAO;YACN,MAAM,EAAE,OAAO;YACf,KAAK;SACL,CAAC;IACH,CAAC;IAGD,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,KAAK,IAAI,EAAE,CAAC;QACnE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,cAAyC,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAgC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,CAAC;YAC/E,IAAI;YACJ,WAAW,EAAE,OAAQ,UAAwC,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAE,UAAuC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YACzJ,WAAW,EAAE,UAAU;SACvB,CAAC,CAAC,CAAC;QAEJ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACpE,CAAC;QAED,OAAO;YACN,MAAM,EAAE,QAAQ;YAChB,KAAK;SACL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;AACpE,CAAC;AAKD,MAAM,UAAU,oBAAoB,CAAC,MAA0B;IAC9D,OAAO,MAAM,CAAC,KAAK;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;SACxD,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACrB,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,WAAW,EAAE,UAAU,CAAC,WAAW,IAAI,GAAG,UAAU,CAAC,IAAI,OAAO;QAChE,WAAW,EAAE,UAAU,CAAC,WAAkC;KAC1D,CAAC,CAAC,CAAC;AACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmindset/hf-mcp",
3
- "version": "0.2.53",
3
+ "version": "0.2.54",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -21,6 +21,8 @@ export * from './readme-utils.js';
21
21
  export * from './use-space.js';
22
22
  export * from './jobs/jobs-tool.js';
23
23
  export * from './space/dynamic-space-tool.js';
24
+ export * from './space/utils/gradio-caller.js';
25
+ export * from './space/utils/gradio-schema.js';
24
26
 
25
27
  // Export shared types
26
28
  export * from './types/tool-result.js';
@@ -1,6 +1,6 @@
1
1
  import type { ToolResult } from '../../types/tool-result.js';
2
2
  import { escapeMarkdown } from '../../utilities.js';
3
- import { VIEW_PARAMETERS } from '../dynamic-space-tool.js';
3
+ import { VIEW_PARAMETERS } from '../types.js';
4
4
 
5
5
  /**
6
6
  * Prompt configuration for discover operation (from DYNAMIC_SPACE_DATA)
@@ -1,12 +1,11 @@
1
1
  import type { ToolResult } from '../../types/tool-result.js';
2
2
  import type { InvokeResult } from '../types.js';
3
3
  import type { Tool, ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js';
4
- import type { RequestHandlerExtra, RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
5
- import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
6
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
- import { SSEClientTransport, type SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
4
+ import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
8
5
  import { analyzeSchemaComplexity, validateParameters, applyDefaults } from '../utils/schema-validator.js';
9
6
  import { formatComplexSchemaError, formatValidationError } from '../utils/parameter-formatter.js';
7
+ import { callGradioToolWithHeaders } from '../utils/gradio-caller.js';
8
+ import { parseGradioSchemaResponse, normalizeParsedTools } from '../utils/gradio-schema.js';
10
9
 
11
10
  /**
12
11
  * Invokes a Gradio space with provided parameters
@@ -88,44 +87,11 @@ export async function invokeSpace(
88
87
  // Step 7: Apply default values for missing optional parameters
89
88
  const finalParameters = applyDefaults(inputParameters, schemaResult);
90
89
 
91
- // Step 8: Create SSE connection and invoke tool
92
- const sseUrl = `https://${metadata.subdomain}.hf.space/gradio_api/mcp/sse`;
93
- const client = await createLazyConnection(sseUrl, hfToken);
94
-
95
- try {
96
- // Check if the client is requesting progress notifications
97
- const progressToken = extra?._meta?.progressToken;
98
- const requestOptions: RequestOptions = {};
99
-
100
- if (progressToken !== undefined && extra) {
101
- // Set up progress relay from remote tool to our client
102
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
103
- requestOptions.onprogress = async (progress) => {
104
- // Relay the progress notification to our client
105
- await extra.sendNotification({
106
- method: 'notifications/progress',
107
- params: {
108
- progressToken,
109
- progress: progress.progress,
110
- total: progress.total,
111
- message: progress.message,
112
- },
113
- });
114
- };
115
- }
116
-
117
- const result = await client.request(
118
- {
119
- method: 'tools/call',
120
- params: {
121
- name: tool.name,
122
- arguments: finalParameters,
123
- _meta: progressToken !== undefined ? { progressToken } : undefined,
124
- },
125
- },
126
- CallToolResultSchema,
127
- requestOptions
128
- );
90
+ // Step 8: Create SSE connection and invoke tool (shared helper)
91
+ const sseUrl = `https://${metadata.subdomain}.hf.space/gradio_api/mcp/sse`;
92
+ const { result } = await callGradioToolWithHeaders(sseUrl, tool.name, finalParameters, hfToken, extra, {
93
+ logProxiedReplica: true,
94
+ });
129
95
 
130
96
  // Return raw MCP result with warnings if any
131
97
  // This ensures the space tool behaves identically to proxied gr_* tools
@@ -136,10 +102,6 @@ export async function invokeSpace(
136
102
  resultsShared: 1,
137
103
  isError: result.isError,
138
104
  };
139
- } finally {
140
- // Clean up connection
141
- await client.close();
142
- }
143
105
  } catch (error) {
144
106
  const errorMessage = error instanceof Error ? error.message : String(error);
145
107
  return {
@@ -230,106 +192,10 @@ async function fetchGradioSchema(subdomain: string, isPrivate: boolean, hfToken?
230
192
 
231
193
  const schemaResponse = (await response.json()) as unknown;
232
194
 
233
- // Parse schema response (handle both array and object formats)
234
- return parseSchemaResponse(schemaResponse);
235
- } finally {
236
- clearTimeout(timeoutId);
237
- }
238
- }
239
-
240
- /**
241
- * Parses schema response and extracts tools
242
- */
243
- function parseSchemaResponse(schemaResponse: unknown): Tool[] {
244
- const tools: Tool[] = [];
245
-
246
- if (Array.isArray(schemaResponse)) {
247
- // Array format: [{ name: "toolName", description: "...", inputSchema: {...} }, ...]
248
- for (const item of schemaResponse) {
249
- if (
250
- typeof item === 'object' &&
251
- item !== null &&
252
- 'name' in item &&
253
- 'inputSchema' in item
254
- ) {
255
- const itemRecord = item as Record<string, unknown>;
256
- if (typeof itemRecord.name === 'string') {
257
- const tool = itemRecord as { name: string; description?: string; inputSchema: unknown };
258
- tools.push({
259
- name: tool.name,
260
- description: tool.description || `${tool.name} tool`,
261
- inputSchema: {
262
- type: 'object',
263
- ...(tool.inputSchema as Record<string, unknown>),
264
- },
265
- });
266
- }
267
- }
268
- }
269
- } else if (typeof schemaResponse === 'object' && schemaResponse !== null) {
270
- // Object format: { "toolName": { properties: {...}, required: [...] }, ... }
271
- for (const [name, toolSchema] of Object.entries(schemaResponse)) {
272
- if (typeof toolSchema === 'object' && toolSchema !== null) {
273
- const schema = toolSchema as { description?: string };
274
- tools.push({
275
- name,
276
- description: schema.description || `${name} tool`,
277
- inputSchema: {
278
- type: 'object',
279
- ...(toolSchema as Record<string, unknown>),
280
- },
281
- });
282
- }
283
- }
284
- }
285
-
286
- return tools.filter((tool) => !tool.name.toLowerCase().includes('<lambda'));
287
- }
288
-
289
- /**
290
- * Creates SSE connection to endpoint for tool execution
291
- */
292
- async function createLazyConnection(sseUrl: string, hfToken?: string): Promise<Client> {
293
- // Create MCP client
294
- const remoteClient = new Client(
295
- {
296
- name: 'hf-mcp-space-client',
297
- version: '1.0.0',
298
- },
299
- {
300
- capabilities: {},
195
+ // Parse schema response (handle both array and object formats)
196
+ const parsed = parseGradioSchemaResponse(schemaResponse);
197
+ return normalizeParsedTools(parsed);
198
+ } finally {
199
+ clearTimeout(timeoutId);
301
200
  }
302
- );
303
-
304
- // Create SSE transport with HF token if available
305
- const transportOptions: SSEClientTransportOptions = {};
306
- if (hfToken) {
307
- const headerName = 'X-HF-Authorization';
308
- const customHeaders = {
309
- [headerName]: `Bearer ${hfToken}`,
310
- };
311
-
312
- // Headers for POST requests
313
- transportOptions.requestInit = {
314
- headers: customHeaders,
315
- };
316
-
317
- // Headers for SSE connection
318
- transportOptions.eventSourceInit = {
319
- fetch: (url, init) => {
320
- const headers = new Headers(init.headers);
321
- Object.entries(customHeaders).forEach(([key, value]) => {
322
- headers.set(key, value);
323
- });
324
- return fetch(url.toString(), { ...init, headers });
325
- },
326
- };
327
201
  }
328
-
329
- const transport = new SSEClientTransport(new URL(sseUrl), transportOptions);
330
-
331
- // Connect the client to the transport
332
- await remoteClient.connect(transport);
333
-
334
- return remoteClient;
335
- }
@@ -10,6 +10,7 @@ import {
10
10
  getOperationNames,
11
11
  getSpaceArgsSchema,
12
12
  VIEW_PARAMETERS,
13
+ FILE_HANDLING_TEXT,
13
14
  } from './types.js';
14
15
  import { findSpaces } from './commands/dynamic-find.js';
15
16
  import { discoverSpaces } from './commands/discover.js';
@@ -25,11 +26,6 @@ export * from './types.js';
25
26
  const USAGE_INSTRUCTIONS = `# Gradio Space Interaction
26
27
 
27
28
  Dynamically interact with any Gradio MCP Space. Find spaces, view space parameter schemas, and invoke spaces.
28
-
29
- ## Supported Schema Types
30
-
31
- ✅ **Simple types** (supported):
32
- - Strings, numbers, booleans
33
29
  - Enums (predefined value sets)
34
30
  - Arrays of primitives
35
31
  - Shallow objects (one level deep)
@@ -79,17 +75,9 @@ Execute a space's first tool with provided parameters.
79
75
  1. **Find Spaces** - Use \`find\` to find MCP-enabled spaces for your task
80
76
  2. **Inspect Parameters** - Use \`${VIEW_PARAMETERS}\` to see what a space accepts
81
77
  3. **Invoke the Space** - Use \`invoke\` with the required parameters
82
-
83
- ## File Handling
84
-
85
78
  For parameters that accept files (FileData types):
86
79
  - Provide a publicly accessible URL (http:// or https://)
87
80
  - Example: \`{"image": "https://example.com/photo.jpg"}\`
88
- - Outputs from one tool may be used as inputs to another
89
-
90
- ## Tips
91
-
92
- - Focus searches on specific tasks (e.g., "video generation", "object detection")
93
81
  - The tool automatically applies default values for optional parameters
94
82
  - Unknown parameters generate warnings but are still passed through (permissive inputs)
95
83
  - Enum parameters show all allowed values in ${VIEW_PARAMETERS} output
@@ -99,9 +87,17 @@ For parameters that accept files (FileData types):
99
87
  /**
100
88
  * Usage instructions for dynamic mode (when DYNAMIC_SPACE_DATA is set)
101
89
  */
102
- const DYNAMIC_USAGE_INSTRUCTIONS = `# Gradio Space Interaction
90
+ const DYNAMIC_USAGE_INSTRUCTIONS = `# Hugging Face Space Dynamic Use
91
+
92
+ Perform Tasks using Hugging Face Spaces.
93
+
94
+ ## Workflow
95
+
96
+ 1. **Discover Taks and Spaces** - Use \`discover\` operation to see available spaces
97
+ 2. **View Parameters** - Use \`${VIEW_PARAMETERS}\` operation to inspect parameter schema
98
+ 3. **Invoke the Space** - Use \`invoke\` operation with the necessary parameters
103
99
 
104
- Use Hugging Face. Discover available spaces, view their parameter schemas, and invoke them. Use "discover" to find recommended spaces for tasks.
100
+ ${FILE_HANDLING_TEXT}
105
101
 
106
102
  ## Available Operations
107
103
 
@@ -116,7 +112,7 @@ List recommended spaces and their categories.
116
112
  \`\`\`
117
113
 
118
114
  ### ${VIEW_PARAMETERS}
119
- Display the parameter schema for a space's first tool.
115
+ Display the parameter schema for the Space.
120
116
 
121
117
  **Example:**
122
118
  \`\`\`json
@@ -127,7 +123,7 @@ Display the parameter schema for a space's first tool.
127
123
  \`\`\`
128
124
 
129
125
  ### invoke
130
- Execute a space's first tool with provided parameters.
126
+ Execute a Task on a Space.
131
127
 
132
128
  **Example:**
133
129
  \`\`\`json
@@ -138,18 +134,7 @@ Execute a space's first tool with provided parameters.
138
134
  }
139
135
  \`\`\`
140
136
 
141
- ## Workflow
142
-
143
- 1. **Discover Spaces** - Use \`discover\` to see available spaces
144
- 2. **Inspect Parameters** - Use \`${VIEW_PARAMETERS}\` to see what a space accepts
145
- 3. **Invoke the Space** - Use \`invoke\` with the required parameters
146
-
147
- ## File Handling
148
137
 
149
- For parameters that accept files (FileData types):
150
- - Provide a publicly accessible URL (http:// or https://)
151
- - Example: \`{"image": "https://example.com/photo.jpg"}\`
152
- - Output url's from one tool may be used as inputs to another.
153
138
  `;
154
139
 
155
140
  /**
@@ -173,10 +158,10 @@ export function getDynamicSpaceToolConfig(): {
173
158
  return {
174
159
  name: 'dynamic_space',
175
160
  description: dynamicMode
176
- ? 'Perform tasks with Hugging Face Spaces. Use "discover" to find available tasks for the User. Task types include Image Generation, Background Removal, Text to Speech, OCR and more ' +
177
- 'Call with no operation for full usage instructions.'
161
+ ? 'Perform Tasks with Hugging Face Spaces. Use "discover" to view available Tasks. Examples are Image Generation/Editing, Background Removal, Text to Speech, OCR and many more. ' +
162
+ 'Call with no arguments for full usage instructions.'
178
163
  : 'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Hugging Face Spaces. ' +
179
- 'Call with no operation for full usage instructions.',
164
+ 'Call with no arguments for full usage instructions.',
180
165
  schema: getSpaceArgsSchema(),
181
166
  annotations: {
182
167
  title: 'Dynamically use Hugging Face Spaces',
@@ -12,6 +12,21 @@ export const DYNAMIC_OPERATION_NAMES = ['discover', VIEW_PARAMETERS, 'invoke'] a
12
12
  export type OperationName = (typeof OPERATION_NAMES)[number];
13
13
  export type DynamicOperationName = (typeof DYNAMIC_OPERATION_NAMES)[number];
14
14
 
15
+ /**
16
+ * File input help message constant
17
+ */
18
+ export const FILE_INPUT_HELP_MESSAGE =
19
+ 'Provide a publicly accessible URL (http:// or https://) for the file input. ' +
20
+ "Previously generated URL's are usable. ";
21
+
22
+ /**
23
+ * File handling instructions for user-facing documentation
24
+ */
25
+ export const FILE_HANDLING_TEXT = `For parameters that accept files (FileData, Images, file URL types):
26
+ - Provide a publicly accessible URL (http:// or https://)
27
+ - Example: \`{"image": "https://example.com/photo.jpg"}\`
28
+ - URL's generated by one Space can be used as inputs to another Space.`;
29
+
15
30
  /**
16
31
  * Check if dynamic space mode is enabled
17
32
  */
@@ -34,10 +49,8 @@ export const spaceArgsSchema = z.object({
34
49
  space_name: z
35
50
  .string()
36
51
  .optional()
37
- .describe(
38
- `The Hugging Face space ID (format: "username/space-name"). Required for ${VIEW_PARAMETERS} and invoke operations.`
39
- ),
40
- parameters: z.string().optional().describe('For invoke operation: JSON object string of parameters'),
52
+ .describe(`Space ID (format: "username/space-name"). Required for operation = ${VIEW_PARAMETERS} or invoke.`),
53
+ parameters: z.string().optional().describe('Required for invoke operation: JSON object string of parameters'),
41
54
  search_query: z
42
55
  .string()
43
56
  .optional()
@@ -56,7 +69,7 @@ export const dynamicSpaceArgsSchema = z.object({
56
69
  .string()
57
70
  .optional()
58
71
  .describe(`Space ID (format: "username/space-name"). Required for "${VIEW_PARAMETERS}" and "invoke" operations.`),
59
- parameters: z.string().optional().describe('JSON object string of parameters. Only required for "invoke" operation.'),
72
+ parameters: z.string().optional().describe('JSON object string of parameters. Only used for "invoke" operation.'),
60
73
  });
61
74
 
62
75
  /**
@@ -130,13 +143,6 @@ export interface JsonSchema {
130
143
  [key: string]: unknown;
131
144
  }
132
145
 
133
- /**
134
- * File input help message constant
135
- */
136
- export const FILE_INPUT_HELP_MESSAGE =
137
- 'Provide a publicly accessible URL (http:// or https://) for the Gradio file input. ' +
138
- "Content previously generated by 'invoke' is usable, as well as Hub Repository URLs. ";
139
-
140
146
  /**
141
147
  * Check if a property is a FileData type
142
148
  */
@@ -0,0 +1,204 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { SSEClientTransport, type SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
3
+ import { CallToolResultSchema, type ServerNotification, type ServerRequest } from '@modelcontextprotocol/sdk/types.js';
4
+ import type { RequestHandlerExtra, RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
5
+
6
+ export interface GradioCallResult {
7
+ result: typeof CallToolResultSchema._type;
8
+ capturedHeaders: Record<string, string>;
9
+ }
10
+
11
+ export interface GradioCallOptions {
12
+ /** Called for every response to capture custom headers */
13
+ onHeaders?: (headers: Headers) => void;
14
+ /** Log the X-Proxied-Replica header to stderr once */
15
+ logProxiedReplica?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Extract the replica ID from the X-Proxied-Replica header.
20
+ * Example: "oyerizs4-dspr4" => "dspr4"
21
+ */
22
+ export function extractReplicaId(headerValue: string | undefined): string | null {
23
+ if (!headerValue) return null;
24
+ const parts = headerValue.split('-');
25
+ if (parts.length < 2) return null;
26
+ const replicaId = parts[parts.length - 1];
27
+ if (!replicaId || replicaId.trim() === '') return null;
28
+ return replicaId;
29
+ }
30
+
31
+ /**
32
+ * Rewrites any Gradio API URLs in text content to include the replica path segment.
33
+ * Example: https://mcp-tools-qwen-image-fast.hf.space/gradio_api =>
34
+ * https://mcp-tools-qwen-image-fast.hf.space/--replicas/<replica_id>/gradio_api
35
+ */
36
+ export function rewriteReplicaUrlsInResult(
37
+ result: typeof CallToolResultSchema._type,
38
+ proxiedReplicaHeader: string | undefined
39
+ ): typeof CallToolResultSchema._type {
40
+ if (process.env.NO_REPLICA_REWRITE) return result;
41
+ const replicaId = extractReplicaId(proxiedReplicaHeader);
42
+ if (!replicaId) return result;
43
+
44
+ const urlPattern = /https:\/\/([^\s"']+)\/gradio_api(\S*)?/g;
45
+
46
+ const rewriteText = (text: string): string =>
47
+ text.replace(urlPattern, (_match, host, rest = '') => {
48
+ return `https://${host}/--replicas/${replicaId}/gradio_api${rest}`;
49
+ });
50
+
51
+ let changed = false;
52
+ const newContent = result.content.map((item) => {
53
+ if (typeof item === 'string') {
54
+ const rewritten = rewriteText(item);
55
+ if (rewritten !== item) {
56
+ changed = true;
57
+ return { type: 'text', text: rewritten } as (typeof result.content)[number];
58
+ }
59
+ return { type: 'text', text: item } as (typeof result.content)[number];
60
+ }
61
+
62
+ if (item && typeof item === 'object' && 'text' in item && typeof item.text === 'string') {
63
+ const rewritten = rewriteText(item.text);
64
+ if (rewritten !== item.text) {
65
+ changed = true;
66
+ return { ...item, text: rewritten };
67
+ }
68
+ }
69
+
70
+ return item;
71
+ });
72
+
73
+ if (!changed) return result;
74
+ return {
75
+ ...result,
76
+ content: newContent,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Shared helper to call a Gradio MCP tool over SSE, capturing response headers (including X-Proxied-Replica).
82
+ * This handles SSE setup, optional progress relay, and cleans up the client connection.
83
+ */
84
+ export async function callGradioToolWithHeaders(
85
+ sseUrl: string,
86
+ toolName: string,
87
+ parameters: Record<string, unknown>,
88
+ hfToken: string | undefined,
89
+ extra: RequestHandlerExtra<ServerRequest, ServerNotification> | undefined,
90
+ options: GradioCallOptions = {}
91
+ ): Promise<GradioCallResult> {
92
+ const capturedHeaders: Record<string, string> = {};
93
+ let loggedHeader = false;
94
+
95
+ const handleHeaders = (headers: Headers): void => {
96
+ const proxiedReplica = headers.get('x-proxied-replica') ?? '';
97
+ if (proxiedReplica) {
98
+ capturedHeaders['x-proxied-replica'] = proxiedReplica;
99
+ }
100
+ if (options.logProxiedReplica && !loggedHeader) {
101
+ loggedHeader = true;
102
+ }
103
+ options.onHeaders?.(headers);
104
+ };
105
+
106
+ const captureHeadersFetch: SSEClientTransportOptions['fetch'] = async (url, init) => {
107
+ const response = await fetch(url, init);
108
+ handleHeaders(response.headers);
109
+ return response;
110
+ };
111
+
112
+ type EventSourceFetch = NonNullable<SSEClientTransportOptions['eventSourceInit']>['fetch'];
113
+ const buildEventSourceFetch =
114
+ (extraHeaders?: Record<string, string>): EventSourceFetch =>
115
+ (url, init) => {
116
+ const headers = new Headers(init?.headers);
117
+ if (extraHeaders) {
118
+ Object.entries(extraHeaders).forEach(([key, value]) => headers.set(key, value));
119
+ }
120
+ const requestInit: RequestInit = { ...(init as RequestInit), headers };
121
+ return captureHeadersFetch(url.toString(), requestInit);
122
+ };
123
+
124
+ // Create MCP client
125
+ const remoteClient = new Client(
126
+ {
127
+ name: 'hf-mcp-gradio-client',
128
+ version: '1.0.0',
129
+ },
130
+ {
131
+ capabilities: {},
132
+ }
133
+ );
134
+
135
+ // Create SSE transport with HF token if available
136
+ const transportOptions: SSEClientTransportOptions = {
137
+ fetch: captureHeadersFetch,
138
+ };
139
+ if (hfToken) {
140
+ const headerName = 'X-HF-Authorization';
141
+ const customHeaders = {
142
+ [headerName]: `Bearer ${hfToken}`,
143
+ };
144
+
145
+ // Headers for POST requests
146
+ transportOptions.requestInit = {
147
+ headers: customHeaders,
148
+ };
149
+
150
+ // Headers for SSE connection
151
+ transportOptions.eventSourceInit = {
152
+ fetch: buildEventSourceFetch(customHeaders),
153
+ };
154
+ } else {
155
+ transportOptions.eventSourceInit = {
156
+ fetch: buildEventSourceFetch(),
157
+ };
158
+ }
159
+
160
+ const transport = new SSEClientTransport(new URL(sseUrl), transportOptions);
161
+ await remoteClient.connect(transport);
162
+
163
+ try {
164
+ // Check if the client is requesting progress notifications
165
+ const progressToken = extra?._meta?.progressToken;
166
+ const requestOptions: RequestOptions = {};
167
+
168
+ if (progressToken !== undefined && extra) {
169
+ // Fire-and-forget; best-effort relay
170
+ requestOptions.onprogress = (progress) => {
171
+ void extra.sendNotification({
172
+ method: 'notifications/progress',
173
+ params: {
174
+ progressToken,
175
+ progress: progress.progress,
176
+ total: progress.total,
177
+ message: progress.message,
178
+ },
179
+ });
180
+ };
181
+ requestOptions.resetTimeoutOnProgress = true;
182
+ }
183
+
184
+ const result = await remoteClient.request(
185
+ {
186
+ method: 'tools/call',
187
+ params: {
188
+ name: toolName,
189
+ arguments: parameters,
190
+ _meta: progressToken !== undefined ? { progressToken } : undefined,
191
+ },
192
+ },
193
+ CallToolResultSchema,
194
+ requestOptions
195
+ );
196
+
197
+ const proxiedReplica = capturedHeaders['x-proxied-replica'];
198
+ const rewritten = rewriteReplicaUrlsInResult(result, proxiedReplica);
199
+
200
+ return { result: rewritten, capturedHeaders };
201
+ } finally {
202
+ await remoteClient.close();
203
+ }
204
+ }