Skip to main content
Glama
index.ts70.7 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import * as z from "zod"; import path from "path"; import dotenv from "dotenv"; import { createServer, Server as HttpServer, IncomingHttpHeaders } from "http"; import { randomUUID } from "crypto"; // Import handler functions import { handleGetProgram } from "./handlers/handleGetProgram"; import { handleGetClass } from "./handlers/handleGetClass"; import { handleGetFunctionGroup } from "./handlers/handleGetFunctionGroup"; import { handleGetFunction } from "./handlers/handleGetFunction"; import { handleGetTable } from "./handlers/handleGetTable"; import { handleGetStructure } from "./handlers/handleGetStructure"; import { handleGetTableContents } from "./handlers/handleGetTableContents"; import { handleGetPackage } from "./handlers/handleGetPackage"; import { handleCreatePackage } from "./handlers/handleCreatePackage"; import { handleGetInclude } from "./handlers/handleGetInclude"; import { handleGetIncludesList } from "./handlers/handleGetIncludesList"; import { handleGetTypeInfo } from "./handlers/handleGetTypeInfo"; import { handleGetInterface } from "./handlers/handleGetInterface"; import { handleGetTransaction } from "./handlers/handleGetTransaction"; import { handleSearchObject } from "./handlers/handleSearchObject"; import { handleGetEnhancements } from "./handlers/handleGetEnhancements"; import { handleGetEnhancementImpl } from "./handlers/handleGetEnhancementImpl"; import { handleGetEnhancementSpot } from "./handlers/handleGetEnhancementSpot"; import { handleGetBdef } from "./handlers/handleGetBdef"; import { handleGetSqlQuery } from "./handlers/handleGetSqlQuery"; import { handleGetObjectsByType } from "./handlers/handleGetObjectsByType"; import { handleGetWhereUsed } from "./handlers/handleGetWhereUsed"; import { handleGetObjectInfo } from "./handlers/handleGetObjectInfo"; import { handleDescribeByList } from "./handlers/handleDescribeByList"; import { handleGetAbapAST } from "./handlers/handleGetAbapAST"; import { handleGetAbapSemanticAnalysis } from "./handlers/handleGetAbapSemanticAnalysis"; import { handleGetAbapSystemSymbols } from "./handlers/handleGetAbapSystemSymbols"; import { handleGetDomain } from "./handlers/handleGetDomain"; import { handleCreateDomain } from "./handlers/handleCreateDomain"; import { handleUpdateDomain } from "./handlers/handleUpdateDomain"; import { handleCreateDataElement } from "./handlers/handleCreateDataElement"; import { handleUpdateDataElement } from "./handlers/handleUpdateDataElement"; import { handleGetDataElement } from "./handlers/handleGetDataElement"; import { handleCreateTransport } from "./handlers/handleCreateTransport"; import { handleGetTransport } from "./handlers/handleGetTransport"; import { handleCreateTable } from "./handlers/handleCreateTable"; import { handleCreateStructure } from "./handlers/handleCreateStructure"; import { handleCreateView } from "./handlers/handleCreateView"; import { handleGetView } from "./handlers/handleGetView"; import { handleCreateClass } from "./handlers/handleCreateClass"; import { handleCreateProgram } from "./handlers/handleCreateProgram"; import { handleCreateInterface } from "./handlers/handleCreateInterface"; import { handleCreateFunctionGroup } from "./handlers/handleCreateFunctionGroup"; import { handleCreateFunctionModule } from "./handlers/handleCreateFunctionModule"; import { handleActivateObject } from "./handlers/handleActivateObject"; import { handleDeleteObject } from "./handlers/handleDeleteObject"; import { handleCheckObject } from "./handlers/handleCheckObject"; import { handleUpdateClassSource } from "./handlers/handleUpdateClassSource"; import { handleUpdateProgramSource } from "./handlers/handleUpdateProgramSource"; import { handleUpdateViewSource } from "./handlers/handleUpdateViewSource"; import { handleUpdateInterfaceSource } from "./handlers/handleUpdateInterfaceSource"; import { handleUpdateFunctionModuleSource } from "./handlers/handleUpdateFunctionModuleSource"; import { handleGetSession } from "./handlers/handleGetSession"; import { handleValidateObject } from "./handlers/handleValidateObject"; import { handleLockObject } from "./handlers/handleLockObject"; import { handleUnlockObject } from "./handlers/handleUnlockObject"; import { handleValidateClass } from "./handlers/handleValidateClass"; import { handleCheckClass } from "./handlers/handleCheckClass"; import { handleValidateTable } from "./handlers/handleValidateTable"; import { handleCheckTable } from "./handlers/handleCheckTable"; import { handleValidateFunctionModule } from "./handlers/handleValidateFunctionModule"; import { handleCheckFunctionModule } from "./handlers/handleCheckFunctionModule"; // Import shared utility functions and types import { getBaseUrl, getAuthHeaders, makeAdtRequest, return_error, return_response, setConfigOverride, setConnectionOverride, } from "./lib/utils"; import { SapConfig, AbapConnection, getConfigFromEnv } from "@mcp-abap-adt/connection"; // Import logger import { logger } from "./lib/logger"; // Import tool registry import { getAllTools } from "./lib/toolsRegistry"; // Import TOOL_DEFINITION from handlers import { TOOL_DEFINITION as GetProgram_Tool } from "./handlers/handleGetProgram"; import { TOOL_DEFINITION as GetClass_Tool } from "./handlers/handleGetClass"; import { TOOL_DEFINITION as GetFunction_Tool } from "./handlers/handleGetFunction"; import { TOOL_DEFINITION as GetFunctionGroup_Tool } from "./handlers/handleGetFunctionGroup"; import { TOOL_DEFINITION as GetTable_Tool } from "./handlers/handleGetTable"; import { TOOL_DEFINITION as GetStructure_Tool } from "./handlers/handleGetStructure"; import { TOOL_DEFINITION as GetTableContents_Tool } from "./handlers/handleGetTableContents"; import { TOOL_DEFINITION as GetPackage_Tool } from "./handlers/handleGetPackage"; import { TOOL_DEFINITION as CreatePackage_Tool } from "./handlers/handleCreatePackage"; import { TOOL_DEFINITION as GetInclude_Tool } from "./handlers/handleGetInclude"; import { TOOL_DEFINITION as GetIncludesList_Tool } from "./handlers/handleGetIncludesList"; import { TOOL_DEFINITION as GetTypeInfo_Tool } from "./handlers/handleGetTypeInfo"; import { TOOL_DEFINITION as GetInterface_Tool } from "./handlers/handleGetInterface"; import { TOOL_DEFINITION as GetTransaction_Tool } from "./handlers/handleGetTransaction"; import { TOOL_DEFINITION as SearchObject_Tool } from "./handlers/handleSearchObject"; import { TOOL_DEFINITION as GetEnhancements_Tool } from "./handlers/handleGetEnhancements"; import { TOOL_DEFINITION as GetEnhancementImpl_Tool } from "./handlers/handleGetEnhancementImpl"; import { TOOL_DEFINITION as GetEnhancementSpot_Tool } from "./handlers/handleGetEnhancementSpot"; import { TOOL_DEFINITION as GetBdef_Tool } from "./handlers/handleGetBdef"; import { TOOL_DEFINITION as GetSqlQuery_Tool } from "./handlers/handleGetSqlQuery"; import { TOOL_DEFINITION as GetWhereUsed_Tool } from "./handlers/handleGetWhereUsed"; import { TOOL_DEFINITION as GetObjectInfo_Tool } from "./handlers/handleGetObjectInfo"; import { TOOL_DEFINITION as GetAbapAST_Tool } from "./handlers/handleGetAbapAST"; import { TOOL_DEFINITION as GetAbapSemanticAnalysis_Tool } from "./handlers/handleGetAbapSemanticAnalysis"; import { TOOL_DEFINITION as GetAbapSystemSymbols_Tool } from "./handlers/handleGetAbapSystemSymbols"; import { TOOL_DEFINITION as GetDomain_Tool } from "./handlers/handleGetDomain"; import { TOOL_DEFINITION as CreateDomain_Tool } from "./handlers/handleCreateDomain"; import { TOOL_DEFINITION as UpdateDomain_Tool } from "./handlers/handleUpdateDomain"; import { TOOL_DEFINITION as CreateDataElement_Tool } from "./handlers/handleCreateDataElement"; import { TOOL_DEFINITION as UpdateDataElement_Tool } from "./handlers/handleUpdateDataElement"; import { TOOL_DEFINITION as GetDataElement_Tool } from "./handlers/handleGetDataElement"; import { TOOL_DEFINITION as CreateTransport_Tool } from "./handlers/handleCreateTransport"; import { TOOL_DEFINITION as GetTransport_Tool } from "./handlers/handleGetTransport"; import { TOOL_DEFINITION as CreateTable_Tool } from "./handlers/handleCreateTable"; import { TOOL_DEFINITION as CreateStructure_Tool } from "./handlers/handleCreateStructure"; import { TOOL_DEFINITION as CreateView_Tool } from "./handlers/handleCreateView"; import { TOOL_DEFINITION as GetView_Tool } from "./handlers/handleGetView"; import { TOOL_DEFINITION as CreateClass_Tool } from "./handlers/handleCreateClass"; import { TOOL_DEFINITION as CreateProgram_Tool } from "./handlers/handleCreateProgram"; import { TOOL_DEFINITION as CreateInterface_Tool } from "./handlers/handleCreateInterface"; import { TOOL_DEFINITION as CreateFunctionGroup_Tool } from "./handlers/handleCreateFunctionGroup"; import { TOOL_DEFINITION as CreateFunctionModule_Tool } from "./handlers/handleCreateFunctionModule"; import { TOOL_DEFINITION as ActivateObject_Tool } from "./handlers/handleActivateObject"; import { TOOL_DEFINITION as DeleteObject_Tool } from "./handlers/handleDeleteObject"; import { TOOL_DEFINITION as CheckObject_Tool } from "./handlers/handleCheckObject"; import { TOOL_DEFINITION as UpdateClassSource_Tool } from "./handlers/handleUpdateClassSource"; import { TOOL_DEFINITION as UpdateProgramSource_Tool } from "./handlers/handleUpdateProgramSource"; import { TOOL_DEFINITION as UpdateViewSource_Tool } from "./handlers/handleUpdateViewSource"; import { TOOL_DEFINITION as UpdateInterfaceSource_Tool } from "./handlers/handleUpdateInterfaceSource"; import { TOOL_DEFINITION as UpdateFunctionModuleSource_Tool } from "./handlers/handleUpdateFunctionModuleSource"; import { TOOL_DEFINITION as GetSession_Tool } from "./handlers/handleGetSession"; import { TOOL_DEFINITION as ValidateObject_Tool } from "./handlers/handleValidateObject"; import { TOOL_DEFINITION as LockObject_Tool } from "./handlers/handleLockObject"; import { TOOL_DEFINITION as UnlockObject_Tool } from "./handlers/handleUnlockObject"; import { TOOL_DEFINITION as ValidateClass_Tool } from "./handlers/handleValidateClass"; import { TOOL_DEFINITION as CheckClass_Tool } from "./handlers/handleCheckClass"; import { TOOL_DEFINITION as ValidateTable_Tool } from "./handlers/handleValidateTable"; import { TOOL_DEFINITION as CheckTable_Tool } from "./handlers/handleCheckTable"; import { TOOL_DEFINITION as ValidateFunctionModule_Tool } from "./handlers/handleValidateFunctionModule"; import { TOOL_DEFINITION as CheckFunctionModule_Tool } from "./handlers/handleCheckFunctionModule"; // --- ENV FILE LOADING LOGIC --- import fs from "fs"; /** * Display help message */ function showHelp(): void { const help = ` MCP ABAP ADT Server - SAP ABAP Development Tools MCP Integration USAGE: mcp-abap-adt [options] # Default stdio transport mcp-abap-adt-http [options] # HTTP StreamableHTTP transport mcp-abap-adt-sse [options] # Server-Sent Events transport OPTIONS: --help Show this help message ENVIRONMENT: --env=<path> Path to .env file (default: ./.env) --env <path> Alternative syntax for --env TRANSPORT: --transport=<type> Transport type: stdio|http|streamable-http|sse (default: stdio) HTTP OPTIONS: --http Use HTTP StreamableHTTP transport --http-port=<port> HTTP server port (default: 3000) --http-host=<host> HTTP server host (default: 0.0.0.0) --http-json-response Enable JSON response format --http-allowed-origins=<list> Comma-separated allowed origins for CORS --http-allowed-hosts=<list> Comma-separated allowed hosts --http-enable-dns-protection Enable DNS rebinding protection SSE OPTIONS: --sse Use Server-Sent Events transport --sse-port=<port> SSE server port (default: 3001) --sse-host=<host> SSE server host (default: 0.0.0.0) --sse-allowed-origins=<list> Comma-separated allowed origins for CORS --sse-allowed-hosts=<list> Comma-separated allowed hosts --sse-enable-dns-protection Enable DNS rebinding protection ENVIRONMENT VARIABLES: MCP_ENV_PATH Path to .env file MCP_SKIP_ENV_LOAD Skip automatic .env loading (true|false) MCP_SKIP_AUTO_START Skip automatic server start (true|false) MCP_TRANSPORT Default transport type MCP_HTTP_PORT Default HTTP port MCP_HTTP_HOST Default HTTP host MCP_HTTP_ENABLE_JSON_RESPONSE Enable JSON responses MCP_HTTP_ALLOWED_ORIGINS Allowed CORS origins MCP_HTTP_ALLOWED_HOSTS Allowed hosts MCP_HTTP_ENABLE_DNS_PROTECTION Enable DNS protection MCP_SSE_PORT Default SSE port MCP_SSE_HOST Default SSE host MCP_SSE_ALLOWED_ORIGINS Allowed CORS origins for SSE MCP_SSE_ALLOWED_HOSTS Allowed hosts for SSE MCP_SSE_ENABLE_DNS_PROTECTION Enable DNS protection for SSE SAP CONNECTION (.env file): SAP_URL SAP system URL (required) SAP_CLIENT SAP client number (required) SAP_AUTH_TYPE Authentication type: basic|jwt (default: basic) SAP_USERNAME SAP username (for basic auth) SAP_PASSWORD SAP password (for basic auth) SAP_JWT_TOKEN JWT token (for jwt auth) EXAMPLES: # Use .env from current directory (default) mcp-abap-adt # Use custom .env file mcp-abap-adt --env=/path/to/my.env # Start HTTP server on port 8080 mcp-abap-adt --transport=http --http-port=8080 # Start SSE server with CORS mcp-abap-adt-sse --sse-allowed-origins=http://localhost:3000 DOCUMENTATION: https://github.com/fr0ster/mcp-abap-adt `; console.log(help); process.exit(0); } // Check for --help flag before anything else if (process.argv.includes("--help") || process.argv.includes("-h")) { showHelp(); } /** * Parses command line arguments to find env file path * Supports both formats: * 1. --env=/path/to/.env * 2. --env /path/to/.env */ function parseEnvArg(): string | undefined { const args = process.argv; for (let i = 0; i < args.length; i++) { // Format: --env=/path/to/.env if (args[i].startsWith("--env=")) { return args[i].slice("--env=".length); } // Format: --env /path/to/.env else if (args[i] === "--env" && i + 1 < args.length) { return args[i + 1]; } } return undefined; } // Find .env file path from arguments const skipEnvAutoload = process.env.MCP_SKIP_ENV_LOAD === "true"; let envFilePath = parseEnvArg() ?? process.env.MCP_ENV_PATH; if (!skipEnvAutoload) { if (!envFilePath) { // Priority order for global installations: // 1. Current working directory (where user runs the command) // 2. Package installation directory const possiblePaths = [ path.resolve(process.cwd(), ".env"), // User's working directory (FIRST!) path.resolve(__dirname, "../.env") // Package directory ]; for (const possiblePath of possiblePaths) { if (fs.existsSync(possiblePath)) { envFilePath = possiblePath; process.stderr.write(`[MCP-ENV] Found .env file: ${envFilePath}\n`); break; } } if (!envFilePath) { // Default to current working directory if nothing found envFilePath = path.resolve(process.cwd(), ".env"); process.stderr.write(`[MCP-ENV] WARNING: No .env file found, will try: ${envFilePath}\n`); } } else { process.stderr.write(`[MCP-ENV] Using .env from argument/env: ${envFilePath}\n`); } if (!path.isAbsolute(envFilePath)) { envFilePath = path.resolve(process.cwd(), envFilePath); process.stderr.write(`[MCP-ENV] Resolved relative path to: ${envFilePath}\n`); } if (fs.existsSync(envFilePath)) { dotenv.config({ path: envFilePath }); process.stderr.write(`[MCP-ENV] ✓ Successfully loaded: ${envFilePath}\n`); } else { logger.error(".env file not found", { path: envFilePath }); process.stderr.write(`[MCP-ENV] ✗ ERROR: .env file not found at: ${envFilePath}\n`); process.stderr.write(`[MCP-ENV] Current working directory: ${process.cwd()}\n`); process.stderr.write(`[MCP-ENV] Use --env=/path/to/.env to specify custom location\n`); process.exit(1); } } else if (envFilePath) { if (!path.isAbsolute(envFilePath)) { envFilePath = path.resolve(process.cwd(), envFilePath); } process.stderr.write(`[MCP-ENV] Environment autoload skipped; using provided path reference: ${envFilePath}\n`); } else { process.stderr.write(`[MCP-ENV] Environment autoload skipped (MCP_SKIP_ENV_LOAD=true).\n`); } // --- END ENV FILE LOADING LOGIC --- // Debug: Log loaded SAP_URL and SAP_CLIENT using the MCP-compatible logger const envLogPath = envFilePath ?? "(skipped)"; logger.info("SAP configuration loaded", { type: "CONFIG_INFO", SAP_URL: process.env.SAP_URL, SAP_CLIENT: process.env.SAP_CLIENT || "(not set)", SAP_AUTH_TYPE: process.env.SAP_AUTH_TYPE || "(not set)", SAP_JWT_TOKEN: process.env.SAP_JWT_TOKEN ? "[set]" : "(not set)", ENV_PATH: envLogPath, CWD: process.cwd(), DIRNAME: __dirname, }); type TransportConfig = | { type: "stdio" } | { type: "streamable-http"; host: string; port: number; enableJsonResponse: boolean; allowedOrigins?: string[]; allowedHosts?: string[]; enableDnsRebindingProtection: boolean; } | { type: "sse"; host: string; port: number; allowedOrigins?: string[]; allowedHosts?: string[]; enableDnsRebindingProtection: boolean; }; function getArgValue(name: string): string | undefined { const args = process.argv; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith(`${name}=`)) { return arg.slice(name.length + 1); } if (arg === name && i + 1 < args.length) { return args[i + 1]; } } return undefined; } function hasFlag(name: string): boolean { return process.argv.includes(name); } function parseBoolean(value?: string): boolean { if (!value) { return false; } const normalized = value.trim().toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; } function resolvePortOption(argName: string, envName: string, defaultValue: number): number { const rawValue = getArgValue(argName) ?? process.env[envName]; if (!rawValue) { return defaultValue; } const port = Number.parseInt(rawValue, 10); if (!Number.isInteger(port) || port <= 0 || port > 65535) { throw new Error(`Invalid port value for ${argName}: ${rawValue}`); } return port; } function resolveBooleanOption(argName: string, envName: string, defaultValue: boolean): boolean { const argValue = getArgValue(argName); if (argValue !== undefined) { return parseBoolean(argValue); } if (hasFlag(argName)) { return true; } const envValue = process.env[envName]; if (envValue !== undefined) { return parseBoolean(envValue); } return defaultValue; } function resolveListOption(argName: string, envName: string): string[] | undefined { const rawValue = getArgValue(argName) ?? process.env[envName]; if (!rawValue) { return undefined; } const items = rawValue .split(",") .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); return items.length > 0 ? items : undefined; } function parseTransportConfig(): TransportConfig { const transportInput = getArgValue("--transport") ?? process.env.MCP_TRANSPORT; const normalized = transportInput ? transportInput.trim().toLowerCase() : undefined; if ( normalized && normalized !== "stdio" && normalized !== "http" && normalized !== "streamable-http" && normalized !== "server" && normalized !== "sse" ) { throw new Error(`Unsupported transport: ${transportInput}`); } const sseRequested = normalized === "sse" || hasFlag("--sse"); if (sseRequested) { const port = resolvePortOption("--sse-port", "MCP_SSE_PORT", 3001); const host = getArgValue("--sse-host") ?? process.env.MCP_SSE_HOST ?? "0.0.0.0"; const allowedOrigins = resolveListOption("--sse-allowed-origins", "MCP_SSE_ALLOWED_ORIGINS"); const allowedHosts = resolveListOption("--sse-allowed-hosts", "MCP_SSE_ALLOWED_HOSTS"); const enableDnsRebindingProtection = resolveBooleanOption( "--sse-enable-dns-protection", "MCP_SSE_ENABLE_DNS_PROTECTION", false ); return { type: "sse", host, port, allowedOrigins, allowedHosts, enableDnsRebindingProtection, }; } const httpRequested = normalized === "http" || normalized === "streamable-http" || normalized === "server" || hasFlag("--http"); if (httpRequested) { const port = resolvePortOption("--http-port", "MCP_HTTP_PORT", 3000); const host = getArgValue("--http-host") ?? process.env.MCP_HTTP_HOST ?? "0.0.0.0"; const enableJsonResponse = resolveBooleanOption( "--http-json-response", "MCP_HTTP_ENABLE_JSON_RESPONSE", false ); const allowedOrigins = resolveListOption("--http-allowed-origins", "MCP_HTTP_ALLOWED_ORIGINS"); const allowedHosts = resolveListOption("--http-allowed-hosts", "MCP_HTTP_ALLOWED_HOSTS"); const enableDnsRebindingProtection = resolveBooleanOption( "--http-enable-dns-protection", "MCP_HTTP_ENABLE_DNS_PROTECTION", false ); return { type: "streamable-http", host, port, enableJsonResponse, allowedOrigins, allowedHosts, enableDnsRebindingProtection, }; } return { type: "stdio" }; } let sapConfigOverride: SapConfig | undefined; export interface ServerOptions { sapConfig?: SapConfig; connection?: AbapConnection; transportConfig?: TransportConfig; allowProcessExit?: boolean; registerSignalHandlers?: boolean; } export function setSapConfigOverride(config?: SapConfig) { sapConfigOverride = config; setConfigOverride(config); } export function setAbapConnectionOverride(connection?: AbapConnection) { setConnectionOverride(connection); } /** * Retrieves SAP configuration from environment variables. * Uses getConfigFromEnv from @mcp-abap-adt/connection package. * * @returns {SapConfig} The SAP configuration object. * @throws {Error} If any required environment variable is missing. */ export function getConfig(): SapConfig { if (sapConfigOverride) { return sapConfigOverride; } return getConfigFromEnv(); } /** * Server class for interacting with ABAP systems via ADT. */ export class mcp_abap_adt_server { private readonly allowProcessExit: boolean; private readonly registerSignalHandlers: boolean; private mcpServer: McpServer; // MCP server for all transports private sapConfig: SapConfig; // SAP configuration private transportConfig: TransportConfig; private httpServer?: HttpServer; private shuttingDown = false; // Client session tracking for StreamableHTTP (like the example) private streamableHttpSessions = new Map<string, { sessionId: string; clientIP: string; connectedAt: Date; requestCount: number; }>(); // SSE session tracking (McpServer + SSEServerTransport per session) private sseSessions = new Map<string, { server: McpServer; transport: SSEServerTransport; }>(); private applyAuthHeaders(headers?: IncomingHttpHeaders) { if (!headers) { return; } const getHeaderValue = (value?: string | string[]) => { if (!value) { return undefined; } return Array.isArray(value) ? value[0] : value; }; // Extract JWT token from Authorization header (Bearer) or x-sap-jwt-token let jwtToken: string | undefined; const authorizationHeader = getHeaderValue(headers["authorization"]); if (authorizationHeader) { const bearerMatch = authorizationHeader.match(/Bearer\s+(.+)/i); if (bearerMatch) { jwtToken = bearerMatch[1]?.trim(); } } // Fallback to x-sap-jwt-token if Authorization header is not present if (!jwtToken) { jwtToken = getHeaderValue(headers["x-sap-jwt-token"])?.trim(); } // If no JWT token found, skip processing if (!jwtToken) { return; } // Extract refresh token const refreshToken = getHeaderValue(headers["x-sap-refresh-token"]); // Extract UAA credentials for token refresh const uaaUrl = getHeaderValue(headers["x-sap-uaa-url"])?.trim(); const uaaClientId = getHeaderValue(headers["x-sap-uaa-client-id"])?.trim(); const uaaClientSecret = getHeaderValue(headers["x-sap-uaa-client-secret"])?.trim(); // Extract SAP URL and auth type from headers const sapUrl = getHeaderValue(headers["x-sap-url"])?.trim(); const sapAuthType = getHeaderValue(headers["x-sap-auth-type"])?.trim(); const sanitizeToken = (token: string) => token.length <= 10 ? token : `${token.substring(0, 6)}…${token.substring(token.length - 4)}`; let baseConfig: SapConfig | undefined = this.sapConfig; if (!baseConfig || baseConfig.url === "http://placeholder") { try { baseConfig = getConfig(); } catch (error) { logger.warn("Failed to load base SAP config when applying headers", { type: "SAP_CONFIG_HEADER_APPLY_FAILED", error: error instanceof Error ? error.message : String(error), }); // If base config is not available, create a minimal config from headers if (sapUrl) { baseConfig = { url: sapUrl, authType: (sapAuthType === "jwt" || sapAuthType === "xsuaa") ? "jwt" : "basic", }; } else { return; } } } // Check if any configuration changed const urlChanged = sapUrl && sapUrl !== baseConfig.url; const authTypeChanged = sapAuthType && ((sapAuthType === "jwt" || sapAuthType === "xsuaa") ? "jwt" : "basic") !== baseConfig.authType; const tokenChanged = baseConfig.jwtToken !== jwtToken || (!!refreshToken && refreshToken.trim() !== baseConfig.refreshToken); if (!urlChanged && !authTypeChanged && !tokenChanged) { return; } const newConfig: SapConfig = { ...baseConfig, authType: sapAuthType ? ((sapAuthType === "jwt" || sapAuthType === "xsuaa") ? "jwt" : "basic") : baseConfig.authType, jwtToken, }; if (sapUrl) { newConfig.url = sapUrl; } if (refreshToken && refreshToken.trim()) { newConfig.refreshToken = refreshToken.trim(); } // Add UAA credentials if provided (required for token refresh) if (uaaUrl) { newConfig.uaaUrl = uaaUrl; } if (uaaClientId) { newConfig.uaaClientId = uaaClientId; } if (uaaClientSecret) { newConfig.uaaClientSecret = uaaClientSecret; } setSapConfigOverride(newConfig); this.sapConfig = newConfig; // Force connection cache invalidation to ensure next getManagedConnection() // will recreate connection with updated token // Import synchronously to avoid async issues const { invalidateConnectionCache } = require('./lib/utils'); try { // Invalidate cache so that next getManagedConnection() will recreate connection // with updated config (including new JWT token) invalidateConnectionCache(); } catch (error) { // If invalidation fails, log but continue - connection will be recreated on next use logger.debug("Connection cache invalidation failed", { type: "CONNECTION_CACHE_INVALIDATION_FAILED", error: error instanceof Error ? error.message : String(error), }); } logger.info("Updated SAP configuration from HTTP headers", { type: "SAP_CONFIG_UPDATED", urlChanged: Boolean(urlChanged), authTypeChanged: Boolean(authTypeChanged), tokenChanged: Boolean(tokenChanged), hasRefreshToken: Boolean(refreshToken), jwtPreview: sanitizeToken(jwtToken), }); } /** * Constructor for the mcp_abap_adt_server class. */ constructor(options?: ServerOptions) { this.allowProcessExit = options?.allowProcessExit ?? true; this.registerSignalHandlers = options?.registerSignalHandlers ?? true; if (options?.connection) { setAbapConnectionOverride(options.connection); } else { setAbapConnectionOverride(undefined); } if (!options?.connection) { setSapConfigOverride(options?.sapConfig); } // CHANGED: Don't validate config in constructor - will validate on actual ABAP requests // This allows creating server instance without .env file when using runtime config (e.g., from HTTP headers) try { if (options?.sapConfig) { this.sapConfig = options.sapConfig; } else if (!options?.connection) { this.sapConfig = getConfig(); } else { this.sapConfig = { url: "http://injected-connection", authType: "jwt", jwtToken: "injected" }; } } catch (error) { // If config is not available yet, that's OK - it will be provided later via setSapConfigOverride or DI logger.warn("SAP config not available at initialization, will use runtime config", { type: "CONFIG_DEFERRED", error: error instanceof Error ? error.message : String(error), }); // Set a placeholder that will be replaced this.sapConfig = { url: "http://placeholder", authType: "jwt", jwtToken: "placeholder" }; } try { this.transportConfig = options?.transportConfig ?? parseTransportConfig(); } catch (error) { const message = error instanceof Error ? error.message : String(error); logger.error("Failed to parse transport configuration", { type: "TRANSPORT_CONFIG_ERROR", error: message, }); process.stderr.write(`ERROR: ${message}\n`); if (this.allowProcessExit) { process.exit(1); } throw error instanceof Error ? error : new Error(message); } // Create McpServer (for all transports) this.mcpServer = new McpServer({ name: "mcp-abap-adt", version: "0.1.0" }); this.setupMcpServerHandlers(); // Setup handlers for McpServer if (this.registerSignalHandlers) { this.setupSignalHandlers(); } if (this.transportConfig.type === "streamable-http") { logger.info("Transport configured", { type: "TRANSPORT_CONFIG", transport: this.transportConfig.type, host: this.transportConfig.host, port: this.transportConfig.port, enableJsonResponse: this.transportConfig.enableJsonResponse, allowedOrigins: this.transportConfig.allowedOrigins ?? [], allowedHosts: this.transportConfig.allowedHosts ?? [], enableDnsRebindingProtection: this.transportConfig.enableDnsRebindingProtection, }); } else if (this.transportConfig.type === "sse") { logger.info("Transport configured", { type: "TRANSPORT_CONFIG", transport: this.transportConfig.type, host: this.transportConfig.host, port: this.transportConfig.port, allowedOrigins: this.transportConfig.allowedOrigins ?? [], allowedHosts: this.transportConfig.allowedHosts ?? [], enableDnsRebindingProtection: this.transportConfig.enableDnsRebindingProtection, }); } else { logger.info("Transport configured", { type: "TRANSPORT_CONFIG", transport: this.transportConfig.type, }); } } /** * Creates a new McpServer instance with all handlers registered * Used for SSE sessions where each session needs its own server instance * @private */ private createMcpServerForSession(): McpServer { const server = new McpServer({ name: "mcp-abap-adt", version: "0.1.0" }); // Register all tools using the same method as main server this.registerAllToolsOnServer(server); return server; } /** * Converts JSON Schema to Zod schema object (not z.object(), but object with Zod fields) * SDK expects inputSchema to be an object with Zod schemas as values, not z.object() */ private jsonSchemaToZod(jsonSchema: any): any { // If already a Zod schema object (object with Zod fields), return as-is if (jsonSchema && typeof jsonSchema === 'object' && !jsonSchema.type && !jsonSchema.properties) { // Check if it looks like a Zod schema object (has Zod types as values) const firstValue = Object.values(jsonSchema)[0]; if (firstValue && ((firstValue as any).def || (firstValue as any)._def || typeof (firstValue as any).parse === 'function')) { return jsonSchema; } } // If it's a JSON Schema object if (jsonSchema && typeof jsonSchema === 'object' && jsonSchema.type === 'object' && jsonSchema.properties) { const zodShape: Record<string, z.ZodTypeAny> = {}; const required = jsonSchema.required || []; for (const [key, prop] of Object.entries(jsonSchema.properties)) { const propSchema = prop as any; let zodType: z.ZodTypeAny; if (propSchema.type === 'string') { if (propSchema.enum && Array.isArray(propSchema.enum) && propSchema.enum.length > 0) { // Use z.enum() for enum values (requires at least 1 element, but z.enum needs 2+) if (propSchema.enum.length === 1) { zodType = z.literal(propSchema.enum[0]); } else { zodType = z.enum(propSchema.enum as [string, ...string[]]); } } else { zodType = z.string(); } } else if (propSchema.type === 'number' || propSchema.type === 'integer') { zodType = z.number(); } else if (propSchema.type === 'boolean') { zodType = z.boolean(); } else if (propSchema.type === 'array') { const items = propSchema.items; if (items?.type === 'string') { zodType = z.array(z.string()); } else if (items?.type === 'number' || items?.type === 'integer') { zodType = z.array(z.number()); } else if (items?.type === 'boolean') { zodType = z.array(z.boolean()); } else if (items?.type === 'object' && items.properties) { // For nested objects in arrays, create object schema const nestedShape: Record<string, z.ZodTypeAny> = {}; const nestedRequired = items.required || []; for (const [nestedKey, nestedProp] of Object.entries(items.properties)) { const nestedPropSchema = nestedProp as any; let nestedZodType: z.ZodTypeAny; if (nestedPropSchema.type === 'string') { if (nestedPropSchema.enum && Array.isArray(nestedPropSchema.enum) && nestedPropSchema.enum.length > 0) { if (nestedPropSchema.enum.length === 1) { nestedZodType = z.literal(nestedPropSchema.enum[0]); } else { nestedZodType = z.enum(nestedPropSchema.enum as [string, ...string[]]); } } else { nestedZodType = z.string(); } } else if (nestedPropSchema.type === 'number' || nestedPropSchema.type === 'integer') { nestedZodType = z.number(); } else if (nestedPropSchema.type === 'boolean') { nestedZodType = z.boolean(); } else { nestedZodType = z.any(); } if (nestedPropSchema.default !== undefined) { nestedZodType = nestedZodType.default(nestedPropSchema.default); } if (!nestedRequired.includes(nestedKey)) { nestedZodType = nestedZodType.optional(); } if (nestedPropSchema.description) { nestedZodType = nestedZodType.describe(nestedPropSchema.description); } nestedShape[nestedKey] = nestedZodType; } zodType = z.array(z.object(nestedShape)); } else { zodType = z.array(z.any()); } } else if (propSchema.type === 'object' && propSchema.properties) { // For nested objects, create object schema const nestedShape: Record<string, z.ZodTypeAny> = {}; const nestedRequired = propSchema.required || []; for (const [nestedKey, nestedProp] of Object.entries(propSchema.properties)) { const nestedPropSchema = nestedProp as any; let nestedZodType: z.ZodTypeAny; if (nestedPropSchema.type === 'string') { if (nestedPropSchema.enum && Array.isArray(nestedPropSchema.enum)) { nestedZodType = z.enum(nestedPropSchema.enum as [string, ...string[]]); } else { nestedZodType = z.string(); } } else if (nestedPropSchema.type === 'number' || nestedPropSchema.type === 'integer') { nestedZodType = z.number(); } else if (nestedPropSchema.type === 'boolean') { nestedZodType = z.boolean(); } else { nestedZodType = z.any(); } if (nestedPropSchema.default !== undefined) { nestedZodType = nestedZodType.default(nestedPropSchema.default); } if (!nestedRequired.includes(nestedKey)) { nestedZodType = nestedZodType.optional(); } if (nestedPropSchema.description) { nestedZodType = nestedZodType.describe(nestedPropSchema.description); } nestedShape[nestedKey] = nestedZodType; } zodType = z.object(nestedShape); } else { zodType = z.any(); } // Add default value if present (before optional) if (propSchema.default !== undefined) { zodType = zodType.default(propSchema.default); } // Make optional if not in required array (must be after default, before describe) if (!required.includes(key)) { zodType = zodType.optional(); } // Add description if present (after optional) if (propSchema.description) { zodType = zodType.describe(propSchema.description); } zodShape[key] = zodType; } // Return object with Zod fields, not z.object() return zodShape; } // Fallback: if it's already a Zod schema object, return as-is if (jsonSchema && typeof jsonSchema === 'object' && !jsonSchema.type) { return jsonSchema; } // Fallback: return empty object for unknown schemas return {}; } /** * Helper function to register a tool on McpServer * Wraps handler to convert our response format to MCP format */ private registerToolOnServer( server: McpServer, toolName: string, description: string, inputSchema: any, handler: (args: any) => Promise<any> ) { // Convert JSON Schema to Zod if needed, otherwise pass as-is (like in the example) const zodSchema = (inputSchema && typeof inputSchema === 'object' && inputSchema.type === 'object' && inputSchema.properties) ? this.jsonSchemaToZod(inputSchema) : inputSchema; server.registerTool( toolName, { description, inputSchema: zodSchema, }, async (args: any) => { const result = await handler(args); // If error, throw it if (result.isError) { const errorText = result.content ?.map((item: any) => { if (item?.type === "json" && item.json !== undefined) { return JSON.stringify(item.json); } return item?.text || String(item); }) .join("\n") || "Unknown error"; throw new McpError(ErrorCode.InternalError, errorText); } // Convert content to MCP format - JSON items become text const content = (result.content || []).map((item: any) => { if (item?.type === "json" && item.json !== undefined) { return { type: "text" as const, text: JSON.stringify(item.json), }; } return { type: "text" as const, text: item?.text || String(item || ""), }; }); return { content }; } ); } /** * Registers all tools on a McpServer instance * Used for both main server and per-session servers */ private registerAllToolsOnServer(server: McpServer) { this.registerToolOnServer(server, GetProgram_Tool.name, GetProgram_Tool.description, GetProgram_Tool.inputSchema as any, handleGetProgram); this.registerToolOnServer(server, GetClass_Tool.name, GetClass_Tool.description, GetClass_Tool.inputSchema as any, handleGetClass); this.registerToolOnServer(server, GetFunction_Tool.name, GetFunction_Tool.description, GetFunction_Tool.inputSchema as any, handleGetFunction); this.registerToolOnServer(server, GetFunctionGroup_Tool.name, GetFunctionGroup_Tool.description, GetFunctionGroup_Tool.inputSchema as any, handleGetFunctionGroup); this.registerToolOnServer(server, GetTable_Tool.name, GetTable_Tool.description, GetTable_Tool.inputSchema as any, handleGetTable); this.registerToolOnServer(server, GetStructure_Tool.name, GetStructure_Tool.description, GetStructure_Tool.inputSchema as any, handleGetStructure); this.registerToolOnServer(server, GetTableContents_Tool.name, GetTableContents_Tool.description, GetTableContents_Tool.inputSchema as any, handleGetTableContents); this.registerToolOnServer(server, GetPackage_Tool.name, GetPackage_Tool.description, GetPackage_Tool.inputSchema as any, handleGetPackage); this.registerToolOnServer(server, CreatePackage_Tool.name, CreatePackage_Tool.description, CreatePackage_Tool.inputSchema as any, handleCreatePackage); this.registerToolOnServer(server, GetInclude_Tool.name, GetInclude_Tool.description, GetInclude_Tool.inputSchema as any, handleGetInclude); this.registerToolOnServer(server, GetIncludesList_Tool.name, GetIncludesList_Tool.description, GetIncludesList_Tool.inputSchema as any, handleGetIncludesList); this.registerToolOnServer(server, GetTypeInfo_Tool.name, GetTypeInfo_Tool.description, GetTypeInfo_Tool.inputSchema as any, handleGetTypeInfo); this.registerToolOnServer(server, GetInterface_Tool.name, GetInterface_Tool.description, GetInterface_Tool.inputSchema as any, handleGetInterface); this.registerToolOnServer(server, GetTransaction_Tool.name, GetTransaction_Tool.description, GetTransaction_Tool.inputSchema as any, handleGetTransaction); this.registerToolOnServer(server, SearchObject_Tool.name, SearchObject_Tool.description, SearchObject_Tool.inputSchema as any, handleSearchObject); this.registerToolOnServer(server, GetEnhancements_Tool.name, GetEnhancements_Tool.description, GetEnhancements_Tool.inputSchema as any, handleGetEnhancements); this.registerToolOnServer(server, GetEnhancementSpot_Tool.name, GetEnhancementSpot_Tool.description, GetEnhancementSpot_Tool.inputSchema as any, handleGetEnhancementSpot); this.registerToolOnServer(server, GetEnhancementImpl_Tool.name, GetEnhancementImpl_Tool.description, GetEnhancementImpl_Tool.inputSchema as any, handleGetEnhancementImpl); this.registerToolOnServer(server, GetBdef_Tool.name, GetBdef_Tool.description, GetBdef_Tool.inputSchema as any, handleGetBdef); this.registerToolOnServer(server, GetSqlQuery_Tool.name, GetSqlQuery_Tool.description, GetSqlQuery_Tool.inputSchema as any, handleGetSqlQuery); this.registerToolOnServer(server, GetWhereUsed_Tool.name, GetWhereUsed_Tool.description, GetWhereUsed_Tool.inputSchema as any, handleGetWhereUsed); this.registerToolOnServer(server, GetObjectInfo_Tool.name, GetObjectInfo_Tool.description, GetObjectInfo_Tool.inputSchema as any, async (args: any) => { if (!args || typeof args !== "object") { throw new McpError(ErrorCode.InvalidParams, "Missing or invalid arguments for GetObjectInfo"); } return await handleGetObjectInfo(args as { parent_type: string; parent_name: string }); }); this.registerToolOnServer(server, GetAbapAST_Tool.name, GetAbapAST_Tool.description, GetAbapAST_Tool.inputSchema as any, handleGetAbapAST); this.registerToolOnServer(server, GetAbapSemanticAnalysis_Tool.name, GetAbapSemanticAnalysis_Tool.description, GetAbapSemanticAnalysis_Tool.inputSchema as any, handleGetAbapSemanticAnalysis); this.registerToolOnServer(server, GetAbapSystemSymbols_Tool.name, GetAbapSystemSymbols_Tool.description, GetAbapSystemSymbols_Tool.inputSchema as any, handleGetAbapSystemSymbols); this.registerToolOnServer(server, GetDomain_Tool.name, GetDomain_Tool.description, GetDomain_Tool.inputSchema as any, handleGetDomain); this.registerToolOnServer(server, CreateDomain_Tool.name, CreateDomain_Tool.description, CreateDomain_Tool.inputSchema as any, handleCreateDomain); this.registerToolOnServer(server, UpdateDomain_Tool.name, UpdateDomain_Tool.description, UpdateDomain_Tool.inputSchema as any, handleUpdateDomain); this.registerToolOnServer(server, CreateDataElement_Tool.name, CreateDataElement_Tool.description, CreateDataElement_Tool.inputSchema as any, handleCreateDataElement); this.registerToolOnServer(server, UpdateDataElement_Tool.name, UpdateDataElement_Tool.description, UpdateDataElement_Tool.inputSchema as any, handleUpdateDataElement); this.registerToolOnServer(server, GetDataElement_Tool.name, GetDataElement_Tool.description, GetDataElement_Tool.inputSchema as any, handleGetDataElement); this.registerToolOnServer(server, CreateTransport_Tool.name, CreateTransport_Tool.description, CreateTransport_Tool.inputSchema as any, handleCreateTransport); this.registerToolOnServer(server, GetTransport_Tool.name, GetTransport_Tool.description, GetTransport_Tool.inputSchema as any, handleGetTransport); this.registerToolOnServer(server, CreateTable_Tool.name, CreateTable_Tool.description, CreateTable_Tool.inputSchema as any, handleCreateTable); this.registerToolOnServer(server, CreateStructure_Tool.name, CreateStructure_Tool.description, CreateStructure_Tool.inputSchema as any, handleCreateStructure); this.registerToolOnServer(server, CreateView_Tool.name, CreateView_Tool.description, CreateView_Tool.inputSchema as any, handleCreateView); this.registerToolOnServer(server, GetView_Tool.name, GetView_Tool.description, GetView_Tool.inputSchema as any, handleGetView); this.registerToolOnServer(server, CreateClass_Tool.name, CreateClass_Tool.description, CreateClass_Tool.inputSchema as any, handleCreateClass); this.registerToolOnServer(server, UpdateClassSource_Tool.name, UpdateClassSource_Tool.description, UpdateClassSource_Tool.inputSchema as any, handleUpdateClassSource); this.registerToolOnServer(server, CreateProgram_Tool.name, CreateProgram_Tool.description, CreateProgram_Tool.inputSchema as any, handleCreateProgram); this.registerToolOnServer(server, UpdateProgramSource_Tool.name, UpdateProgramSource_Tool.description, UpdateProgramSource_Tool.inputSchema as any, handleUpdateProgramSource); this.registerToolOnServer(server, CreateInterface_Tool.name, CreateInterface_Tool.description, CreateInterface_Tool.inputSchema as any, handleCreateInterface); this.registerToolOnServer(server, CreateFunctionGroup_Tool.name, CreateFunctionGroup_Tool.description, CreateFunctionGroup_Tool.inputSchema as any, handleCreateFunctionGroup); this.registerToolOnServer(server, CreateFunctionModule_Tool.name, CreateFunctionModule_Tool.description, CreateFunctionModule_Tool.inputSchema as any, handleCreateFunctionModule); this.registerToolOnServer(server, UpdateViewSource_Tool.name, UpdateViewSource_Tool.description, UpdateViewSource_Tool.inputSchema as any, handleUpdateViewSource); this.registerToolOnServer(server, UpdateInterfaceSource_Tool.name, UpdateInterfaceSource_Tool.description, UpdateInterfaceSource_Tool.inputSchema as any, handleUpdateInterfaceSource); this.registerToolOnServer(server, UpdateFunctionModuleSource_Tool.name, UpdateFunctionModuleSource_Tool.description, UpdateFunctionModuleSource_Tool.inputSchema as any, handleUpdateFunctionModuleSource); this.registerToolOnServer(server, ActivateObject_Tool.name, ActivateObject_Tool.description, ActivateObject_Tool.inputSchema as any, handleActivateObject); this.registerToolOnServer(server, DeleteObject_Tool.name, DeleteObject_Tool.description, DeleteObject_Tool.inputSchema as any, handleDeleteObject); this.registerToolOnServer(server, CheckObject_Tool.name, CheckObject_Tool.description, CheckObject_Tool.inputSchema as any, handleCheckObject); this.registerToolOnServer(server, GetSession_Tool.name, GetSession_Tool.description, GetSession_Tool.inputSchema as any, handleGetSession); this.registerToolOnServer(server, ValidateObject_Tool.name, ValidateObject_Tool.description, ValidateObject_Tool.inputSchema as any, handleValidateObject); this.registerToolOnServer(server, LockObject_Tool.name, LockObject_Tool.description, LockObject_Tool.inputSchema as any, handleLockObject); this.registerToolOnServer(server, UnlockObject_Tool.name, UnlockObject_Tool.description, UnlockObject_Tool.inputSchema as any, handleUnlockObject); this.registerToolOnServer(server, ValidateClass_Tool.name, ValidateClass_Tool.description, ValidateClass_Tool.inputSchema as any, handleValidateClass); this.registerToolOnServer(server, CheckClass_Tool.name, CheckClass_Tool.description, CheckClass_Tool.inputSchema as any, handleCheckClass); this.registerToolOnServer(server, ValidateTable_Tool.name, ValidateTable_Tool.description, ValidateTable_Tool.inputSchema as any, handleValidateTable); this.registerToolOnServer(server, CheckTable_Tool.name, CheckTable_Tool.description, CheckTable_Tool.inputSchema as any, handleCheckTable); this.registerToolOnServer(server, ValidateFunctionModule_Tool.name, ValidateFunctionModule_Tool.description, ValidateFunctionModule_Tool.inputSchema as any, handleValidateFunctionModule); this.registerToolOnServer(server, CheckFunctionModule_Tool.name, CheckFunctionModule_Tool.description, CheckFunctionModule_Tool.inputSchema as any, handleCheckFunctionModule); // Dynamic import tools this.registerToolOnServer(server, "GetAdtTypes", "Get all ADT types available in the system", { type: "object", properties: {}, required: [] } as any, async (args: any) => { return await (await import("./handlers/handleGetAllTypes.js")).handleGetAdtTypes(args); }); this.registerToolOnServer(server, "GetObjectStructure", "Get object structure with includes hierarchy", { type: "object", properties: { object_name: { type: "string" }, object_type: { type: "string" } }, required: ["object_name", "object_type"] } as any, async (args: any) => { return await (await import("./handlers/handleGetObjectStructure.js")).handleGetObjectStructure(args); }); this.registerToolOnServer(server, "GetObjectsList", "Get list of objects by package", { type: "object", properties: { package_name: { type: "string" } }, required: ["package_name"] } as any, async (args: any) => { return await (await import("./handlers/handleGetObjectsList.js")).handleGetObjectsList(args); }); this.registerToolOnServer(server, "GetObjectsByType", "Get objects by type", { type: "object", properties: { object_type: { type: "string" }, package_name: { type: "string" } }, required: ["object_type"] } as any, async (args: any) => { return await (await import("./handlers/handleGetObjectsByType.js")).handleGetObjectsByType(args); }); this.registerToolOnServer(server, "GetProgFullCode", "Get full program code with includes", { type: "object", properties: { program_name: { type: "string" } }, required: ["program_name"] } as any, async (args: any) => { return await (await import("./handlers/handleGetProgFullCode.js")).handleGetProgFullCode(args); }); this.registerToolOnServer(server, "GetObjectNodeFromCache", "Get object node from cache", { type: "object", properties: { object_name: { type: "string" }, object_type: { type: "string" } }, required: ["object_name", "object_type"] } as any, async (args: any) => { return await (await import("./handlers/handleGetObjectNodeFromCache.js")).handleGetObjectNodeFromCache(args); }); this.registerToolOnServer(server, "DescribeByList", "Describe objects by list", { type: "object", properties: { objects: { type: "array", items: { type: "string" } } }, required: ["objects"] } as any, async (args: any) => { return await (await import("./handlers/handleDescribeByList.js")).handleDescribeByList(args); }); } /** * Sets up handlers for new McpServer using registerTool (recommended API) * @private */ private setupMcpServerHandlers() { // Register all tools using TOOL_DEFINITION from handlers // McpServer automatically handles listTools requests for registered tools this.registerAllToolsOnServer(this.mcpServer); } private setupSignalHandlers() { const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM"]; for (const signal of signals) { process.on(signal, () => { if (this.shuttingDown) { return; } this.shuttingDown = true; logger.info("Received shutdown signal", { type: "SERVER_SHUTDOWN_SIGNAL", signal, transport: this.transportConfig.type, }); void this.shutdown().finally(() => { if (this.allowProcessExit) { process.exit(0); } }); }); } } private async shutdown() { try { await this.mcpServer.close(); } catch (error) { logger.error("Failed to close MCP server", { type: "SERVER_SHUTDOWN_ERROR", error: error instanceof Error ? error.message : String(error), }); } // Close all SSE sessions for (const [sessionId, session] of this.sseSessions.entries()) { try { await session.transport.close(); session.server.server.close(); logger.debug("SSE session closed during shutdown", { type: "SSE_SESSION_SHUTDOWN", sessionId, }); } catch (error) { logger.error("Failed to close SSE session", { type: "SSE_SHUTDOWN_ERROR", error: error instanceof Error ? error.message : String(error), sessionId, }); } } this.sseSessions.clear(); if (this.httpServer) { await new Promise<void>((resolve) => { this.httpServer?.close((closeError) => { if (closeError) { logger.error("Failed to close HTTP server", { type: "HTTP_SERVER_SHUTDOWN_ERROR", error: closeError instanceof Error ? closeError.message : String(closeError), }); } resolve(); }); }); this.httpServer = undefined; } } /** * Starts the MCP server and connects it to the transport. */ async run() { if (this.transportConfig.type === "stdio") { const transport = new StdioServerTransport(); await this.mcpServer.server.connect(transport); logger.info("Server connected", { type: "SERVER_READY", transport: "stdio", }); return; } if (this.transportConfig.type === "streamable-http") { const httpConfig = this.transportConfig; // HTTP Server wrapper for StreamableHTTP transport (like the SDK example) const httpServer = createServer(async (req, res) => { // Only handle POST requests (like the example) if (req.method !== "POST") { res.writeHead(405, { "Content-Type": "text/plain" }); res.end("Method not allowed"); return; } // Track client (like the example) const clientID = `${req.socket.remoteAddress}:${req.socket.remotePort}`; logger.debug("Client connected", { type: "STREAMABLE_HTTP_CLIENT_CONNECTED", clientID, }); // Extract session ID from headers (like the example) const clientSessionId = (req.headers["x-session-id"] || req.headers["mcp-session-id"]) as string | undefined; let session = this.streamableHttpSessions.get(clientID); // If client sent session ID, try to find existing session if (clientSessionId && !session) { // Search for existing session by sessionId (client might have new IP:PORT) for (const [key, sess] of this.streamableHttpSessions.entries()) { if (sess.sessionId === clientSessionId) { session = sess; // Update clientID (port might have changed) this.streamableHttpSessions.delete(key); this.streamableHttpSessions.set(clientID, session); logger.debug("Existing session restored", { type: "STREAMABLE_HTTP_SESSION_RESTORED", sessionId: session.sessionId, clientID, }); break; } } } // If no session found, create new one if (!session) { session = { sessionId: randomUUID(), clientIP: req.socket.remoteAddress || "unknown", connectedAt: new Date(), requestCount: 0, }; this.streamableHttpSessions.set(clientID, session); logger.debug("New session created", { type: "STREAMABLE_HTTP_SESSION_CREATED", sessionId: session.sessionId, clientID, totalSessions: this.streamableHttpSessions.size, }); } session.requestCount++; logger.debug("Request received", { type: "STREAMABLE_HTTP_REQUEST", sessionId: session.sessionId, requestNumber: session.requestCount, clientID, }); // Handle client disconnect (like the example) req.on("close", () => { this.streamableHttpSessions.delete(clientID); logger.debug("Session closed", { type: "STREAMABLE_HTTP_SESSION_CLOSED", sessionId: session!.sessionId, requestCount: session!.requestCount, totalSessions: this.streamableHttpSessions.size, }); }); try { // Apply auth headers before processing this.applyAuthHeaders(req.headers); // Read request body (like the SDK example with Express) let body: any = null; const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } if (chunks.length > 0) { const bodyString = Buffer.concat(chunks).toString('utf-8'); try { body = JSON.parse(bodyString); } catch (parseError) { // If body is not JSON, pass as string or null body = bodyString || null; } } // KEY MOMENT: Create new StreamableHTTP transport for each request (like the SDK example) // SDK automatically handles: // - Chunked transfer encoding // - Session tracking // - JSON-RPC protocol const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // Stateless mode (like the SDK example) enableJsonResponse: httpConfig.enableJsonResponse, allowedOrigins: httpConfig.allowedOrigins, allowedHosts: httpConfig.allowedHosts, enableDnsRebindingProtection: httpConfig.enableDnsRebindingProtection, }); // Close transport when response closes (like the SDK example) res.on("close", () => { transport.close(); }); // Connect transport to new McpServer (like the SDK example) await this.mcpServer.connect(transport); logger.debug("Transport connected", { type: "STREAMABLE_HTTP_TRANSPORT_CONNECTED", sessionId: session.sessionId, clientID, }); // Handle HTTP request through transport (like the SDK example) // Pass body as third parameter if available (like the SDK example) await transport.handleRequest(req, res, body); logger.debug("Request completed", { type: "STREAMABLE_HTTP_REQUEST_COMPLETED", sessionId: session.sessionId, clientID, }); } catch (error) { logger.error("Failed to handle HTTP request", { type: "HTTP_REQUEST_ERROR", error: error instanceof Error ? error.message : String(error), sessionId: session.sessionId, clientID, }); if (!res.headersSent) { res.writeHead(500).end("Internal Server Error"); } else { res.end(); } } }); httpServer.on("clientError", (err, socket) => { logger.error("HTTP client error", { type: "HTTP_CLIENT_ERROR", error: err instanceof Error ? err.message : String(err), }); socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }); await new Promise<void>((resolve, reject) => { const onError = (error: Error) => { logger.error("HTTP server failed to start", { type: "HTTP_SERVER_ERROR", error: error.message, }); httpServer.off("error", onError); reject(error); }; httpServer.once("error", onError); httpServer.listen(httpConfig.port, httpConfig.host, () => { httpServer.off("error", onError); logger.info("HTTP server listening", { type: "HTTP_SERVER_LISTENING", host: httpConfig.host, port: httpConfig.port, enableJsonResponse: httpConfig.enableJsonResponse, }); resolve(); }); }); this.httpServer = httpServer; return; } const sseConfig = this.transportConfig; const streamPathMap = new Map<string, string>([ ["/", "/messages"], ["/mcp/events", "/mcp/messages"], ["/sse", "/messages"], ]); const streamPaths = Array.from(streamPathMap.keys()); const postPathSet = new Set(streamPathMap.values()); postPathSet.add("/messages"); postPathSet.add("/mcp/messages"); const httpServer = createServer(async (req, res) => { const requestUrl = req.url ? new URL(req.url, `http://${req.headers.host ?? `${sseConfig.host}:${sseConfig.port}`}`) : undefined; let pathname = requestUrl?.pathname ?? "/"; if (pathname.length > 1 && pathname.endsWith("/")) { pathname = pathname.slice(0, -1); } this.applyAuthHeaders(req.headers); logger.debug("SSE request received", { type: "SSE_HTTP_REQUEST", method: req.method, pathname, originalUrl: req.url, headers: { accept: req.headers.accept, "content-type": req.headers["content-type"], }, }); // GET /sse, /mcp/events, or / - establish SSE connection if (req.method === "GET" && streamPathMap.has(pathname)) { const postEndpoint = streamPathMap.get(pathname) ?? "/messages"; logger.debug("SSE client connecting", { type: "SSE_CLIENT_CONNECTING", pathname, postEndpoint, }); // Create new McpServer instance for this session (like the working example) const server = this.createMcpServerForSession(); // Create SSE transport const transport = new SSEServerTransport(postEndpoint, res, { allowedHosts: sseConfig.allowedHosts, allowedOrigins: sseConfig.allowedOrigins, enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection, }); const sessionId = transport.sessionId; logger.info("New SSE session created", { type: "SSE_SESSION_CREATED", sessionId, pathname, }); // Store transport and server for this session this.sseSessions.set(sessionId, { server, transport, }); // Connect transport to server (using server.server like in the example) try { await server.server.connect(transport); logger.info("SSE transport connected", { type: "SSE_CONNECTION_READY", sessionId, pathname, postEndpoint, }); } catch (error) { logger.error("Failed to connect SSE transport", { type: "SSE_CONNECT_ERROR", error: error instanceof Error ? error.message : String(error), sessionId, }); this.sseSessions.delete(sessionId); if (!res.headersSent) { res.writeHead(500).end("Internal Server Error"); } else { res.end(); } return; } // Cleanup on connection close res.on("close", () => { logger.info("SSE connection closed", { type: "SSE_CONNECTION_CLOSED", sessionId, pathname, }); this.sseSessions.delete(sessionId); server.server.close(); }); transport.onerror = (error) => { logger.error("SSE transport error", { type: "SSE_TRANSPORT_ERROR", error: error instanceof Error ? error.message : String(error), sessionId, }); }; return; } // POST /messages or /mcp/messages - handle client messages if (req.method === "POST" && postPathSet.has(pathname)) { // Extract sessionId from query string or header let sessionId: string | undefined; if (requestUrl) { sessionId = requestUrl.searchParams.get("sessionId") || undefined; } if (!sessionId) { sessionId = req.headers["x-session-id"] as string | undefined; } logger.debug("SSE POST request received", { type: "SSE_POST_REQUEST", sessionId, pathname, }); if (!sessionId || !this.sseSessions.has(sessionId)) { logger.error("Invalid or missing SSE session", { type: "SSE_INVALID_SESSION", sessionId, }); res.writeHead(400, { "Content-Type": "application/json" }).end( JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message: "Invalid or missing sessionId", }, id: null, }) ); return; } const session = this.sseSessions.get(sessionId)!; const { transport } = session; try { // Read request body let body: any = null; const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } if (chunks.length > 0) { const bodyString = Buffer.concat(chunks).toString('utf-8'); try { body = JSON.parse(bodyString); } catch (parseError) { body = bodyString || null; } } // Handle POST message through transport (like the working example) await transport.handlePostMessage(req, res, body); logger.debug("SSE POST request processed", { type: "SSE_POST_PROCESSED", sessionId, }); } catch (error) { logger.error("Failed to handle SSE POST message", { type: "SSE_POST_ERROR", error: error instanceof Error ? error.message : String(error), sessionId, }); if (!res.headersSent) { res.writeHead(500).end("Internal Server Error"); } else { res.end(); } } return; } // OPTIONS - CORS preflight if (req.method === "OPTIONS" && (streamPathMap.has(pathname) || postPathSet.has(pathname))) { res.writeHead(204, { "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }).end(); return; } res.writeHead(404, { "Content-Type": "application/json" }).end( JSON.stringify({ error: "Not Found" }) ); }); httpServer.on("clientError", (err, socket) => { logger.error("SSE HTTP client error", { type: "SSE_HTTP_CLIENT_ERROR", error: err instanceof Error ? err.message : String(err), }); socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }); await new Promise<void>((resolve, reject) => { const onError = (error: Error) => { logger.error("SSE HTTP server failed to start", { type: "SSE_HTTP_SERVER_ERROR", error: error.message, }); httpServer.off("error", onError); reject(error); }; httpServer.once("error", onError); httpServer.listen(sseConfig.port, sseConfig.host, () => { httpServer.off("error", onError); logger.info("SSE HTTP server listening", { type: "SSE_HTTP_SERVER_LISTENING", host: sseConfig.host, port: sseConfig.port, streamPaths, postPaths: Array.from(postPathSet.values()), }); resolve(); }); }); this.httpServer = httpServer; } } if (process.env.MCP_SKIP_AUTO_START !== "true") { const server = new mcp_abap_adt_server(); server.run().catch((error) => { logger.error("Fatal error while running MCP server", { type: "SERVER_FATAL_ERROR", error: error instanceof Error ? error.message : String(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/fr0ster/mcp-abap-adt'

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