@llmindset/hf-mcp 0.3.2 → 0.3.4
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 +202 -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 +46 -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 +58 -0
- package/src/network/ip-policy.ts +252 -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
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
export interface UrlPathRules {
|
|
2
|
+
requiredPrefix?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type UrlProtocol = 'https:' | 'http:';
|
|
6
|
+
|
|
7
|
+
export interface UrlQueryRules {
|
|
8
|
+
allowAny?: boolean;
|
|
9
|
+
allowKeys?: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UrlPolicy {
|
|
13
|
+
allowedProtocols: ReadonlySet<UrlProtocol>;
|
|
14
|
+
allowedHosts?: ReadonlySet<string>;
|
|
15
|
+
allowSubdomainsOf?: readonly string[];
|
|
16
|
+
requireDefaultPort?: boolean;
|
|
17
|
+
pathRules?: UrlPathRules;
|
|
18
|
+
queryRules?: UrlQueryRules;
|
|
19
|
+
allowCredentials?: boolean;
|
|
20
|
+
customValidator?: (url: URL) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LOCALHOST_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
24
|
+
|
|
25
|
+
const ENCODED_SEPARATOR_RE = /%(?:2f|5c)/i;
|
|
26
|
+
const ENCODED_BYTE_RE = /%[0-9a-f]{2}/i;
|
|
27
|
+
const INVALID_PERCENT_ENCODING_RE = /%(?![0-9a-f]{2})/i;
|
|
28
|
+
|
|
29
|
+
function normalizeHostname(hostname: string): string {
|
|
30
|
+
return hostname.toLowerCase().replace(/\.+$/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeDecodeURIComponent(value: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return decodeURIComponent(value);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error('URL contains invalid percent-encoding');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectDecodedPathVariants(pathname: string): string[] {
|
|
42
|
+
const variants = [pathname];
|
|
43
|
+
let current = pathname;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < 2; i += 1) {
|
|
46
|
+
if (!current.includes('%')) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const decoded = safeDecodeURIComponent(current);
|
|
51
|
+
if (decoded === current) {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
variants.push(decoded);
|
|
56
|
+
current = decoded;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return variants;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasDotSegments(pathname: string): boolean {
|
|
63
|
+
const normalized = pathname.replace(/\\/g, '/');
|
|
64
|
+
const segments = normalized.split('/');
|
|
65
|
+
return segments.some((segment) => segment === '.' || segment === '..');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function matchesRequiredPrefix(pathname: string, requiredPrefix: string): boolean {
|
|
69
|
+
const normalizedPath = pathname.replace(/\\/g, '/');
|
|
70
|
+
const normalizedPrefix = requiredPrefix.replace(/\\/g, '/');
|
|
71
|
+
|
|
72
|
+
if (normalizedPath === normalizedPrefix) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (normalizedPrefix.endsWith('/') && normalizedPath === normalizedPrefix.slice(0, -1)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return normalizedPath.startsWith(normalizedPrefix);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function assertHostAllowed(hostname: string, policy: UrlPolicy): void {
|
|
84
|
+
if (!policy.allowedHosts && (!policy.allowSubdomainsOf || policy.allowSubdomainsOf.length === 0)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const normalized = normalizeHostname(hostname);
|
|
89
|
+
|
|
90
|
+
if (policy.allowedHosts) {
|
|
91
|
+
for (const host of policy.allowedHosts) {
|
|
92
|
+
if (normalizeHostname(host) === normalized) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (policy.allowSubdomainsOf) {
|
|
99
|
+
for (const domain of policy.allowSubdomainsOf) {
|
|
100
|
+
const normalizedDomain = normalizeHostname(domain);
|
|
101
|
+
if (normalized === normalizedDomain || normalized.endsWith(`.${normalizedDomain}`)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
throw new Error(`URL hostname is not allowed: ${hostname}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertPathAllowed(url: URL, pathRules?: UrlPathRules): void {
|
|
111
|
+
const pathname = url.pathname;
|
|
112
|
+
|
|
113
|
+
if (pathname.includes('%') && INVALID_PERCENT_ENCODING_RE.test(pathname)) {
|
|
114
|
+
throw new Error('URL path contains invalid percent-encoding');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const variants = collectDecodedPathVariants(pathname);
|
|
118
|
+
|
|
119
|
+
const hasEncodedSeparators = variants.some((variant) => ENCODED_SEPARATOR_RE.test(variant));
|
|
120
|
+
if (hasEncodedSeparators) {
|
|
121
|
+
throw new Error('URL path contains encoded separators');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hasUnsafeDotSegments = variants.some((variant) => hasDotSegments(variant));
|
|
125
|
+
if (hasUnsafeDotSegments) {
|
|
126
|
+
throw new Error('URL path contains dot-segments');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (variants.length > 1) {
|
|
130
|
+
const decodedOnce = variants[1] ?? '';
|
|
131
|
+
if (ENCODED_BYTE_RE.test(decodedOnce)) {
|
|
132
|
+
throw new Error('URL path appears to use double-encoding');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (pathRules?.requiredPrefix) {
|
|
137
|
+
const hasPrefix = variants.some((variant) => matchesRequiredPrefix(variant, pathRules.requiredPrefix ?? ''));
|
|
138
|
+
if (!hasPrefix) {
|
|
139
|
+
throw new Error(`URL path must start with ${pathRules.requiredPrefix}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function assertQueryAllowed(url: URL, policy: UrlPolicy): void {
|
|
145
|
+
const rules = policy.queryRules;
|
|
146
|
+
if (!rules || rules.allowAny === true) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!rules.allowKeys) {
|
|
151
|
+
if (url.search.length > 0) {
|
|
152
|
+
throw new Error('URL query string is not allowed');
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const key of url.searchParams.keys()) {
|
|
158
|
+
if (!rules.allowKeys.has(key)) {
|
|
159
|
+
throw new Error(`URL query parameter is not allowed: ${key}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function assertPortAllowed(url: URL, policy: UrlPolicy): void {
|
|
165
|
+
if (!policy.requireDefaultPort || url.port.length === 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const defaultPort = url.protocol === 'https:' ? '443' : url.protocol === 'http:' ? '80' : '';
|
|
170
|
+
if (!defaultPort || url.port !== defaultPort) {
|
|
171
|
+
throw new Error(`URL port is not allowed for protocol ${url.protocol}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function validateUrlAgainstPolicy(url: URL, policy: UrlPolicy): void {
|
|
176
|
+
if (!policy.allowedProtocols.has(url.protocol as UrlProtocol)) {
|
|
177
|
+
throw new Error(`URL protocol is not allowed: ${url.protocol}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!policy.allowCredentials && (url.username.length > 0 || url.password.length > 0)) {
|
|
181
|
+
throw new Error('URL credentials are not allowed');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
assertHostAllowed(url.hostname, policy);
|
|
185
|
+
assertPortAllowed(url, policy);
|
|
186
|
+
assertPathAllowed(url, policy.pathRules);
|
|
187
|
+
assertQueryAllowed(url, policy);
|
|
188
|
+
|
|
189
|
+
policy.customValidator?.(url);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function parseAndValidateUrl(input: string | URL, policy: UrlPolicy): URL {
|
|
193
|
+
const parsed = input instanceof URL ? new URL(input.toString()) : new URL(input.trim());
|
|
194
|
+
validateUrlAgainstPolicy(parsed, policy);
|
|
195
|
+
return parsed;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function createHfDocsPolicy(): UrlPolicy {
|
|
199
|
+
const hfHosts = new Set(['huggingface.co', 'www.huggingface.co']);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
allowedProtocols: new Set(['https:']),
|
|
203
|
+
allowedHosts: new Set(['huggingface.co', 'www.huggingface.co', 'gradio.app', 'www.gradio.app']),
|
|
204
|
+
allowCredentials: false,
|
|
205
|
+
queryRules: { allowAny: true },
|
|
206
|
+
customValidator: (url) => {
|
|
207
|
+
const host = normalizeHostname(url.hostname);
|
|
208
|
+
if (hfHosts.has(host) && !matchesRequiredPrefix(url.pathname, '/docs/')) {
|
|
209
|
+
throw new Error('Hugging Face docs URLs must remain under /docs/');
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createGradioMcpPolicy(): UrlPolicy {
|
|
216
|
+
return {
|
|
217
|
+
allowedProtocols: new Set(['https:', 'http:']),
|
|
218
|
+
pathRules: {
|
|
219
|
+
requiredPrefix: '/gradio_api/mcp',
|
|
220
|
+
},
|
|
221
|
+
allowCredentials: false,
|
|
222
|
+
queryRules: { allowAny: true },
|
|
223
|
+
customValidator: (url) => {
|
|
224
|
+
const enforceLocalHttpOnly = process.env.NODE_ENV === 'production';
|
|
225
|
+
if (enforceLocalHttpOnly && url.protocol === 'http:' && !isLocalhostHostname(url.hostname)) {
|
|
226
|
+
throw new Error('HTTP is only allowed for localhost Gradio MCP endpoints');
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function isLocalhostHostname(hostname: string): boolean {
|
|
233
|
+
return LOCALHOST_HOSTS.has(normalizeHostname(hostname));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createExternalHttpsPolicy(): UrlPolicy {
|
|
237
|
+
return {
|
|
238
|
+
allowedProtocols: new Set(['https:']),
|
|
239
|
+
allowCredentials: false,
|
|
240
|
+
queryRules: { allowAny: true },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function createHuggingFaceHubPolicy(): UrlPolicy {
|
|
245
|
+
return {
|
|
246
|
+
allowedProtocols: new Set(['https:']),
|
|
247
|
+
allowedHosts: new Set(['huggingface.co', 'www.huggingface.co', 'hf.co']),
|
|
248
|
+
allowCredentials: false,
|
|
249
|
+
queryRules: { allowAny: true },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function createLocalhostHttpPolicy(): UrlPolicy {
|
|
254
|
+
return {
|
|
255
|
+
allowedProtocols: new Set(['https:', 'http:']),
|
|
256
|
+
allowedHosts: new Set(['localhost', '127.0.0.1', '[::1]']),
|
|
257
|
+
allowCredentials: false,
|
|
258
|
+
queryRules: { allowAny: true },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function createExactHostPolicy(hostname: string, allowedProtocol: UrlProtocol): UrlPolicy {
|
|
263
|
+
return {
|
|
264
|
+
allowedProtocols: new Set([allowedProtocol]),
|
|
265
|
+
allowedHosts: new Set([hostname.toLowerCase()]),
|
|
266
|
+
allowCredentials: false,
|
|
267
|
+
queryRules: { allowAny: true },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function createHostPrefixPolicy(
|
|
272
|
+
hostname: string,
|
|
273
|
+
requiredPrefix: string,
|
|
274
|
+
allowedProtocol: UrlProtocol = 'https:'
|
|
275
|
+
): UrlPolicy {
|
|
276
|
+
return {
|
|
277
|
+
allowedProtocols: new Set([allowedProtocol]),
|
|
278
|
+
allowedHosts: new Set([hostname.toLowerCase()]),
|
|
279
|
+
pathRules: {
|
|
280
|
+
requiredPrefix,
|
|
281
|
+
},
|
|
282
|
+
allowCredentials: false,
|
|
283
|
+
queryRules: { allowAny: true },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function createGradioMcpHostPolicy(
|
|
288
|
+
hostname: string,
|
|
289
|
+
allowedProtocol: UrlProtocol
|
|
290
|
+
): UrlPolicy {
|
|
291
|
+
return createHostPrefixPolicy(hostname, '/gradio_api/mcp', allowedProtocol);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function createGradioSchemaHostPolicy(hostname: string): UrlPolicy {
|
|
295
|
+
return createHostPrefixPolicy(hostname, '/gradio_api/mcp/schema');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function createHttpOrHttpsPolicy(): UrlPolicy {
|
|
299
|
+
return {
|
|
300
|
+
allowedProtocols: new Set(['https:', 'http:']),
|
|
301
|
+
allowCredentials: false,
|
|
302
|
+
queryRules: { allowAny: true },
|
|
303
|
+
};
|
|
304
|
+
}
|
package/src/readme-utils.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Utility functions for fetching and processing README files from Hugging Face repositories
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { fetchWithProfile, NETWORK_FETCH_PROFILES } from './network/fetch-profile.js';
|
|
6
|
+
|
|
5
7
|
// Maximum number of characters to include from a README
|
|
6
8
|
const DEFAULT_MAX_README_CHARS = 10_000;
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Fetches README content from a Hugging Face repository
|
|
10
|
-
*
|
|
12
|
+
*
|
|
11
13
|
* @param repoName The resolved repository name (e.g., 'rajpurkar/squad', 'openai-community/gpt2')
|
|
12
14
|
* @param type The repository type ('models' or 'datasets')
|
|
13
15
|
* @param includeYaml Whether to include YAML frontmatter (default: false)
|
|
@@ -20,14 +22,13 @@ export async function fetchReadmeContent(
|
|
|
20
22
|
): Promise<string | null> {
|
|
21
23
|
try {
|
|
22
24
|
// Construct the URL based on repository type
|
|
23
|
-
const baseUrl =
|
|
24
|
-
? `https://huggingface.co/datasets/${repoName}`
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const baseUrl =
|
|
26
|
+
type === 'datasets' ? `https://huggingface.co/datasets/${repoName}` : `https://huggingface.co/${repoName}`;
|
|
27
|
+
|
|
27
28
|
const url = `${baseUrl}/resolve/main/README.md`;
|
|
28
29
|
|
|
29
|
-
const response = await
|
|
30
|
-
|
|
30
|
+
const { response } = await fetchWithProfile(url, NETWORK_FETCH_PROFILES.hfHub());
|
|
31
|
+
|
|
31
32
|
if (!response.ok) {
|
|
32
33
|
if (response.status === 404) {
|
|
33
34
|
// README doesn't exist, return null silently
|
|
@@ -64,7 +65,7 @@ export async function fetchReadmeContent(
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Strips YAML frontmatter from markdown content
|
|
67
|
-
*
|
|
68
|
+
*
|
|
68
69
|
* @param content The full markdown content
|
|
69
70
|
* @returns The content with YAML frontmatter removed
|
|
70
71
|
*/
|
|
@@ -72,12 +73,12 @@ function stripYamlFrontmatter(content: string): string {
|
|
|
72
73
|
// Match YAML frontmatter: starts with ---, ends with ---
|
|
73
74
|
const yamlPattern = /^(\s*---[\r\n]+)([\S\s]*?)([\r\n]+---(\r\n|\n|$))/;
|
|
74
75
|
const match = content.match(yamlPattern);
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
if (match) {
|
|
77
78
|
// Return everything after the closing ---
|
|
78
79
|
return content.substring(match[0].length);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
// No YAML frontmatter found, return original content
|
|
82
83
|
return content;
|
|
83
84
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
}
|
|
@@ -2,6 +2,7 @@ import type { ToolResult } from '../../types/tool-result.js';
|
|
|
2
2
|
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
import { analyzeSchemaComplexity } from '../utils/schema-validator.js';
|
|
4
4
|
import { formatParameters, formatComplexSchemaError } from '../utils/parameter-formatter.js';
|
|
5
|
+
import { fetchGradioSchema, fetchSpaceMetadata } from '../utils/space-http.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Fetches space metadata and schema to discover parameters
|
|
@@ -56,7 +57,8 @@ export async function viewParameters(spaceName: string, hfToken?: string): Promi
|
|
|
56
57
|
let formattedError = `Error fetching parameters for space '${spaceName}': ${errorMessage}`;
|
|
57
58
|
|
|
58
59
|
if (is404) {
|
|
59
|
-
formattedError +=
|
|
60
|
+
formattedError +=
|
|
61
|
+
'\n\nNote: The space MUST be an MCP enabled space. Use the `space_search` tool to find MCP enabled spaces.';
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
return {
|
|
@@ -67,138 +69,3 @@ export async function viewParameters(spaceName: string, hfToken?: string): Promi
|
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Fetches space metadata from HuggingFace API
|
|
73
|
-
*/
|
|
74
|
-
async function fetchSpaceMetadata(
|
|
75
|
-
spaceName: string,
|
|
76
|
-
hfToken?: string
|
|
77
|
-
): Promise<{ subdomain: string; private: boolean }> {
|
|
78
|
-
const url = `https://huggingface.co/api/spaces/${spaceName}`;
|
|
79
|
-
const headers: Record<string, string> = {};
|
|
80
|
-
|
|
81
|
-
if (hfToken) {
|
|
82
|
-
headers['Authorization'] = `Bearer ${hfToken}`;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const controller = new AbortController();
|
|
86
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
const response = await fetch(url, {
|
|
90
|
-
headers,
|
|
91
|
-
signal: controller.signal,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
clearTimeout(timeoutId);
|
|
95
|
-
|
|
96
|
-
if (!response.ok) {
|
|
97
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const info = (await response.json()) as {
|
|
101
|
-
subdomain?: string;
|
|
102
|
-
private?: boolean;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (!info.subdomain) {
|
|
106
|
-
throw new Error('Space does not have a subdomain');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
subdomain: info.subdomain,
|
|
111
|
-
private: info.private || false,
|
|
112
|
-
};
|
|
113
|
-
} finally {
|
|
114
|
-
clearTimeout(timeoutId);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Fetches schema from Gradio endpoint
|
|
120
|
-
*/
|
|
121
|
-
async function fetchGradioSchema(subdomain: string, isPrivate: boolean, hfToken?: string): Promise<Tool[]> {
|
|
122
|
-
const schemaUrl = `https://${subdomain}.hf.space/gradio_api/mcp/schema`;
|
|
123
|
-
|
|
124
|
-
const headers: Record<string, string> = {
|
|
125
|
-
'Content-Type': 'application/json',
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (isPrivate && hfToken) {
|
|
129
|
-
headers['X-HF-Authorization'] = `Bearer ${hfToken}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const controller = new AbortController();
|
|
133
|
-
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const response = await fetch(schemaUrl, {
|
|
137
|
-
method: 'GET',
|
|
138
|
-
headers,
|
|
139
|
-
signal: controller.signal,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
clearTimeout(timeoutId);
|
|
143
|
-
|
|
144
|
-
if (!response.ok) {
|
|
145
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const schemaResponse = (await response.json()) as unknown;
|
|
149
|
-
|
|
150
|
-
// Parse schema response (handle both array and object formats)
|
|
151
|
-
return parseSchemaResponse(schemaResponse);
|
|
152
|
-
} finally {
|
|
153
|
-
clearTimeout(timeoutId);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Parses schema response and extracts tools
|
|
159
|
-
*/
|
|
160
|
-
function parseSchemaResponse(schemaResponse: unknown): Tool[] {
|
|
161
|
-
const tools: Tool[] = [];
|
|
162
|
-
|
|
163
|
-
if (Array.isArray(schemaResponse)) {
|
|
164
|
-
// Array format: [{ name: "toolName", description: "...", inputSchema: {...} }, ...]
|
|
165
|
-
for (const item of schemaResponse) {
|
|
166
|
-
if (
|
|
167
|
-
typeof item === 'object' &&
|
|
168
|
-
item !== null &&
|
|
169
|
-
'name' in item &&
|
|
170
|
-
'inputSchema' in item
|
|
171
|
-
) {
|
|
172
|
-
const itemRecord = item as Record<string, unknown>;
|
|
173
|
-
if (typeof itemRecord.name === 'string') {
|
|
174
|
-
const tool = itemRecord as { name: string; description?: string; inputSchema: unknown };
|
|
175
|
-
tools.push({
|
|
176
|
-
name: tool.name,
|
|
177
|
-
description: tool.description || `${tool.name} tool`,
|
|
178
|
-
inputSchema: {
|
|
179
|
-
type: 'object',
|
|
180
|
-
...(tool.inputSchema as Record<string, unknown>),
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} else if (typeof schemaResponse === 'object' && schemaResponse !== null) {
|
|
187
|
-
// Object format: { "toolName": { properties: {...}, required: [...] }, ... }
|
|
188
|
-
for (const [name, toolSchema] of Object.entries(schemaResponse)) {
|
|
189
|
-
if (typeof toolSchema === 'object' && toolSchema !== null) {
|
|
190
|
-
const schema = toolSchema as { description?: string };
|
|
191
|
-
tools.push({
|
|
192
|
-
name,
|
|
193
|
-
description: schema.description || `${name} tool`,
|
|
194
|
-
inputSchema: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
...(toolSchema as Record<string, unknown>),
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return tools.filter((tool) => !tool.name.toLowerCase().includes('<lambda'));
|
|
204
|
-
}
|
|
@@ -144,6 +144,10 @@ function getUsageInstructions(): string {
|
|
|
144
144
|
return isDynamicSpaceMode() ? DYNAMIC_USAGE_INSTRUCTIONS : USAGE_INSTRUCTIONS;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function formatUnknownOperationLine(requestedOperation?: string): string {
|
|
148
|
+
return `Unknown operation: "${requestedOperation ?? 'unknown'}"`;
|
|
149
|
+
}
|
|
150
|
+
|
|
147
151
|
/**
|
|
148
152
|
* Space tool configuration
|
|
149
153
|
* Returns dynamic config based on environment
|
|
@@ -222,7 +226,7 @@ export class SpaceTool {
|
|
|
222
226
|
const validOperations = getOperationNames();
|
|
223
227
|
if (!validOperations.includes(normalizedOperation)) {
|
|
224
228
|
return {
|
|
225
|
-
formatted:
|
|
229
|
+
formatted: `${formatUnknownOperationLine(requestedOperation)}
|
|
226
230
|
Available operations: ${validOperations.join(', ')}
|
|
227
231
|
|
|
228
232
|
Call this tool with no operation for full usage instructions.`,
|
|
@@ -249,7 +253,7 @@ Call this tool with no operation for full usage instructions.`,
|
|
|
249
253
|
|
|
250
254
|
default:
|
|
251
255
|
return {
|
|
252
|
-
formatted:
|
|
256
|
+
formatted: formatUnknownOperationLine(requestedOperation),
|
|
253
257
|
totalResults: 0,
|
|
254
258
|
resultsShared: 0,
|
|
255
259
|
isError: true,
|