@llmindset/hf-mcp 0.3.2 → 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.
- package/dist/docs-search/doc-fetch.d.ts +1 -0
- package/dist/docs-search/doc-fetch.d.ts.map +1 -1
- package/dist/docs-search/doc-fetch.js +9 -12
- package/dist/docs-search/doc-fetch.js.map +1 -1
- package/dist/docs-search/doc-fetch.test.js +56 -11
- package/dist/docs-search/doc-fetch.test.js.map +1 -1
- package/dist/file-icons.d.ts +3 -0
- package/dist/file-icons.d.ts.map +1 -0
- package/dist/file-icons.js +38 -0
- package/dist/file-icons.js.map +1 -0
- package/dist/gradio-files.d.ts +0 -1
- package/dist/gradio-files.d.ts.map +1 -1
- package/dist/gradio-files.js +2 -35
- package/dist/gradio-files.js.map +1 -1
- package/dist/hf-api-call.d.ts.map +1 -1
- package/dist/hf-api-call.js +7 -7
- package/dist/hf-api-call.js.map +1 -1
- package/dist/index.browser.d.ts +48 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +153 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jobs/commands/uv-utils.d.ts +0 -3
- package/dist/jobs/commands/uv-utils.d.ts.map +1 -1
- package/dist/jobs/commands/uv-utils.js +2 -2
- package/dist/jobs/commands/uv-utils.js.map +1 -1
- package/dist/jobs/jobs-tool.d.ts.map +1 -1
- package/dist/jobs/jobs-tool.js +11 -12
- package/dist/jobs/jobs-tool.js.map +1 -1
- package/dist/jobs/schema-help.d.ts +2 -9
- package/dist/jobs/schema-help.d.ts.map +1 -1
- package/dist/jobs/schema-help.js +3 -3
- package/dist/jobs/schema-help.js.map +1 -1
- package/dist/jobs/sse-handler.d.ts +3 -2
- package/dist/jobs/sse-handler.d.ts.map +1 -1
- package/dist/jobs/sse-handler.js +8 -4
- package/dist/jobs/sse-handler.js.map +1 -1
- package/dist/jobs/types.d.ts +1 -1
- package/dist/logger.d.ts +2 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/network/fetch-profile.d.ts +24 -0
- package/dist/network/fetch-profile.d.ts.map +1 -0
- package/dist/network/fetch-profile.js +80 -0
- package/dist/network/fetch-profile.js.map +1 -0
- package/dist/network/index.d.ts +5 -0
- package/dist/network/index.d.ts.map +1 -0
- package/dist/network/index.js +5 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/ip-policy.d.ts +6 -0
- package/dist/network/ip-policy.d.ts.map +1 -0
- package/dist/network/ip-policy.js +166 -0
- package/dist/network/ip-policy.js.map +1 -0
- package/dist/network/ip-policy.test.d.ts +2 -0
- package/dist/network/ip-policy.test.d.ts.map +1 -0
- package/dist/network/ip-policy.test.js +26 -0
- package/dist/network/ip-policy.test.js.map +1 -0
- package/dist/network/safe-fetch.d.ts +16 -0
- package/dist/network/safe-fetch.d.ts.map +1 -0
- package/dist/network/safe-fetch.js +124 -0
- package/dist/network/safe-fetch.js.map +1 -0
- package/dist/network/safe-fetch.test.d.ts +2 -0
- package/dist/network/safe-fetch.test.d.ts.map +1 -0
- package/dist/network/safe-fetch.test.js +136 -0
- package/dist/network/safe-fetch.test.js.map +1 -0
- package/dist/network/url-policy.d.ts +32 -0
- package/dist/network/url-policy.d.ts.map +1 -0
- package/dist/network/url-policy.js +230 -0
- package/dist/network/url-policy.js.map +1 -0
- package/dist/network/url-policy.test.d.ts +2 -0
- package/dist/network/url-policy.test.d.ts.map +1 -0
- package/dist/network/url-policy.test.js +57 -0
- package/dist/network/url-policy.test.js.map +1 -0
- package/dist/readme-utils.d.ts.map +1 -1
- package/dist/readme-utils.js +3 -4
- package/dist/readme-utils.js.map +1 -1
- package/dist/space/commands/discover.d.ts +0 -5
- package/dist/space/commands/discover.d.ts.map +1 -1
- package/dist/space/commands/discover.js +9 -2
- package/dist/space/commands/discover.js.map +1 -1
- package/dist/space/commands/invoke.js +1 -59
- package/dist/space/commands/invoke.js.map +1 -1
- package/dist/space/commands/view-parameters.d.ts.map +1 -1
- package/dist/space/commands/view-parameters.js +3 -98
- package/dist/space/commands/view-parameters.js.map +1 -1
- package/dist/space/dynamic-space-tool.d.ts.map +1 -1
- package/dist/space/dynamic-space-tool.js +5 -2
- package/dist/space/dynamic-space-tool.js.map +1 -1
- package/dist/space/utils/gradio-caller.d.ts.map +1 -1
- package/dist/space/utils/gradio-caller.js +13 -6
- package/dist/space/utils/gradio-caller.js.map +1 -1
- package/dist/space/utils/space-http.d.ts +8 -0
- package/dist/space/utils/space-http.d.ts.map +1 -0
- package/dist/space/utils/space-http.js +49 -0
- package/dist/space/utils/space-http.js.map +1 -0
- package/dist/space-files.d.ts +0 -1
- package/dist/space-files.d.ts.map +1 -1
- package/dist/space-files.js +3 -36
- package/dist/space-files.js.map +1 -1
- package/package.json +6 -2
- package/src/docs-search/doc-fetch.test.ts +98 -28
- package/src/docs-search/doc-fetch.ts +9 -16
- package/src/file-icons.ts +39 -0
- package/src/gradio-files.ts +2 -40
- package/src/hf-api-call.ts +8 -10
- package/src/index.browser.ts +183 -0
- package/src/index.ts +1 -0
- package/src/jobs/commands/uv-utils.ts +2 -2
- package/src/jobs/jobs-tool.ts +13 -12
- package/src/jobs/schema-help.ts +4 -4
- package/src/jobs/sse-handler.ts +12 -7
- package/src/logger.ts +2 -2
- package/src/network/fetch-profile.ts +112 -0
- package/src/network/index.ts +4 -0
- package/src/network/ip-policy.test.ts +29 -0
- package/src/network/ip-policy.ts +206 -0
- package/src/network/safe-fetch.test.ts +181 -0
- package/src/network/safe-fetch.ts +174 -0
- package/src/network/url-policy.test.ts +100 -0
- package/src/network/url-policy.ts +304 -0
- package/src/readme-utils.ts +11 -10
- package/src/space/commands/discover.ts +10 -2
- package/src/space/commands/invoke.ts +1 -88
- package/src/space/commands/view-parameters.ts +3 -136
- package/src/space/dynamic-space-tool.ts +6 -2
- package/src/space/utils/gradio-caller.ts +25 -12
- package/src/space/utils/space-http.ts +75 -0
- package/src/space-files.ts +3 -41
- package/test/fetch-guard.spec.ts +70 -0
- package/test/jobs/sse-handler.spec.ts +60 -0
- package/dist/space/utils/result-formatter.d.ts +0 -4
- package/dist/space/utils/result-formatter.d.ts.map +0 -1
- package/dist/space/utils/result-formatter.js +0 -146
- package/dist/space/utils/result-formatter.js.map +0 -1
- package/src/space/utils/result-formatter.ts +0 -226
|
@@ -19,13 +19,13 @@ const createMockResponse = ({
|
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
const stubFetch = (factory: () => Response) => {
|
|
22
|
-
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(factory()));
|
|
22
|
+
const fetchMock = vi.fn<typeof fetch>().mockImplementation(() => Promise.resolve(factory()));
|
|
23
23
|
vi.stubGlobal('fetch', fetchMock);
|
|
24
24
|
return fetchMock;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
describe('DocFetchTool', () => {
|
|
28
|
-
|
|
28
|
+
const tool = new DocFetchTool();
|
|
29
29
|
|
|
30
30
|
afterEach(() => {
|
|
31
31
|
vi.clearAllMocks();
|
|
@@ -72,27 +72,28 @@ describe('DocFetchTool', () => {
|
|
|
72
72
|
createMockResponse({
|
|
73
73
|
content: markdown,
|
|
74
74
|
contentType: 'text/markdown',
|
|
75
|
-
})
|
|
75
|
+
})
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
const result = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test' });
|
|
79
|
-
expect(fetchMock).
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
80
|
+
const [calledUrl, calledInit] = fetchMock.mock.calls[0] ?? [];
|
|
81
|
+
expect(calledUrl).toBe('https://huggingface.co/docs/test');
|
|
82
|
+
expect(calledInit?.redirect).toBe('manual');
|
|
83
|
+
expect(new Headers(calledInit?.headers).get('accept')).toBe('text/markdown');
|
|
82
84
|
expect(result).toBe(markdown);
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
it('should return small documents without chunking', async () => {
|
|
86
|
-
|
|
87
88
|
// Mock fetch to return HTML that converts to short markdown
|
|
88
89
|
stubFetch(() =>
|
|
89
90
|
createMockResponse({
|
|
90
91
|
content: '<h1>Short Document</h1><p>This is a short document.</p>',
|
|
91
|
-
})
|
|
92
|
+
})
|
|
92
93
|
);
|
|
93
94
|
|
|
94
95
|
const result = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test' });
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
expect(result).toContain('# Short Document');
|
|
97
98
|
expect(result).toContain('This is a short document');
|
|
98
99
|
expect(result).not.toContain('DOCUMENT TRUNCATED');
|
|
@@ -100,16 +101,20 @@ describe('DocFetchTool', () => {
|
|
|
100
101
|
|
|
101
102
|
it('should chunk large documents and show truncation message', async () => {
|
|
102
103
|
// Mock fetch to return HTML that converts to long markdown
|
|
103
|
-
const longHtml =
|
|
104
|
-
|
|
104
|
+
const longHtml =
|
|
105
|
+
'<h1>Long Document</h1>' +
|
|
106
|
+
'<p>This is a very long sentence that will be repeated many times to create a document that exceeds the 7500 token limit for testing chunking functionality.</p>'.repeat(
|
|
107
|
+
200
|
|
108
|
+
);
|
|
109
|
+
|
|
105
110
|
stubFetch(() =>
|
|
106
111
|
createMockResponse({
|
|
107
112
|
content: longHtml,
|
|
108
|
-
})
|
|
113
|
+
})
|
|
109
114
|
);
|
|
110
115
|
|
|
111
116
|
const result = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test' });
|
|
112
|
-
|
|
117
|
+
|
|
113
118
|
expect(result).toContain('# Long Document');
|
|
114
119
|
expect(result).toContain('DOCUMENT TRUNCATED');
|
|
115
120
|
expect(result).toContain('CALL hf_doc_fetch WITH AN OFFSET OF');
|
|
@@ -133,13 +138,15 @@ describe('DocFetchTool', () => {
|
|
|
133
138
|
const fetchMock = stubFetch(() =>
|
|
134
139
|
createMockResponse({
|
|
135
140
|
content: '<h1>Title</h1><p>Body</p>',
|
|
136
|
-
})
|
|
141
|
+
})
|
|
137
142
|
);
|
|
138
143
|
|
|
139
144
|
const result = await tool.fetch({ doc_url: '/docs/test' });
|
|
140
|
-
expect(fetchMock).
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
146
|
+
const [calledUrl, calledInit] = fetchMock.mock.calls[0] ?? [];
|
|
147
|
+
expect(calledUrl).toBe('https://huggingface.co/docs/test');
|
|
148
|
+
expect(calledInit?.redirect).toBe('manual');
|
|
149
|
+
expect(new Headers(calledInit?.headers).get('accept')).toBe('text/markdown');
|
|
143
150
|
expect(result).toContain('# Title');
|
|
144
151
|
});
|
|
145
152
|
|
|
@@ -147,28 +154,34 @@ describe('DocFetchTool', () => {
|
|
|
147
154
|
const fetchMock = stubFetch(() =>
|
|
148
155
|
createMockResponse({
|
|
149
156
|
content: '<h1>Another Title</h1><p>Body</p>',
|
|
150
|
-
})
|
|
157
|
+
})
|
|
151
158
|
);
|
|
152
159
|
|
|
153
160
|
await tool.fetch({ doc_url: './docs/another' });
|
|
154
|
-
expect(fetchMock).
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
162
|
+
const [calledUrl, calledInit] = fetchMock.mock.calls[0] ?? [];
|
|
163
|
+
expect(calledUrl).toBe('https://huggingface.co/docs/another');
|
|
164
|
+
expect(calledInit?.redirect).toBe('manual');
|
|
165
|
+
expect(new Headers(calledInit?.headers).get('accept')).toBe('text/markdown');
|
|
157
166
|
});
|
|
158
167
|
|
|
159
168
|
it('should return subsequent chunks with offset', async () => {
|
|
160
169
|
// Mock fetch to return the same long HTML
|
|
161
|
-
const longHtml =
|
|
162
|
-
|
|
170
|
+
const longHtml =
|
|
171
|
+
'<h1>Long Document</h1>' +
|
|
172
|
+
'<p>This is a very long sentence that will be repeated many times to create a document that exceeds the 7500 token limit for testing chunking functionality.</p>'.repeat(
|
|
173
|
+
200
|
|
174
|
+
);
|
|
175
|
+
|
|
163
176
|
stubFetch(() =>
|
|
164
177
|
createMockResponse({
|
|
165
178
|
content: longHtml,
|
|
166
|
-
})
|
|
179
|
+
})
|
|
167
180
|
);
|
|
168
181
|
|
|
169
182
|
// Get first chunk
|
|
170
183
|
const firstChunk = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test' });
|
|
171
|
-
|
|
184
|
+
|
|
172
185
|
// Extract offset from truncation message
|
|
173
186
|
const offsetMatch = firstChunk.match(/OFFSET OF (\d+)/);
|
|
174
187
|
expect(offsetMatch).toBeTruthy();
|
|
@@ -176,7 +189,7 @@ describe('DocFetchTool', () => {
|
|
|
176
189
|
|
|
177
190
|
// Get second chunk
|
|
178
191
|
const secondChunk = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test', offset });
|
|
179
|
-
|
|
192
|
+
|
|
180
193
|
expect(secondChunk).not.toEqual(firstChunk);
|
|
181
194
|
expect(secondChunk.length).toBeGreaterThan(0);
|
|
182
195
|
});
|
|
@@ -185,12 +198,69 @@ describe('DocFetchTool', () => {
|
|
|
185
198
|
stubFetch(() =>
|
|
186
199
|
createMockResponse({
|
|
187
200
|
content: '<h1>Short Document</h1><p>This is short.</p>',
|
|
188
|
-
})
|
|
201
|
+
})
|
|
189
202
|
);
|
|
190
203
|
|
|
191
204
|
const result = await tool.fetch({ doc_url: 'https://huggingface.co/docs/test', offset: 10000 });
|
|
192
|
-
|
|
205
|
+
|
|
193
206
|
expect(result).toContain('Error: Offset 10000 is beyond');
|
|
194
207
|
});
|
|
195
208
|
});
|
|
209
|
+
|
|
210
|
+
describe('security hardening', () => {
|
|
211
|
+
it('rejects traversal payload variants', async () => {
|
|
212
|
+
const traversalUrls = [
|
|
213
|
+
'https://huggingface.co/docs/../x',
|
|
214
|
+
'https://huggingface.co/docs/%2e%2e/x',
|
|
215
|
+
'https://huggingface.co/docs/%2e%2e%2fx',
|
|
216
|
+
'https://huggingface.co/docs/..%2fx',
|
|
217
|
+
'https://huggingface.co/docs/%2e%2e%5cx',
|
|
218
|
+
'https://huggingface.co/docs/%252e%252e%252fx',
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const fetchMock = stubFetch(() =>
|
|
222
|
+
createMockResponse({
|
|
223
|
+
content: 'should never be fetched',
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
for (const docUrl of traversalUrls) {
|
|
228
|
+
await expect(tool.fetch({ doc_url: docUrl })).rejects.toThrow('Failed to fetch document');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('rejects redirect to non-allowlisted host', async () => {
|
|
235
|
+
const fetchMock = vi.fn().mockResolvedValueOnce(
|
|
236
|
+
new Response('', {
|
|
237
|
+
status: 302,
|
|
238
|
+
headers: { location: 'https://example.com/evil' },
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
242
|
+
|
|
243
|
+
await expect(tool.fetch({ doc_url: 'https://huggingface.co/docs/transformers' })).rejects.toThrow(
|
|
244
|
+
'Failed to fetch document'
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('rejects redirect to http downgrade', async () => {
|
|
251
|
+
const fetchMock = vi.fn().mockResolvedValueOnce(
|
|
252
|
+
new Response('', {
|
|
253
|
+
status: 302,
|
|
254
|
+
headers: { location: 'http://huggingface.co/docs/transformers' },
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
258
|
+
|
|
259
|
+
await expect(tool.fetch({ doc_url: 'https://huggingface.co/docs/transformers' })).rejects.toThrow(
|
|
260
|
+
'Failed to fetch document'
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
196
266
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import TurndownService from 'turndown';
|
|
3
3
|
import { estimateTokens } from '../utilities.js';
|
|
4
|
+
import { createHfDocsPolicy, parseAndValidateUrl } from '../network/url-policy.js';
|
|
5
|
+
import { fetchWithProfile, NETWORK_FETCH_PROFILES } from '../network/fetch-profile.js';
|
|
4
6
|
|
|
5
7
|
export const DOC_FETCH_CONFIG = {
|
|
6
8
|
name: 'hf_doc_fetch',
|
|
@@ -26,6 +28,7 @@ export type DocFetchParams = z.infer<typeof DOC_FETCH_CONFIG.schema>;
|
|
|
26
28
|
|
|
27
29
|
export class DocFetchTool {
|
|
28
30
|
private turndownService: TurndownService;
|
|
31
|
+
private readonly docsPolicy = createHfDocsPolicy();
|
|
29
32
|
|
|
30
33
|
constructor() {
|
|
31
34
|
this.turndownService = new TurndownService({
|
|
@@ -120,19 +123,7 @@ export class DocFetchTool {
|
|
|
120
123
|
*/
|
|
121
124
|
validateUrl(hfUrl: string): void {
|
|
122
125
|
try {
|
|
123
|
-
|
|
124
|
-
if (url.protocol !== 'https:') {
|
|
125
|
-
throw new Error('That was not a valid documentation URL');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const hostname = url.hostname.toLowerCase();
|
|
129
|
-
const isHfDocs =
|
|
130
|
-
(hostname === 'huggingface.co' || hostname === 'www.huggingface.co') && url.pathname.startsWith('/docs/');
|
|
131
|
-
const isGradio = hostname === 'gradio.app' || hostname === 'www.gradio.app';
|
|
132
|
-
|
|
133
|
-
if (!isHfDocs && !isGradio) {
|
|
134
|
-
throw new Error('That was not a valid documentation URL');
|
|
135
|
-
}
|
|
126
|
+
parseAndValidateUrl(hfUrl, this.docsPolicy);
|
|
136
127
|
} catch {
|
|
137
128
|
throw new Error('That was not a valid documentation URL');
|
|
138
129
|
}
|
|
@@ -144,9 +135,11 @@ export class DocFetchTool {
|
|
|
144
135
|
async fetch(params: DocFetchParams): Promise<string> {
|
|
145
136
|
try {
|
|
146
137
|
const normalizedUrl = normalizeDocUrl(params.doc_url);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
const { response } = await fetchWithProfile(normalizedUrl, NETWORK_FETCH_PROFILES.hfDocs(), {
|
|
139
|
+
requestInit: {
|
|
140
|
+
headers: { accept: 'text/markdown' },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
150
143
|
if (!response.ok) {
|
|
151
144
|
throw new Error(`Failed to fetch document: ${response.status} ${response.statusText}`);
|
|
152
145
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const FILE_ICON_BY_EXTENSION: Readonly<Record<string, string>> = {
|
|
2
|
+
py: '🐍',
|
|
3
|
+
js: '📜',
|
|
4
|
+
ts: '📘',
|
|
5
|
+
md: '📝',
|
|
6
|
+
txt: '📄',
|
|
7
|
+
json: '📊',
|
|
8
|
+
yaml: '⚙️',
|
|
9
|
+
yml: '⚙️',
|
|
10
|
+
png: '🖼️',
|
|
11
|
+
jpg: '🖼️',
|
|
12
|
+
jpeg: '🖼️',
|
|
13
|
+
gif: '🖼️',
|
|
14
|
+
svg: '🎨',
|
|
15
|
+
mp4: '🎬',
|
|
16
|
+
mp3: '🎵',
|
|
17
|
+
pdf: '📕',
|
|
18
|
+
zip: '📦',
|
|
19
|
+
tar: '📦',
|
|
20
|
+
gz: '📦',
|
|
21
|
+
html: '🌐',
|
|
22
|
+
css: '🎨',
|
|
23
|
+
ipynb: '📓',
|
|
24
|
+
csv: '📊',
|
|
25
|
+
parquet: '🗄️',
|
|
26
|
+
safetensors: '🤖',
|
|
27
|
+
bin: '💾',
|
|
28
|
+
pkl: '🥒',
|
|
29
|
+
h5: '🗃️',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function getFileIcon(filename: string): string {
|
|
33
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
34
|
+
if (!extension) {
|
|
35
|
+
return '📄';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return FILE_ICON_BY_EXTENSION[extension] ?? '📄';
|
|
39
|
+
}
|
package/src/gradio-files.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { listFiles } from '@huggingface/hub';
|
|
|
3
3
|
import { formatBytes, escapeMarkdown } from './utilities.js';
|
|
4
4
|
import { HfApiError } from './hf-api-call.js';
|
|
5
5
|
import { explain } from './error-messages.js';
|
|
6
|
+
import { getFileIcon } from './file-icons.js';
|
|
6
7
|
|
|
7
8
|
// Define the FileWithUrl interface
|
|
8
9
|
interface FileWithUrl {
|
|
@@ -207,7 +208,7 @@ export class GradioFilesTool {
|
|
|
207
208
|
|
|
208
209
|
for (const file of files) {
|
|
209
210
|
const fileName = file.path.split('/').pop() || file.path;
|
|
210
|
-
const icon =
|
|
211
|
+
const icon = getFileIcon(fileName);
|
|
211
212
|
const lastMod = file.lastModified ? new Date(file.lastModified).toLocaleDateString() : '-';
|
|
212
213
|
|
|
213
214
|
markdown += `| ${escapeMarkdown(fileName)} | ${file.sizeFormatted} | ${icon} ${file.type} | ${lastMod} | ${file.url} |\n`;
|
|
@@ -215,43 +216,4 @@ export class GradioFilesTool {
|
|
|
215
216
|
|
|
216
217
|
return markdown;
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Get file icon based on extension
|
|
221
|
-
*/
|
|
222
|
-
private getFileIcon(filename: string): string {
|
|
223
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
224
|
-
const iconMap: Record<string, string> = {
|
|
225
|
-
py: '🐍',
|
|
226
|
-
js: '📜',
|
|
227
|
-
ts: '📘',
|
|
228
|
-
md: '📝',
|
|
229
|
-
txt: '📄',
|
|
230
|
-
json: '📊',
|
|
231
|
-
yaml: '⚙️',
|
|
232
|
-
yml: '⚙️',
|
|
233
|
-
png: '🖼️',
|
|
234
|
-
jpg: '🖼️',
|
|
235
|
-
jpeg: '🖼️',
|
|
236
|
-
gif: '🖼️',
|
|
237
|
-
svg: '🎨',
|
|
238
|
-
mp4: '🎬',
|
|
239
|
-
mp3: '🎵',
|
|
240
|
-
pdf: '📕',
|
|
241
|
-
zip: '📦',
|
|
242
|
-
tar: '📦',
|
|
243
|
-
gz: '📦',
|
|
244
|
-
html: '🌐',
|
|
245
|
-
css: '🎨',
|
|
246
|
-
ipynb: '📓',
|
|
247
|
-
csv: '📊',
|
|
248
|
-
parquet: '🗄️',
|
|
249
|
-
safetensors: '🤖',
|
|
250
|
-
bin: '💾',
|
|
251
|
-
pkl: '🥒',
|
|
252
|
-
h5: '🗃️',
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
return iconMap[ext || ''] || '📄';
|
|
256
|
-
}
|
|
257
219
|
}
|
package/src/hf-api-call.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fetchWithProfile, NETWORK_FETCH_PROFILES } from './network/fetch-profile.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Custom error class that includes HTTP status information
|
|
3
5
|
*/
|
|
@@ -99,18 +101,14 @@ export class HfApiCall<TParams = Record<string, string | undefined>, TResponse =
|
|
|
99
101
|
headers['Authorization'] = `Bearer ${this.hfToken}`;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
headers,
|
|
109
|
-
signal: controller.signal,
|
|
104
|
+
const { response } = await fetchWithProfile(url.toString(), NETWORK_FETCH_PROFILES.externalHttps(), {
|
|
105
|
+
timeoutMs: this.apiTimeout,
|
|
106
|
+
requestInit: {
|
|
107
|
+
...options,
|
|
108
|
+
headers,
|
|
109
|
+
},
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
clearTimeout(timeoutId);
|
|
113
|
-
|
|
114
112
|
const responseBodyText = await response.text();
|
|
115
113
|
|
|
116
114
|
if (!response.ok) {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
const KNOWLEDGE_DATE = new Intl.DateTimeFormat('en-GB', {
|
|
2
|
+
day: 'numeric',
|
|
3
|
+
month: 'long',
|
|
4
|
+
year: 'numeric',
|
|
5
|
+
timeZone: 'UTC',
|
|
6
|
+
}).format(new Date());
|
|
7
|
+
|
|
8
|
+
interface BrowserToolConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
annotations: {
|
|
12
|
+
title: string;
|
|
13
|
+
destructiveHint: boolean;
|
|
14
|
+
readOnlyHint: boolean;
|
|
15
|
+
openWorldHint: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const SEMANTIC_SEARCH_TOOL_CONFIG: BrowserToolConfig = {
|
|
20
|
+
name: 'space_search',
|
|
21
|
+
description:
|
|
22
|
+
'Find Hugging Face Spaces using semantic search. IMPORTANT Only MCP Servers can be used with the dynamic_space tool' +
|
|
23
|
+
'Include links to the Space when presenting the results.',
|
|
24
|
+
annotations: {
|
|
25
|
+
title: 'Hugging Face Space Search',
|
|
26
|
+
destructiveHint: false,
|
|
27
|
+
readOnlyHint: true,
|
|
28
|
+
openWorldHint: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const REPO_SEARCH_TOOL_CONFIG: BrowserToolConfig = {
|
|
33
|
+
name: 'hub_repo_search',
|
|
34
|
+
description:
|
|
35
|
+
'Search Hugging Face repositories with a shared query interface. ' +
|
|
36
|
+
'You can target models, datasets, spaces, or aggregate across multiple repo types in one call. ' +
|
|
37
|
+
'Use space_search for semantic-first discovery of Spaces. ' +
|
|
38
|
+
'Include links to repositories in your response.',
|
|
39
|
+
annotations: {
|
|
40
|
+
title: 'Repo Search',
|
|
41
|
+
destructiveHint: false,
|
|
42
|
+
readOnlyHint: true,
|
|
43
|
+
openWorldHint: true,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const PAPER_SEARCH_TOOL_CONFIG: BrowserToolConfig = {
|
|
48
|
+
name: 'paper_search',
|
|
49
|
+
description:
|
|
50
|
+
'Find Machine Learning research papers on the Hugging Face hub. ' +
|
|
51
|
+
"Include 'Link to paper' When presenting the results. " +
|
|
52
|
+
'Consider whether tabulating results matches user intent.',
|
|
53
|
+
annotations: {
|
|
54
|
+
title: 'Paper Search',
|
|
55
|
+
destructiveHint: false,
|
|
56
|
+
readOnlyHint: true,
|
|
57
|
+
openWorldHint: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const HUB_REPO_DETAILS_TOOL_CONFIG: BrowserToolConfig = {
|
|
62
|
+
name: 'hub_repo_details',
|
|
63
|
+
description:
|
|
64
|
+
'Get details for one or more Hugging Face repos (model, dataset, or space). ' +
|
|
65
|
+
'Auto-detects type unless specified.',
|
|
66
|
+
annotations: {
|
|
67
|
+
title: 'Hub Repo Details',
|
|
68
|
+
destructiveHint: false,
|
|
69
|
+
readOnlyHint: true,
|
|
70
|
+
openWorldHint: false,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const DUPLICATE_SPACE_TOOL_CONFIG: BrowserToolConfig = {
|
|
75
|
+
name: 'duplicate_space',
|
|
76
|
+
description: '',
|
|
77
|
+
annotations: {
|
|
78
|
+
title: 'Duplicate Hugging Face Space',
|
|
79
|
+
destructiveHint: false,
|
|
80
|
+
readOnlyHint: false,
|
|
81
|
+
openWorldHint: true,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const SPACE_FILES_TOOL_CONFIG: BrowserToolConfig = {
|
|
86
|
+
name: 'space_files',
|
|
87
|
+
description: '',
|
|
88
|
+
annotations: {
|
|
89
|
+
title: 'Space Files List',
|
|
90
|
+
destructiveHint: false,
|
|
91
|
+
readOnlyHint: true,
|
|
92
|
+
openWorldHint: true,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const DOCS_SEMANTIC_SEARCH_CONFIG: BrowserToolConfig = {
|
|
97
|
+
name: 'hf_doc_search',
|
|
98
|
+
description:
|
|
99
|
+
'Search and Discover Hugging Face Product and Library documentation. Send an empty query to discover structure and navigation instructions. ' +
|
|
100
|
+
`Knowledge up-to-date as at ${KNOWLEDGE_DATE}. Combine with the Product filter to focus results.`,
|
|
101
|
+
annotations: {
|
|
102
|
+
title: 'Hugging Face Documentation Search',
|
|
103
|
+
destructiveHint: false,
|
|
104
|
+
readOnlyHint: true,
|
|
105
|
+
openWorldHint: true,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const DOC_FETCH_CONFIG: BrowserToolConfig = {
|
|
110
|
+
name: 'hf_doc_fetch',
|
|
111
|
+
description:
|
|
112
|
+
'Fetch a document from the Hugging Face or Gradio documentation library. For large documents, use offset to get subsequent chunks.',
|
|
113
|
+
annotations: {
|
|
114
|
+
title: 'Fetch a document from the Hugging Face documentation library',
|
|
115
|
+
destructiveHint: false,
|
|
116
|
+
readOnlyHint: true,
|
|
117
|
+
openWorldHint: true,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const SPACE_SEARCH_TOOL_ID = SEMANTIC_SEARCH_TOOL_CONFIG.name;
|
|
122
|
+
export const MODEL_SEARCH_TOOL_ID = 'model_search';
|
|
123
|
+
export const REPO_SEARCH_TOOL_ID = REPO_SEARCH_TOOL_CONFIG.name;
|
|
124
|
+
export const MODEL_DETAIL_TOOL_ID = 'model_details';
|
|
125
|
+
export const PAPER_SEARCH_TOOL_ID = PAPER_SEARCH_TOOL_CONFIG.name;
|
|
126
|
+
export const DATASET_SEARCH_TOOL_ID = 'dataset_search';
|
|
127
|
+
export const DATASET_DETAIL_TOOL_ID = 'dataset_details';
|
|
128
|
+
export const HUB_REPO_DETAILS_TOOL_ID = HUB_REPO_DETAILS_TOOL_CONFIG.name;
|
|
129
|
+
export const DUPLICATE_SPACE_TOOL_ID = DUPLICATE_SPACE_TOOL_CONFIG.name;
|
|
130
|
+
export const SPACE_INFO_TOOL_ID = 'space_info';
|
|
131
|
+
export const SPACE_FILES_TOOL_ID = SPACE_FILES_TOOL_CONFIG.name;
|
|
132
|
+
export const USE_SPACE_TOOL_ID = 'use_space';
|
|
133
|
+
export const DOCS_SEMANTIC_SEARCH_TOOL_ID = DOCS_SEMANTIC_SEARCH_CONFIG.name;
|
|
134
|
+
export const DOC_FETCH_TOOL_ID = DOC_FETCH_CONFIG.name;
|
|
135
|
+
export const HF_JOBS_TOOL_ID = 'hf_jobs';
|
|
136
|
+
export const DYNAMIC_SPACE_TOOL_ID = 'dynamic_space';
|
|
137
|
+
|
|
138
|
+
export const ALL_BUILTIN_TOOL_IDS = [
|
|
139
|
+
SPACE_SEARCH_TOOL_ID,
|
|
140
|
+
MODEL_SEARCH_TOOL_ID,
|
|
141
|
+
REPO_SEARCH_TOOL_ID,
|
|
142
|
+
MODEL_DETAIL_TOOL_ID,
|
|
143
|
+
PAPER_SEARCH_TOOL_ID,
|
|
144
|
+
DATASET_SEARCH_TOOL_ID,
|
|
145
|
+
DATASET_DETAIL_TOOL_ID,
|
|
146
|
+
HUB_REPO_DETAILS_TOOL_ID,
|
|
147
|
+
DUPLICATE_SPACE_TOOL_ID,
|
|
148
|
+
SPACE_INFO_TOOL_ID,
|
|
149
|
+
SPACE_FILES_TOOL_ID,
|
|
150
|
+
DOCS_SEMANTIC_SEARCH_TOOL_ID,
|
|
151
|
+
DOC_FETCH_TOOL_ID,
|
|
152
|
+
USE_SPACE_TOOL_ID,
|
|
153
|
+
HF_JOBS_TOOL_ID,
|
|
154
|
+
DYNAMIC_SPACE_TOOL_ID,
|
|
155
|
+
] as const;
|
|
156
|
+
|
|
157
|
+
export const TOOL_ID_GROUPS = {
|
|
158
|
+
search: [SPACE_SEARCH_TOOL_ID, REPO_SEARCH_TOOL_ID, PAPER_SEARCH_TOOL_ID, DOCS_SEMANTIC_SEARCH_TOOL_ID] as const,
|
|
159
|
+
spaces: [
|
|
160
|
+
SPACE_SEARCH_TOOL_ID,
|
|
161
|
+
DUPLICATE_SPACE_TOOL_ID,
|
|
162
|
+
SPACE_INFO_TOOL_ID,
|
|
163
|
+
SPACE_FILES_TOOL_ID,
|
|
164
|
+
USE_SPACE_TOOL_ID,
|
|
165
|
+
] as const,
|
|
166
|
+
detail: [MODEL_DETAIL_TOOL_ID, DATASET_DETAIL_TOOL_ID, HUB_REPO_DETAILS_TOOL_ID] as const,
|
|
167
|
+
docs: [DOCS_SEMANTIC_SEARCH_TOOL_ID, DOC_FETCH_TOOL_ID] as const,
|
|
168
|
+
hf_api: [
|
|
169
|
+
SPACE_SEARCH_TOOL_ID,
|
|
170
|
+
REPO_SEARCH_TOOL_ID,
|
|
171
|
+
PAPER_SEARCH_TOOL_ID,
|
|
172
|
+
HUB_REPO_DETAILS_TOOL_ID,
|
|
173
|
+
DOCS_SEMANTIC_SEARCH_TOOL_ID,
|
|
174
|
+
] as const,
|
|
175
|
+
dynamic_space: [DYNAMIC_SPACE_TOOL_ID] as const,
|
|
176
|
+
all: [...ALL_BUILTIN_TOOL_IDS] as const,
|
|
177
|
+
} as const;
|
|
178
|
+
|
|
179
|
+
export type BuiltinToolId = (typeof ALL_BUILTIN_TOOL_IDS)[number];
|
|
180
|
+
|
|
181
|
+
export function isValidBuiltinToolId(toolId: string): toolId is BuiltinToolId {
|
|
182
|
+
return (ALL_BUILTIN_TOOL_IDS as readonly string[]).includes(toolId);
|
|
183
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ export * from './jobs/jobs-tool.js';
|
|
|
24
24
|
export * from './space/dynamic-space-tool.js';
|
|
25
25
|
export * from './space/utils/gradio-caller.js';
|
|
26
26
|
export * from './space/utils/gradio-schema.js';
|
|
27
|
+
export * from './network/url-policy.js';
|
|
27
28
|
|
|
28
29
|
// Export shared types
|
|
29
30
|
export * from './types/tool-result.js';
|
|
@@ -6,7 +6,7 @@ export const UV_DEFAULT_IMAGE = 'ghcr.io/astral-sh/uv:python3.12-bookworm';
|
|
|
6
6
|
type UvCommandOptions = Pick<UvArgs, 'with_deps' | 'python' | 'script_args'>;
|
|
7
7
|
type UvCommandLikeArgs = Pick<UvArgs, 'script' | 'with_deps' | 'python' | 'script_args'>;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
function buildUvCommand(script: string, args: UvCommandOptions): string[] {
|
|
10
10
|
const parts: string[] = ['uv', 'run'];
|
|
11
11
|
|
|
12
12
|
if (args.with_deps && args.with_deps.length > 0) {
|
|
@@ -28,7 +28,7 @@ export function buildUvCommand(script: string, args: UvCommandOptions): string[]
|
|
|
28
28
|
return parts;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
function wrapInlineScript(script: string, args: UvCommandOptions): string {
|
|
32
32
|
const encoded = Buffer.from(script, 'utf-8').toString('base64');
|
|
33
33
|
const baseCommand = shellQuote(buildUvCommand('-', args));
|
|
34
34
|
return `echo "${encoded}" | base64 -d | ${baseCommand}`;
|
package/src/jobs/jobs-tool.ts
CHANGED
|
@@ -162,6 +162,17 @@ const HARDWARE_FLAVORS_SECTION = [
|
|
|
162
162
|
.filter((line): line is string => Boolean(line))
|
|
163
163
|
.join('\n');
|
|
164
164
|
|
|
165
|
+
const UNKNOWN_OPERATION_INSTRUCTIONS = `Available operations:
|
|
166
|
+
- run, uv, ps, logs, inspect, cancel
|
|
167
|
+
- scheduled run, scheduled uv, scheduled ps, scheduled inspect, scheduled delete, scheduled suspend, scheduled resume
|
|
168
|
+
|
|
169
|
+
Call this tool with no operation for full usage instructions.`;
|
|
170
|
+
|
|
171
|
+
function formatUnknownOperationMessage(requestedOperation?: string): string {
|
|
172
|
+
return `Unknown operation: "${requestedOperation ?? 'unknown'}"
|
|
173
|
+
${UNKNOWN_OPERATION_INSTRUCTIONS}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
function isHelpRequested(args: Record<string, unknown> | undefined): boolean {
|
|
166
177
|
if (!args) {
|
|
167
178
|
return false;
|
|
@@ -426,12 +437,7 @@ export class HfJobsTool {
|
|
|
426
437
|
const normalizedOperation = requestedOperation.toLowerCase();
|
|
427
438
|
if (!isOperationName(normalizedOperation)) {
|
|
428
439
|
return {
|
|
429
|
-
formatted:
|
|
430
|
-
Available operations:
|
|
431
|
-
- run, uv, ps, logs, inspect, cancel
|
|
432
|
-
- scheduled run, scheduled uv, scheduled ps, scheduled inspect, scheduled delete, scheduled suspend, scheduled resume
|
|
433
|
-
|
|
434
|
-
Call this tool with no operation for full usage instructions.`,
|
|
440
|
+
formatted: formatUnknownOperationMessage(requestedOperation),
|
|
435
441
|
totalResults: 0,
|
|
436
442
|
resultsShared: 0,
|
|
437
443
|
};
|
|
@@ -540,12 +546,7 @@ Call this tool with no operation for full usage instructions.`,
|
|
|
540
546
|
|
|
541
547
|
default:
|
|
542
548
|
return {
|
|
543
|
-
formatted:
|
|
544
|
-
Available operations:
|
|
545
|
-
- run, uv, ps, logs, inspect, cancel
|
|
546
|
-
- scheduled run, scheduled uv, scheduled ps, scheduled inspect, scheduled delete, scheduled suspend, scheduled resume
|
|
547
|
-
|
|
548
|
-
Call this tool with no operation for full usage instructions.`,
|
|
549
|
+
formatted: formatUnknownOperationMessage(requestedOperation),
|
|
549
550
|
totalResults: 0,
|
|
550
551
|
resultsShared: 0,
|
|
551
552
|
};
|