Skip to content

Commit 56a3679

Browse files
authored
Merge pull request #130 from huggingface/fix/gradio-files
enable gradio_files if dynamic_spaces is present too
2 parents c2d091b + 36cf138 commit 56a3679

File tree

12 files changed

+245
-100
lines changed

12 files changed

+245
-100
lines changed

.npmrc

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
# Use pnpm for package management
2-
# Disable package-lock generation
1+
# npm settings
32
package-lock=true
43

5-
# Hoist dependencies to root node_modules for compatibility
6-
shamefully-hoist=true
7-
8-
# Enable strict peer dependencies
9-
strict-peer-dependencies=false
10-
11-
# Cache settings
12-
store-dir=~/.pnpm-store
4+
# pnpm-specific settings are in package.json under "pnpm" key
5+
# (shamefullyHoist, strictPeerDependencies)
6+
# store-dir uses pnpm default (~/.local/share/pnpm/store)

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"version": "0.2.52",
55
"type": "module",
66
"packageManager": "[email protected]",
7+
"pnpm": {
8+
"shamefullyHoist": true,
9+
"strictPeerDependencies": false
10+
},
711
"scripts": {
812
"dev": "concurrently \"pnpm run --filter=@llmindset/hf-mcp dev\" \"pnpm run --filter=@llmindset/hf-mcp-server dev\"",
913
"dev:sse": "concurrently \"pnpm run --filter=@llmindset/hf-mcp dev\" \"pnpm run --filter=@llmindset/hf-mcp-server dev:sse\"",

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

Lines changed: 51 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { GRADIO_IMAGE_FILTER_FLAG } from '../shared/behavior-flags.js';
77
import { logger } from './utils/logger.js';
88
import { registerRemoteTools } from './gradio-endpoint-connector.js';
99
import { extractAuthBouquetAndMix } from './utils/auth-utils.js';
10-
import { parseGradioSpaceIds } from './utils/gradio-utils.js';
10+
import { parseGradioSpaceIds, shouldRegisterGradioFilesTool } from './utils/gradio-utils.js';
1111
import { getGradioSpaces } from './utils/gradio-discovery.js';
1212
import { repoExists } from '@huggingface/hub';
1313
import type { GradioFilesParams } from '@llmindset/hf-mcp';
14-
import { GRADIO_FILES_TOOL_CONFIG, GradioFilesTool } from '@llmindset/hf-mcp';
14+
import { GRADIO_FILES_TOOL_CONFIG, GradioFilesTool, DYNAMIC_SPACE_TOOL_ID } from '@llmindset/hf-mcp';
1515
import { logSearchQuery } from './utils/query-logger.js';
1616
import { z } from 'zod';
1717
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -159,6 +159,55 @@ export const createProxyServerFactory = (
159159
gradioParam: gradio,
160160
}, 'Collected Gradio space names');
161161

162+
// Register gradio_files tool if eligible
163+
// This is needed when Gradio spaces are configured OR when dynamic_space is enabled
164+
if (sessionInfo?.isAuthenticated && userDetails?.name && hfToken) {
165+
const username = userDetails.name;
166+
const token = hfToken;
167+
const datasetExists = await repoExists({
168+
repo: { type: 'dataset', name: `${username}/gradio-files` },
169+
accessToken: token,
170+
});
171+
172+
const shouldRegister = shouldRegisterGradioFilesTool({
173+
gradioSpaceCount: allSpaceNames.length,
174+
builtInTools: settings?.builtInTools ?? [],
175+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
176+
datasetExists,
177+
});
178+
179+
if (shouldRegister) {
180+
server.tool(
181+
GRADIO_FILES_TOOL_CONFIG.name,
182+
GRADIO_FILES_TOOL_CONFIG.description,
183+
GRADIO_FILES_TOOL_CONFIG.schema.shape,
184+
GRADIO_FILES_TOOL_CONFIG.annotations,
185+
async (params: GradioFilesParams) => {
186+
const tool = new GradioFilesTool(token, username);
187+
const markdown = await tool.generateDetailedMarkdown(params.fileType);
188+
189+
// Log the tool usage
190+
logSearchQuery(
191+
GRADIO_FILES_TOOL_CONFIG.name,
192+
`${username}/gradio-files`,
193+
{ fileType: params.fileType },
194+
{
195+
clientSessionId: sessionInfo?.clientSessionId,
196+
isAuthenticated: sessionInfo?.isAuthenticated ?? true,
197+
clientName: sessionInfo?.clientInfo?.name,
198+
clientVersion: sessionInfo?.clientInfo?.version,
199+
responseCharCount: markdown.length,
200+
}
201+
);
202+
203+
return {
204+
content: [{ type: 'text', text: markdown }],
205+
};
206+
}
207+
);
208+
}
209+
}
210+
162211
if (allSpaceNames.length === 0) {
163212
logger.debug('No Gradio spaces configured, using local tools only');
164213
return result;
@@ -212,64 +261,6 @@ export const createProxyServerFactory = (
212261
}
213262
}
214263

