@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
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@llmindset/hf-mcp",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
+ "browser": "./dist/index.browser.js",
10
11
  "default": "./dist/index.js"
11
12
  },
13
+ "./network": {
14
+ "types": "./dist/network/index.d.ts",
15
+ "default": "./dist/network/index.js"
16
+ },
12
17
  "./semantic": {
13
18
  "types": "./dist/semantic-search.d.ts",
14
19
  "default": "./dist/semantic-search.js"
@@ -25,7 +30,7 @@
25
30
  "dependencies": {
26
31
  "@huggingface/hub": "^2.6.12",
27
32
  "@mcp-ui/server": "^5.12.0",
28
- "@modelcontextprotocol/sdk": "^1.25.3",
33
+ "@modelcontextprotocol/sdk": "^1.26.0",
29
34
  "shell-quote": "^1.8.3",
30
35
  "turndown": "^7.2.0",
31
36
  "zod": "^3.24.4"
@@ -36,7 +41,6 @@
36
41
  "@types/shell-quote": "^1.7.5",
37
42
  "@types/turndown": "^5.0.5",
38
43
  "eslint": "^9.25.0",
39
- "globals": "^16.0.0",
40
44
  "rimraf": "^6.0.1",
41
45
  "typescript-eslint": "^8.32.1",
42
46
  "vitest": "^3.1.3"
@@ -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
- const tool = new DocFetchTool();
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).toHaveBeenCalledWith('https://huggingface.co/docs/test', {
80
- headers: { accept: 'text/markdown' },
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 = '<h1>Long Document</h1>' + '<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(200);
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).toHaveBeenCalledWith('https://huggingface.co/docs/test', {
141
- headers: { accept: 'text/markdown' },
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).toHaveBeenCalledWith('https://huggingface.co/docs/another', {
155
- headers: { accept: 'text/markdown' },
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 = '<h1>Long Document</h1>' + '<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(200);
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
- const url = new URL(hfUrl);
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
- this.validateUrl(normalizedUrl);
148
-
149
- const response = await fetch(normalizedUrl, { headers: { accept: 'text/markdown' } });
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
  }
@@ -7,11 +7,18 @@ import type { ToolResult } from '../types/tool-result.js';
7
7
  /** token estimation. initial results for "how to load a image to image model in transformers" returned
8
8
  * 121973 characters (36711 anthropic tokens) */
9
9
 
10
+ const KNOWLEDGE_DATE = new Intl.DateTimeFormat('en-GB', {
11
+ day: 'numeric',
12
+ month: 'long',
13
+ year: 'numeric',
14
+ timeZone: 'UTC',
15
+ }).format(new Date());
16
+
10
17
  export const DOCS_SEMANTIC_SEARCH_CONFIG = {
11
18
  name: 'hf_doc_search',
12
19
  description:
13
20
  'Search and Discover Hugging Face Product and Library documentation. Send an empty query to discover structure and navigation instructions. ' +
14
- 'You MUST consult this tool for the most up-to-date information when using Hugging Face libraries. Combine with the Product filter to focus results.',
21
+ `Knowledge up-to-date as at ${KNOWLEDGE_DATE}. Combine with the Product filter to focus results.`,
15
22
  schema: z.object({
16
23
  query: z
17
24
  .string()
@@ -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
+ }
@@ -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 = this.getFileIcon(fileName);
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
  }
@@ -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
- // Add timeout using AbortController
103
- const controller = new AbortController();
104
- const timeoutId = setTimeout(() => controller.abort(), this.apiTimeout);
105
-
106
- const response = await fetch(url.toString(), {
107
- ...options,
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) {
@@ -5,7 +5,7 @@ import { DatasetDetailTool } from './dataset-detail.js';
5
5
  import { spaceInfo } from '@huggingface/hub';
6
6
  import { formatDate } from './utilities.js';
7
7
 
8
- export const HUB_INSPECT_TOOL_CONFIG = {
8
+ export const HUB_REPO_DETAILS_TOOL_CONFIG = {
9
9
  name: 'hub_repo_details',
10
10
  description:
11
11
  'Get details for one or more Hugging Face repos (model, dataset, or space). ' +
@@ -27,7 +27,7 @@ export const HUB_INSPECT_TOOL_CONFIG = {
27
27
  },
28
28
  } as const;
29
29
 
30
- export type HubInspectParams = z.infer<typeof HUB_INSPECT_TOOL_CONFIG.schema>;
30
+ export type HubInspectParams = z.infer<typeof HUB_REPO_DETAILS_TOOL_CONFIG.schema>;
31
31
 
32
32
  export class HubInspectTool {
33
33
  private readonly modelDetail: ModelDetailTool;
@@ -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
@@ -7,6 +7,7 @@ export * from './model-detail.js';
7
7
  export * from './utilities.js';
8
8
  export * from './paper-search.js';
9
9
  export * from './dataset-search.js';
10
+ export * from './repo-search.js';
10
11
  export * from './dataset-detail.js';
11
12
  export * from './hub-inspect.js';
12
13
  export * from './duplicate-space.js';
@@ -23,6 +24,7 @@ export * from './jobs/jobs-tool.js';
23
24
  export * from './space/dynamic-space-tool.js';
24
25
  export * from './space/utils/gradio-caller.js';
25
26
  export * from './space/utils/gradio-schema.js';
27
+ export * from './network/url-policy.js';
26
28
 
27
29
  // Export shared types
28
30
  export * from './types/tool-result.js';