Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions packages/app/src/server/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
type DocFetchParams,
HF_JOBS_TOOL_CONFIG,
HfJobsTool,
DYNAMIC_SPACE_TOOL_CONFIG,
getDynamicSpaceToolConfig,
SpaceTool,
type SpaceArgs,
type InvokeResult,
Expand Down Expand Up @@ -820,11 +820,13 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
}
);

toolInstances[DYNAMIC_SPACE_TOOL_CONFIG.name] = server.tool(
DYNAMIC_SPACE_TOOL_CONFIG.name,
DYNAMIC_SPACE_TOOL_CONFIG.description,
DYNAMIC_SPACE_TOOL_CONFIG.schema.shape,
DYNAMIC_SPACE_TOOL_CONFIG.annotations,
// Get dynamic config based on environment (uses DYNAMIC_SPACE_DATA env var)
const dynamicSpaceToolConfig = getDynamicSpaceToolConfig();
toolInstances[dynamicSpaceToolConfig.name] = server.tool(
dynamicSpaceToolConfig.name,
dynamicSpaceToolConfig.description,
dynamicSpaceToolConfig.schema.shape,
dynamicSpaceToolConfig.annotations,
async (params: SpaceArgs, extra) => {
// Check if invoke operation is disabled by gradio=none
const { gradio } = extractAuthBouquetAndMix(headers);
Expand Down Expand Up @@ -857,8 +859,8 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
noImageContentHeaderEnabled || toolSelection.enabledToolIds.includes('NO_GRADIO_IMAGE_CONTENT');
const postProcessOptions: GradioToolCallOptions = {
stripImageContent,
toolName: DYNAMIC_SPACE_TOOL_CONFIG.name,
outwardFacingName: DYNAMIC_SPACE_TOOL_CONFIG.name,
toolName: dynamicSpaceToolConfig.name,
outwardFacingName: dynamicSpaceToolConfig.name,
sessionInfo,
spaceName: params.space_name,
};
Expand Down Expand Up @@ -904,7 +906,7 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
success = !toolResult.isError;

const durationMs = Date.now() - startTime;
logSearchQuery(DYNAMIC_SPACE_TOOL_CONFIG.name, loggedOperation, params, {
logSearchQuery(dynamicSpaceToolConfig.name, loggedOperation, params, {
...getLoggingOptions(),
totalResults: toolResult.totalResults,
resultsShared: toolResult.resultsShared,
Expand Down Expand Up @@ -935,7 +937,7 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
const toolResult = await runWithQueryLogging(
logSearchQuery,
{
methodName: DYNAMIC_SPACE_TOOL_CONFIG.name,
methodName: dynamicSpaceToolConfig.name,
query: loggedOperation,
parameters: params,
baseOptions: getLoggingOptions(),
Expand Down
120 changes: 120 additions & 0 deletions packages/mcp/src/space/commands/discover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { ToolResult } from '../../types/tool-result.js';
import { escapeMarkdown } from '../../utilities.js';

/**
* Prompt configuration for discover operation (from DYNAMIC_SPACE_DATA)
* These prompts can be easily tweaked to adjust behavior
*/
export const DISCOVER_PROMPTS = {
// Header for results
RESULTS_HEADER: `**Available Spaces:**

These spaces can be invoked using the \`dynamic_space\` tool.
Use \`"operation": "view_parameters"\` to inspect a space's parameters before invoking.

`,

// No results message
NO_RESULTS: `No spaces available in the configured list.`,

// Error fetching
FETCH_ERROR: (url: string, error: string): string => `Error fetching space list from ${url}: ${error}`,
};

/**
* Parse CSV content into space entries
* Expected format: space_id,category,description
*/
function parseCsvContent(content: string): Array<{ id: string; category: string; description: string }> {
const lines = content.trim().split('\n');
const results: Array<{ id: string; category: string; description: string }> = [];

for (const line of lines) {
if (!line.trim()) continue;

// Parse CSV with quoted fields
const match = line.match(/^([^,]+),([^,]+),"([^"]*)"$/) || line.match(/^([^,]+),([^,]+),(.*)$/);

if (match && match[1] && match[2] && match[3]) {
results.push({
id: match[1].trim(),
category: match[2].trim(),
description: match[3].trim(),
});
}
}

return results;
}

/**
* Format results as a markdown table
*/
function formatDiscoverResults(results: Array<{ id: string; category: string; description: string }>): string {
if (results.length === 0) {
return DISCOVER_PROMPTS.NO_RESULTS;
}

let markdown = DISCOVER_PROMPTS.RESULTS_HEADER;

// Table header
markdown += '| Space ID | Category | Description |\n';
markdown += '|----------|----------|-------------|\n';

// Table rows
for (const result of results) {
markdown +=
`| \`${escapeMarkdown(result.id)}\` ` +
`| ${escapeMarkdown(result.category)} ` +
`| ${escapeMarkdown(result.description)} |\n`;
}

return markdown;
}

/**
* Discover spaces from a configured URL (DYNAMIC_SPACE_DATA)
* Fetches CSV content and returns as markdown table
*/
export async function discoverSpaces(): Promise<ToolResult> {
const url = process.env.DYNAMIC_SPACE_DATA;

if (!url) {
return {
formatted: 'Error: DYNAMIC_SPACE_DATA environment variable is not set.',
totalResults: 0,
resultsShared: 0,
isError: true,
};
}

try {
const response = await fetch(url);

if (!response.ok) {
return {
formatted: DISCOVER_PROMPTS.FETCH_ERROR(url, `HTTP ${response.status}`),
totalResults: 0,
resultsShared: 0,
isError: true,
};
}

const content = await response.text();
const results = parseCsvContent(content);

return {
formatted: formatDiscoverResults(results),
totalResults: results.length,
resultsShared: results.length,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
formatted: DISCOVER_PROMPTS.FETCH_ERROR(url, errorMessage),
totalResults: 0,
resultsShared: 0,
isError: true,
};
}
}
130 changes: 117 additions & 13 deletions packages/mcp/src/space/space-tool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { ToolResult } from '../types/tool-result.js';
import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import { spaceArgsSchema, OPERATION_NAMES, type OperationName, type SpaceArgs, type InvokeResult } from './types.js';
import { findSpaces as findSpaces } from './commands/dynamic-find.js';
import type { z } from 'zod';
import {
spaceArgsSchema,
type SpaceArgs,
type InvokeResult,
isDynamicSpaceMode,
getOperationNames,
getSpaceArgsSchema,
} from './types.js';
import { findSpaces } from './commands/dynamic-find.js';
import { discoverSpaces } from './commands/discover.js';
import { viewParameters } from './commands/view-parameters.js';
import { invokeSpace } from './commands/invoke.js';

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

/**
* Usage instructions for dynamic mode (when DYNAMIC_SPACE_DATA is set)
* Simplified instructions focusing on discover/view_parameters/invoke workflow
*/
const DYNAMIC_USAGE_INSTRUCTIONS = `# Gradio Space Interaction

Dynamically use Gradio MCP Spaces. Discover available spaces, view their parameter schemas, and invoke them. Use "discover" to find recommended spaces for tasks.

## Available Operations

### discover
List recommended spaces and their categories.

**Example:**
\`\`\`json
{
"operation": "discover"
}
\`\`\`

### view_parameters
Display the parameter schema for a space's first tool.

**Example:**
\`\`\`json
{
"operation": "view_parameters",
"space_name": "evalstate/FLUX1_schnell"
}
\`\`\`

### invoke
Execute a space's first tool with provided parameters.

**Example:**
\`\`\`json
{
"operation": "invoke",
"space_name": "evalstate/FLUX1_schnell",
"parameters": "{\\"prompt\\": \\"a cute cat\\", \\"num_steps\\": 4}"
}
\`\`\`

## Workflow

1. **Discover Spaces** - Use \`discover\` to see available spaces
2. **Inspect Parameters** - Use \`view_parameters\` to see what a space accepts
3. **Invoke the Space** - Use \`invoke\` with the required parameters

## File Handling

For parameters that accept files (FileData types):
- Provide a publicly accessible URL (http:// or https://)
- Example: \`{"image": "https://example.com/photo.jpg"}\`
- Output url's from one tool may be used as inputs to another.
`;

/**
* Get the appropriate usage instructions based on mode
*/
function getUsageInstructions(): string {
return isDynamicSpaceMode() ? DYNAMIC_USAGE_INSTRUCTIONS : USAGE_INSTRUCTIONS;
}

/**
* Space tool configuration
* Returns dynamic config based on environment
*/
export function getDynamicSpaceToolConfig(): {
name: string;
description: string;
schema: z.ZodObject<z.ZodRawShape>;
annotations: { title: string; readOnlyHint: boolean; openWorldHint: boolean };
} {
const dynamicMode = isDynamicSpaceMode();
return {
name: 'dynamic_space',
description: dynamicMode
? '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 ' +
'Call with no operation for full usage instructions.'
: 'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces. ' +
'Call with no operation for full usage instructions.',
schema: getSpaceArgsSchema(),
annotations: {
title: 'Dynamically use Gradio Applications',
readOnlyHint: false,
openWorldHint: true,
},
};
}

/**
* Space tool configuration (static, for backward compatibility)
*/
export const DYNAMIC_SPACE_TOOL_CONFIG = {
name: 'dynamic_space',
description:
'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces to perform various ML Tasks. ' +
'Find (semantic/task search), inspect (view parameter schema) and dynamically invoke Gradio MCP Spaces. ' +
'Call with no operation for full usage instructions.',
schema: spaceArgsSchema,
annotations: {
Expand Down Expand Up @@ -126,18 +226,19 @@ export class SpaceTool {
// If no operation provided, return usage instructions
if (!requestedOperation) {
return {
formatted: USAGE_INSTRUCTIONS,
formatted: getUsageInstructions(),
totalResults: 1,
resultsShared: 1,
};
}

// Validate operation
const normalizedOperation = requestedOperation.toLowerCase();
if (!isOperationName(normalizedOperation)) {
const validOperations = getOperationNames();
if (!validOperations.includes(normalizedOperation)) {
return {
formatted: `Unknown operation: "${requestedOperation}"
Available operations: ${OPERATION_NAMES.join(', ')}
Available operations: ${validOperations.join(', ')}

Call this tool with no operation for full usage instructions.`,
totalResults: 0,
Expand All @@ -152,6 +253,9 @@ Call this tool with no operation for full usage instructions.`,
case 'find':
return await this.handleFind(params);

case 'discover':
return await this.handleDiscover();

case 'view_parameters':
return await this.handleViewParameters(params);

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

/**
* Handle discover operation (for dynamic space mode)
*/
private async handleDiscover(): Promise<ToolResult> {
return await discoverSpaces();
}

/**
* Handle view_parameters operation
*/
Expand Down Expand Up @@ -260,10 +371,3 @@ Use "view_parameters" to see what parameters this space accepts.`,
return await invokeSpace(params.space_name, params.parameters, this.hfToken, extra);
}
}

/**
* Type guard for operation names
*/
function isOperationName(value: string): value is OperationName {
return (OPERATION_NAMES as readonly string[]).includes(value);
}
Loading