Buckets:
| import express, { type Express } from 'express'; | |
| import cors from 'cors'; | |
| import type { CorsOptions, CorsRequest, CorsOptionsDelegate } from 'cors'; | |
| import path from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import type { Server } from 'node:http'; | |
| import type { TransportInfo } from '../shared/transport-info.js'; | |
| import { settingsService, type SpaceTool } from '../shared/settings.js'; | |
| import { logger } from './utils/logger.js'; | |
| import type { BaseTransport } from './transport/base-transport.js'; | |
| import type { McpApiClient } from './utils/mcp-api-client.js'; | |
| import { formatMetricsForAPI } from '../shared/transport-metrics.js'; | |
| import { ALL_BUILTIN_TOOL_IDS } from '@llmindset/hf-mcp'; | |
| import { CORS_ALLOWED_ORIGINS, CORS_EXPOSED_HEADERS } from '../shared/constants.js'; | |
| import { apiMetrics } from './utils/api-metrics.js'; | |
| import { gradioMetrics } from './utils/gradio-metrics.js'; | |
| import { formatCacheMetricsForAPI } from './utils/gradio-cache.js'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| export class WebServer { | |
| private app: Express; | |
| private server: Server | null = null; | |
| private transportInfo: TransportInfo = { | |
| transport: 'unknown', | |
| defaultHfTokenSet: false, | |
| externalApiMode: false, | |
| stdioClient: null, | |
| }; | |
| private localSharedToolStates: Map<string, boolean> = new Map(); | |
| private transport?: BaseTransport; | |
| private apiClient?: McpApiClient; | |
| constructor() { | |
| this.app = express() as Express; | |
| this.setupMiddleware(); | |
| } | |
| private setupMiddleware(): void { | |
| this.app.disable('x-powered-by'); | |
| this.app.set('trust proxy', true); | |
| // Inbound body size limit to prevent abuse | |
| this.app.use(express.json({ limit: '1mb' })); | |
| // Basic security headers (complementary to CORS) | |
| this.app.use((_, res, next) => { | |
| res.setHeader('X-Content-Type-Options', 'nosniff'); | |
| res.setHeader('Referrer-Policy', 'no-referrer'); | |
| if (process.env.HSTS === 'true') { | |
| res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); | |
| } | |
| next(); | |
| }); | |
| // Global CORS for all routes (API + MCP endpoints) | |
| // Simple exact-match allowlist with optional env override | |
| const envOrigins = (process.env.CORS_ALLOWED_ORIGINS || '') | |
| .split(',') | |
| .map((s) => s.trim()) | |
| .filter(Boolean); | |
| const normalize = (s: string) => s.replace(/\/+$/, ''); | |
| const envOriginsNorm = envOrigins.map(normalize); | |
| const allowedOrigins = (envOriginsNorm.length > 0 ? envOriginsNorm : CORS_ALLOWED_ORIGINS).map(normalize); | |
| // Support wildcard "*" to allow all origins explicitly | |
| let originSetting: CorsOptions['origin']; | |
| if (allowedOrigins.length === 1 && allowedOrigins[0] === '*') { | |
| originSetting = '*'; | |
| } else if (allowedOrigins.some((o) => o.includes('*'))) { | |
| // Support basic subdomain wildcards like "https://*.use-mcp.dev" or "*.use-mcp.dev" | |
| const exact = new Set(allowedOrigins.filter((o) => !o.includes('*'))); | |
| const patterns = allowedOrigins.filter((o) => o.includes('*')); | |
| originSetting = (requestOrigin: string | undefined, cb: (err: Error | null, allow?: boolean) => void) => { | |
| if (!requestOrigin) return cb(null, true); | |
| const reqOrigin = normalize(requestOrigin); | |
| if (exact.has(reqOrigin)) return cb(null, true); | |
| try { | |
| const u = new URL(requestOrigin); | |
| for (const p of patterns) { | |
| let scheme: string | undefined; | |
| let hostPattern = p; | |
| if (p.startsWith('http://') || p.startsWith('https://')) { | |
| scheme = p.split('://', 1)[0]; | |
| hostPattern = p.slice((scheme + '://').length); | |
| } | |
| // Only support leading wildcard: *.domain.tld | |
| if (!hostPattern.startsWith('*.')) continue; | |
| const suffix = hostPattern.slice(2); // domain.tld | |
| const host = u.hostname; | |
| if (scheme && u.protocol !== scheme + ':') continue; | |
| if (host.endsWith('.' + suffix) && host !== suffix) { | |
| return cb(null, true); | |
| } | |
| } | |
| return cb(null, false); | |
| } catch { | |
| return cb(null, false); | |
| } | |
| }; | |
| } else { | |
| originSetting = allowedOrigins; | |
| } | |
| const corsOptions: CorsOptions | CorsOptionsDelegate<CorsRequest> = { | |
| origin: originSetting, | |
| exposedHeaders: CORS_EXPOSED_HEADERS, | |
| }; | |
| this.app.use(cors(corsOptions)); | |
| // Ensure preflight requests succeed for any path | |
| this.app.options('*', cors(corsOptions)); | |
| } | |
| public getApp(): Express { | |
| return this.app; | |
| } | |
| public setTransportInfo(info: TransportInfo): void { | |
| this.transportInfo = info; | |
| } | |
| public setClientInfo(clientInfo: { name: string; version: string } | null): void { | |
| this.transportInfo.stdioClient = clientInfo; | |
| } | |
| public initializeToolStates(): void { | |
| // Initialize local shared tool states based on current settings to prevent initial event burst | |
| const currentSettings = settingsService.getSettings(); | |
| for (const toolId of ALL_BUILTIN_TOOL_IDS) { | |
| const isEnabled = currentSettings.builtInTools.includes(toolId); | |
| this.localSharedToolStates.set(toolId, isEnabled); | |
| } | |
| } | |
| public setTransport(transport: BaseTransport): void { | |
| this.transport = transport; | |
| } | |
| public setApiClient(apiClient: McpApiClient): void { | |
| this.apiClient = apiClient; | |
| } | |
| public getTransportInfo(): TransportInfo { | |
| return this.transportInfo; | |
| } | |
| public async start(port: number): Promise<void> { | |
| if (this.server) { | |
| throw new Error('Server is already running'); | |
| } | |
| return new Promise((resolve, reject) => { | |
| this.server = this.app | |
| .listen(port, () => { | |
| this.transportInfo.port = port; | |
| resolve(); | |
| }) | |
| .on('error', reject); | |
| }); | |
| } | |
| public async stop(): Promise<void> { | |
| if (!this.server) { | |
| return; | |
| } | |
| return new Promise((resolve, reject) => { | |
| this.server?.close((err) => { | |
| if (err) { | |
| reject(err); | |
| } else { | |
| this.server = null; | |
| resolve(); | |
| } | |
| }); | |
| }); | |
| } | |
| public async setupStaticFiles(isDevelopment: boolean): Promise<void> { | |
| if (isDevelopment) { | |
| // In development mode, use Vite's dev server middleware | |
| try { | |
| const { createServer: createViteServer } = await import('vite'); | |
| const rootDir = path.resolve(__dirname, '..', '..', '..', 'app', 'src', 'web'); | |
| // Create Vite server with proper HMR configuration | |
| const vite = await createViteServer({ | |
| configFile: path.resolve(__dirname, '..', '..', '..', 'app', 'vite.config.ts'), | |
| server: { | |
| middlewareMode: true, | |
| hmr: true, // Explicitly enable HMR | |
| }, | |
| appType: 'spa', | |
| root: rootDir, | |
| }); | |
| // Use Vite's middleware for dev server with HMR | |
| this.app.use(vite.middlewares); | |
| logger.info('Using Vite middleware in development mode with HMR enabled'); | |
| logger.info({ rootDir }, 'Vite root directory'); | |
| } catch (err) { | |
| logger.error({ err }, 'Error setting up Vite middleware'); | |
| throw err; | |
| } | |
| } else { | |
| // In production, serve static files | |
| const staticPath = path.join(__dirname, '..', 'web'); | |
| this.app.use(express.static(staticPath)); | |
| // Fallback to index.html for SPA routing | |
| this.app.get('*', (req, res) => { | |
| if (!req.path.startsWith('/api/')) { | |
| res.sendFile(path.join(staticPath, 'index.html')); | |
| } | |
| }); | |
| } | |
| } | |
| public setupApiRoutes(): void { | |
| // Transport info endpoint | |
| this.app.get('/api/transport', (_req, res) => { | |
| res.json(this.transportInfo); | |
| }); | |
| // Sessions endpoint | |
| this.app.get('/api/sessions', (_req, res) => { | |
| if (!this.transport) { | |
| res.json([]); | |
| return; | |
| } | |
| const sessions = this.transport.getSessions(); | |
| // For STDIO transport, also update the stdioClient info if we have a session | |
| if (this.transportInfo.transport === 'stdio' && sessions.length > 0) { | |
| const stdioSession = sessions[0]; | |
| if (stdioSession?.clientInfo && !this.transportInfo.stdioClient) { | |
| this.transportInfo.stdioClient = { | |
| name: stdioSession.clientInfo.name, | |
| version: stdioSession.clientInfo.version, | |
| }; | |
| } | |
| } | |
| res.json(sessions); | |
| }); | |
| // Transport metrics endpoint | |
| this.app.get('/api/transport-metrics', (req, res) => { | |
| if (!this.transport) { | |
| res.status(503).json({ error: 'Transport not initialized' }); | |
| return; | |
| } | |
| try { | |
| // Check for templog query parameter | |
| const tempLogParam = req.query.templog; | |
| let tempLogStatus: { activated: boolean; remaining: number; maxAllowed: number } | undefined = undefined; | |
| if (tempLogParam && this.transportInfo.transport === 'streamableHttpJson') { | |
| // Only activate for stateless transport with analytics mode | |
| // We need to import StatelessHttpTransport type or use method check | |
| const statelessTransport = this.transport as { | |
| activateTempLogging?: (count: number) => number; | |
| getTempLogStatus?: () => { enabled: boolean; remaining: number; maxAllowed: number }; | |
| }; | |
| if (statelessTransport.activateTempLogging && statelessTransport.getTempLogStatus) { | |
| const requestedCount = parseInt(tempLogParam as string, 10); | |
| if (!isNaN(requestedCount) && requestedCount > 0) { | |
| const activated = statelessTransport.activateTempLogging(requestedCount); | |
| tempLogStatus = { | |
| activated: true, | |
| remaining: activated, | |
| maxAllowed: statelessTransport.getTempLogStatus().maxAllowed, | |
| }; | |
| } | |
| } | |
| } | |
| // Get raw metrics from transport | |
| const metrics = this.transport.getMetrics(); | |
| // Determine if transport is stateless | |
| const isStateless = this.transportInfo.transport === 'streamableHttpJson'; | |
| // Get configuration for stateful transports | |
| const config = this.transport.getConfiguration(); | |
| // Get sessions (empty for stateless transports) | |
| const sessions = this.transport.getSessions().map((session) => { | |
| // Determine connection status: Connected, Distressed, or Disconnected | |
| const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); | |
| const hasRecentActivity = session.lastActivity > fiveMinutesAgo; | |
| const hasPingFailures = (session.pingFailures || 0) >= 1; | |
| // Note that from the WebUI this is provided as a courtesy. If a Client connects and | |
| // disconnects before the Client refresh it will not be shown in the Client list. | |
| let connectionStatus: 'Connected' | 'Distressed' | 'Disconnected'; | |
| if (!hasRecentActivity) { | |
| connectionStatus = 'Disconnected'; | |
| } else if (hasPingFailures) { | |
| connectionStatus = 'Distressed'; | |
| } else { | |
| connectionStatus = 'Connected'; | |
| } | |
| return { | |
| id: session.id, | |
| connectedAt: session.connectedAt.toISOString(), | |
| lastActivity: session.lastActivity.toISOString(), | |
| requestCount: session.requestCount, | |
| clientInfo: session.clientInfo, | |
| isConnected: hasRecentActivity, | |
| connectionStatus, | |
| pingFailures: session.pingFailures || 0, | |
| lastPingAttempt: session.lastPingAttempt?.toISOString(), | |
| ipAddress: session.ipAddress, | |
| }; | |
| }); | |
| // Format for API response | |
| const formattedMetrics = formatMetricsForAPI(metrics, this.transportInfo.transport, isStateless, sessions); | |
| // Add configuration if available | |
| if (!isStateless && config.staleCheckInterval && config.staleTimeout) { | |
| formattedMetrics.configuration = { | |
| heartbeatInterval: config.heartbeatInterval || 30000, | |
| staleCheckInterval: config.staleCheckInterval, | |
| staleTimeout: config.staleTimeout, | |
| pingEnabled: config.pingEnabled, | |
| pingInterval: config.pingInterval, | |
| pingFailureThreshold: config.pingFailureThreshold || 1, | |
| }; | |
| } | |
| // Add API metrics if in external API mode | |
| if (this.transportInfo.externalApiMode) { | |
| formattedMetrics.apiMetrics = apiMetrics.getMetrics(); | |
| } | |
| // Add Gradio metrics | |
| formattedMetrics.gradioMetrics = gradioMetrics.getMetrics(); | |
| // Add Gradio cache metrics | |
| formattedMetrics.gradioCacheMetrics = formatCacheMetricsForAPI(); | |
| // Add temp log status if it was activated or if we need to check current status | |
| const extendedMetrics = formattedMetrics as typeof formattedMetrics & { tempLogStatus?: unknown }; | |
| if (tempLogStatus) { | |
| extendedMetrics.tempLogStatus = tempLogStatus; | |
| } else if (this.transportInfo.transport === 'streamableHttpJson') { | |
| // Include current status even if not activating | |
| const statelessTransport = this.transport as { | |
| getTempLogStatus?: () => { enabled: boolean; remaining: number; maxAllowed: number }; | |
| }; | |
| if (statelessTransport.getTempLogStatus) { | |
| const status = statelessTransport.getTempLogStatus(); | |
| if (status.enabled) { | |
| extendedMetrics.tempLogStatus = status; | |
| } | |
| } | |
| } | |
| res.json(formattedMetrics); | |
| } catch (error) { | |
| logger.error({ error }, 'Error retrieving transport metrics'); | |
| res.status(500).json({ error: 'Failed to retrieve transport metrics' }); | |
| } | |
| }); | |
| // Settings endpoint | |
| this.app.get('/api/settings', (_req, res) => { | |
| res.json(settingsService.getSettings()); | |
| }); | |
| // Update tool settings endpoint | |
| this.app.post('/api/settings', express.json(), (req, res) => { | |
| const { builtInTools, spaceTools } = req.body as { builtInTools?: string[]; spaceTools?: SpaceTool[] }; | |
| let updatedSettings = settingsService.getSettings(); | |
| if (builtInTools !== undefined) { | |
| updatedSettings = settingsService.updateBuiltInTools(builtInTools); | |
| } | |
| if (spaceTools !== undefined) { | |
| updatedSettings = settingsService.updateSpaceTools(spaceTools); | |
| } | |
| // Enable or disable only the tools that actually changed state | |
| if (builtInTools !== undefined) { | |
| for (const toolId of ALL_BUILTIN_TOOL_IDS) { | |
| const shouldBeEnabled = builtInTools.includes(toolId); | |
| const currentlyEnabled = this.localSharedToolStates.get(toolId) ?? false; | |
| // Only update state and emit events if state actually changed | |
| if (currentlyEnabled !== shouldBeEnabled) { | |
| this.localSharedToolStates.set(toolId, shouldBeEnabled); | |
| // Emit event for MCP server instances | |
| if (this.apiClient) { | |
| this.apiClient.emit('toolStateChange', toolId, shouldBeEnabled); | |
| } | |
| logger.info(`Tool ${toolId} has been ${shouldBeEnabled ? 'enabled' : 'disabled'} via API`); | |
| } | |
| } | |
| } | |
| res.json(updatedSettings); | |
| }); | |
| // Gradio endpoints endpoint | |
| this.app.get('/api/gradio-endpoints', (_req, res) => { | |
| if (!this.apiClient) { | |
| res.json([]); | |
| return; | |
| } | |
| res.json(this.apiClient.getGradioEndpoints()); | |
| }); | |
| // Update Gradio endpoint status | |
| this.app.post('/api/gradio-endpoints/:index', express.json(), (req, res) => { | |
| const index = parseInt(req.params.index); | |
| const { enabled } = req.body as { enabled: boolean }; | |
| if (!this.apiClient) { | |
| res.status(500).json({ error: 'API client not initialized' }); | |
| return; | |
| } | |
| const endpoints = this.apiClient.getGradioEndpoints(); | |
| if (index < 0 || index >= endpoints.length) { | |
| res.status(404).json({ error: 'Endpoint not found' }); | |
| return; | |
| } | |
| // Update the state in the API client | |
| this.apiClient.updateGradioEndpointState(index, enabled); | |
| // Emit tool state change event for Gradio endpoint | |
| const endpoint = endpoints[index]; | |
| if (endpoint) { | |
| const toolId = `gradio_${endpoint.subdomain}`; | |
| this.apiClient.emit('toolStateChange', toolId, enabled); | |
| } | |
| // Get the updated endpoint | |
| const updatedEndpoint = endpoints[index]; | |
| res.json(updatedEndpoint); | |
| }); | |
| // Update Gradio endpoint | |
| this.app.put('/api/gradio-endpoints/:index', express.json(), (req, res) => { | |
| const index = parseInt(req.params.index); | |
| const { name, subdomain, id, emoji } = req.body as { | |
| name: string; | |
| subdomain: string; | |
| id?: string; | |
| emoji?: string; | |
| }; | |
| if (!this.apiClient) { | |
| res.status(500).json({ error: 'API client not initialized' }); | |
| return; | |
| } | |
| const endpoints = this.apiClient.getGradioEndpoints(); | |
| if (index < 0 || index >= endpoints.length) { | |
| res.status(404).json({ error: 'Endpoint not found' }); | |
| return; | |
| } | |
| // Validate required fields | |
| if (!name || !subdomain) { | |
| res.status(400).json({ error: 'Name and subdomain are required' }); | |
| return; | |
| } | |
| // Update the endpoint in the API client | |
| const updatedEndpoint = { name, subdomain, id, emoji }; | |
| this.apiClient.updateGradioEndpoint(index, updatedEndpoint); | |
| res.json(updatedEndpoint); | |
| }); | |
| } | |
| } | |
Xet Storage Details
- Size:
- 16.3 kB
- Xet hash:
- 975a64b7c580fbe10ad8fb79a2464e0189f50739a4ab144232c27a399f3a8d2e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.