@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
|
@@ -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,
|
|
@@ -3,10 +3,17 @@ import {
|
|
|
3
3
|
StreamableHTTPClientTransport,
|
|
4
4
|
type StreamableHTTPClientTransportOptions,
|
|
5
5
|
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CallToolResultSchema,
|
|
8
|
+
type CallToolResult,
|
|
9
|
+
type ServerNotification,
|
|
10
|
+
type ServerRequest,
|
|
11
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
12
|
import { Protocol, type RequestHandlerExtra, type RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
|
8
13
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
9
14
|
import { logger } from '../../logger.js';
|
|
15
|
+
import { fetchWithProfile, NETWORK_FETCH_PROFILES } from '../../network/fetch-profile.js';
|
|
16
|
+
import { createGradioMcpPolicy, parseAndValidateUrl } from '../../network/url-policy.js';
|
|
10
17
|
|
|
11
18
|
class GradioClient extends Client {
|
|
12
19
|
override async connect(transport: Transport, _options?: RequestOptions): Promise<void> {
|
|
@@ -102,6 +109,10 @@ export async function callGradioToolWithHeaders(
|
|
|
102
109
|
extra: RequestHandlerExtra<ServerRequest, ServerNotification> | undefined,
|
|
103
110
|
options: GradioCallOptions = {}
|
|
104
111
|
): Promise<GradioCallResult> {
|
|
112
|
+
const validatedMcpUrl = parseAndValidateUrl(mcpUrl, createGradioMcpPolicy());
|
|
113
|
+
const protocol = validatedMcpUrl.protocol === 'http:' ? 'http:' : 'https:';
|
|
114
|
+
const mcpRequestProfile = NETWORK_FETCH_PROFILES.gradioMcpHost(validatedMcpUrl.hostname, protocol);
|
|
115
|
+
|
|
105
116
|
const capturedHeaders: Record<string, string> = {};
|
|
106
117
|
let loggedHeader = false;
|
|
107
118
|
|
|
@@ -154,7 +165,9 @@ export async function callGradioToolWithHeaders(
|
|
|
154
165
|
hasBody: Boolean(init?.body),
|
|
155
166
|
requestSummary,
|
|
156
167
|
});
|
|
157
|
-
const response = await
|
|
168
|
+
const { response } = await fetchWithProfile(url.toString(), mcpRequestProfile, {
|
|
169
|
+
requestInit: init,
|
|
170
|
+
});
|
|
158
171
|
logger.trace('[gradio] upstream response', {
|
|
159
172
|
method,
|
|
160
173
|
url: url.toString(),
|
|
@@ -196,22 +209,22 @@ export async function callGradioToolWithHeaders(
|
|
|
196
209
|
}
|
|
197
210
|
|
|
198
211
|
logger.trace('[gradio] connecting streamable client', {
|
|
199
|
-
mcpUrl,
|
|
212
|
+
mcpUrl: validatedMcpUrl.toString(),
|
|
200
213
|
hasToken: Boolean(hfToken),
|
|
201
214
|
skipInitialize,
|
|
202
215
|
});
|
|
203
|
-
const transport = new StreamableHTTPClientTransport(
|
|
216
|
+
const transport = new StreamableHTTPClientTransport(validatedMcpUrl, transportOptions);
|
|
204
217
|
let isClosing = false;
|
|
205
218
|
transport.onmessage = (message) => {
|
|
206
219
|
const messageInfo =
|
|
207
220
|
message && typeof message === 'object'
|
|
208
221
|
? {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
hasId: 'id' in message,
|
|
223
|
+
id: (message as { id?: unknown }).id ?? null,
|
|
224
|
+
method: 'method' in message ? (message as { method?: unknown }).method : null,
|
|
225
|
+
isResult: 'result' in message,
|
|
226
|
+
isError: 'error' in message,
|
|
227
|
+
}
|
|
215
228
|
: { messageType: typeof message };
|
|
216
229
|
logger.trace('[gradio] transport message', messageInfo);
|
|
217
230
|
};
|
|
@@ -228,13 +241,13 @@ export async function callGradioToolWithHeaders(
|
|
|
228
241
|
let connectCompleted = false;
|
|
229
242
|
const connectWatchdog = setTimeout(() => {
|
|
230
243
|
if (!connectCompleted) {
|
|
231
|
-
logger.trace('[gradio] connect still pending', { mcpUrl });
|
|
244
|
+
logger.trace('[gradio] connect still pending', { mcpUrl: validatedMcpUrl.toString() });
|
|
232
245
|
}
|
|
233
246
|
}, 15000);
|
|
234
247
|
await remoteClient.connect(transport);
|
|
235
248
|
connectCompleted = true;
|
|
236
249
|
clearTimeout(connectWatchdog);
|
|
237
|
-
logger.trace('[gradio] connected streamable client', { mcpUrl });
|
|
250
|
+
logger.trace('[gradio] connected streamable client', { mcpUrl: validatedMcpUrl.toString() });
|
|
238
251
|
|
|
239
252
|
try {
|
|
240
253
|
// Check if the client is requesting progress notifications
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { fetchWithProfile, NETWORK_FETCH_PROFILES } from '../../network/fetch-profile.js';
|
|
3
|
+
import { normalizeParsedTools, parseGradioSchemaResponse } from './gradio-schema.js';
|
|
4
|
+
|
|
5
|
+
const SPACE_HTTP_TIMEOUT_MS = 10_000;
|
|
6
|
+
|
|
7
|
+
export interface SpaceMetadata {
|
|
8
|
+
subdomain: string;
|
|
9
|
+
private: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function fetchSpaceMetadata(spaceName: string, hfToken?: string): Promise<SpaceMetadata> {
|
|
13
|
+
const url = `https://huggingface.co/api/spaces/${spaceName}`;
|
|
14
|
+
const headers: Record<string, string> = {};
|
|
15
|
+
|
|
16
|
+
if (hfToken) {
|
|
17
|
+
headers['Authorization'] = `Bearer ${hfToken}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { response } = await fetchWithProfile(url, NETWORK_FETCH_PROFILES.hfHub(), {
|
|
21
|
+
timeoutMs: SPACE_HTTP_TIMEOUT_MS,
|
|
22
|
+
requestInit: {
|
|
23
|
+
headers,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const info = (await response.json()) as {
|
|
32
|
+
subdomain?: string;
|
|
33
|
+
private?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (!info.subdomain) {
|
|
37
|
+
throw new Error('Space does not have a subdomain');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
subdomain: info.subdomain,
|
|
42
|
+
private: info.private || false,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function fetchGradioSchema(subdomain: string, isPrivate: boolean, hfToken?: string): Promise<Tool[]> {
|
|
47
|
+
const schemaUrl = `https://${subdomain}.hf.space/gradio_api/mcp/schema`;
|
|
48
|
+
|
|
49
|
+
const headers: Record<string, string> = {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isPrivate && hfToken) {
|
|
54
|
+
headers['X-HF-Authorization'] = `Bearer ${hfToken}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { response } = await fetchWithProfile(
|
|
58
|
+
schemaUrl,
|
|
59
|
+
NETWORK_FETCH_PROFILES.gradioSchemaHost(`${subdomain}.hf.space`),
|
|
60
|
+
{
|
|
61
|
+
timeoutMs: SPACE_HTTP_TIMEOUT_MS,
|
|
62
|
+
requestInit: {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
headers,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const schemaResponse = (await response.json()) as unknown;
|
|
74
|
+
return normalizeParsedTools(parseGradioSchemaResponse(schemaResponse));
|
|
75
|
+
}
|
package/src/space-files.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { listFiles, spaceInfo } 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
|
export interface FileWithUrl {
|
|
@@ -226,7 +227,7 @@ export class SpaceFilesTool {
|
|
|
226
227
|
for (const file of dirFiles) {
|
|
227
228
|
const fileName = file.path.split('/').pop() || file.path;
|
|
228
229
|
const indent = dir === '/' ? '' : ' ';
|
|
229
|
-
const icon =
|
|
230
|
+
const icon = getFileIcon(fileName);
|
|
230
231
|
const lastMod = file.lastModified ? new Date(file.lastModified).toLocaleDateString() : '-';
|
|
231
232
|
|
|
232
233
|
markdown += `| ${indent}${icon} ${escapeMarkdown(fileName)} | ${file.sizeFormatted} | ${file.lfs ? 'LFS' : 'Regular'} | ${lastMod} | ${file.url} |\n`;
|
|
@@ -265,7 +266,7 @@ export class SpaceFilesTool {
|
|
|
265
266
|
|
|
266
267
|
for (const file of files) {
|
|
267
268
|
const fileName = file.path.split('/').pop() || file.path;
|
|
268
|
-
const icon =
|
|
269
|
+
const icon = getFileIcon(fileName);
|
|
269
270
|
markdown += `| ${icon} ${escapeMarkdown(fileName)} | ${escapeMarkdown(file.path)} | ${file.sizeFormatted} | [Link](${file.url}) |\n`;
|
|
270
271
|
}
|
|
271
272
|
|
|
@@ -283,43 +284,4 @@ export class SpaceFilesTool {
|
|
|
283
284
|
|
|
284
285
|
return this.generateDetailedMarkdown(spaceName, fileType);
|
|
285
286
|
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Get file icon based on extension
|
|
289
|
-
*/
|
|
290
|
-
private getFileIcon(filename: string): string {
|
|
291
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
292
|
-
const iconMap: Record<string, string> = {
|
|
293
|
-
py: '🐍',
|
|
294
|
-
js: '📜',
|
|
295
|
-
ts: '📘',
|
|
296
|
-
md: '📝',
|
|
297
|
-
txt: '📄',
|
|
298
|
-
json: '📊',
|
|
299
|
-
yaml: '⚙️',
|
|
300
|
-
yml: '⚙️',
|
|
301
|
-
png: '🖼️',
|
|
302
|
-
jpg: '🖼️',
|
|
303
|
-
jpeg: '🖼️',
|
|
304
|
-
gif: '🖼️',
|
|
305
|
-
svg: '🎨',
|
|
306
|
-
mp4: '🎬',
|
|
307
|
-
mp3: '🎵',
|
|
308
|
-
pdf: '📕',
|
|
309
|
-
zip: '📦',
|
|
310
|
-
tar: '📦',
|
|
311
|
-
gz: '📦',
|
|
312
|
-
html: '🌐',
|
|
313
|
-
css: '🎨',
|
|
314
|
-
ipynb: '📓',
|
|
315
|
-
csv: '📊',
|
|
316
|
-
parquet: '🗄️',
|
|
317
|
-
safetensors: '🤖',
|
|
318
|
-
bin: '💾',
|
|
319
|
-
pkl: '🥒',
|
|
320
|
-
h5: '🗃️',
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
return iconMap[ext || ''] || '📄';
|
|
324
|
-
}
|
|
325
287
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const SRC_ROOT = join(process.cwd(), 'src');
|
|
6
|
+
|
|
7
|
+
function collectTsFiles(root: string): string[] {
|
|
8
|
+
const entries = readdirSync(root);
|
|
9
|
+
const files: string[] = [];
|
|
10
|
+
|
|
11
|
+
for (const entry of entries) {
|
|
12
|
+
const fullPath = join(root, entry);
|
|
13
|
+
const stat = statSync(fullPath);
|
|
14
|
+
if (stat.isDirectory()) {
|
|
15
|
+
files.push(...collectTsFiles(fullPath));
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (fullPath.endsWith('.ts')) {
|
|
20
|
+
files.push(fullPath);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasDirectFetchCall(content: string): boolean {
|
|
28
|
+
const regex = /fetch\s*\(/g;
|
|
29
|
+
let match: RegExpExecArray | null;
|
|
30
|
+
|
|
31
|
+
while ((match = regex.exec(content)) !== null) {
|
|
32
|
+
const index = match.index;
|
|
33
|
+
const previousChar = index > 0 ? content[index - 1] : '';
|
|
34
|
+
|
|
35
|
+
if (/[\w$.]/.test(previousChar)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const prefix = content.slice(Math.max(0, index - 20), index);
|
|
40
|
+
if (/\basync\s+$/.test(prefix) || /\bfunction\s+$/.test(prefix)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('fetch usage guard', () => {
|
|
51
|
+
it('only allows direct fetch calls in network/safe-fetch.ts', () => {
|
|
52
|
+
const allowedSuffixes = new Set(['/network/safe-fetch.ts']);
|
|
53
|
+
const offenders: string[] = [];
|
|
54
|
+
const files = collectTsFiles(SRC_ROOT);
|
|
55
|
+
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const content = readFileSync(file, 'utf8');
|
|
58
|
+
if (!hasDirectFetchCall(content)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const normalized = file.replace(SRC_ROOT, '').replace(/\\/g, '/');
|
|
63
|
+
if (!allowedSuffixes.has(normalized)) {
|
|
64
|
+
offenders.push(normalized);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expect(offenders).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fetchJobLogs } from '../../src/jobs/sse-handler.js';
|
|
3
|
+
import { safeFetch } from '../../src/network/safe-fetch.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../src/network/safe-fetch.js', () => ({
|
|
6
|
+
safeFetch: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
function createSseResponse(read: () => Promise<ReadableStreamReadResult<Uint8Array>>): Response {
|
|
10
|
+
return {
|
|
11
|
+
ok: true,
|
|
12
|
+
body: {
|
|
13
|
+
getReader: () => ({
|
|
14
|
+
read,
|
|
15
|
+
cancel: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
} as unknown as Response;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('fetchJobLogs', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('treats timeout-aborted SSE reads as expected truncation', async () => {
|
|
27
|
+
const abortedRead = async (): Promise<ReadableStreamReadResult<Uint8Array>> => {
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
29
|
+
throw new DOMException('The operation was aborted.', 'AbortError');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
vi.mocked(safeFetch).mockResolvedValue({
|
|
33
|
+
response: createSseResponse(abortedRead),
|
|
34
|
+
finalUrl: new URL('https://example.com/logs'),
|
|
35
|
+
redirectsFollowed: 0,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await fetchJobLogs('https://example.com/logs', { maxDuration: 1, maxLines: 5 });
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual({
|
|
41
|
+
logs: [],
|
|
42
|
+
finished: false,
|
|
43
|
+
truncated: true,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws non-timeout stream errors', async () => {
|
|
48
|
+
const failingRead = async (): Promise<ReadableStreamReadResult<Uint8Array>> => {
|
|
49
|
+
throw new Error('stream read failed');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
vi.mocked(safeFetch).mockResolvedValue({
|
|
53
|
+
response: createSseResponse(failingRead),
|
|
54
|
+
finalUrl: new URL('https://example.com/logs'),
|
|
55
|
+
redirectsFollowed: 0,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await expect(fetchJobLogs('https://example.com/logs', { maxDuration: 100 })).rejects.toThrow('stream read failed');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"result-formatter.d.ts","sourceRoot":"","sources":["../../../src/space/utils/result-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAUzE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAa/D;AAmMD,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAOzD"}
|