evalstate's picture
download
raw
10.1 kB
import { readFile } from 'node:fs/promises';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { fetchWithProfile, NETWORK_FETCH_PROFILES, parseAndValidateUrl } from '@llmindset/hf-mcp/network';
import { logger } from './logger.js';
const PROXY_TOOLS_ENV_VAR = 'PROXY_TOOLS_CSV';
const VALID_RESPONSE_TYPES = new Set(['JSON', 'SSE']);
const PROXY_SCHEMA_TIMEOUT_MS = 10_000;
const PROXY_CSV_SOURCE_PROFILE = NETWORK_FETCH_PROFILES.externalHttps();
const PROXY_TOOL_URL_POLICY = NETWORK_FETCH_PROFILES.httpOrHttpsPermissive().urlPolicy;
let cachedTools = null;
let cachedConfigPromise = null;
let cachedToolsByName = new Map();
export function getProxyToolsConfig() {
return cachedTools ?? [];
}
export function getProxyToolDefinition(toolName) {
return cachedToolsByName.get(toolName);
}
export function cacheDiscoveredProxyAppTool(parentConfig, toolName, argumentKeys) {
const existing = cachedToolsByName.get(toolName);
const existingProperties = existing?.inputSchema?.properties ?? {};
const discoveredProperties = Object.fromEntries(argumentKeys.map((key) => [key, {}]));
const inputSchema = {
type: 'object',
properties: {
...existingProperties,
...discoveredProperties,
},
};
const definition = {
...parentConfig,
toolName,
upstreamToolName: toolName,
description: `FastMCP app backend action discovered from ${parentConfig.toolName}.`,
inputSchema,
meta: {
visibility: ['app'],
hfProxy: {
discoveredFrom: parentConfig.toolName,
upstreamToolName: toolName,
},
},
};
cachedTools = cachedTools ?? [];
const index = cachedTools.findIndex((tool) => tool.toolName === toolName);
if (index === -1) {
cachedTools.push(definition);
}
else {
cachedTools[index] = definition;
}
cachedToolsByName.set(toolName, definition);
return definition;
}
export async function loadProxyToolsConfig() {
if (cachedTools) {
return cachedTools;
}
if (cachedConfigPromise) {
return cachedConfigPromise;
}
cachedConfigPromise = (async () => {
const source = process.env[PROXY_TOOLS_ENV_VAR]?.trim();
if (!source) {
cachedTools = [];
cachedToolsByName = new Map();
logger.debug({ envVar: PROXY_TOOLS_ENV_VAR }, 'Proxy tools CSV not configured');
return cachedTools;
}
let content = null;
if (source.startsWith('https://')) {
try {
const { response } = await fetchWithProfile(source, PROXY_CSV_SOURCE_PROFILE, {
timeoutMs: PROXY_SCHEMA_TIMEOUT_MS,
});
if (!response.ok) {
logger.error({ status: response.status, source }, 'Failed to fetch proxy tools CSV');
cachedTools = [];
cachedToolsByName = new Map();
return cachedTools;
}
content = await response.text();
}
catch (error) {
logger.error({ error, source }, 'Error fetching proxy tools CSV');
cachedTools = [];
cachedToolsByName = new Map();
return cachedTools;
}
}
else {
try {
content = await readFile(source, 'utf8');
}
catch (error) {
logger.error({ error, source }, 'Proxy tools CSV file not found');
cachedTools = [];
cachedToolsByName = new Map();
return cachedTools;
}
}
const parsedSources = parseProxyToolsCsv(content);
const toolDefinitions = await loadProxyToolSchemas(parsedSources);
if (parsedSources.length > 0 && toolDefinitions.length === 0) {
logger.error('Proxy tools configured but no tool schemas were loaded');
}
cachedTools = toolDefinitions;
cachedToolsByName = new Map(toolDefinitions.map((entry) => [entry.toolName, entry]));
logger.info({ toolCount: toolDefinitions.length }, 'Loaded proxy tools configuration');
return toolDefinitions;
})();
return cachedConfigPromise;
}
export function resetProxyToolsConfigForTest() {
cachedTools = null;
cachedConfigPromise = null;
cachedToolsByName = new Map();
}
async function loadProxyToolSchemas(sources) {
if (sources.length === 0) {
return [];
}
const hfToken = process.env.DEFAULT_HF_TOKEN || process.env.HF_TOKEN || process.env.LOGGING_HF_TOKEN;
const schemaTasks = sources.map((source) => Promise.race([fetchProxyToolSchemas(source, hfToken), createTimeout(PROXY_SCHEMA_TIMEOUT_MS)])
.then((tools) => ({ source, tools }))
.catch((error) => {
logger.error({ error, proxyId: source.proxyId, url: source.url }, 'Failed to fetch proxy tool schemas');
return { source, tools: [] };
}));
const results = await Promise.all(schemaTasks);
return results.flatMap((result) => result.tools);
}
async function fetchProxyToolSchemas(source, hfToken) {
const client = new Client({
name: 'hf-mcp-proxy-loader',
version: '1.0.0',
}, { capabilities: {} });
const headers = buildAuthHeaders(hfToken);
const transport = new StreamableHTTPClientTransport(new URL(source.url), {
requestInit: headers ? { headers } : undefined,
});
try {
await client.connect(transport, { timeout: PROXY_SCHEMA_TIMEOUT_MS });
const result = await client.listTools({}, { timeout: PROXY_SCHEMA_TIMEOUT_MS });
const tools = result.tools || [];
if (tools.length === 0) {
logger.error({ proxyId: source.proxyId, url: source.url }, 'No tools returned from proxy server');
return [];
}
return tools
.map((tool) => buildProxyToolDefinition(source, tool, tools.length === 1 ? source.proxyId : tool.name))
.filter((tool) => Boolean(tool));
}
catch (error) {
logger.error({ error, proxyId: source.proxyId, url: source.url }, 'Proxy tool schema fetch failed');
return [];
}
finally {
try {
await client.close();
}
catch (error) {
logger.debug({ error, proxyId: source.proxyId, url: source.url }, 'Failed to close proxy tool client');
}
}
}
function buildProxyToolDefinition(source, tool, toolName) {
const inputSchema = tool.inputSchema;
if (!inputSchema || inputSchema.type !== 'object') {
logger.error({ proxyId: source.proxyId, toolName: tool.name }, 'Proxy tool schema missing or invalid');
return null;
}
return {
proxyId: source.proxyId,
toolName,
upstreamToolName: tool.name,
url: source.url,
responseType: source.responseType,
description: tool.description,
inputSchema,
meta: tool._meta,
};
}
function buildAuthHeaders(hfToken) {
if (!hfToken) {
return undefined;
}
return {
Authorization: `Bearer ${hfToken}`,
'X-HF-Authorization': `Bearer ${hfToken}`,
};
}
function createTimeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Connection timeout after ${ms.toString()}ms`));
}, ms);
});
}
function parseProxyToolsCsv(content) {
const lines = content.split(/\r?\n/);
const results = [];
const seen = new Set();
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
continue;
}
const fields = parseCsvLine(line);
if (fields.length < 3) {
logger.warn({ line }, 'Skipping proxy tools CSV row with insufficient fields');
continue;
}
const [proxyIdRaw, urlRaw, responseTypeRaw] = fields;
if (!proxyIdRaw || !urlRaw || !responseTypeRaw) {
logger.warn({ line }, 'Skipping proxy tools CSV row with missing values');
continue;
}
const proxyId = proxyIdRaw.trim();
if (proxyId.toLowerCase() === 'proxy_id') {
continue;
}
if (seen.has(proxyId)) {
logger.warn({ proxyId }, 'Duplicate proxy id encountered, skipping');
continue;
}
const url = urlRaw.trim();
let parsedUrl;
try {
parsedUrl = parseAndValidateUrl(url, PROXY_TOOL_URL_POLICY);
}
catch (error) {
logger.warn({ proxyId, url, error }, 'Skipping proxy tool with invalid URL');
continue;
}
const responseType = responseTypeRaw.trim().toUpperCase();
if (!VALID_RESPONSE_TYPES.has(responseType)) {
logger.warn({ proxyId, responseType }, 'Skipping proxy tool with invalid response_type');
continue;
}
results.push({
proxyId,
url: parsedUrl.toString(),
responseType,
});
seen.add(proxyId);
}
return results;
}
function parseCsvLine(line) {
const fields = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i += 1) {
const char = line[i];
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"';
i += 1;
continue;
}
inQuotes = !inQuotes;
continue;
}
if (char === ',' && !inQuotes) {
fields.push(current.trim());
current = '';
continue;
}
current += char;
}
if (current.length > 0) {
fields.push(current.trim());
}
return fields;
}
//# sourceMappingURL=proxy-tools-config.js.map

Xet Storage Details

Size:
10.1 kB
·
Xet hash:
b2811d102480f3740300a5d54b3068666563708e6fee320d2097d7a559a5e1f6

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.