evalstate's picture
download
raw
10.6 kB
import { EventEmitter } from 'events';
import { logger } from './logger.js';
import { BOUQUET_FALLBACK } from '../mcp-server.js';
import { ALL_BUILTIN_TOOL_IDS } from '@llmindset/hf-mcp';
import { fetchWithProfile, isLocalhostHostname, NETWORK_FETCH_PROFILES, parseAndValidateUrl, } from '@llmindset/hf-mcp/network';
import { normalizeBuiltInTools } from '../../shared/tool-normalizer.js';
import { apiMetrics } from '../utils/api-metrics.js';
function withNormalizedFlags(settings) {
const normalizedBuiltInTools = normalizeBuiltInTools(settings.builtInTools);
const isIdentical = normalizedBuiltInTools.length === settings.builtInTools.length &&
normalizedBuiltInTools.every((value, index) => value === settings.builtInTools[index]);
return isIdentical ? settings : { ...settings, builtInTools: normalizedBuiltInTools };
}
export class McpApiClient extends EventEmitter {
config;
pollTimer = null;
cache = new Map();
gradioEndpoints = [];
gradioEndpointStates = new Map();
isPolling = false;
transportInfo = null;
constructor(config, transportInfo) {
super();
this.config = config;
this.transportInfo = transportInfo || null;
if (config.staticGradioEndpoints) {
this.gradioEndpoints = [...config.staticGradioEndpoints];
}
}
getTransportInfo() {
return this.transportInfo;
}
async getSettings(overrideToken) {
const apiTimeout = process.env.HF_API_TIMEOUT ? parseInt(process.env.HF_API_TIMEOUT, 10) : 12500;
switch (this.config.type) {
case 'polling':
if (!this.config.baseUrl) {
logger.error('baseUrl required for polling mode');
return withNormalizedFlags(BOUQUET_FALLBACK);
}
try {
const settingsUrl = new URL('/api/settings', this.config.baseUrl).toString();
const parsedSettingsUrl = new URL(settingsUrl);
const localhost = isLocalhostHostname(parsedSettingsUrl.hostname);
if (!localhost && parsedSettingsUrl.protocol === 'http:') {
throw new Error('Polling settings URL must use HTTPS unless it is localhost');
}
const settingsProfile = localhost
? NETWORK_FETCH_PROFILES.localhostHttp()
: NETWORK_FETCH_PROFILES.externalHttps();
parseAndValidateUrl(settingsUrl, settingsProfile.urlPolicy);
const { response } = await fetchWithProfile(settingsUrl, settingsProfile, {
timeoutMs: apiTimeout,
});
if (!response.ok) {
logger.error(`Failed to fetch settings: ${response.status.toString()} ${response.statusText}`);
return withNormalizedFlags(BOUQUET_FALLBACK);
}
return withNormalizedFlags((await response.json()));
}
catch (error) {
logger.error({ error }, 'Error fetching settings from local API');
return withNormalizedFlags(BOUQUET_FALLBACK);
}
case 'external':
if (!this.config.externalUrl) {
logger.error('externalUrl required for external mode');
return withNormalizedFlags(BOUQUET_FALLBACK);
}
try {
const token = overrideToken || this.config.hfToken;
if (!token || token.trim() === '') {
apiMetrics.recordCall(false, 200);
logger.debug('No HF token available for external config API - using fallback');
return withNormalizedFlags(BOUQUET_FALLBACK);
}
const headers = {};
const hasToken = true;
headers['Authorization'] = `Bearer ${token}`;
headers['accept'] = 'application/json';
headers['cache-control'] = 'no-cache';
const parsedExternalUrl = new URL(this.config.externalUrl);
const externalIsLocalhost = isLocalhostHostname(parsedExternalUrl.hostname);
if (!externalIsLocalhost && parsedExternalUrl.protocol === 'http:') {
throw new Error('External settings URL must use HTTPS unless it is localhost');
}
const externalProfile = externalIsLocalhost
? NETWORK_FETCH_PROFILES.localhostHttp()
: NETWORK_FETCH_PROFILES.externalHttps();
parseAndValidateUrl(this.config.externalUrl, externalProfile.urlPolicy);
logger.debug(`Fetching external settings from ${this.config.externalUrl} with timeout ${apiTimeout}ms`);
const { response } = await fetchWithProfile(this.config.externalUrl, externalProfile, {
timeoutMs: apiTimeout,
requestInit: {
headers,
},
});
if (!response.ok) {
apiMetrics.recordCall(hasToken, response.status);
if (response.status === 401 || response.status === 403) {
logger.debug(`External config API ${response.status} ${response.statusText}: ${this.config.externalUrl}`);
}
logger.debug(`Failed to fetch external settings: ${response.status.toString()} ${response.statusText} - using fallback bouquet`);
return withNormalizedFlags(BOUQUET_FALLBACK);
}
apiMetrics.recordCall(hasToken, response.status);
return withNormalizedFlags((await response.json()));
}
catch (error) {
logger.warn({ error }, 'Error fetching settings from external API - defaulting to fallback bouquet');
return withNormalizedFlags(BOUQUET_FALLBACK);
}
default:
logger.error(`Unknown API client type: ${String(this.config.type)}`);
return withNormalizedFlags(BOUQUET_FALLBACK);
}
}
async getToolStates(overrideToken) {
const settings = await this.getSettings(overrideToken);
if (!settings) {
return null;
}
logger.trace({ settings: settings }, 'Fetched tool settings from API');
if (settings.spaceTools && settings.spaceTools.length > 0) {
this.gradioEndpoints = settings.spaceTools.map((spaceTool) => ({
name: spaceTool.name,
subdomain: spaceTool.subdomain,
id: spaceTool._id,
emoji: spaceTool.emoji,
}));
logger.trace({ gradioEndpoints: this.gradioEndpoints }, 'Updated gradio endpoints from external API');
}
const toolStates = {};
for (const toolId of ALL_BUILTIN_TOOL_IDS) {
toolStates[toolId] = settings.builtInTools.includes(toolId);
}
for (const id of settings.builtInTools) {
if (!(id in toolStates)) {
toolStates[id] = true;
}
}
return toolStates;
}
getGradioEndpoints() {
return this.gradioEndpoints;
}
updateGradioEndpointState(index, enabled) {
if (index >= 0 && index < this.gradioEndpoints.length) {
this.gradioEndpointStates.set(index, enabled);
const endpoint = this.gradioEndpoints[index];
if (endpoint) {
logger.info(`Gradio endpoint ${(index + 1).toString()} set to ${enabled ? 'enabled' : 'disabled'}`);
}
}
}
updateGradioEndpoint(index, endpoint) {
if (index >= 0 && index < this.gradioEndpoints.length) {
this.gradioEndpoints[index] = endpoint;
logger.info(`Gradio endpoint ${(index + 1).toString()} updated to ${endpoint.name}`);
}
}
async startPolling(onUpdate) {
if (this.isPolling) {
logger.warn('Polling already started');
return;
}
this.isPolling = true;
if (this.config.type === 'external') {
logger.debug('Using external user config API - no startup fetching, will fetch on first user request');
return;
}
const pollInterval = this.config.pollInterval || 5000;
logger.info(`Starting API polling with interval ${pollInterval.toString()}ms`);
const initialStates = await this.getToolStates();
if (initialStates) {
for (const [toolId, enabled] of Object.entries(initialStates)) {
this.cache.set(toolId, enabled);
onUpdate(toolId, enabled);
}
}
this.pollTimer = setInterval(() => {
void (async () => {
const states = await this.getToolStates();
if (!states) {
logger.warn('Failed to fetch tool states during polling');
return;
}
for (const [toolId, enabled] of Object.entries(states)) {
const cachedState = this.cache.get(toolId);
if (cachedState !== enabled) {
logger.info(`Tool ${toolId} state changed: ${String(cachedState)} -> ${String(enabled)}`);
this.cache.set(toolId, enabled);
onUpdate(toolId, enabled);
if (this.config.type === 'external') {
this.emit('toolStateChange', toolId, enabled);
}
}
}
for (const [toolId, _] of this.cache) {
if (!(toolId in states)) {
logger.info(`Tool ${toolId} removed from settings`);
this.cache.delete(toolId);
}
}
})();
}, pollInterval);
}
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
this.isPolling = false;
logger.info('Stopped API polling');
}
}
destroy() {
this.stopPolling();
this.cache.clear();
}
}
//# sourceMappingURL=mcp-api-client.js.map

Xet Storage Details

Size:
10.6 kB
·
Xet hash:
52476f56fee695974de34aac7fc4e3fe9c1191cd2706090588b2ccf10aa48a4a

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