@llmindset/hf-mcp 0.3.1 → 0.3.3

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.
Files changed (161) hide show
  1. package/dist/docs-search/doc-fetch.d.ts +1 -0
  2. package/dist/docs-search/doc-fetch.d.ts.map +1 -1
  3. package/dist/docs-search/doc-fetch.js +9 -12
  4. package/dist/docs-search/doc-fetch.js.map +1 -1
  5. package/dist/docs-search/doc-fetch.test.js +56 -11
  6. package/dist/docs-search/doc-fetch.test.js.map +1 -1
  7. package/dist/docs-search/docs-semantic-search.d.ts.map +1 -1
  8. package/dist/docs-search/docs-semantic-search.js +7 -1
  9. package/dist/docs-search/docs-semantic-search.js.map +1 -1
  10. package/dist/file-icons.d.ts +3 -0
  11. package/dist/file-icons.d.ts.map +1 -0
  12. package/dist/file-icons.js +38 -0
  13. package/dist/file-icons.js.map +1 -0
  14. package/dist/gradio-files.d.ts +0 -1
  15. package/dist/gradio-files.d.ts.map +1 -1
  16. package/dist/gradio-files.js +2 -35
  17. package/dist/gradio-files.js.map +1 -1
  18. package/dist/hf-api-call.d.ts.map +1 -1
  19. package/dist/hf-api-call.js +7 -7
  20. package/dist/hf-api-call.js.map +1 -1
  21. package/dist/hub-inspect.d.ts +2 -2
  22. package/dist/hub-inspect.d.ts.map +1 -1
  23. package/dist/hub-inspect.js +1 -1
  24. package/dist/hub-inspect.js.map +1 -1
  25. package/dist/index.browser.d.ts +48 -0
  26. package/dist/index.browser.d.ts.map +1 -0
  27. package/dist/index.browser.js +153 -0
  28. package/dist/index.browser.js.map +1 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/jobs/commands/uv-utils.d.ts +0 -3
  34. package/dist/jobs/commands/uv-utils.d.ts.map +1 -1
  35. package/dist/jobs/commands/uv-utils.js +2 -2
  36. package/dist/jobs/commands/uv-utils.js.map +1 -1
  37. package/dist/jobs/jobs-tool.d.ts.map +1 -1
  38. package/dist/jobs/jobs-tool.js +11 -12
  39. package/dist/jobs/jobs-tool.js.map +1 -1
  40. package/dist/jobs/schema-help.d.ts +2 -9
  41. package/dist/jobs/schema-help.d.ts.map +1 -1
  42. package/dist/jobs/schema-help.js +3 -3
  43. package/dist/jobs/schema-help.js.map +1 -1
  44. package/dist/jobs/sse-handler.d.ts +3 -2
  45. package/dist/jobs/sse-handler.d.ts.map +1 -1
  46. package/dist/jobs/sse-handler.js +8 -4
  47. package/dist/jobs/sse-handler.js.map +1 -1
  48. package/dist/jobs/types.d.ts +1 -1
  49. package/dist/logger.d.ts +2 -2
  50. package/dist/logger.d.ts.map +1 -1
  51. package/dist/network/fetch-profile.d.ts +24 -0
  52. package/dist/network/fetch-profile.d.ts.map +1 -0
  53. package/dist/network/fetch-profile.js +80 -0
  54. package/dist/network/fetch-profile.js.map +1 -0
  55. package/dist/network/index.d.ts +5 -0
  56. package/dist/network/index.d.ts.map +1 -0
  57. package/dist/network/index.js +5 -0
  58. package/dist/network/index.js.map +1 -0
  59. package/dist/network/ip-policy.d.ts +6 -0
  60. package/dist/network/ip-policy.d.ts.map +1 -0
  61. package/dist/network/ip-policy.js +166 -0
  62. package/dist/network/ip-policy.js.map +1 -0
  63. package/dist/network/ip-policy.test.d.ts +2 -0
  64. package/dist/network/ip-policy.test.d.ts.map +1 -0
  65. package/dist/network/ip-policy.test.js +26 -0
  66. package/dist/network/ip-policy.test.js.map +1 -0
  67. package/dist/network/safe-fetch.d.ts +16 -0
  68. package/dist/network/safe-fetch.d.ts.map +1 -0
  69. package/dist/network/safe-fetch.js +124 -0
  70. package/dist/network/safe-fetch.js.map +1 -0
  71. package/dist/network/safe-fetch.test.d.ts +2 -0
  72. package/dist/network/safe-fetch.test.d.ts.map +1 -0
  73. package/dist/network/safe-fetch.test.js +136 -0
  74. package/dist/network/safe-fetch.test.js.map +1 -0
  75. package/dist/network/url-policy.d.ts +32 -0
  76. package/dist/network/url-policy.d.ts.map +1 -0
  77. package/dist/network/url-policy.js +230 -0
  78. package/dist/network/url-policy.js.map +1 -0
  79. package/dist/network/url-policy.test.d.ts +2 -0
  80. package/dist/network/url-policy.test.d.ts.map +1 -0
  81. package/dist/network/url-policy.test.js +57 -0
  82. package/dist/network/url-policy.test.js.map +1 -0
  83. package/dist/readme-utils.d.ts.map +1 -1
  84. package/dist/readme-utils.js +3 -4
  85. package/dist/readme-utils.js.map +1 -1
  86. package/dist/repo-search.d.ts +46 -0
  87. package/dist/repo-search.d.ts.map +1 -0
  88. package/dist/repo-search.js +310 -0
  89. package/dist/repo-search.js.map +1 -0
  90. package/dist/repo-search.test.d.ts +2 -0
  91. package/dist/repo-search.test.d.ts.map +1 -0
  92. package/dist/repo-search.test.js +130 -0
  93. package/dist/repo-search.test.js.map +1 -0
  94. package/dist/space/commands/discover.d.ts +0 -5
  95. package/dist/space/commands/discover.d.ts.map +1 -1
  96. package/dist/space/commands/discover.js +9 -2
  97. package/dist/space/commands/discover.js.map +1 -1
  98. package/dist/space/commands/invoke.js +1 -59
  99. package/dist/space/commands/invoke.js.map +1 -1
  100. package/dist/space/commands/view-parameters.d.ts.map +1 -1
  101. package/dist/space/commands/view-parameters.js +3 -98
  102. package/dist/space/commands/view-parameters.js.map +1 -1
  103. package/dist/space/dynamic-space-tool.d.ts.map +1 -1
  104. package/dist/space/dynamic-space-tool.js +5 -2
  105. package/dist/space/dynamic-space-tool.js.map +1 -1
  106. package/dist/space/utils/gradio-caller.d.ts.map +1 -1
  107. package/dist/space/utils/gradio-caller.js +13 -6
  108. package/dist/space/utils/gradio-caller.js.map +1 -1
  109. package/dist/space/utils/space-http.d.ts +8 -0
  110. package/dist/space/utils/space-http.d.ts.map +1 -0
  111. package/dist/space/utils/space-http.js +49 -0
  112. package/dist/space/utils/space-http.js.map +1 -0
  113. package/dist/space-files.d.ts +0 -1
  114. package/dist/space-files.d.ts.map +1 -1
  115. package/dist/space-files.js +3 -36
  116. package/dist/space-files.js.map +1 -1
  117. package/dist/tool-ids.d.ts +6 -5
  118. package/dist/tool-ids.d.ts.map +1 -1
  119. package/dist/tool-ids.js +9 -14
  120. package/dist/tool-ids.js.map +1 -1
  121. package/package.json +7 -3
  122. package/src/docs-search/doc-fetch.test.ts +98 -28
  123. package/src/docs-search/doc-fetch.ts +9 -16
  124. package/src/docs-search/docs-semantic-search.ts +8 -1
  125. package/src/file-icons.ts +39 -0
  126. package/src/gradio-files.ts +2 -40
  127. package/src/hf-api-call.ts +8 -10
  128. package/src/hub-inspect.ts +2 -2
  129. package/src/index.browser.ts +183 -0
  130. package/src/index.ts +2 -0
  131. package/src/jobs/commands/uv-utils.ts +2 -2
  132. package/src/jobs/jobs-tool.ts +13 -12
  133. package/src/jobs/schema-help.ts +4 -4
  134. package/src/jobs/sse-handler.ts +12 -7
  135. package/src/logger.ts +2 -2
  136. package/src/network/fetch-profile.ts +112 -0
  137. package/src/network/index.ts +4 -0
  138. package/src/network/ip-policy.test.ts +29 -0
  139. package/src/network/ip-policy.ts +206 -0
  140. package/src/network/safe-fetch.test.ts +181 -0
  141. package/src/network/safe-fetch.ts +174 -0
  142. package/src/network/url-policy.test.ts +100 -0
  143. package/src/network/url-policy.ts +304 -0
  144. package/src/readme-utils.ts +11 -10
  145. package/src/repo-search.test.ts +155 -0
  146. package/src/repo-search.ts +414 -0
  147. package/src/space/commands/discover.ts +10 -2
  148. package/src/space/commands/invoke.ts +1 -88
  149. package/src/space/commands/view-parameters.ts +3 -136
  150. package/src/space/dynamic-space-tool.ts +6 -2
  151. package/src/space/utils/gradio-caller.ts +25 -12
  152. package/src/space/utils/space-http.ts +75 -0
  153. package/src/space-files.ts +3 -41
  154. package/src/tool-ids.ts +10 -14
  155. package/test/fetch-guard.spec.ts +70 -0
  156. package/test/jobs/sse-handler.spec.ts +60 -0
  157. package/dist/space/utils/result-formatter.d.ts +0 -4
  158. package/dist/space/utils/result-formatter.d.ts.map +0 -1
  159. package/dist/space/utils/result-formatter.js +0 -146
  160. package/dist/space/utils/result-formatter.js.map +0 -1
  161. package/src/space/utils/result-formatter.ts +0 -226
