Skip to main content
Glama

1MCP Server

serve.ts16.6 kB
import fs from 'fs'; import path from 'path'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import configReloadService from '@src/application/services/configReloadService.js'; import ConfigContext from '@src/config/configContext.js'; import { McpConfigManager } from '@src/config/mcpConfigManager.js'; import { getDefaultInstructionsTemplatePath } from '@src/constants.js'; import { getConfigDir } from '@src/constants.js'; import { InstructionAggregator } from '@src/core/instructions/instructionAggregator.js'; import { formatValidationError, validateTemplateContent } from '@src/core/instructions/templateValidator.js'; import { McpLoadingManager } from '@src/core/loading/mcpLoadingManager.js'; import { AgentConfigManager } from '@src/core/server/agentConfig.js'; import { registerPidFileCleanup, writePidFile } from '@src/core/server/pidFileManager.js'; import { ServerManager } from '@src/core/server/serverManager.js'; import { TagExpression, TagQueryParser } from '@src/domains/preset/parsers/tagQueryParser.js'; import logger, { debugIf } from '@src/logger/logger.js'; import { setupServer } from '@src/server.js'; import { ExpressServer } from '@src/transport/http/server.js'; import { displayLogo } from '@src/utils/ui/logo.js'; export interface ServeOptions { config?: string; 'config-dir'?: string; 'log-level'?: string; 'log-file'?: string; transport: string; port: number; host: string; 'external-url'?: string; filter?: string; pagination: boolean; auth: boolean; 'enable-auth': boolean; 'enable-scope-validation': boolean; 'enable-enhanced-security': boolean; 'session-ttl': number; 'session-storage-path'?: string; 'rate-limit-window': number; 'rate-limit-max': number; 'trust-proxy': string; 'health-info-level': string; 'enable-async-loading': boolean; 'instructions-template'?: string; } /** * Load custom instructions template from file with validation * @param templatePath Path to template file (CLI option or default) * @param configDir Config directory for default template location * @returns Template content or undefined if not found/error */ function loadInstructionsTemplate(templatePath?: string, configDir?: string): string | undefined { let templateFilePath: string; if (templatePath) { // Use provided template path (resolve relative paths) templateFilePath = path.isAbsolute(templatePath) ? templatePath : path.resolve(process.cwd(), templatePath); } else { // Use default template file in config directory templateFilePath = getDefaultInstructionsTemplatePath(configDir); } try { if (fs.existsSync(templateFilePath)) { const templateContent = fs.readFileSync(templateFilePath, 'utf-8'); // Validate template content and syntax const validation = validateTemplateContent(templateContent, templateFilePath); if (!validation.valid) { const errorMessage = formatValidationError(validation); logger.error(`Invalid instructions template: ${errorMessage}`); // For explicit template paths, this is a hard error if (templatePath) { logger.error('Template validation failed. Server will use built-in template.'); } return undefined; } logger.info(`Loaded and validated custom instructions template from: ${templateFilePath}`); debugIf(() => ({ message: 'Template length details', meta: { templateLength: templateContent.length, templateFilePath }, })); return templateContent; } else { if (templatePath) { // If user explicitly provided a template path, warn about missing file logger.warn(`Custom instructions template file not found: ${templateFilePath}`); logger.info('Template file resolution:'); logger.info(` • Check that the file path is correct`); logger.info(` • Ensure the file has read permissions`); logger.info(` • Use absolute paths or paths relative to current directory`); logger.info(` • Server will use built-in template as fallback`); } else { // If using default path, just log debug (it's optional) debugIf(() => ({ message: 'Default instructions template file not found, using built-in template', meta: { templateFilePath, usingBuiltIn: true }, })); } return undefined; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Failed to load instructions template from ${templateFilePath}: ${errorMessage}`); // Provide helpful troubleshooting guidance logger.info('Template loading failed. Troubleshooting steps:'); logger.info(` • Verify file exists and has read permissions`); logger.info(` • Check file encoding (should be UTF-8)`); logger.info(` • Ensure no other process is locking the file`); logger.info(` • Try using an absolute file path`); logger.info(` • Server will use built-in template as fallback`); return undefined; } } /** * Set up graceful shutdown handling */ function setupGracefulShutdown( serverManager: ServerManager, loadingManager?: McpLoadingManager, expressServer?: ExpressServer, instructionAggregator?: InstructionAggregator, ): void { const shutdown = async () => { logger.info('Shutting down server...'); // Stop the configuration reload service configReloadService.stop(); // Shutdown loading manager if it exists if (loadingManager && typeof loadingManager.shutdown === 'function') { try { loadingManager.shutdown(); logger.info('Loading manager shutdown complete'); } catch (error) { logger.error(`Error shutting down loading manager: ${error}`); } } // Shutdown ExpressServer if it exists if (expressServer) { try { expressServer.shutdown(); logger.info('ExpressServer shutdown complete'); } catch (error) { logger.error(`Error shutting down ExpressServer: ${error}`); } } // Close all transports for (const [sessionId, transport] of serverManager.getTransports().entries()) { try { transport?.close(); logger.info(`Closed transport: ${sessionId}`); } catch (error) { logger.error(`Error closing transport ${sessionId}: ${error}`); } } // Cleanup InstructionAggregator if it exists if (instructionAggregator && typeof instructionAggregator.cleanup === 'function') { try { instructionAggregator.cleanup(); logger.info('InstructionAggregator cleanup complete'); } catch (error) { logger.error(`Error cleaning up InstructionAggregator: ${error}`); } } // Cleanup PresetManager if it exists try { const PresetManager = (await import('@src/domains/preset/manager/presetManager.js')).PresetManager; const presetManager = PresetManager.getInstance(); if (presetManager && typeof presetManager.cleanup === 'function') { await presetManager.cleanup(); logger.info('PresetManager cleanup complete'); } } catch (error) { logger.error(`Error cleaning up PresetManager: ${error}`); } logger.info('Server shutdown complete'); process.exit(0); }; // Handle various signals for graceful shutdown process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); process.on('SIGHUP', shutdown); } /** * Start the server using the specified transport. */ export async function serveCommand(parsedArgv: ServeOptions): Promise<void> { try { // Initialize ConfigContext with CLI options const configContext = ConfigContext.getInstance(); if (parsedArgv.config) { configContext.setConfigPath(parsedArgv.config); } else if (parsedArgv['config-dir']) { configContext.setConfigDir(parsedArgv['config-dir']); } else { configContext.reset(); } // Initialize MCP config manager using resolved config path const configFilePath = configContext.getResolvedConfigPath(); const mcpConfigManager = McpConfigManager.getInstance(configFilePath); // Get server count for logo display const transportConfig = mcpConfigManager.getTransportConfig(); const serverCount = Object.keys(transportConfig).length; // Handle backward compatibility for auth flag const authEnabled = parsedArgv['enable-auth'] ?? parsedArgv['auth'] ?? false; // Display logo with runtime information (skip for stdio or when logging to file) if (parsedArgv.transport !== 'stdio' && !parsedArgv['log-file']) { displayLogo({ transport: parsedArgv.transport, port: parsedArgv.port, host: parsedArgv.host, serverCount, authEnabled, logLevel: parsedArgv['log-level'], configDir: getConfigDir(parsedArgv['config-dir']), }); } // Configure server settings from CLI arguments const serverConfigManager = AgentConfigManager.getInstance(); const scopeValidationEnabled = parsedArgv['enable-scope-validation'] ?? authEnabled; const enhancedSecurityEnabled = parsedArgv['enable-enhanced-security'] ?? false; // Handle trust proxy configuration (convert 'true'/'false' strings to boolean) const trustProxyValue = parsedArgv['trust-proxy']; const trustProxy = trustProxyValue === 'true' ? true : trustProxyValue === 'false' ? false : trustProxyValue; // Derive session storage path: explicit option > config-dir/sessions > global default let sessionStoragePath = parsedArgv['session-storage-path']; if (!sessionStoragePath && parsedArgv['config-dir']) { // When config-dir is specified but session-storage-path is not, // store sessions within the config directory to maintain isolation sessionStoragePath = path.join(parsedArgv['config-dir'], 'sessions'); } serverConfigManager.updateConfig({ host: parsedArgv.host, port: parsedArgv.port, externalUrl: parsedArgv['external-url'], trustProxy, auth: { enabled: authEnabled, sessionTtlMinutes: parsedArgv['session-ttl'], sessionStoragePath, oauthCodeTtlMs: 60 * 1000, // 1 minute oauthTokenTtlMs: parsedArgv['session-ttl'] * 60 * 1000, // Convert minutes to milliseconds }, rateLimit: { windowMs: parsedArgv['rate-limit-window'] * 60 * 1000, // Convert minutes to milliseconds max: parsedArgv['rate-limit-max'], }, features: { auth: authEnabled, scopeValidation: scopeValidationEnabled, enhancedSecurity: enhancedSecurityEnabled, }, health: { detailLevel: parsedArgv['health-info-level'] as 'full' | 'basic' | 'minimal', }, ...(parsedArgv['enable-async-loading'] && { asyncLoading: { enabled: true, notifyOnServerReady: true, waitForMinimumServers: 1, initialLoadTimeoutMs: 30000, batchNotifications: true, batchDelayMs: 100, }, }), }); // Initialize PresetManager with config directory option before server setup // This ensures the singleton is created with the correct config directory const PresetManager = (await import('@src/domains/preset/manager/presetManager.js')).PresetManager; PresetManager.getInstance(parsedArgv['config-dir']); // Initialize server and get server manager with custom config path if provided const { serverManager, loadingManager, asyncOrchestrator, instructionAggregator } = await setupServer(); // Load custom instructions template if provided (applies to all transport types) const customTemplate = loadInstructionsTemplate(parsedArgv['instructions-template'], parsedArgv['config-dir']); let expressServer: ExpressServer | undefined; switch (parsedArgv.transport) { case 'stdio': { // DEPRECATION WARNING logger.warn('⚠️ DEPRECATION WARNING: `serve --transport stdio` is deprecated'); logger.warn('⚠️ Please use `1mcp proxy` instead for better compatibility'); logger.warn('⚠️ This mode may be removed in a future major version'); logger.warn(''); logger.warn('Migration guide:'); logger.warn(' 1. Start HTTP server: 1mcp serve'); logger.warn(' 2. Use proxy command: 1mcp proxy'); logger.warn(''); // Use stdio transport const transport = new StdioServerTransport(); // Parse and validate filter from CLI if provided let tags: string[] | undefined; let tagExpression: TagExpression | undefined; let tagFilterMode: 'simple-or' | 'advanced' | 'none' = 'none'; if (parsedArgv.filter) { try { // First try to parse as advanced expression tagExpression = TagQueryParser.parseAdvanced(parsedArgv.filter); tagFilterMode = 'advanced'; // Provide simple tags for backward compat where possible if (tagExpression.type === 'tag') { tags = [tagExpression.value!]; } } catch (_advancedError) { // Fall back to simple parsing for comma-separated tags try { tags = TagQueryParser.parseSimple(parsedArgv.filter); tagFilterMode = 'simple-or'; if (!tags || tags.length === 0) { logger.warn('No valid tags provided, ignoring filter parameter'); tags = undefined; tagFilterMode = 'none'; } } catch (simpleError) { logger.error( `Invalid filter expression: ${simpleError instanceof Error ? simpleError.message : 'Unknown error'}`, ); logger.error('Examples:'); logger.error(' --filter "web,api,database" # OR logic (comma-separated)'); logger.error(' --filter "web AND database" # AND logic'); logger.error(' --filter "(web OR api) AND database" # Complex expressions'); process.exit(1); } } } await serverManager.connectTransport(transport, 'stdio', { tags, tagExpression, tagFilterMode, enablePagination: parsedArgv.pagination, customTemplate, }); // Initialize notifications for async loading if enabled if (asyncOrchestrator) { const inboundConnection = serverManager.getServer('stdio'); if (inboundConnection) { asyncOrchestrator.initializeNotifications(inboundConnection); logger.info('Async loading notifications initialized for stdio transport'); } } logger.info('Server started with stdio transport'); break; } case 'sse': { logger.warning('sse option is deprecated, use http instead'); } // eslint-disable-next-line no-fallthrough case 'http': { // Use HTTP/SSE transport expressServer = new ExpressServer(serverManager, loadingManager, asyncOrchestrator, customTemplate); expressServer.start(); // Write PID file for proxy auto-discovery const configDir = getConfigDir(parsedArgv['config-dir']); const serverUrl = serverConfigManager.getUrl(); writePidFile(configDir, { pid: process.pid, url: `${serverUrl}/mcp`, port: parsedArgv.port, host: parsedArgv.host, transport: 'http', startedAt: new Date().toISOString(), configDir, }); // Register cleanup handlers registerPidFileCleanup(configDir); break; } default: logger.error(`Invalid transport: ${parsedArgv.transport}`); process.exit(1); } // Set up graceful shutdown handling setupGracefulShutdown(serverManager, loadingManager, expressServer, instructionAggregator); // Log MCP loading progress (non-blocking) loadingManager.on('loading-progress', (summary) => { logger.info( `MCP loading progress: ${summary.ready}/${summary.totalServers} servers ready (${summary.loading} loading, ${summary.failed} failed)`, ); }); loadingManager.on('loading-complete', (summary) => { logger.info( `MCP loading complete: ${summary.ready}/${summary.totalServers} servers ready (${summary.successRate.toFixed(1)}% success rate)`, ); }); } catch (error) { logger.error('Server error:', error); process.exit(1); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1mcp-app/agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server