@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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/space/commands/discover.js +1 -1
- package/dist/space/commands/discover.js.map +1 -1
- package/dist/space/commands/invoke.d.ts.map +1 -1
- package/dist/space/commands/invoke.js +14 -110
- package/dist/space/commands/invoke.js.map +1 -1
- package/dist/space/dynamic-space-tool.d.ts.map +1 -1
- package/dist/space/dynamic-space-tool.js +16 -32
- package/dist/space/dynamic-space-tool.js.map +1 -1
- package/dist/space/types.d.ts +2 -1
- package/dist/space/types.d.ts.map +1 -1
- package/dist/space/types.js +9 -5
- package/dist/space/types.js.map +1 -1
- package/dist/space/utils/gradio-caller.d.ts +14 -0
- package/dist/space/utils/gradio-caller.d.ts.map +1 -0
- package/dist/space/utils/gradio-caller.js +138 -0
- package/dist/space/utils/gradio-caller.js.map +1 -0
- package/dist/space/utils/gradio-schema.d.ts +13 -0
- package/dist/space/utils/gradio-schema.d.ts.map +1 -0
- package/dist/space/utils/gradio-schema.js +44 -0
- package/dist/space/utils/gradio-schema.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/space/commands/discover.ts +1 -1
- package/src/space/commands/invoke.ts +13 -147
- package/src/space/dynamic-space-tool.ts +16 -31
- package/src/space/types.ts +18 -12
- package/src/space/utils/gradio-caller.ts +204 -0
- package/src/space/utils/gradio-schema.ts +74 -0
- package/test/gradio-caller.test.ts +77 -0
|
@@ -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
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 '../
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 = `#
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
177
|
-
'Call with no
|
|
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
|
|
164
|
+
'Call with no arguments for full usage instructions.',
|
|
180
165
|
schema: getSpaceArgsSchema(),
|
|
181
166
|
annotations: {
|
|
182
167
|
title: 'Dynamically use Hugging Face Spaces',
|
package/src/space/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
}
|