@@ -0,0 +1,155 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { RepoSearchTool } from './repo-search.js';
3
+
4
+ interface MockFetchCall {
5
+ input: string;
6
+ init?: RequestInit;
7
+ }
8
+
9
+ describe('RepoSearchTool', () => {
10
+ const originalFetch = globalThis.fetch;
11
+ let calls: MockFetchCall[] = [];
12
+
13
+ beforeEach(() => {
14
+ calls = [];
15
+ vi.stubGlobal('fetch', (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
16
+ const inputString = stringifyRequestInput(input);
17
+ calls.push({ input: inputString, init });
18
+
19
+ if (inputString.includes('/api/models')) {
20
+ return Promise.resolve(jsonResponse([
21
+ {
22
+ id: 'meta-llama/Llama-3.1-8B-Instruct',
23
+ pipeline_tag: 'text-generation',
24
+ library_name: 'transformers',
25
+ downloads: 123,
26
+ likes: 10,
27
+ tags: ['text-generation'],
28
+ },
29
+ ]));
30
+ }
31
+
32
+ if (inputString.includes('/api/datasets')) {
33
+ return Promise.resolve(jsonResponse([
34
+ {
35
+ id: 'openbmb/UltraData-Math',
36
+ description: 'Large-scale mathematical dataset',
37
+ downloads: 50,
38
+ likes: 3,
39
+ tags: ['math'],
40
+ },
41
+ ]));
42
+ }
43
+
44
+ if (inputString.includes('/api/spaces')) {
45
+ return Promise.resolve(jsonResponse([
46
+ {
47
+ id: 'mrfakename/Z-Image-Turbo',
48
+ title: 'Z Image Turbo',
49
+ sdk: 'gradio',
50
+ likes: 20,
51
+ },
52
+ ]));
53
+ }
54
+
55
+ return Promise.resolve(jsonResponse([]));
56
+ });
57
+ });
58
+
59
+ afterEach(() => {
60
+ vi.unstubAllGlobals();
61
+ globalThis.fetch = originalFetch;
62
+ });
63
+
64
+ it('aggregates model and dataset results in one response', async () => {
65
+ const tool = new RepoSearchTool('token');
66
+ const result = await tool.searchWithParams({
67
+ query: 'llama',
68
+ repo_types: ['model', 'dataset'],
69
+ limit: 5,
70
+ });
71
+
72
+ expect(calls).toHaveLength(2);
73
+ expect(calls[0]?.input).toContain('/api/models');
74
+ expect(calls[1]?.input).toContain('/api/datasets');
75
+ expect(result.totalResults).toBe(2);
76
+ expect(result.formatted).toContain('## Models (1)');
77
+ expect(result.formatted).toContain('## Datasets (1)');
78
+ expect(result.formatted).toContain('[https://hf.co/meta-llama/Llama-3.1-8B-Instruct]');
79
+ expect(result.formatted).toContain('[https://hf.co/datasets/openbmb/UltraData-Math]');
80
+ });
81
+
82
+ it('supports searching spaces through the same interface', async () => {
83
+ const tool = new RepoSearchTool('token');
84
+ const result = await tool.searchWithParams({
85
+ query: 'image generation',
86
+ repo_types: ['space'],
87
+ limit: 3,
88
+ });
89
+
90
+ expect(calls).toHaveLength(1);
91
+ expect(calls[0]?.input).toContain('/api/spaces');
92
+ expect(result.totalResults).toBe(1);
93
+ expect(result.formatted).toContain('## Spaces (1)');
94
+ expect(result.formatted).toContain('[https://hf.co/spaces/mrfakename/Z-Image-Turbo]');
95
+ });
96
+
97
+ it('applies an overall output length guard with truncation notice', async () => {
98
+ vi.stubGlobal('fetch', (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
99
+ const inputString = stringifyRequestInput(input);
100
+ calls.push({ input: inputString, init });
101
+
102
+ if (inputString.includes('/api/models')) {
103
+ const longTag = 'very-long-tag-name-for-output-growth';
104
+ const models = Array.from({ length: 100 }, (_, index) => ({
105
+ id: `org/super-long-model-name-${index.toString().padStart(3, '0')}-with-extra-context`,
106
+ pipeline_tag: 'text-generation',
107
+ library_name: 'transformers',
108
+ downloads: 100000 - index,
109
+ likes: 1000 - index,
110
+ tags: Array.from({ length: 30 }, (_unused, tagIndex) => `${longTag}-${tagIndex.toString()}`),
111
+ }));
112
+ return Promise.resolve(jsonResponse(models));
113
+ }
114
+
115
+ return Promise.resolve(jsonResponse([]));
116
+ });
117
+
118
+ const tool = new RepoSearchTool('token');
119
+ const result = await tool.searchWithParams({
120
+ repo_types: ['model'],
121
+ limit: 100,
122
+ });
123
+
124
+ expect(result.totalResults).toBe(100);
125
+ expect(result.resultsShared).toBeLessThan(result.totalResults);
126
+ expect(result.formatted.length).toBeLessThanOrEqual(12_500 * 3);
127
+ expect(result.formatted).toContain('Results truncated at approximately 12,500 tokens');
128
+ expect(result.formatted).toContain('Included');
129
+ });
130
+ });
131
+
132
+ function jsonResponse(payload: unknown): Response {
133
+ return new Response(JSON.stringify(payload), {
134
+ status: 200,
135
+ headers: {
136
+ 'content-type': 'application/json',
137
+ },
138
+ });
139
+ }
140
+
141
+ function stringifyRequestInput(input: RequestInfo | URL): string {
142
+ if (typeof input === 'string') {
143
+ return input;
144
+ }
145
+
146
+ if (input instanceof URL) {
147
+ return input.toString();
148
+ }
149
+
150
+ if (input instanceof Request) {
151
+ return input.url;
152
+ }
153
+
154
+ return input;
155
+ }
@@ -0,0 +1,414 @@
1
+ import { z } from 'zod';
2
+ import { HfApiCall } from './hf-api-call.js';
3
+ import { formatDate, formatNumber } from './utilities.js';
4
+ import type { ToolResult } from './types/tool-result.js';
5
+
6
+ const TAGS_TO_RETURN = 20;
7
+ const TOKEN_CAP = 12_500;
8
+ const CHARS_PER_TOKEN = 3;
9
+ const MAX_OUTPUT_CHARS = TOKEN_CAP * CHARS_PER_TOKEN;
10
+
11
+ const REPO_TYPES = ['model', 'dataset', 'space'] as const;
12
+ export type RepoType = (typeof REPO_TYPES)[number];
13
+
14
+ const REPO_TYPE_LABELS: Record<RepoType, string> = {
15
+ model: 'Models',
16
+ dataset: 'Datasets',
17
+ space: 'Spaces',
18
+ };
19
+
20
+ const DEFAULT_REPO_TYPES: RepoType[] = ['model', 'dataset'];
21
+
22
+ export const REPO_SEARCH_TOOL_CONFIG = {
23
+ name: 'hub_repo_search',
24
+ description:
25
+ 'Search Hugging Face repositories with a shared query interface. ' +
26
+ 'You can target models, datasets, spaces, or aggregate across multiple repo types in one call. ' +
27
+ 'Use space_search for semantic-first discovery of Spaces. ' +
28
+ 'Include links to repositories in your response.',
29
+ schema: z.object({
30
+ query: z
31
+ .string()
32
+ .optional()
33
+ .describe('Search term. Leave blank and specify sort + limit to browse trending or recent repositories.'),
34
+ repo_types: z
35
+ .array(z.enum(REPO_TYPES))
36
+ .min(1)
37
+ .max(3)
38
+ .optional()
39
+ .default(DEFAULT_REPO_TYPES)
40
+ .describe(
41
+ 'Repository types to search. Defaults to ["model", "dataset"]. space uses keyword search via /api/spaces.'
42
+ ),
43
+ author: z
44
+ .string()
45
+ .optional()
46
+ .describe("Organization or user namespace to filter by (e.g. 'google', 'meta-llama', 'huggingface')."),
47
+ filters: z
48
+ .array(z.string())
49
+ .optional()
50
+ .describe(
51
+ 'Optional hub filter tags. Applied to each selected repo type (e.g. ["text-generation"], ["language:en"], ["mcp-server"]).'
52
+ ),
53
+ sort: z
54
+ .enum(['trendingScore', 'downloads', 'likes', 'createdAt', 'lastModified'])
55
+ .optional()
56
+ .describe('Sort order (descending): trendingScore, downloads, likes, createdAt, lastModified'),
57
+ limit: z
58
+ .number()
59
+ .min(1)
60
+ .max(100)
61
+ .optional()
62
+ .default(20)
63
+ .describe('Maximum number of results to return per selected repo type'),
64
+ }),
65
+ annotations: {
66
+ title: 'Repo Search',
67
+ destructiveHint: false,
68
+ readOnlyHint: true,
69
+ openWorldHint: true,
70
+ },
71
+ } as const;
72
+
73
+ export type RepoSearchParams = z.infer<typeof REPO_SEARCH_TOOL_CONFIG.schema>;
74
+
75
+ interface RepoApiParams {
76
+ search?: string;
77
+ author?: string;
78
+ filter?: string;
79
+ sort?: string;
80
+ direction?: string;
81
+ limit?: string;
82
+ }
83
+
84
+ interface RepoResultBase {
85
+ id: string;
86
+ author?: string | null;
87
+ likes?: number;
88
+ downloads?: number;
89
+ trendingScore?: number;
90
+ private?: boolean;
91
+ tags?: string[];
92
+ createdAt?: string;
93
+ lastModified?: string | null;
94
+ }
95
+
96
+ interface ModelRepoResult extends RepoResultBase {
97
+ pipeline_tag?: string;
98
+ library_name?: string;
99
+ }
100
+
101
+ interface DatasetRepoResult extends RepoResultBase {
102
+ description?: string;
103
+ gated?: boolean;
104
+ }
105
+
106
+ interface SpaceRepoResult extends RepoResultBase {
107
+ title?: string | null;
108
+ emoji?: string | null;
109
+ sdk?: string;
110
+ shortDescription?: string;
111
+ ai_short_description?: string;
112
+ disabled?: boolean | null;
113
+ }
114
+
115
+ type RepoSearchResult = ModelRepoResult | DatasetRepoResult | SpaceRepoResult;
116
+
117
+ interface RepoSearchBatchResult {
118
+ repoType: RepoType;
119
+ results: RepoSearchResult[];
120
+ }
121
+
122
+ /**
123
+ * Service for searching Hugging Face repositories with shared parameters.
124
+ *
125
+ * Uses listing endpoints:
126
+ * - /api/models
127
+ * - /api/datasets
128
+ * - /api/spaces
129
+ */
130
+ export class RepoSearchTool extends HfApiCall<Record<string, string>, unknown> {
131
+ constructor(hfToken?: string) {
132
+ super('https://huggingface.co/api', hfToken);
133
+ }
134
+
135
+ async searchWithParams(params: Partial<RepoSearchParams>): Promise<ToolResult> {
136
+ try {
137
+ const repoTypes = normalizeRepoTypes(params.repo_types);
138
+ const apiParams = this.toApiParams(params);
139
+
140
+ const searchBatches = await Promise.all(
141
+ repoTypes.map(async (repoType): Promise<RepoSearchBatchResult> => {
142
+ const results = await this.searchByType(repoType, apiParams);
143
+ return { repoType, results };
144
+ })
145
+ );
146
+
147
+ const totalResults = searchBatches.reduce((sum, batch) => sum + batch.results.length, 0);
148
+ if (totalResults === 0) {
149
+ return {
150
+ formatted: `No repositories found for the given criteria.`,
151
+ totalResults: 0,
152
+ resultsShared: 0,
153
+ };
154
+ }
155
+
156
+ return formatSearchResults(searchBatches, params, repoTypes);
157
+ } catch (error) {
158
+ if (error instanceof Error) {
159
+ throw new Error(`Failed to search repositories: ${error.message}`);
160
+ }
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ private toApiParams(params: Partial<RepoSearchParams>): RepoApiParams {
166
+ const apiParams: RepoApiParams = {};
167
+
168
+ if (params.query) {
169
+ apiParams.search = params.query;
170
+ }
171
+ if (params.author) {
172
+ apiParams.author = params.author;
173
+ }
174
+ if (params.filters && params.filters.length > 0) {
175
+ apiParams.filter = params.filters.join(',');
176
+ }
177
+ if (params.sort) {
178
+ apiParams.sort = params.sort;
179
+ apiParams.direction = '-1';
180
+ }
181
+ if (params.limit) {
182
+ apiParams.limit = params.limit.toString();
183
+ }
184
+
185
+ return apiParams;
186
+ }
187
+
188
+ private async searchByType(repoType: RepoType, params: RepoApiParams): Promise<RepoSearchResult[]> {
189
+ const endpoint = repoType === 'model' ? 'models' : repoType === 'dataset' ? 'datasets' : 'spaces';
190
+ const url = new URL(`${this.apiUrl}/${endpoint}`);
191
+
192
+ if (params.search !== undefined) {
193
+ url.searchParams.set('search', params.search);
194
+ }
195
+ if (params.author !== undefined) {
196
+ url.searchParams.set('author', params.author);
197
+ }
198
+ if (params.filter !== undefined) {
199
+ url.searchParams.set('filter', params.filter);
200
+ }
201
+ if (params.sort !== undefined) {
202
+ url.searchParams.set('sort', params.sort);
203
+ }
204
+ if (params.direction !== undefined) {
205
+ url.searchParams.set('direction', params.direction);
206
+ }
207
+ if (params.limit !== undefined) {
208
+ url.searchParams.set('limit', params.limit);
209
+ }
210
+
211
+ return this.fetchFromApi<RepoSearchResult[]>(url);
212
+ }
213
+ }
214
+
215
+ function normalizeRepoTypes(repoTypes: RepoType[] | undefined): RepoType[] {
216
+ if (!repoTypes || repoTypes.length === 0) {
217
+ return [...DEFAULT_REPO_TYPES];
218
+ }
219
+
220
+ const seen = new Set<RepoType>();
221
+ const normalized: RepoType[] = [];
222
+
223
+ for (const repoType of repoTypes) {
224
+ if (!seen.has(repoType)) {
225
+ seen.add(repoType);
226
+ normalized.push(repoType);
227
+ }
228
+ }
229
+
230
+ return normalized.length > 0 ? normalized : [...DEFAULT_REPO_TYPES];
231
+ }
232
+
233
+ function formatSearchResults(
234
+ searchBatches: RepoSearchBatchResult[],
235
+ params: Partial<RepoSearchParams>,
236
+ repoTypes: RepoType[]
237
+ ): ToolResult {
238
+ const lines: string[] = [];
239
+ const totalResults = searchBatches.reduce((sum, batch) => sum + batch.results.length, 0);
240
+ let resultsShared = 0;
241
+ let truncated = false;
242
+
243
+ const tryAppendLines = (nextLines: string[]): boolean => {
244
+ const candidate = [...lines, ...nextLines].join('\n');
245
+ if (candidate.length > MAX_OUTPUT_CHARS) {
246
+ return false;
247
+ }
248
+
249
+ lines.push(...nextLines);
250
+ return true;
251
+ };
252
+
253
+ const searchTerms: string[] = [];
254
+ if (params.query) searchTerms.push(`query "${params.query}"`);
255
+ if (params.author) searchTerms.push(`author "${params.author}"`);
256
+ if (params.filters && params.filters.length > 0) searchTerms.push(`filters [${params.filters.join(', ')}]`);
257
+ if (params.sort) searchTerms.push(`sorted by ${params.sort} (descending)`);
258
+
259
+ const repoTypesText = repoTypes.map((repoType) => REPO_TYPE_LABELS[repoType].toLowerCase()).join(', ');
260
+ const searchDesc = searchTerms.length > 0 ? ` matching ${searchTerms.join(', ')}` : '';
261
+ if (!tryAppendLines([`Found ${totalResults.toString()} repositories across ${repoTypesText}${searchDesc}.`, ''])) {
262
+ truncated = true;
263
+ }
264
+
265
+ outer: for (const batch of searchBatches) {
266
+ if (truncated) {
267
+ break;
268
+ }
269
+
270
+ const sectionLabel = REPO_TYPE_LABELS[batch.repoType];
271
+ if (!tryAppendLines([`## ${sectionLabel} (${batch.results.length.toString()})`, ''])) {
272
+ truncated = true;
273
+ break;
274
+ }
275
+
276
+ if (batch.results.length === 0) {
277
+ if (!tryAppendLines([`No ${sectionLabel.toLowerCase()} matched this query.`, ''])) {
278
+ truncated = true;
279
+ break;
280
+ }
281
+ continue;
282
+ }
283
+
284
+ for (const result of batch.results) {
285
+ const repoLines: string[] = [];
286
+ appendRepoResult(repoLines, batch.repoType, result);
287
+
288
+ if (!tryAppendLines(repoLines)) {
289
+ truncated = true;
290
+ break outer;
291
+ }
292
+
293
+ resultsShared += 1;
294
+ }
295
+ }
296
+
297
+ if (truncated) {
298
+ const truncationLines = [
299
+ '',
300
+ `⚠️ Results truncated at approximately ${TOKEN_CAP.toLocaleString()} tokens (${MAX_OUTPUT_CHARS.toLocaleString()} characters).`,
301
+ `Included ${resultsShared.toString()} of ${totalResults.toString()} repositories. Narrow the query, reduce limit, or filter repo_types to see more.`,
302
+ ];
303
+
304
+ while (lines.length > 0 && [...lines, ...truncationLines].join('\n').length > MAX_OUTPUT_CHARS) {
305
+ lines.pop();
306
+ }
307
+
308
+ if ([...lines, ...truncationLines].join('\n').length <= MAX_OUTPUT_CHARS) {
309
+ lines.push(...truncationLines);
310
+ }
311
+ }
312
+
313
+ return {
314
+ formatted: lines.join('\n'),
315
+ totalResults,
316
+ resultsShared,
317
+ };
318
+ }
319
+
320
+ function appendRepoResult(lines: string[], repoType: RepoType, result: RepoSearchResult): void {
321
+ const heading = repoType === 'space' ? getSpaceHeading(result) : result.id;
322
+ lines.push(`### ${heading}`);
323
+ lines.push('');
324
+
325
+ if (repoType === 'dataset') {
326
+ const dataset = result as DatasetRepoResult;
327
+ if (dataset.description) {
328
+ const trimmed = dataset.description.substring(0, 200);
329
+ lines.push(`${trimmed}${dataset.description.length > 200 ? '...' : ''}`);
330
+ lines.push('');
331
+ }
332
+ }
333
+
334
+ if (repoType === 'space') {
335
+ const space = result as SpaceRepoResult;
336
+ const description = space.shortDescription || space.ai_short_description;
337
+ if (description) {
338
+ lines.push(description);
339
+ lines.push('');
340
+ }
341
+ }
342
+
343
+ const info: string[] = [];
344
+ if (result.author) info.push(`**Author:** ${result.author}`);
345
+
346
+ if (repoType === 'model') {
347
+ const model = result as ModelRepoResult;
348
+ if (model.pipeline_tag) info.push(`**Task:** ${model.pipeline_tag}`);
349
+ if (model.library_name) info.push(`**Library:** ${model.library_name}`);
350
+ }
351
+
352
+ if (repoType === 'space') {
353
+ const space = result as SpaceRepoResult;
354
+ if (space.sdk) info.push(`**SDK:** ${space.sdk}`);
355
+ }
356
+
357
+ if (typeof result.downloads === 'number') info.push(`**Downloads:** ${formatNumber(result.downloads)}`);
358
+ if (typeof result.likes === 'number') info.push(`**Likes:** ${result.likes.toString()}`);
359
+ if (typeof result.trendingScore === 'number') info.push(`**Trending Score:** ${result.trendingScore.toString()}`);
360
+
361
+ if (info.length > 0) {
362
+ lines.push(info.join(' | '));
363
+ lines.push('');
364
+ }
365
+
366
+ if (result.tags && result.tags.length > 0) {
367
+ lines.push(`**Tags:** ${result.tags.slice(0, TAGS_TO_RETURN).join(', ')}`);
368
+ if (result.tags.length > TAGS_TO_RETURN) {
369
+ lines.push(`*and ${(result.tags.length - TAGS_TO_RETURN).toString()} more...*`);
370
+ }
371
+ lines.push('');
372
+ }
373
+
374
+ const status: string[] = [];
375
+ if (result.private) status.push('🔐 Private');
376
+ if (repoType === 'dataset' && (result as DatasetRepoResult).gated) status.push('🔒 Gated');
377
+ if (repoType === 'space' && (result as SpaceRepoResult).disabled) status.push('⛔ Disabled');
378
+
379
+ if (status.length > 0) {
380
+ lines.push(status.join(' | '));
381
+ lines.push('');
382
+ }
383
+
384
+ if (result.createdAt) {
385
+ lines.push(`**Created:** ${formatDate(result.createdAt)}`);
386
+ }
387
+ if (result.lastModified && result.lastModified !== result.createdAt) {
388
+ lines.push(`**Last Modified:** ${formatDate(result.lastModified)}`);
389
+ }
390
+
391
+ lines.push(`**Link:** [${getRepoLink(repoType, result.id)}](${getRepoLink(repoType, result.id)})`);
392
+ lines.push('');
393
+ lines.push('---');
394
+ lines.push('');
395
+ }
396
+
397
+ function getSpaceHeading(result: RepoSearchResult): string {
398
+ const space = result as SpaceRepoResult;
399
+ const title = space.title?.trim();
400
+ if (title && title.length > 0) {
401
+ return `${title} (\`${result.id}\`)`;
402
+ }
403
+ return result.id;
404
+ }
405
+
406
+ function getRepoLink(repoType: RepoType, id: string): string {
407
+ if (repoType === 'dataset') {
408
+ return `https://hf.co/datasets/${id}`;
409
+ }
410
+ if (repoType === 'space') {
411
+ return `https://hf.co/spaces/${id}`;
412
+ }
413
+ return `https://hf.co/${id}`;
414
+ }
@@ -1,12 +1,13 @@
1
1
  import type { ToolResult } from '../../types/tool-result.js';
2
2
  import { escapeMarkdown } from '../../utilities.js';
3
3
  import { VIEW_PARAMETERS } from '../types.js';
4
+ import { fetchWithProfile, NETWORK_FETCH_PROFILES } from '../../network/fetch-profile.js';
4
5
 
5
6
  /**
6
7
  * Prompt configuration for discover operation (from DYNAMIC_SPACE_DATA)
7
8
  * These prompts can be easily tweaked to adjust behavior
8
9
  */
9
- export const DISCOVER_PROMPTS = {
10
+ const DISCOVER_PROMPTS = {
10
11
  // Header for results
11
12
  RESULTS_HEADER: `**Available Spaces:**
12
13
 
@@ -90,7 +91,14 @@ export async function discoverSpaces(): Promise<ToolResult> {
90
91
  }
91
92
 
92
93
  try {
93
- const response = await fetch(url);
94
+ const allowPermissiveUrls = process.env.ALLOW_PERMISSIVE_URLS === 'true';
95
+ const profile = allowPermissiveUrls
96
+ ? NETWORK_FETCH_PROFILES.httpOrHttpsPermissive()
97
+ : NETWORK_FETCH_PROFILES.externalHttps();
98
+
99
+ const { response } = await fetchWithProfile(url, profile, {
100
+ timeoutMs: 10000,
101
+ });
94
102
 
95
103
  if (!response.ok) {
96
104
  return {
@@ -5,7 +5,7 @@ import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/proto
5
5
  import { analyzeSchemaComplexity, validateParameters, applyDefaults } from '../utils/schema-validator.js';
6
6
  import { formatComplexSchemaError, formatValidationError } from '../utils/parameter-formatter.js';
7
7
  import { callGradioToolWithHeaders } from '../utils/gradio-caller.js';
8
- import { parseGradioSchemaResponse, normalizeParsedTools } from '../utils/gradio-schema.js';
8
+ import { fetchGradioSchema, fetchSpaceMetadata } from '../utils/space-http.js';
9
9
 
10
10
  /**
11
11
  * Invokes a Gradio space with provided parameters
@@ -112,90 +112,3 @@ export async function invokeSpace(
112
112
  };
113
113
  }
114
114
  }
115
-
116
- /**
117
- * Fetches space metadata from HuggingFace API
118
- */
119
- async function fetchSpaceMetadata(
120
- spaceName: string,
121
- hfToken?: string
122
- ): Promise<{ subdomain: string; private: boolean }> {
123
- const url = `https://huggingface.co/api/spaces/${spaceName}`;
124
- const headers: Record<string, string> = {};
125
-
126
- if (hfToken) {
127
- headers['Authorization'] = `Bearer ${hfToken}`;
128
- }
129
-
130
- const controller = new AbortController();
131
- const timeoutId = setTimeout(() => controller.abort(), 10000);
132
-
133
- try {
134
- const response = await fetch(url, {
135
- headers,
136
- signal: controller.signal,
137
- });
138
-
139
- clearTimeout(timeoutId);
140
-
141
- if (!response.ok) {
142
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
143
- }
144
-
145
- const info = (await response.json()) as {
146
- subdomain?: string;
147
- private?: boolean;
148
- };
149
-
150
- if (!info.subdomain) {
151
- throw new Error('Space does not have a subdomain');
152
- }
153
-
154
- return {
155
- subdomain: info.subdomain,
156
- private: info.private || false,
157
- };
158
- } finally {
159
- clearTimeout(timeoutId);
160
- }
161
- }
162
-
163
- /**
164
- * Fetches schema from Gradio endpoint
165
- */
166
- async function fetchGradioSchema(subdomain: string, isPrivate: boolean, hfToken?: string): Promise<Tool[]> {
167
- const schemaUrl = `https://${subdomain}.hf.space/gradio_api/mcp/schema`;
168
-
169
- const headers: Record<string, string> = {
170
- 'Content-Type': 'application/json',
171
- };
172
-
173
- if (isPrivate && hfToken) {
174
- headers['X-HF-Authorization'] = `Bearer ${hfToken}`;
175
- }
176
-
177
- const controller = new AbortController();
178
- const timeoutId = setTimeout(() => controller.abort(), 10000);
179
-
180
- try {
181
- const response = await fetch(schemaUrl, {
182
- method: 'GET',
183
- headers,
184
- signal: controller.signal,
185
- });
186
-
187
- clearTimeout(timeoutId);
188
-
189
- if (!response.ok) {
190
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
191
- }
192
-
193
- const schemaResponse = (await response.json()) as unknown;
194
-
195
- // Parse schema response (handle both array and object formats)
196
- const parsed = parseGradioSchemaResponse(schemaResponse);
197
- return normalizeParsedTools(parsed);
198
- } finally {
199
- clearTimeout(timeoutId);
200
- }
201
- }