215-
if (sessionInfo?.isAuthenticated && userDetails?.name && hfToken) {
216-
const username = userDetails.name; // Capture username for closure
217-
const token = hfToken; // Capture token for closure
218-
const exists = await repoExists({
219-
repo: { type: 'dataset', name: `${username}/gradio-files` },
220-
});
221-
if (exists)
222-
server.tool(
223-
GRADIO_FILES_TOOL_CONFIG.name,
224-
GRADIO_FILES_TOOL_CONFIG.description,
225-
GRADIO_FILES_TOOL_CONFIG.schema.shape,
226-
GRADIO_FILES_TOOL_CONFIG.annotations,
227-
async (params: GradioFilesParams) => {
228-
const tool = new GradioFilesTool(token, username);
229-
const markdown = await tool.generateDetailedMarkdown(params.fileType);
230-
231-
// Log the tool usage
232-
logSearchQuery(
233-
GRADIO_FILES_TOOL_CONFIG.name,
234-
`${username}/gradio-files`,
235-
{ fileType: params.fileType },
236-
{
237-
clientSessionId: sessionInfo?.clientSessionId,
238-
isAuthenticated: sessionInfo?.isAuthenticated ?? true,
239-
clientName: sessionInfo?.clientInfo?.name,
240-
clientVersion: sessionInfo?.clientInfo?.version,
241-
responseCharCount: markdown.length,
242-
}
243-
);
244-
245-
return {
246-
content: [{ type: 'text', text: markdown }],
247-
};
248-
}
249-
);
250-
/* TODO -- reinstate once method handling is improved;
251-
server.prompt(
252-
GRADIO_FILES_PROMPT_CONFIG.name,
253-
GRADIO_FILES_PROMPT_CONFIG.description,
254-
GRADIO_FILES_PROMPT_CONFIG.schema.shape,
255-
async () => {
256-
return {
257-
description: `Gradio Files summary for ${username}`,
258-
messages: [
259-
{
260-
role: 'user' as const,
261-
content: {
262-
type: 'text' as const,
263-
text: await new GradioFilesTool(token, username).generateDetailedMarkdown('all'),
264-
},
265-
},
266-
],
267-
};
268-
}
269-
);
270-
*/
271-
}
272-
273264
logger.debug('Server ready with local and remote tools');
274265
return result;
275266
};

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {
6161
type SpaceArgs,
6262
type InvokeResult,
6363
type ToolResult,
64+
VIEW_PARAMETERS,
6465
} from '@llmindset/hf-mcp';
6566

