Skip to content

Commit ef347a7

Browse files
authored
Merge pull request #127 from huggingface/claude/document-repo-guidelines-018EP3YubH17b8RkUQ9NgPMS
New, simpler non-search discover tool
2 parents 0378484 + 7bf1284 commit ef347a7

File tree

4 files changed

+289
-24
lines changed

4 files changed

+289
-24
lines changed

packages/app/src/server/mcp-server.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import {
5656
type DocFetchParams,
5757
HF_JOBS_TOOL_CONFIG,
5858
HfJobsTool,
59-
DYNAMIC_SPACE_TOOL_CONFIG,
59+
getDynamicSpaceToolConfig,
6060
SpaceTool,
6161
type SpaceArgs,
6262
type InvokeResult,
@@ -820,11 +820,13 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
820820
}
821821
);
822822

823-
toolInstances[DYNAMIC_SPACE_TOOL_CONFIG.name] = server.tool(
824-
DYNAMIC_SPACE_TOOL_CONFIG.name,
825-
DYNAMIC_SPACE_TOOL_CONFIG.description,
826-
DYNAMIC_SPACE_TOOL_CONFIG.schema.shape,
827-
DYNAMIC_SPACE_TOOL_CONFIG.annotations,
823+
// Get dynamic config based on environment (uses DYNAMIC_SPACE_DATA env var)
824+
const dynamicSpaceToolConfig = getDynamicSpaceToolConfig();
825+
toolInstances[dynamicSpaceToolConfig.name] = server.tool(
826+
dynamicSpaceToolConfig.name,
827+
dynamicSpaceToolConfig.description,
828+
dynamicSpaceToolConfig.schema.shape,
829+
dynamicSpaceToolConfig.annotations,
828830
async (params: SpaceArgs, extra) => {
829831
// Check if invoke operation is disabled by gradio=none
830832
const { gradio } = extractAuthBouquetAndMix(headers);
@@ -857,8 +859,8 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
857859
noImageContentHeaderEnabled || toolSelection.enabledToolIds.includes('NO_GRADIO_IMAGE_CONTENT');
858860
const postProcessOptions: GradioToolCallOptions = {
859861
stripImageContent,
860-
toolName: DYNAMIC_SPACE_TOOL_CONFIG.name,
861-
outwardFacingName: DYNAMIC_SPACE_TOOL_CONFIG.name,
862+
toolName: dynamicSpaceToolConfig.name,
863+
outwardFacingName: dynamicSpaceToolConfig.name,
862864
sessionInfo,
863865
spaceName: params.space_name,
864866
};
@@ -904,7 +906,7 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
904906
success = !toolResult.isError;
905907

906908
const durationMs = Date.now() - startTime;
907-
logSearchQuery(DYNAMIC_SPACE_TOOL_CONFIG.name, loggedOperation, params, {
909+
logSearchQuery(dynamicSpaceToolConfig.name, loggedOperation, params, {
908910
...getLoggingOptions(),
909911
totalResults: toolResult.totalResults,
910912
resultsShared: toolResult.resultsShared,
@@ -935,7 +937,7 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
935937
const toolResult = await runWithQueryLogging(
936938
logSearchQuery,
937939
{
938-
methodName: DYNAMIC_SPACE_TOOL_CONFIG.name,
940+
methodName: dynamicSpaceToolConfig.name,
939941
query: loggedOperation,
940942
parameters: params,
941943
baseOptions: getLoggingOptions(),
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { ToolResult } from '../../types/tool-result.js';
2+
import { escapeMarkdown } from '../../utilities.js';
3+
4+
/**
5+
* Prompt configuration for discover operation (from DYNAMIC_SPACE_DATA)
6+
* These prompts can be easily tweaked to adjust behavior
7+
*/
8+
export const DISCOVER_PROMPTS = {
9+
// Header for results
10+
RESULTS_HEADER: `**Available Spaces:**
11+
12+
These spaces can be invoked using the \`dynamic_space\` tool.
13+
Use \`"operation": "view_parameters"\` to inspect a space's parameters before invoking.
14+
15+
`,
16+
17+
// No results message
18+
NO_RESULTS: `No spaces available in the configured list.`,
19+
20+
// Error fetching
21+
FETCH_ERROR: (url: string, error: string): string => `Error fetching space list from ${url}: ${error}`,
22+
};
23+
24+
/**
25+
* Parse CSV content into space entries
26+
* Expected format: space_id,category,description
27+
*/
28+
function parseCsvContent(content: string): Array<{ id: string; category: string; description: string }> {
29+
const lines = content.trim().split('\n');
30+
const results: Array<{ id: string; category: string; description: string }> = [];
31+
32+
for (const line of lines) {
33+
if (!line.trim()) continue;
34+
35+
// Parse CSV with quoted fields
36+
const match = line.match(/^([^,]+),([^,]+),"([^"]*)"$/) || line.match(/^([^,]+),([^,]+),(.*)$/);
37+
38+
if (match && match[1] && match[2] && match[3]) {
39+
results.push({
40+
id: match[1].trim(),
41+
category: match[2].trim(),
42+
description: match[3].trim(),
43+
});
44+
}
45+
}
46+
47+
return results;
48+
}
49+
50+
/**
51+
* Format results as a markdown table
52+
*/
53+
function formatDiscoverResults(results: Array<{ id: string; category: string; description: string }>): string {
54+
if (results.length === 0) {
55+
return DISCOVER_PROMPTS.NO_RESULTS;
56+
}
57+
58+
let markdown = DISCOVER_PROMPTS.RESULTS_HEADER;
59+
60+
// Table header
61+
markdown += '| Space ID | Category | Description |\n';
62+
markdown += '|----------|----------|-------------|\n';
63+
64+
// Table rows
65+
for (const result of results) {
66+
markdown +=
67+
`| \`${escapeMarkdown(result.id)}\` ` +
68+
`| ${escapeMarkdown(result.category)} ` +
69+
`| ${escapeMarkdown(result.description)} |\n`;
70+
}
71+
72+
return markdown;
73+
}
74+
75+
/**
76+
* Discover spaces from a configured URL (DYNAMIC_SPACE_DATA)
77+
* Fetches CSV content and returns as markdown table
78+
*/
79+
export async function discoverSpaces(): Promise<ToolResult> {
80+
const url = process.env.DYNAMIC_SPACE_DATA;
81+
82+
if (!url) {
83+
return {
84+
formatted: 'Error: DYNAMIC_SPACE_DATA environment variable is not set.',
85+
totalResults: 0,
86+
resultsShared: 0,
87+
isError: true,
88+
};
89+
}
90+
91+
try {
92+
const response = await fetch(url);
93+
94+
if (!response.ok) {
95+
return {
96+
formatted: DISCOVER_PROMPTS.FETCH_ERROR(url, `HTTP ${response.status}`),
97+
totalResults: 0,
98+
resultsShared: 0,
99+
isError: true,
100+
};
101+
}
102+
103+
const content = await response.text();
104+
const results = parseCsvContent(content);
105+
106+
return {
107+
formatted: formatDiscoverResults(results),
108+
totalResults: results.length,
109+
resultsShared: results.length,
110+
};
111+
} catch (error) {
112+
const errorMessage = error instanceof Error ? error.message : String(error);
113+
return {
114+
formatted: DISCOVER_PROMPTS.FETCH_ERROR(url, errorMessage),
115+
totalResults: 0,
116+
resultsShared: 0,
117+
isError: true,
118+
};
119+
}
120+
}

packages/mcp/src/space/space-tool.ts

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import type { ToolResult } from '../types/tool-result.js';
22
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js';
33
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4-
import { spaceArgsSchema, OPERATION_NAMES, type OperationName, type SpaceArgs, type InvokeResult } from './types.js';
5-
import { findSpaces as findSpaces } from './commands/dynamic-find.js';
4+
import type { z } from 'zod';
5+
import {
6+
spaceArgsSchema,
7+
type SpaceArgs,
8+
type InvokeResult,
9+
isDynamicSpaceMode,
10+
getOperationNames,
11+
getSpaceArgsSchema,
12+
} from './types.js';
13+
import { findSpaces } from './commands/dynamic-find.js';
14+
import { discoverSpaces } from './commands/discover.js';
615
import { viewParameters } from './commands/view-parameters.js';
716
import { invokeSpace } from './commands/invoke.js';
817

@@ -86,13 +95,104 @@ For parameters that accept files (FileData types):
8695
- Required parameters are clearly marked and validated
8796
`;
8897

98+
/**
99+
* Usage instructions for dynamic mode (when DYNAMIC_SPACE_DATA is set)
100+
* Simplified instructions focusing on discover/view_parameters/invoke workflow
101+
*/
102+
const DYNAMIC_USAGE_INSTRUCTIONS = `# Gradio Space Interaction
103+
104+
Dynamically use Gradio MCP Spaces. Discover available spaces, view their parameter schemas, and invoke them. Use "discover" to find recommended spaces for tasks.
105+
106+
## Available Operations
107+
108+
### discover
109+
List recommended spaces and their categories.
110+
111+
**Example:**
112+
\`\`\`json
113+
{
114+
"operation": "discover"
115+
}
116+
\`\`\`
117+
118+
### view_parameters
119+
Display the parameter schema for a space's first tool.
120+
121+
**Example:**
122+
\`\`\`json
123+
{
124+
"operation": "view_parameters",
125+
"space_name": "evalstate/FLUX1_schnell"
126+
}
127+
\`\`\`
128+
129+
### invoke
130+
Execute a space's first tool with provided parameters.
131+
132+
**Example:**
133+
\`\`\`json
134+
{
135+
"operation": "invoke",
136+
"space_name": "evalstate/FLUX1_schnell",
137+
"parameters": "{\\"prompt\\": \\"a cute cat\\", \\"num_steps\\": 4}"
138+
}
139+
\`\`\`
140+
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+
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+
`;
154+
155+
/**
156+
* Get the appropriate usage instructions based on mode
157+
*/
158+
function getUsageInstructions(): string {
159+
return isDynamicSpaceMode() ? DYNAMIC_USAGE_INSTRUCTIONS : USAGE_INSTRUCTIONS;
160+
}
161+
89162
/**
90163
* Space tool configuration
164+
* Returns dynamic config based on environment
165+
*/
166+
export function getDynamicSpaceToolConfig(): {
167+
name: string;
168+
description: string;
169+
schema: z.ZodObject<z.ZodRawShape>;
170+
annotations: { title: string; readOnlyHint: boolean; openWorldHint: boolean };
171+
} {
172+
const dynamicMode = isDynamicSpaceMode();
173+
return {
174+
name: 'dynamic_space',
175+
description: dynamicMode
176+
? 'Discover, inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces to conduct ML Tasks including Image Generation, Background Removal, Text to Speech and more ' +
177+
'Call with no operation for full usage instructions.'
178+
: 'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces. ' +
179+
'Call with no operation for full usage instructions.',
180+
schema: getSpaceArgsSchema(),
181+
annotations: {
182+
title: 'Dynamically use Gradio Applications',
183+
readOnlyHint: false,
184+
openWorldHint: true,
185+
},
186+
};
187+
}
188+
189+
/**
190+
* Space tool configuration (static, for backward compatibility)
91191
*/
92192
export const DYNAMIC_SPACE_TOOL_CONFIG = {
93193
name: 'dynamic_space',
94194
description:
95-
'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces to perform various ML Tasks. ' +
195+
'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces. ' +
96196
'Call with no operation for full usage instructions.',
97197
schema: spaceArgsSchema,
98198
annotations: {
@@ -126,18 +226,19 @@ export class SpaceTool {
126226
// If no operation provided, return usage instructions
127227
if (!requestedOperation) {
128228
return {
129-
formatted: USAGE_INSTRUCTIONS,
229+
formatted: getUsageInstructions(),
130230
totalResults: 1,
131231
resultsShared: 1,
132232
};
133233
}
134234

135235
// Validate operation
136236
const normalizedOperation = requestedOperation.toLowerCase();
137-
if (!isOperationName(normalizedOperation)) {
237+
const validOperations = getOperationNames();
238+
if (!validOperations.includes(normalizedOperation)) {
138239
return {
139240
formatted: `Unknown operation: "${requestedOperation}"
140-
Available operations: ${OPERATION_NAMES.join(', ')}
241+
Available operations: ${validOperations.join(', ')}
141242
142243
Call this tool with no operation for full usage instructions.`,
143244
totalResults: 0,
@@ -152,6 +253,9 @@ Call this tool with no operation for full usage instructions.`,
152253
case 'find':
153254
return await this.handleFind(params);
154255

256+
case 'discover':
257+
return await this.handleDiscover();
258+
155259
case 'view_parameters':
156260
return await this.handleViewParameters(params);
157261

@@ -184,6 +288,13 @@ Call this tool with no operation for full usage instructions.`,
184288
return await findSpaces(params.search_query, params.limit, this.hfToken);
185289
}
186290

291+
/**
292+
* Handle discover operation (for dynamic space mode)
293+
*/
294+
private async handleDiscover(): Promise<ToolResult> {
295+
return await discoverSpaces();
296+
}
297+
187298
/**
188299
* Handle view_parameters operation
189300
*/
@@ -260,10 +371,3 @@ Use "view_parameters" to see what parameters this space accepts.`,
260371
return await invokeSpace(params.space_name, params.parameters, this.hfToken, extra);
261372
}
262373
}
263-
264-
/**
265-
* Type guard for operation names
266-
*/
267-
function isOperationName(value: string): value is OperationName {
268-
return (OPERATION_NAMES as readonly string[]).includes(value);
269-
}

0 commit comments

Comments
 (0)