6667
import type { ServerFactory, ServerFactoryResult } from './transport/base-transport.js';
@@ -832,7 +833,7 @@ export const createServerFactory = (_webServerInstance: WebServer, sharedApiClie
832833
const errorMessage =
833834
'The invoke operation is disabled because gradio=none is set. ' +
834835
'To use invoke, remove gradio=none from your headers or set gradio to a space ID. ' +
835-
'You can still use operation=view_parameters to inspect the tool schema.';
836+
`You can still use operation=${VIEW_PARAMETERS} to inspect the tool schema.`;
836837
return {
837838
content: [{ type: 'text', text: errorMessage }],
838839
isError: true,

packages/app/src/server/utils/gradio-utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,64 @@ export async function parseAndFetchGradioEndpoints(gradioParam: string, hfToken?
206206

207207
return fetchGradioSubdomains(parsedSpaces, hfToken);
208208
}
209+
210+
/**
211+
* Input parameters for determining if gradio_files tool should be registered
212+
*/
213+
export interface GradioFilesEligibilityParams {
214+
/** Number of configured Gradio spaces */
215+
gradioSpaceCount: number;
216+
/** List of enabled built-in tool IDs */
217+
builtInTools: string[];
218+
/** The tool ID that represents dynamic_space */
219+
dynamicSpaceToolId: string;
220+
/** Whether the user's gradio-files dataset exists */
221+
datasetExists: boolean;
222+
}
223+
224+
/**
225+
* Pure function to determine if the gradio_files tool should be registered.
226+
*
227+
* The tool should be registered when:
228+
* 1. The user's gradio-files dataset exists, AND
229+
* 2. Either Gradio spaces are configured OR dynamic_space tool is enabled
230+
*
231+
* @param params - The eligibility parameters
232+
* @returns true if gradio_files tool should be registered
233+
*
234+
* @example
235+
* shouldRegisterGradioFilesTool({
236+
* gradioSpaceCount: 0,
237+
* builtInTools: ['dynamic_space'],
238+
* dynamicSpaceToolId: 'dynamic_space',
239+
* datasetExists: true,
240+
* }) // true - dynamic_space enabled with existing dataset
241+
*
242+
* @example
243+
* shouldRegisterGradioFilesTool({
244+
* gradioSpaceCount: 2,
245+
* builtInTools: [],
246+
* dynamicSpaceToolId: 'dynamic_space',
247+
* datasetExists: true,
248+
* }) // true - Gradio spaces configured with existing dataset
249+
*
250+
* @example
251+
* shouldRegisterGradioFilesTool({
252+
* gradioSpaceCount: 0,
253+
* builtInTools: [],
254+
* dynamicSpaceToolId: 'dynamic_space',
255+
* datasetExists: true,
256+
* }) // false - no Gradio spaces and dynamic_space not enabled
257+
*/
258+
export function shouldRegisterGradioFilesTool(params: GradioFilesEligibilityParams): boolean {
259+
const { gradioSpaceCount, builtInTools, dynamicSpaceToolId, datasetExists } = params;
260+
261+
if (!datasetExists) {
262+
return false;
263+
}
264+
265+
const hasGradioSpaces = gradioSpaceCount > 0;
266+
const isDynamicSpaceEnabled = builtInTools.includes(dynamicSpaceToolId);
267+
268+
return hasGradioSpaces || isDynamicSpaceEnabled;
269+
}

packages/app/test/server/utils/gradio-utils.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isGradioTool,
44
createGradioToolName,
55
parseGradioSpaceIds,
6+
shouldRegisterGradioFilesTool,
67
} from '../../../src/server/utils/gradio-utils.js';
78

89
describe('isGradioTool', () => {
@@ -444,3 +445,94 @@ describe('parseGradioSpaceIds', () => {
444445
});
445446
});
446447
});
448+
449+
describe('shouldRegisterGradioFilesTool', () => {
450+
const DYNAMIC_SPACE_TOOL_ID = 'dynamic_space';
451+
452+
describe('should register when conditions are met', () => {
453+
it('should register when Gradio spaces configured and dataset exists', () => {
454+
expect(shouldRegisterGradioFilesTool({
455+
gradioSpaceCount: 2,
456+
builtInTools: [],
457+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
458+
datasetExists: true,
459+
})).toBe(true);
460+
});
461+
462+
it('should register when dynamic_space enabled and dataset exists', () => {
463+
expect(shouldRegisterGradioFilesTool({
464+
gradioSpaceCount: 0,
465+
builtInTools: [DYNAMIC_SPACE_TOOL_ID],
466+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
467+
datasetExists: true,
468+
})).toBe(true);
469+
});
470+
471+
it('should register when both Gradio spaces and dynamic_space enabled', () => {
472+
expect(shouldRegisterGradioFilesTool({
473+
gradioSpaceCount: 3,
474+
builtInTools: [DYNAMIC_SPACE_TOOL_ID, 'other_tool'],
475+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
476+
datasetExists: true,
477+
})).toBe(true);
478+
});
479+
});
480+
481+
describe('should NOT register when conditions are not met', () => {
482+
it('should NOT register when dataset does not exist', () => {
483+
expect(shouldRegisterGradioFilesTool({
484+
gradioSpaceCount: 2,
485+
builtInTools: [DYNAMIC_SPACE_TOOL_ID],
486+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
487+
datasetExists: false,
488+
})).toBe(false);
489+
});
490+
491+
it('should NOT register when no Gradio spaces and dynamic_space not enabled', () => {
492+
expect(shouldRegisterGradioFilesTool({
493+
gradioSpaceCount: 0,
494+
builtInTools: ['other_tool', 'another_tool'],
495+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
496+
datasetExists: true,
497+
})).toBe(false);
498+
});
499+
500+
it('should NOT register with empty builtInTools and no spaces', () => {
501+
expect(shouldRegisterGradioFilesTool({
502+
gradioSpaceCount: 0,
503+
builtInTools: [],
504+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
505+
datasetExists: true,
506+
})).toBe(false);
507+
});
508+
});
509+
510+
describe('edge cases', () => {
511+
it('should handle gradioSpaceCount of 1', () => {
512+
expect(shouldRegisterGradioFilesTool({
513+
gradioSpaceCount: 1,
514+
builtInTools: [],
515+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
516+
datasetExists: true,
517+
})).toBe(true);
518+
});
519+
520+
it('should handle dynamic_space among many tools', () => {
521+
expect(shouldRegisterGradioFilesTool({
522+
gradioSpaceCount: 0,
523+
builtInTools: ['tool1', 'tool2', DYNAMIC_SPACE_TOOL_ID, 'tool3'],
524+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
525+
datasetExists: true,
526+
})).toBe(true);
527+
});
528+
529+
it('should be case-sensitive for tool ID matching', () => {
530+
expect(shouldRegisterGradioFilesTool({
531+
gradioSpaceCount: 0,
532+
builtInTools: ['DYNAMIC_SPACE', 'Dynamic_Space'],
533+
dynamicSpaceToolId: DYNAMIC_SPACE_TOOL_ID,
534+
datasetExists: true,
535+
})).toBe(false);
536+
});
537+
});
538+
});

packages/mcp/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export * from './docs-search/doc-fetch.js';
2020
export * from './readme-utils.js';
2121
export * from './use-space.js';
2222
export * from './jobs/jobs-tool.js';
23-
export * from './space/space-tool.js';
23+
export * from './space/dynamic-space-tool.js';
2424

2525
// Export shared types
2626
export * from './types/tool-result.js';

packages/mcp/src/space/commands/discover.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ToolResult } from '../../types/tool-result.js';
22
import { escapeMarkdown } from '../../utilities.js';
3+
import { VIEW_PARAMETERS } from '../dynamic-space-tool.js';
34

45
/**
56
* Prompt configuration for discover operation (from DYNAMIC_SPACE_DATA)
@@ -10,7 +11,7 @@ export const DISCOVER_PROMPTS = {
1011
RESULTS_HEADER: `**Available Spaces:**
1112
1213
These spaces can be invoked using the \`dynamic_space\` tool.
13-
Use \`"operation": "view_parameters"\` to inspect a space's parameters before invoking.
14+
Use \`"operation": "${VIEW_PARAMETERS}"\` to inspect a space's parameters before invoking.
1415
1516
`,
1617

packages/mcp/src/space/commands/dynamic-find.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ToolResult } from '../../types/tool-result.js';
22
import { SpaceSearchTool, type SpaceSearchResult } from '../../space-search.js';
33
import { escapeMarkdown } from '../../utilities.js';
4+
import { VIEW_PARAMETERS } from '../dynamic-space-tool.js';
45

56
// Default number of results to return
67
const DEFAULT_RESULTS_LIMIT = 10;
@@ -51,7 +52,7 @@ To find MCP-enabled Spaces for a specific task, call this operation with a searc
5152
return `# MCP Space Find Results for "${query}" (${showingText})
5253
5354
These MCP-enabled Spaces can be invoked using the \`dynamic_space\` tool.
54-
Use \`"operation": "view_parameters"\` to inspect a space's parameters before invoking.
55+
Use \`"operation": "${VIEW_PARAMETERS}"\` to inspect a space's parameters before invoking.
5556
5657
`;
5758
},

0 commit comments

Comments
 (0)