@mcp-abap-adt/core 2.1.3 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bin/help.d.ts +6 -0
- package/dist/bin/help.d.ts.map +1 -0
- package/dist/bin/help.js +59 -0
- package/dist/bin/mcp-abap-adt-v2.d.ts +9 -0
- package/dist/bin/mcp-abap-adt-v2.d.ts.map +1 -0
- package/dist/bin/mcp-abap-adt-v2.js +247 -0
- package/dist/handlers/behavior_definition/readonly/handleGetBdef.d.ts +21 -0
- package/dist/handlers/behavior_definition/readonly/handleGetBdef.d.ts.map +1 -0
- package/dist/handlers/behavior_definition/readonly/handleGetBdef.js +24 -0
- package/dist/handlers/behavior_definition/readonly/handleGetBdef.js.map +1 -0
- package/dist/handlers/class/high/handleCreateLocalTestClass.d.ts.map +1 -1
- package/dist/handlers/class/high/handleCreateLocalTestClass.js +6 -1
- package/dist/handlers/class/high/handleCreateLocalTestClass.js.map +1 -1
- package/dist/handlers/class/high/handleGetUnitTest.d.ts +38 -0
- package/dist/handlers/class/high/handleGetUnitTest.d.ts.map +1 -0
- package/dist/handlers/class/high/handleGetUnitTest.js +73 -0
- package/dist/handlers/class/high/handleGetUnitTest.js.map +1 -0
- package/dist/handlers/class/high/handleRunUnitTest.d.ts +122 -0
- package/dist/handlers/class/high/handleRunUnitTest.d.ts.map +1 -0
- package/dist/handlers/class/high/handleRunUnitTest.js +133 -0
- package/dist/handlers/class/high/handleRunUnitTest.js.map +1 -0
- package/dist/handlers/class/readonly/handleGetClass.d.ts +23 -0
- package/dist/handlers/class/readonly/handleGetClass.d.ts.map +1 -0
- package/dist/handlers/class/readonly/handleGetClass.js +77 -0
- package/dist/handlers/class/readonly/handleGetClass.js.map +1 -0
- package/dist/handlers/data_element/readonly/handleGetDataElement.d.ts +23 -0
- package/dist/handlers/data_element/readonly/handleGetDataElement.d.ts.map +1 -0
- package/dist/handlers/data_element/readonly/handleGetDataElement.js +79 -0
- package/dist/handlers/data_element/readonly/handleGetDataElement.js.map +1 -0
- package/dist/handlers/domain/readonly/handleGetDomain.d.ts +23 -0
- package/dist/handlers/domain/readonly/handleGetDomain.d.ts.map +1 -0
- package/dist/handlers/domain/readonly/handleGetDomain.js +79 -0
- package/dist/handlers/domain/readonly/handleGetDomain.js.map +1 -0
- package/dist/handlers/enhancement/readonly/handleGetEnhancements.d.ts.map +1 -1
- package/dist/handlers/enhancement/readonly/handleGetEnhancements.js +7 -7
- package/dist/handlers/enhancement/readonly/handleGetEnhancements.js.map +1 -1
- package/dist/handlers/function/readonly/handleGetFunctionGroup.d.ts +23 -0
- package/dist/handlers/function/readonly/handleGetFunctionGroup.d.ts.map +1 -0
- package/dist/handlers/function/readonly/handleGetFunctionGroup.js +77 -0
- package/dist/handlers/function/readonly/handleGetFunctionGroup.js.map +1 -0
- package/dist/handlers/include/readonly/handleGetIncludesList.d.ts.map +1 -1
- package/dist/handlers/include/readonly/handleGetIncludesList.js +8 -5
- package/dist/handlers/include/readonly/handleGetIncludesList.js.map +1 -1
- package/dist/handlers/interface/readonly/handleGetInterface.d.ts +23 -0
- package/dist/handlers/interface/readonly/handleGetInterface.d.ts.map +1 -0
- package/dist/handlers/interface/readonly/handleGetInterface.js +77 -0
- package/dist/handlers/interface/readonly/handleGetInterface.js.map +1 -0
- package/dist/handlers/package/readonly/handleGetPackage.d.ts +22 -0
- package/dist/handlers/package/readonly/handleGetPackage.d.ts.map +1 -0
- package/dist/handlers/package/readonly/handleGetPackage.js +108 -0
- package/dist/handlers/package/readonly/handleGetPackage.js.map +1 -0
- package/dist/handlers/program/readonly/handleGetProgram.d.ts +23 -0
- package/dist/handlers/program/readonly/handleGetProgram.d.ts.map +1 -0
- package/dist/handlers/program/readonly/handleGetProgram.js +77 -0
- package/dist/handlers/program/readonly/handleGetProgram.js.map +1 -0
- package/dist/handlers/search/readonly/handleGetObjectsByType.d.ts.map +1 -1
- package/dist/handlers/search/readonly/handleGetObjectsByType.js +5 -1
- package/dist/handlers/search/readonly/handleGetObjectsByType.js.map +1 -1
- package/dist/handlers/search/readonly/handleGetObjectsList.d.ts.map +1 -1
- package/dist/handlers/search/readonly/handleGetObjectsList.js +8 -4
- package/dist/handlers/search/readonly/handleGetObjectsList.js.map +1 -1
- package/dist/handlers/service_definition/readonly/handleGetServiceDefinition.d.ts +18 -0
- package/dist/handlers/service_definition/readonly/handleGetServiceDefinition.d.ts.map +1 -0
- package/dist/handlers/service_definition/readonly/handleGetServiceDefinition.js +109 -0
- package/dist/handlers/service_definition/readonly/handleGetServiceDefinition.js.map +1 -0
- package/dist/handlers/structure/readonly/handleGetStructure.d.ts +23 -0
- package/dist/handlers/structure/readonly/handleGetStructure.d.ts.map +1 -0
- package/dist/handlers/structure/readonly/handleGetStructure.js +77 -0
- package/dist/handlers/structure/readonly/handleGetStructure.js.map +1 -0
- package/dist/handlers/system/readonly/handleGetObjectInfo.d.ts.map +1 -1
- package/dist/handlers/system/readonly/handleGetObjectInfo.js +8 -14
- package/dist/handlers/system/readonly/handleGetObjectInfo.js.map +1 -1
- package/dist/handlers/table/readonly/handleGetTable.d.ts +23 -0
- package/dist/handlers/table/readonly/handleGetTable.d.ts.map +1 -0
- package/dist/handlers/table/readonly/handleGetTable.js +77 -0
- package/dist/handlers/table/readonly/handleGetTable.js.map +1 -0
- package/dist/handlers/view/readonly/handleGetView.d.ts +33 -0
- package/dist/handlers/view/readonly/handleGetView.d.ts.map +1 -0
- package/dist/handlers/view/readonly/handleGetView.js +90 -0
- package/dist/handlers/view/readonly/handleGetView.js.map +1 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2979 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/ServerConfigManager.d.ts +90 -0
- package/dist/lib/ServerConfigManager.d.ts.map +1 -0
- package/dist/lib/ServerConfigManager.js +237 -0
- package/dist/lib/ServerConfigManager.js.map +1 -0
- package/dist/lib/activationUtils.d.ts +59 -0
- package/dist/lib/activationUtils.d.ts.map +1 -0
- package/dist/lib/activationUtils.js +168 -0
- package/dist/lib/authBrokerFactory.d.ts +96 -0
- package/dist/lib/authBrokerFactory.d.ts.map +1 -0
- package/dist/lib/authBrokerFactory.js +531 -0
- package/dist/lib/authBrokerFactory.js.map +1 -0
- package/dist/lib/config/ServerConfig.d.ts +17 -0
- package/dist/lib/config/ServerConfig.d.ts.map +1 -0
- package/dist/lib/config/ServerConfig.js +7 -0
- package/dist/lib/config/ServerConfig.js.map +1 -0
- package/dist/lib/getFullCodeCache.d.ts +3 -0
- package/dist/lib/getFullCodeCache.d.ts.map +1 -0
- package/dist/lib/getFullCodeCache.js +56 -0
- package/dist/lib/handlers/handlers/base/BaseHandlerGroup.d.ts +36 -0
- package/dist/lib/handlers/handlers/base/BaseHandlerGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/base/BaseHandlerGroup.js +268 -0
- package/dist/lib/handlers/handlers/base/BaseHandlerGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/examples/usage-example.d.ts +29 -0
- package/dist/lib/handlers/handlers/examples/usage-example.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/examples/usage-example.js +119 -0
- package/dist/lib/handlers/handlers/examples/usage-example.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/HighLevelHandlersGroup.d.ts +14 -0
- package/dist/lib/handlers/handlers/groups/HighLevelHandlersGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/HighLevelHandlersGroup.js +322 -0
- package/dist/lib/handlers/handlers/groups/HighLevelHandlersGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/LowLevelHandlersGroup.d.ts +14 -0
- package/dist/lib/handlers/handlers/groups/LowLevelHandlersGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/LowLevelHandlersGroup.js +1214 -0
- package/dist/lib/handlers/handlers/groups/LowLevelHandlersGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/ReadOnlyHandlersGroup.d.ts +14 -0
- package/dist/lib/handlers/handlers/groups/ReadOnlyHandlersGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/ReadOnlyHandlersGroup.js +232 -0
- package/dist/lib/handlers/handlers/groups/ReadOnlyHandlersGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/SearchHandlersGroup.d.ts +14 -0
- package/dist/lib/handlers/handlers/groups/SearchHandlersGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/SearchHandlersGroup.js +57 -0
- package/dist/lib/handlers/handlers/groups/SearchHandlersGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/SystemHandlersGroup.d.ts +14 -0
- package/dist/lib/handlers/handlers/groups/SystemHandlersGroup.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/SystemHandlersGroup.js +176 -0
- package/dist/lib/handlers/handlers/groups/SystemHandlersGroup.js.map +1 -0
- package/dist/lib/handlers/handlers/groups/index.d.ts +13 -0
- package/dist/lib/handlers/handlers/groups/index.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/groups/index.js +21 -0
- package/dist/lib/handlers/handlers/groups/index.js.map +1 -0
- package/dist/lib/handlers/handlers/index.d.ts +14 -0
- package/dist/lib/handlers/handlers/index.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/index.js +37 -0
- package/dist/lib/handlers/handlers/index.js.map +1 -0
- package/dist/lib/handlers/handlers/interfaces.d.ts +67 -0
- package/dist/lib/handlers/handlers/interfaces.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/interfaces.js +3 -0
- package/dist/lib/handlers/handlers/interfaces.js.map +1 -0
- package/dist/lib/handlers/handlers/registry/CompositeHandlersRegistry.d.ts +49 -0
- package/dist/lib/handlers/handlers/registry/CompositeHandlersRegistry.d.ts.map +1 -0
- package/dist/lib/handlers/handlers/registry/CompositeHandlersRegistry.js +88 -0
- package/dist/lib/handlers/handlers/registry/CompositeHandlersRegistry.js.map +1 -0
- package/dist/lib/runtimeConfig.d.ts +20 -0
- package/dist/lib/runtimeConfig.d.ts.map +1 -0
- package/dist/lib/runtimeConfig.js +130 -0
- package/dist/lib/runtimeConfig.js.map +1 -0
- package/dist/lib/servers/BaseMcpServer.d.ts +56 -0
- package/dist/lib/servers/BaseMcpServer.d.ts.map +1 -0
- package/dist/lib/servers/BaseMcpServer.js +198 -0
- package/dist/lib/servers/BaseMcpServer.js.map +1 -0
- package/dist/lib/servers/ConnectionContext.d.ts +20 -0
- package/dist/lib/servers/ConnectionContext.d.ts.map +1 -0
- package/dist/lib/servers/ConnectionContext.js +3 -0
- package/dist/lib/servers/ConnectionContext.js.map +1 -0
- package/dist/lib/servers/SseServer.d.ts +32 -0
- package/dist/lib/servers/SseServer.d.ts.map +1 -0
- package/dist/lib/servers/SseServer.js +139 -0
- package/dist/lib/servers/SseServer.js.map +1 -0
- package/dist/lib/servers/StdioServer.d.ts +20 -0
- package/dist/lib/servers/StdioServer.d.ts.map +1 -0
- package/dist/lib/servers/StdioServer.js +32 -0
- package/dist/lib/servers/StdioServer.js.map +1 -0
- package/dist/lib/servers/StreamableHttpServer.d.ts +30 -0
- package/dist/lib/servers/StreamableHttpServer.d.ts.map +1 -0
- package/dist/lib/servers/StreamableHttpServer.js +83 -0
- package/dist/lib/servers/StreamableHttpServer.js.map +1 -0
- package/dist/lib/servers/handlers/base/BaseHandlerGroup.d.ts +36 -0
- package/dist/lib/servers/handlers/base/BaseHandlerGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/base/BaseHandlerGroup.js +268 -0
- package/dist/lib/servers/handlers/base/BaseHandlerGroup.js.map +1 -0
- package/dist/lib/servers/handlers/examples/usage-example.d.ts +29 -0
- package/dist/lib/servers/handlers/examples/usage-example.d.ts.map +1 -0
- package/dist/lib/servers/handlers/examples/usage-example.js +119 -0
- package/dist/lib/servers/handlers/examples/usage-example.js.map +1 -0
- package/dist/lib/servers/handlers/groups/HighLevelHandlersGroup.d.ts +14 -0
- package/dist/lib/servers/handlers/groups/HighLevelHandlersGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/HighLevelHandlersGroup.js +322 -0
- package/dist/lib/servers/handlers/groups/HighLevelHandlersGroup.js.map +1 -0
- package/dist/lib/servers/handlers/groups/LowLevelHandlersGroup.d.ts +14 -0
- package/dist/lib/servers/handlers/groups/LowLevelHandlersGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/LowLevelHandlersGroup.js +1214 -0
- package/dist/lib/servers/handlers/groups/LowLevelHandlersGroup.js.map +1 -0
- package/dist/lib/servers/handlers/groups/ReadOnlyHandlersGroup.d.ts +14 -0
- package/dist/lib/servers/handlers/groups/ReadOnlyHandlersGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/ReadOnlyHandlersGroup.js +232 -0
- package/dist/lib/servers/handlers/groups/ReadOnlyHandlersGroup.js.map +1 -0
- package/dist/lib/servers/handlers/groups/SearchHandlersGroup.d.ts +14 -0
- package/dist/lib/servers/handlers/groups/SearchHandlersGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/SearchHandlersGroup.js +57 -0
- package/dist/lib/servers/handlers/groups/SearchHandlersGroup.js.map +1 -0
- package/dist/lib/servers/handlers/groups/SystemHandlersGroup.d.ts +14 -0
- package/dist/lib/servers/handlers/groups/SystemHandlersGroup.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/SystemHandlersGroup.js +176 -0
- package/dist/lib/servers/handlers/groups/SystemHandlersGroup.js.map +1 -0
- package/dist/lib/servers/handlers/groups/index.d.ts +13 -0
- package/dist/lib/servers/handlers/groups/index.d.ts.map +1 -0
- package/dist/lib/servers/handlers/groups/index.js +21 -0
- package/dist/lib/servers/handlers/groups/index.js.map +1 -0
- package/dist/lib/servers/handlers/index.d.ts +14 -0
- package/dist/lib/servers/handlers/index.d.ts.map +1 -0
- package/dist/lib/servers/handlers/index.js +37 -0
- package/dist/lib/servers/handlers/index.js.map +1 -0
- package/dist/lib/servers/handlers/interfaces.d.ts +67 -0
- package/dist/lib/servers/handlers/interfaces.d.ts.map +1 -0
- package/dist/lib/servers/handlers/interfaces.js +3 -0
- package/dist/lib/servers/handlers/interfaces.js.map +1 -0
- package/dist/lib/servers/handlers/registry/CompositeHandlersRegistry.d.ts +49 -0
- package/dist/lib/servers/handlers/registry/CompositeHandlersRegistry.d.ts.map +1 -0
- package/dist/lib/servers/handlers/registry/CompositeHandlersRegistry.js +88 -0
- package/dist/lib/servers/handlers/registry/CompositeHandlersRegistry.js.map +1 -0
- package/dist/lib/servers/index.d.ts +5 -0
- package/dist/lib/servers/index.d.ts.map +1 -0
- package/dist/lib/servers/index.js +21 -0
- package/dist/lib/servers/index.js.map +1 -0
- package/dist/lib/servers/launcher.d.ts +2 -0
- package/dist/lib/servers/launcher.d.ts.map +1 -0
- package/dist/lib/servers/launcher.js +123 -0
- package/dist/lib/servers/launcher.js.map +1 -0
- package/dist/lib/servers/mcp_handlers.d.ts +25 -0
- package/dist/lib/servers/mcp_handlers.d.ts.map +1 -0
- package/dist/lib/servers/mcp_handlers.js +822 -0
- package/dist/lib/servers/mcp_handlers.js.map +1 -0
- package/dist/lib/servers/mcp_server.d.ts +7 -0
- package/dist/lib/servers/mcp_server.d.ts.map +1 -0
- package/dist/lib/servers/mcp_server.js +12 -0
- package/dist/lib/servers/utils.d.ts +42 -0
- package/dist/lib/servers/utils.d.ts.map +1 -0
- package/dist/lib/servers/utils.js +620 -0
- package/dist/lib/servers/utils.js.map +1 -0
- package/dist/lib/servers/v2/__tests__/unit/McpServer.test.d.ts +7 -0
- package/dist/lib/servers/v2/__tests__/unit/McpServer.test.d.ts.map +1 -0
- package/dist/lib/servers/v2/__tests__/unit/McpServer.test.js +218 -0
- package/dist/lib/servers/v2/connection/LocalConnectionProvider.d.ts +20 -0
- package/dist/lib/servers/v2/connection/LocalConnectionProvider.d.ts.map +1 -0
- package/dist/lib/servers/v2/connection/LocalConnectionProvider.js +70 -0
- package/dist/lib/servers/v2/connection/RemoteConnectionProvider.d.ts +21 -0
- package/dist/lib/servers/v2/connection/RemoteConnectionProvider.d.ts.map +1 -0
- package/dist/lib/servers/v2/connection/RemoteConnectionProvider.js +36 -0
- package/dist/lib/servers/v2/connection/index.d.ts +6 -0
- package/dist/lib/servers/v2/connection/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/connection/index.js +10 -0
- package/dist/lib/servers/v2/factory/AuthBrokerFactory.d.ts +29 -0
- package/dist/lib/servers/v2/factory/AuthBrokerFactory.d.ts.map +1 -0
- package/dist/lib/servers/v2/factory/AuthBrokerFactory.js +79 -0
- package/dist/lib/servers/v2/factory/LocalModeFactory.d.ts +38 -0
- package/dist/lib/servers/v2/factory/LocalModeFactory.d.ts.map +1 -0
- package/dist/lib/servers/v2/factory/LocalModeFactory.js +38 -0
- package/dist/lib/servers/v2/factory/index.d.ts +6 -0
- package/dist/lib/servers/v2/factory/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/factory/index.js +10 -0
- package/dist/lib/servers/v2/index.d.ts +9 -0
- package/dist/lib/servers/v2/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/index.js +27 -0
- package/dist/lib/servers/v2/index.js.map +1 -0
- package/dist/lib/servers/v2/interfaces/connection.d.ts +188 -0
- package/dist/lib/servers/v2/interfaces/connection.d.ts.map +1 -0
- package/dist/lib/servers/v2/interfaces/connection.js +7 -0
- package/dist/lib/servers/v2/interfaces/index.d.ts +10 -0
- package/dist/lib/servers/v2/interfaces/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/interfaces/index.js +25 -0
- package/dist/lib/servers/v2/interfaces/protocol.d.ts +30 -0
- package/dist/lib/servers/v2/interfaces/protocol.d.ts.map +1 -0
- package/dist/lib/servers/v2/interfaces/protocol.js +7 -0
- package/dist/lib/servers/v2/interfaces/session.d.ts +98 -0
- package/dist/lib/servers/v2/interfaces/session.d.ts.map +1 -0
- package/dist/lib/servers/v2/interfaces/session.js +7 -0
- package/dist/lib/servers/v2/interfaces/transport.d.ts +88 -0
- package/dist/lib/servers/v2/interfaces/transport.d.ts.map +1 -0
- package/dist/lib/servers/v2/interfaces/transport.js +7 -0
- package/dist/lib/servers/v2/protocol/ProtocolHandler.d.ts +21 -0
- package/dist/lib/servers/v2/protocol/ProtocolHandler.d.ts.map +1 -0
- package/dist/lib/servers/v2/protocol/ProtocolHandler.js +77 -0
- package/dist/lib/servers/v2/protocol/index.d.ts +5 -0
- package/dist/lib/servers/v2/protocol/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/protocol/index.js +8 -0
- package/dist/lib/servers/v2/server/McpServer.d.ts +76 -0
- package/dist/lib/servers/v2/server/McpServer.d.ts.map +1 -0
- package/dist/lib/servers/v2/server/McpServer.js +243 -0
- package/dist/lib/servers/v2/server/index.d.ts +5 -0
- package/dist/lib/servers/v2/server/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/server/index.js +8 -0
- package/dist/lib/servers/v2/session/SessionManager.d.ts +40 -0
- package/dist/lib/servers/v2/session/SessionManager.d.ts.map +1 -0
- package/dist/lib/servers/v2/session/SessionManager.js +136 -0
- package/dist/lib/servers/v2/session/index.d.ts +5 -0
- package/dist/lib/servers/v2/session/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/session/index.js +8 -0
- package/dist/lib/servers/v2/transports/SseTransport.d.ts +49 -0
- package/dist/lib/servers/v2/transports/SseTransport.d.ts.map +1 -0
- package/dist/lib/servers/v2/transports/SseTransport.js +89 -0
- package/dist/lib/servers/v2/transports/StdioTransport.d.ts +31 -0
- package/dist/lib/servers/v2/transports/StdioTransport.d.ts.map +1 -0
- package/dist/lib/servers/v2/transports/StdioTransport.js +91 -0
- package/dist/lib/servers/v2/transports/StreamableHttpTransport.d.ts +51 -0
- package/dist/lib/servers/v2/transports/StreamableHttpTransport.d.ts.map +1 -0
- package/dist/lib/servers/v2/transports/StreamableHttpTransport.js +147 -0
- package/dist/lib/servers/v2/transports/index.d.ts +7 -0
- package/dist/lib/servers/v2/transports/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/transports/index.js +12 -0
- package/dist/lib/servers/v2/types/common.d.ts +17 -0
- package/dist/lib/servers/v2/types/common.d.ts.map +1 -0
- package/dist/lib/servers/v2/types/common.js +6 -0
- package/dist/lib/servers/v2/types/common.js.map +1 -0
- package/dist/lib/servers/v2/types/index.d.ts +8 -0
- package/dist/lib/servers/v2/types/index.d.ts.map +1 -0
- package/dist/lib/servers/v2/types/index.js +24 -0
- package/dist/lib/servers/v2/types/index.js.map +1 -0
- package/dist/lib/servers/v2/types/transport.d.ts +28 -0
- package/dist/lib/servers/v2/types/transport.d.ts.map +1 -0
- package/dist/lib/servers/v2/types/transport.js +8 -0
- package/dist/lib/servers/v2/types/transport.js.map +1 -0
- package/dist/lib/servers/v2/utils/StdioLogger.d.ts +17 -0
- package/dist/lib/servers/v2/utils/StdioLogger.d.ts.map +1 -0
- package/dist/lib/servers/v2/utils/StdioLogger.js +33 -0
- package/dist/lib/servers/v2/utils/StdioLogger.js.map +1 -0
- package/dist/lib/stores/UnixFileServiceKeyStore.d.ts +42 -0
- package/dist/lib/stores/UnixFileServiceKeyStore.d.ts.map +1 -0
- package/dist/lib/stores/UnixFileServiceKeyStore.js +53 -0
- package/dist/lib/stores/UnixFileSessionStore.d.ts +54 -0
- package/dist/lib/stores/UnixFileSessionStore.d.ts.map +1 -0
- package/dist/lib/stores/UnixFileSessionStore.js +70 -0
- package/dist/lib/stores/WindowsFileServiceKeyStore.d.ts +42 -0
- package/dist/lib/stores/WindowsFileServiceKeyStore.d.ts.map +1 -0
- package/dist/lib/stores/WindowsFileServiceKeyStore.js +53 -0
- package/dist/lib/stores/WindowsFileSessionStore.d.ts +54 -0
- package/dist/lib/stores/WindowsFileSessionStore.d.ts.map +1 -0
- package/dist/lib/stores/WindowsFileSessionStore.js +70 -0
- package/dist/lib/toolsRegistry.d.ts +13 -0
- package/dist/lib/toolsRegistry.d.ts.map +1 -0
- package/dist/lib/toolsRegistry.js +410 -0
- package/dist/lib/transportConfig.d.ts +27 -0
- package/dist/lib/transportConfig.d.ts.map +1 -0
- package/dist/lib/transportConfig.js +124 -0
- package/dist/lib/transports/stdio.d.ts +10 -0
- package/dist/lib/transports/stdio.d.ts.map +1 -0
- package/dist/lib/transports/stdio.js +73 -0
- package/dist/lib/utils.d.ts +37 -0
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +83 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/yamlConfig.d.ts +59 -0
- package/dist/lib/yamlConfig.d.ts.map +1 -0
- package/dist/lib/yamlConfig.js +349 -0
- package/dist/lib/yamlConfig.js.map +1 -0
- package/dist/server/launcher.js +49 -0
- package/dist/server/launcher.js.map +1 -1
- package/dist/server/v1/AuthBrokerConfig.d.ts +19 -0
- package/dist/server/v1/AuthBrokerConfig.d.ts.map +1 -0
- package/dist/server/v1/AuthBrokerConfig.js +44 -0
- package/dist/server/v1/AuthBrokerConfig.js.map +1 -0
- package/dist/server/v1/IServerConfig.d.ts +17 -0
- package/dist/server/v1/IServerConfig.d.ts.map +1 -0
- package/dist/server/v1/IServerConfig.js +7 -0
- package/dist/server/v1/IServerConfig.js.map +1 -0
- package/dist/server/v1/ServerConfig.d.ts +16 -0
- package/dist/server/v1/ServerConfig.d.ts.map +1 -0
- package/dist/server/v1/ServerConfig.js +6 -0
- package/dist/server/v1/ServerConfig.js.map +1 -0
- package/dist/server/v1/embeddable-server.d.ts +89 -0
- package/dist/server/v1/embeddable-server.d.ts.map +1 -0
- package/dist/server/v1/embeddable-server.js +83 -0
- package/dist/server/v1/embeddable-server.js.map +1 -0
- package/dist/server/v1/index.d.ts +30 -0
- package/dist/server/v1/index.d.ts.map +1 -0
- package/dist/server/v1/index.js +65 -0
- package/dist/server/v1/index.js.map +1 -0
- package/dist/server/v1/legacy-server.d.ts +141 -0
- package/dist/server/v1/legacy-server.d.ts.map +1 -0
- package/dist/server/v1/legacy-server.js +2099 -0
- package/dist/server/v1/legacy-server.js.map +1 -0
- package/dist/server/v1/mcp_handlers.d.ts +19 -0
- package/dist/server/v1/mcp_handlers.d.ts.map +1 -0
- package/dist/server/v1/mcp_handlers.js +85 -0
- package/dist/server/v1/mcp_handlers.js.map +1 -0
- package/dist/server/v2/AuthBrokerConfig.d.ts +22 -0
- package/dist/server/v2/AuthBrokerConfig.d.ts.map +1 -0
- package/dist/server/v2/AuthBrokerConfig.js +46 -0
- package/dist/server/v2/AuthBrokerConfig.js.map +1 -0
- package/dist/server/v2/BaseMcpServer.d.ts +67 -0
- package/dist/server/v2/BaseMcpServer.d.ts.map +1 -0
- package/dist/server/v2/BaseMcpServer.js +273 -0
- package/dist/server/v2/BaseMcpServer.js.map +1 -0
- package/dist/server/v2/ConnectionContext.d.ts +20 -0
- package/dist/server/v2/ConnectionContext.d.ts.map +1 -0
- package/dist/server/v2/ConnectionContext.js +3 -0
- package/dist/server/v2/ConnectionContext.js.map +1 -0
- package/dist/server/v2/IHttpApplication.d.ts +42 -0
- package/dist/server/v2/IHttpApplication.d.ts.map +1 -0
- package/dist/server/v2/IHttpApplication.js +3 -0
- package/dist/server/v2/IHttpApplication.js.map +1 -0
- package/dist/server/v2/IServerConfig.d.ts +15 -0
- package/dist/server/v2/IServerConfig.d.ts.map +1 -0
- package/dist/server/v2/IServerConfig.js +7 -0
- package/dist/server/v2/IServerConfig.js.map +1 -0
- package/dist/server/v2/ServerConfig.d.ts +16 -0
- package/dist/server/v2/ServerConfig.d.ts.map +1 -0
- package/dist/server/v2/ServerConfig.js +6 -0
- package/dist/server/v2/ServerConfig.js.map +1 -0
- package/dist/server/v2/SseServer.d.ts +95 -0
- package/dist/server/v2/SseServer.d.ts.map +1 -0
- package/dist/server/v2/SseServer.js +217 -0
- package/dist/server/v2/SseServer.js.map +1 -0
- package/dist/server/v2/StdioServer.d.ts +20 -0
- package/dist/server/v2/StdioServer.d.ts.map +1 -0
- package/dist/server/v2/StdioServer.js +32 -0
- package/dist/server/v2/StdioServer.js.map +1 -0
- package/dist/server/v2/StreamableHttpServer.d.ts +94 -0
- package/dist/server/v2/StreamableHttpServer.d.ts.map +1 -0
- package/dist/server/v2/StreamableHttpServer.js +170 -0
- package/dist/server/v2/StreamableHttpServer.js.map +1 -0
- package/dist/server/v2/index.d.ts +6 -0
- package/dist/server/v2/index.d.ts.map +1 -0
- package/dist/server/v2/index.js +22 -0
- package/dist/server/v2/index.js.map +1 -0
- package/dist/server/v2/launcher.d.ts +2 -0
- package/dist/server/v2/launcher.d.ts.map +1 -0
- package/dist/server/v2/launcher.js +181 -0
- package/dist/server/v2/launcher.js.map +1 -0
- package/dist/server/v2/mcp_handlers.d.ts +25 -0
- package/dist/server/v2/mcp_handlers.d.ts.map +1 -0
- package/dist/server/v2/mcp_handlers.js +828 -0
- package/dist/server/v2/mcp_handlers.js.map +1 -0
- package/dist/server/v2/utils.d.ts +42 -0
- package/dist/server/v2/utils.d.ts.map +1 -0
- package/dist/server/v2/utils.js +620 -0
- package/dist/server/v2/utils.js.map +1 -0
- package/dist/tools/test-v2-server-stdio-compiled.js +132 -0
- package/dist/utils/lockStateManager.d.ts +2 -12
- package/dist/utils/lockStateManager.d.ts.map +1 -1
- package/dist/utils/lockStateManager.js +4 -63
- package/dist/utils/lockStateManager.js.map +1 -1
- package/docs/user-guide/AVAILABLE_TOOLS.md +1 -1
- package/package.json +1 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,2979 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// Simple stdio mode detection (like reference implementation)
|
|
4
|
+
// No output suppression needed - dotenv removed, manual .env parsing used
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
39
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
40
|
+
};
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.mcp_abap_adt_server = exports.getConfig = exports.setSapConfigOverride = void 0;
|
|
46
|
+
exports.setAbapConnectionOverride = setAbapConnectionOverride;
|
|
47
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
48
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
49
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
50
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
51
|
+
const mcp_handlers_1 = require("./lib/servers/mcp_handlers");
|
|
52
|
+
const path_1 = __importDefault(require("path"));
|
|
53
|
+
const os = __importStar(require("os"));
|
|
54
|
+
// dotenv removed - using manual .env parsing for all modes to avoid stdout pollution
|
|
55
|
+
const http_1 = require("http");
|
|
56
|
+
const crypto_1 = require("crypto");
|
|
57
|
+
const header_validator_1 = require("@mcp-abap-adt/header-validator");
|
|
58
|
+
const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
59
|
+
const authBrokerFactory_1 = require("./lib/authBrokerFactory");
|
|
60
|
+
const platformPaths_1 = require("./lib/stores/platformPaths");
|
|
61
|
+
const runtimeConfig_1 = require("./lib/runtimeConfig");
|
|
62
|
+
const yamlConfig_1 = require("./lib/yamlConfig");
|
|
63
|
+
// Import handler functions
|
|
64
|
+
// Import handler functions
|
|
65
|
+
// New low-level handlers imports
|
|
66
|
+
// Import shared utility functions and types
|
|
67
|
+
const utils_1 = require("./lib/utils");
|
|
68
|
+
const config_js_1 = require("./lib/config.js");
|
|
69
|
+
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_js_1.getConfig; } });
|
|
70
|
+
Object.defineProperty(exports, "setSapConfigOverride", { enumerable: true, get: function () { return config_js_1.setSapConfigOverride; } });
|
|
71
|
+
const connection_1 = require("@mcp-abap-adt/connection");
|
|
72
|
+
const loggerAdapter_1 = require("./lib/loggerAdapter");
|
|
73
|
+
// Import logger
|
|
74
|
+
const logger_1 = require("./lib/logger");
|
|
75
|
+
// import { defaultLogger as logger } from "@mcp-abap-adt/logger";
|
|
76
|
+
// Import tool registry
|
|
77
|
+
// Import TOOL_DEFINITION from handlers
|
|
78
|
+
// New low-level handlers TOOL_DEFINITION imports
|
|
79
|
+
// --- ENV FILE LOADING LOGIC ---
|
|
80
|
+
const fs_1 = __importDefault(require("fs"));
|
|
81
|
+
/**
|
|
82
|
+
* Display help message
|
|
83
|
+
*/
|
|
84
|
+
function showHelp() {
|
|
85
|
+
const help = `
|
|
86
|
+
MCP ABAP ADT Server - SAP ABAP Development Tools MCP Integration
|
|
87
|
+
|
|
88
|
+
USAGE:
|
|
89
|
+
mcp-abap-adt [options]
|
|
90
|
+
|
|
91
|
+
DESCRIPTION:
|
|
92
|
+
MCP server for interacting with SAP ABAP systems via ADT (ABAP Development Tools).
|
|
93
|
+
Supports multiple transport modes: HTTP (default), stdio, and SSE.
|
|
94
|
+
|
|
95
|
+
TRANSPORT MODES:
|
|
96
|
+
Default: stdio (for MCP clients like Cline, Cursor, Claude Desktop)
|
|
97
|
+
HTTP: --transport=http (for web interfaces, receives config via HTTP headers)
|
|
98
|
+
SSE: --transport=sse
|
|
99
|
+
|
|
100
|
+
OPTIONS:
|
|
101
|
+
--help Show this help message
|
|
102
|
+
--config=<path> Path to YAML configuration file
|
|
103
|
+
If file doesn't exist, generates a template and exits
|
|
104
|
+
Command-line arguments override YAML values
|
|
105
|
+
Example: --config=config.yaml
|
|
106
|
+
|
|
107
|
+
YAML CONFIGURATION:
|
|
108
|
+
Instead of passing many command-line arguments, you can use a YAML config file:
|
|
109
|
+
|
|
110
|
+
mcp-abap-adt --config=config.yaml
|
|
111
|
+
|
|
112
|
+
If the file doesn't exist, a template will be generated automatically and the command will exit.
|
|
113
|
+
Edit the template to configure your server settings, then run the command again.
|
|
114
|
+
|
|
115
|
+
The YAML file is validated on load - invalid configurations will cause the command to exit with an error.
|
|
116
|
+
|
|
117
|
+
Command-line arguments always override YAML values for flexibility.
|
|
118
|
+
|
|
119
|
+
See docs/configuration/YAML_CONFIG.md for detailed documentation.
|
|
120
|
+
|
|
121
|
+
ENVIRONMENT FILE:
|
|
122
|
+
--env=<path> Path to .env file (uses .env instead of auth-broker)
|
|
123
|
+
--env <path> Alternative syntax for --env
|
|
124
|
+
--auth-broker Force use of auth-broker (service keys) instead of .env file
|
|
125
|
+
Ignores .env file even if present in current directory
|
|
126
|
+
By default, .env in current directory is used automatically (if exists)
|
|
127
|
+
--auth-broker-path=<path> Custom path for auth-broker service keys and sessions
|
|
128
|
+
Creates service-keys and sessions subdirectories in this path
|
|
129
|
+
Example: --auth-broker-path=~/prj/tmp/
|
|
130
|
+
This will use ~/prj/tmp/service-keys and ~/prj/tmp/sessions
|
|
131
|
+
--mcp=<destination> Default MCP destination name (overrides x-mcp-destination header)
|
|
132
|
+
If specified, this destination will be used when x-mcp-destination
|
|
133
|
+
header is not provided in the request
|
|
134
|
+
Example: --mcp=TRIAL
|
|
135
|
+
This allows using auth-broker with stdio and SSE transports
|
|
136
|
+
When --mcp is specified, .env file is not loaded automatically
|
|
137
|
+
(even if it exists in current directory)
|
|
138
|
+
|
|
139
|
+
TRANSPORT SELECTION:
|
|
140
|
+
--transport=<type> Transport type: stdio|http|streamable-http|sse
|
|
141
|
+
Default: stdio (for MCP clients)
|
|
142
|
+
Shortcuts: --http (same as --transport=http)
|
|
143
|
+
--sse (same as --transport=sse)
|
|
144
|
+
--stdio (same as --transport=stdio)
|
|
145
|
+
|
|
146
|
+
HTTP/STREAMABLE-HTTP OPTIONS:
|
|
147
|
+
--http-port=<port> HTTP server port (default: 3000)
|
|
148
|
+
--http-host=<host> HTTP server host (default: 127.0.0.1 for local only, use 0.0.0.0 for all interfaces)
|
|
149
|
+
Security: When listening on 0.0.0.0, client must provide all connection headers
|
|
150
|
+
Server will not use default destination for non-local connections
|
|
151
|
+
--http-json-response Enable JSON response format
|
|
152
|
+
--http-allowed-origins=<list> Comma-separated allowed origins for CORS
|
|
153
|
+
Example: --http-allowed-origins=http://localhost:3000,https://example.com
|
|
154
|
+
--http-allowed-hosts=<list> Comma-separated allowed hosts
|
|
155
|
+
--http-enable-dns-protection Enable DNS rebinding protection
|
|
156
|
+
|
|
157
|
+
SSE (SERVER-SENT EVENTS) OPTIONS:
|
|
158
|
+
--sse-port=<port> SSE server port (default: 3001)
|
|
159
|
+
--sse-host=<host> SSE server host (default: 127.0.0.1 for local only, use 0.0.0.0 for all interfaces)
|
|
160
|
+
Security: When listening on 0.0.0.0, client must provide all connection headers
|
|
161
|
+
Server will not use default destination for non-local connections
|
|
162
|
+
--sse-allowed-origins=<list> Comma-separated allowed origins for CORS
|
|
163
|
+
Example: --sse-allowed-origins=http://localhost:3000
|
|
164
|
+
--sse-allowed-hosts=<list> Comma-separated allowed hosts
|
|
165
|
+
--sse-enable-dns-protection Enable DNS rebinding protection
|
|
166
|
+
|
|
167
|
+
ENVIRONMENT VARIABLES:
|
|
168
|
+
MCP_ENV_PATH Path to .env file
|
|
169
|
+
MCP_SKIP_ENV_LOAD Skip automatic .env loading (true|false)
|
|
170
|
+
MCP_SKIP_AUTO_START Skip automatic server start (true|false)
|
|
171
|
+
MCP_TRANSPORT Transport type (stdio|http|sse)
|
|
172
|
+
Default: stdio if not specified
|
|
173
|
+
MCP_HTTP_PORT Default HTTP port (default: 3000)
|
|
174
|
+
MCP_HTTP_HOST Default HTTP host (default: 127.0.0.1 for local only, use 0.0.0.0 for all interfaces)
|
|
175
|
+
MCP_HTTP_ENABLE_JSON_RESPONSE Enable JSON responses (true|false)
|
|
176
|
+
MCP_HTTP_ALLOWED_ORIGINS Allowed CORS origins (comma-separated)
|
|
177
|
+
MCP_HTTP_ALLOWED_HOSTS Allowed hosts (comma-separated)
|
|
178
|
+
MCP_HTTP_ENABLE_DNS_PROTECTION Enable DNS protection (true|false)
|
|
179
|
+
MCP_SSE_PORT Default SSE port (default: 3001)
|
|
180
|
+
MCP_SSE_HOST Default SSE host (default: 127.0.0.1 for local only, use 0.0.0.0 for all interfaces)
|
|
181
|
+
MCP_SSE_ALLOWED_ORIGINS Allowed CORS origins for SSE (comma-separated)
|
|
182
|
+
MCP_SSE_ALLOWED_HOSTS Allowed hosts for SSE (comma-separated)
|
|
183
|
+
MCP_SSE_ENABLE_DNS_PROTECTION Enable DNS protection for SSE (true|false)
|
|
184
|
+
AUTH_BROKER_PATH Custom paths for service keys and sessions
|
|
185
|
+
Unix: colon-separated (e.g., /path1:/path2)
|
|
186
|
+
Windows: semicolon-separated (e.g., C:\\path1;C:\\path2)
|
|
187
|
+
If not set, uses platform defaults:
|
|
188
|
+
Unix: ~/.config/mcp-abap-adt/service-keys
|
|
189
|
+
Windows: %USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys
|
|
190
|
+
DEBUG_AUTH_LOG Enable debug logging for auth-broker (true|false)
|
|
191
|
+
Default: false (only info messages shown)
|
|
192
|
+
When true: shows detailed debug messages
|
|
193
|
+
DEBUG_AUTH_BROKER Alias for DEBUG_AUTH_LOG (true|false)
|
|
194
|
+
Same as DEBUG_AUTH_LOG - enables debug logging for auth-broker
|
|
195
|
+
When true: automatically sets DEBUG_AUTH_LOG=true
|
|
196
|
+
DEBUG_HTTP_REQUESTS Enable logging of HTTP requests and MCP calls (true|false)
|
|
197
|
+
Default: false
|
|
198
|
+
When true: logs all incoming HTTP requests, methods, URLs,
|
|
199
|
+
headers (sensitive data redacted), and MCP JSON-RPC calls
|
|
200
|
+
Also enabled by DEBUG_CONNECTORS=true
|
|
201
|
+
DEBUG_CONNECTORS Enable debug logging for connection layer (true|false)
|
|
202
|
+
Default: false
|
|
203
|
+
When true: shows HTTP requests, CSRF tokens, cookies,
|
|
204
|
+
session management, and connection details
|
|
205
|
+
Also enables DEBUG_HTTP_REQUESTS automatically
|
|
206
|
+
DEBUG_HANDLERS Enable debug logging for MCP handlers (true|false)
|
|
207
|
+
Default: false
|
|
208
|
+
When true: shows handler entry/exit, session state,
|
|
209
|
+
lock handles, property validation
|
|
210
|
+
DEBUG_CONNECTION_MANAGER Enable debug logging for connection manager (true|false)
|
|
211
|
+
Default: false
|
|
212
|
+
When true: shows connection cache operations
|
|
213
|
+
|
|
214
|
+
SAP CONNECTION (.env file):
|
|
215
|
+
SAP_URL SAP system URL (required)
|
|
216
|
+
Example: https://your-system.sap.com
|
|
217
|
+
SAP_CLIENT SAP client number (required)
|
|
218
|
+
Example: 100
|
|
219
|
+
SAP_AUTH_TYPE Authentication type: basic|jwt (default: basic)
|
|
220
|
+
SAP_USERNAME SAP username (required for basic auth)
|
|
221
|
+
SAP_PASSWORD SAP password (required for basic auth)
|
|
222
|
+
SAP_JWT_TOKEN JWT token (required for jwt auth)
|
|
223
|
+
|
|
224
|
+
GENERATING .ENV FROM SERVICE KEY (JWT Authentication):
|
|
225
|
+
To generate .env file from SAP BTP service key JSON file, install the
|
|
226
|
+
connection package globally:
|
|
227
|
+
|
|
228
|
+
npm install -g @mcp-abap-adt/connection
|
|
229
|
+
|
|
230
|
+
Then use the sap-abap-auth command:
|
|
231
|
+
|
|
232
|
+
sap-abap-auth auth -k path/to/service-key.json
|
|
233
|
+
|
|
234
|
+
This will create/update .env file with JWT tokens and connection details.
|
|
235
|
+
|
|
236
|
+
EXAMPLES:
|
|
237
|
+
# Default stdio mode (for MCP clients, requires .env file or --mcp parameter)
|
|
238
|
+
mcp-abap-adt
|
|
239
|
+
|
|
240
|
+
# HTTP mode (for web interfaces)
|
|
241
|
+
mcp-abap-adt --transport=http
|
|
242
|
+
|
|
243
|
+
# HTTP server on custom port, localhost only (default)
|
|
244
|
+
mcp-abap-adt --transport=http --http-port=8080
|
|
245
|
+
|
|
246
|
+
# HTTP server accepting connections from all interfaces (less secure)
|
|
247
|
+
mcp-abap-adt --transport=http --http-host=0.0.0.0 --http-port=8080
|
|
248
|
+
|
|
249
|
+
# Use YAML configuration file
|
|
250
|
+
mcp-abap-adt --config=config.yaml
|
|
251
|
+
|
|
252
|
+
# Use stdio mode with --mcp parameter (uses auth-broker, skips .env file)
|
|
253
|
+
mcp-abap-adt --mcp=TRIAL
|
|
254
|
+
|
|
255
|
+
# Default: uses .env from current directory if exists, otherwise auth-broker
|
|
256
|
+
mcp-abap-adt
|
|
257
|
+
|
|
258
|
+
# Force use of auth-broker (service keys), ignore .env file even if exists
|
|
259
|
+
mcp-abap-adt --auth-broker
|
|
260
|
+
|
|
261
|
+
# Use custom path for auth-broker (creates service-keys and sessions subdirectories)
|
|
262
|
+
mcp-abap-adt --auth-broker --auth-broker-path=~/prj/tmp/
|
|
263
|
+
|
|
264
|
+
# Use SSE transport with --mcp parameter (allows auth-broker with SSE transport)
|
|
265
|
+
mcp-abap-adt --transport=sse --mcp=TRIAL
|
|
266
|
+
|
|
267
|
+
# Use .env file from custom path
|
|
268
|
+
mcp-abap-adt --env=/path/to/my.env
|
|
269
|
+
|
|
270
|
+
# Start HTTP server with CORS enabled
|
|
271
|
+
mcp-abap-adt --transport=http --http-port=3000 \\
|
|
272
|
+
--http-allowed-origins=http://localhost:3000,https://example.com
|
|
273
|
+
|
|
274
|
+
# Start SSE server on custom port
|
|
275
|
+
mcp-abap-adt --transport=sse --sse-port=3001
|
|
276
|
+
|
|
277
|
+
# Start SSE server with CORS and DNS protection
|
|
278
|
+
mcp-abap-adt --transport=sse --sse-port=3001 \\
|
|
279
|
+
--sse-allowed-origins=http://localhost:3000 \\
|
|
280
|
+
--sse-enable-dns-protection
|
|
281
|
+
|
|
282
|
+
# Using shortcuts
|
|
283
|
+
mcp-abap-adt --http --http-port=8080
|
|
284
|
+
mcp-abap-adt --sse --sse-port=3001
|
|
285
|
+
|
|
286
|
+
QUICK REFERENCE:
|
|
287
|
+
Transport types:
|
|
288
|
+
http - HTTP StreamableHTTP transport (default)
|
|
289
|
+
streamable-http - Same as http
|
|
290
|
+
stdio - Standard input/output (for MCP clients, requires .env file or --mcp parameter)
|
|
291
|
+
sse - Server-Sent Events transport
|
|
292
|
+
|
|
293
|
+
Common use cases:
|
|
294
|
+
Web interfaces (HTTP): mcp-abap-adt (default, no .env needed)
|
|
295
|
+
MCP clients (Cline, Cursor): mcp-abap-adt --transport=stdio
|
|
296
|
+
MCP clients with auth-broker: mcp-abap-adt --transport=stdio --mcp=TRIAL (skips .env)
|
|
297
|
+
Web interfaces (SSE): mcp-abap-adt --transport=sse --sse-port=3001
|
|
298
|
+
SSE with auth-broker: mcp-abap-adt --transport=sse --mcp=TRIAL (skips .env)
|
|
299
|
+
|
|
300
|
+
DOCUMENTATION:
|
|
301
|
+
https://github.com/fr0ster/mcp-abap-adt
|
|
302
|
+
Installation: docs/installation/INSTALLATION.md
|
|
303
|
+
Configuration: docs/user-guide/CLIENT_CONFIGURATION.md
|
|
304
|
+
Available Tools: docs/user-guide/AVAILABLE_TOOLS.md
|
|
305
|
+
|
|
306
|
+
AUTHENTICATION:
|
|
307
|
+
For JWT authentication with SAP BTP service keys:
|
|
308
|
+
1. Install: npm install -g @mcp-abap-adt/connection
|
|
309
|
+
2. Run: sap-abap-auth auth -k path/to/service-key.json
|
|
310
|
+
3. This generates .env file with JWT tokens automatically
|
|
311
|
+
|
|
312
|
+
SERVICE KEYS (Destination-Based Authentication):
|
|
313
|
+
The server supports destination-based authentication using service keys stored locally.
|
|
314
|
+
This allows you to configure authentication once per destination and reuse it.
|
|
315
|
+
|
|
316
|
+
IMPORTANT: Auth-broker (service keys) is only available for HTTP/streamable-http transport.
|
|
317
|
+
For stdio and SSE transports, use .env file instead.
|
|
318
|
+
|
|
319
|
+
How to Save Service Keys:
|
|
320
|
+
|
|
321
|
+
Linux:
|
|
322
|
+
1. Create service keys directory:
|
|
323
|
+
mkdir -p ~/.config/mcp-abap-adt/service-keys
|
|
324
|
+
|
|
325
|
+
2. Download service key from SAP BTP (from the corresponding service instance)
|
|
326
|
+
and copy it to: ~/.config/mcp-abap-adt/service-keys/{destination}.json
|
|
327
|
+
(e.g., TRIAL.json - the filename without .json extension becomes the destination name)
|
|
328
|
+
|
|
329
|
+
Storage locations:
|
|
330
|
+
Service keys: ~/.config/mcp-abap-adt/service-keys/{destination}.json
|
|
331
|
+
Sessions: ~/.config/mcp-abap-adt/sessions/{destination}.env
|
|
332
|
+
|
|
333
|
+
macOS:
|
|
334
|
+
1. Create service keys directory:
|
|
335
|
+
mkdir -p ~/.config/mcp-abap-adt/service-keys
|
|
336
|
+
|
|
337
|
+
2. Download service key from SAP BTP (from the corresponding service instance)
|
|
338
|
+
and copy it to: ~/.config/mcp-abap-adt/service-keys/{destination}.json
|
|
339
|
+
(e.g., TRIAL.json - the filename without .json extension becomes the destination name)
|
|
340
|
+
|
|
341
|
+
Storage locations:
|
|
342
|
+
Service keys: ~/.config/mcp-abap-adt/service-keys/{destination}.json
|
|
343
|
+
Sessions: ~/.config/mcp-abap-adt/sessions/{destination}.env
|
|
344
|
+
|
|
345
|
+
Windows:
|
|
346
|
+
1. Create service keys directory (PowerShell):
|
|
347
|
+
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\\Documents\\mcp-abap-adt\\service-keys"
|
|
348
|
+
|
|
349
|
+
2. Download service key from SAP BTP (from the corresponding service instance)
|
|
350
|
+
and copy it to: %USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys\\{destination}.json
|
|
351
|
+
(e.g., TRIAL.json - the filename without .json extension becomes the destination name)
|
|
352
|
+
|
|
353
|
+
Or using Command Prompt (cmd):
|
|
354
|
+
mkdir "%USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys"
|
|
355
|
+
(Then copy the downloaded service key file to this directory)
|
|
356
|
+
|
|
357
|
+
Storage locations:
|
|
358
|
+
Service keys: %USERPROFILE%\\Documents\\mcp-abap-adt\\service-keys\\{destination}.json
|
|
359
|
+
Sessions: %USERPROFILE%\\Documents\\mcp-abap-adt\\sessions\\{destination}.env
|
|
360
|
+
|
|
361
|
+
Fallback: Server also searches in current working directory (where server is launched)
|
|
362
|
+
|
|
363
|
+
Service Key:
|
|
364
|
+
Download the service key JSON file from SAP BTP (from the corresponding service instance)
|
|
365
|
+
and save it as {destination}.json (e.g., TRIAL.json).
|
|
366
|
+
The filename without .json extension becomes the destination name (case-sensitive).
|
|
367
|
+
|
|
368
|
+
Using Destinations:
|
|
369
|
+
In HTTP headers, use:
|
|
370
|
+
x-sap-destination: TRIAL (for SAP Cloud, URL derived from service key)
|
|
371
|
+
x-mcp-destination: TRIAL (for MCP destinations, URL derived from service key)
|
|
372
|
+
|
|
373
|
+
The destination name must exactly match the service key filename (without .json extension, case-sensitive).
|
|
374
|
+
|
|
375
|
+
Example Cline configuration (~/.cline/mcp.json):
|
|
376
|
+
{
|
|
377
|
+
"mcpServers": {
|
|
378
|
+
"mcp-abap-adt": {
|
|
379
|
+
"command": "npx",
|
|
380
|
+
"args": ["-y", "@mcp-abap-adt/server", "--transport=http", "--http-port=3000"],
|
|
381
|
+
"env": {}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Then in Cline, use destination in requests:
|
|
387
|
+
Headers: x-sap-destination: TRIAL
|
|
388
|
+
|
|
389
|
+
First-Time Authentication:
|
|
390
|
+
- Server reads service key from {destination}.json
|
|
391
|
+
- Opens browser for OAuth2 authentication (if no valid session exists)
|
|
392
|
+
- Saves tokens to {destination}.env for future use
|
|
393
|
+
- Subsequent requests use cached tokens automatically
|
|
394
|
+
|
|
395
|
+
Automatic Token Management:
|
|
396
|
+
- Validates tokens before use
|
|
397
|
+
- Refreshes expired tokens using refresh tokens
|
|
398
|
+
- Caches valid tokens for performance
|
|
399
|
+
- Falls back to browser authentication if refresh fails
|
|
400
|
+
|
|
401
|
+
Custom Paths:
|
|
402
|
+
Set AUTH_BROKER_PATH environment variable to override default paths:
|
|
403
|
+
Linux/macOS: export AUTH_BROKER_PATH="/custom/path:/another/path"
|
|
404
|
+
Windows: set AUTH_BROKER_PATH=C:\\custom\\path;C:\\another\\path
|
|
405
|
+
|
|
406
|
+
Or use --auth-broker-path command-line option:
|
|
407
|
+
mcp-abap-adt --auth-broker --auth-broker-path=~/prj/tmp/
|
|
408
|
+
This creates service-keys and sessions subdirectories in the specified path.
|
|
409
|
+
|
|
410
|
+
For more details, see: docs/user-guide/CLIENT_CONFIGURATION.md#destination-based-authentication
|
|
411
|
+
|
|
412
|
+
`;
|
|
413
|
+
console.log(help);
|
|
414
|
+
process.exit(0);
|
|
415
|
+
}
|
|
416
|
+
// Check for version/help flags
|
|
417
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
418
|
+
const packageJsonPath = path_1.default.join(__dirname, "..", "package.json");
|
|
419
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
420
|
+
console.log(packageJson.version);
|
|
421
|
+
process.exit(0);
|
|
422
|
+
}
|
|
423
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
424
|
+
showHelp();
|
|
425
|
+
}
|
|
426
|
+
// Load YAML config if --config parameter is provided
|
|
427
|
+
// YAML config values are applied to process.argv (command-line args override YAML)
|
|
428
|
+
const configPath = (0, yamlConfig_1.parseConfigArg)();
|
|
429
|
+
if (configPath) {
|
|
430
|
+
try {
|
|
431
|
+
// Generate template if file doesn't exist
|
|
432
|
+
const templateGenerated = (0, yamlConfig_1.generateConfigTemplateIfNeeded)(configPath);
|
|
433
|
+
// If template was just generated, exit successfully
|
|
434
|
+
// User needs to edit the file before running the server
|
|
435
|
+
if (templateGenerated) {
|
|
436
|
+
if (process.platform === 'win32') {
|
|
437
|
+
setTimeout(() => process.exit(0), 100);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
process.exit(0);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Load and apply YAML config (only if file exists)
|
|
444
|
+
const yamlConfig = (0, yamlConfig_1.loadYamlConfig)(configPath);
|
|
445
|
+
if (yamlConfig) {
|
|
446
|
+
(0, yamlConfig_1.applyYamlConfigToArgs)(yamlConfig);
|
|
447
|
+
// Only write to stderr if not in stdio mode (stdio mode requires clean JSON only)
|
|
448
|
+
const isStdioMode = process.env.MCP_TRANSPORT === "stdio" || !process.stdin.isTTY;
|
|
449
|
+
if (!isStdioMode) {
|
|
450
|
+
process.stderr.write(`[MCP-CONFIG] Loaded and validated configuration from: ${configPath}\n`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const errorMsg = `Failed to load YAML config: ${error instanceof Error ? error.message : String(error)}`;
|
|
456
|
+
process.stderr.write(`[MCP-CONFIG] ✗ ERROR: ${errorMsg}\n`);
|
|
457
|
+
if (process.platform === 'win32') {
|
|
458
|
+
setTimeout(() => process.exit(1), 100);
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Runtime config (shared, no side-effects)
|
|
466
|
+
const { useAuthBroker, isTestEnv, authBrokerPath, defaultMcpDestination, unsafe, explicitTransportType, transportType, isHttp, isSse, isStdio, isEnvMandatory, envFilePath: initialEnvFilePath, } = (0, runtimeConfig_1.buildRuntimeConfig)();
|
|
467
|
+
// Skip .env autoload under explicit instructions, auth-broker, mcp default, or test env
|
|
468
|
+
const skipEnvAutoload = process.env.MCP_SKIP_ENV_LOAD === "true" ||
|
|
469
|
+
process.env.MCP_ENV_LOADED_BY_LAUNCHER === "true" ||
|
|
470
|
+
useAuthBroker ||
|
|
471
|
+
!!defaultMcpDestination ||
|
|
472
|
+
isTestEnv;
|
|
473
|
+
// If using auth-broker (--mcp or --auth-broker), clear SAP_* env vars to prevent .env from being used
|
|
474
|
+
// This ensures that even if .env was loaded earlier or vars are set in system, they won't be used
|
|
475
|
+
if (skipEnvAutoload && (useAuthBroker || !!defaultMcpDestination)) {
|
|
476
|
+
const sapEnvKeys = Object.keys(process.env).filter(key => key.startsWith('SAP_'));
|
|
477
|
+
for (const key of sapEnvKeys) {
|
|
478
|
+
delete process.env[key];
|
|
479
|
+
}
|
|
480
|
+
if (sapEnvKeys.length > 0 && !isStdio) {
|
|
481
|
+
process.stderr.write(`[MCP-ENV] Cleared ${sapEnvKeys.length} SAP_* environment variables (using auth-broker)\n`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
let envFilePath = initialEnvFilePath;
|
|
485
|
+
// Debug: Always log on Windows to help diagnose issues
|
|
486
|
+
if (process.platform === 'win32' && !isStdio) {
|
|
487
|
+
process.stderr.write(`[MCP-ENV] parseEnvArg() returned: ${envFilePath || '(undefined)'}\n`);
|
|
488
|
+
process.stderr.write(`[MCP-ENV] MCP_ENV_PATH: ${process.env.MCP_ENV_PATH || '(not set)'}\n`);
|
|
489
|
+
process.stderr.write(`[MCP-ENV] Final envFilePath: ${envFilePath || '(will search for .env)'}\n`);
|
|
490
|
+
}
|
|
491
|
+
if (!skipEnvAutoload) {
|
|
492
|
+
if (!envFilePath) {
|
|
493
|
+
// Default behavior: search in current working directory (where user runs the command)
|
|
494
|
+
// If .env exists, use it; otherwise will use auth-broker
|
|
495
|
+
const cwdEnvPath = path_1.default.resolve(process.cwd(), ".env");
|
|
496
|
+
if (fs_1.default.existsSync(cwdEnvPath)) {
|
|
497
|
+
envFilePath = cwdEnvPath;
|
|
498
|
+
// Only write to stderr if not in stdio mode (stdio mode requires clean JSON only)
|
|
499
|
+
if (!isStdio) {
|
|
500
|
+
process.stderr.write(`[MCP-ENV] Found .env file: ${envFilePath}\n`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
// Only write to stderr if not in stdio mode
|
|
506
|
+
if (!isStdio) {
|
|
507
|
+
process.stderr.write(`[MCP-ENV] Using .env from argument/env: ${envFilePath}\n`);
|
|
508
|
+
// On Windows, also log the resolved path for debugging
|
|
509
|
+
if (process.platform === 'win32') {
|
|
510
|
+
const resolvedPath = path_1.default.isAbsolute(envFilePath)
|
|
511
|
+
? envFilePath
|
|
512
|
+
: path_1.default.resolve(process.cwd(), envFilePath);
|
|
513
|
+
process.stderr.write(`[MCP-ENV] Will resolve to: ${resolvedPath}\n`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (envFilePath) {
|
|
518
|
+
// Normalize path separators for Windows compatibility
|
|
519
|
+
// On Windows, backslashes in paths need special handling
|
|
520
|
+
// path.resolve() and path.normalize() should handle this, but let's be explicit
|
|
521
|
+
// First, normalize all backslashes to forward slashes for consistent processing
|
|
522
|
+
// Then use path methods which handle platform-specific separators correctly
|
|
523
|
+
const normalizedPath = envFilePath.replace(/\\/g, '/');
|
|
524
|
+
if (!path_1.default.isAbsolute(normalizedPath)) {
|
|
525
|
+
// For relative paths, use path.resolve which handles .\ and ./ correctly on all platforms
|
|
526
|
+
// path.resolve automatically handles both .\mdd.env and ./mdd.env formats
|
|
527
|
+
envFilePath = path_1.default.resolve(process.cwd(), normalizedPath);
|
|
528
|
+
// Only write to stderr if not in stdio mode
|
|
529
|
+
if (!isStdio) {
|
|
530
|
+
process.stderr.write(`[MCP-ENV] Resolved relative path to: ${envFilePath}\n`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
// For absolute paths, normalize using path.normalize
|
|
535
|
+
envFilePath = path_1.default.normalize(envFilePath);
|
|
536
|
+
}
|
|
537
|
+
// Verify file exists before attempting to load
|
|
538
|
+
if (!fs_1.default.existsSync(envFilePath)) {
|
|
539
|
+
const errorMsg = `[MCP-ENV] ✗ ERROR: .env file not found at: ${envFilePath}\n` +
|
|
540
|
+
`[MCP-ENV] Current working directory: ${process.cwd()}\n` +
|
|
541
|
+
`[MCP-ENV] Please check the path and try again.\n`;
|
|
542
|
+
process.stderr.write(errorMsg);
|
|
543
|
+
if (process.platform === 'win32') {
|
|
544
|
+
setTimeout(() => process.exit(1), 100);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (fs_1.default.existsSync(envFilePath)) {
|
|
551
|
+
// For stdio mode, load .env manually to avoid any output from dotenv library
|
|
552
|
+
if (isStdio) {
|
|
553
|
+
// Manual .env parsing for stdio mode (no library output)
|
|
554
|
+
try {
|
|
555
|
+
const envContent = fs_1.default.readFileSync(envFilePath, "utf8");
|
|
556
|
+
const lines = envContent.split(/\r?\n/);
|
|
557
|
+
for (const line of lines) {
|
|
558
|
+
const trimmed = line.trim();
|
|
559
|
+
// Skip empty lines and comments
|
|
560
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
// Parse KEY=VALUE format
|
|
564
|
+
const eqIndex = trimmed.indexOf("=");
|
|
565
|
+
if (eqIndex === -1) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
569
|
+
let value = trimmed.substring(eqIndex + 1);
|
|
570
|
+
// Remove inline comments (everything after #)
|
|
571
|
+
// This handles cases like: KEY=value # comment or KEY=value#comment
|
|
572
|
+
// Find first # and remove everything after it (including the #)
|
|
573
|
+
const commentIndex = value.indexOf('#');
|
|
574
|
+
if (commentIndex !== -1) {
|
|
575
|
+
// Remove everything from # onwards, then trim trailing whitespace
|
|
576
|
+
const beforeComment = value.substring(0, commentIndex);
|
|
577
|
+
value = beforeComment.trim();
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
value = value.trim();
|
|
581
|
+
}
|
|
582
|
+
// Parse value: remove quotes and trim
|
|
583
|
+
let unquotedValue = value.trim();
|
|
584
|
+
unquotedValue = unquotedValue.replace(/^["']+|["']+$/g, '').trim();
|
|
585
|
+
// URLs from .env files are expected to be clean - just use as-is
|
|
586
|
+
if (key === 'SAP_URL') {
|
|
587
|
+
// No special processing needed
|
|
588
|
+
// Debug logging for Windows
|
|
589
|
+
if (process.platform === 'win32' && !isStdio) {
|
|
590
|
+
process.stderr.write(`[MCP-ENV] Parsed SAP_URL: "${unquotedValue}" (length: ${unquotedValue.length})\n`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// Only set if not already in process.env (don't override launcher's cleaned values)
|
|
594
|
+
if (key && !process.env[key]) {
|
|
595
|
+
process.env[key] = unquotedValue;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
// Silent fail for stdio mode - just exit
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
// For non-stdio modes, use manual parsing (dotenv removed to avoid stdout pollution)
|
|
606
|
+
try {
|
|
607
|
+
const envContent = fs_1.default.readFileSync(envFilePath, "utf8");
|
|
608
|
+
const lines = envContent.split(/\r?\n/);
|
|
609
|
+
for (const line of lines) {
|
|
610
|
+
const trimmed = line.trim();
|
|
611
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const eqIndex = trimmed.indexOf("=");
|
|
615
|
+
if (eqIndex === -1) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
619
|
+
let value = trimmed.substring(eqIndex + 1);
|
|
620
|
+
// Remove inline comments (everything after #)
|
|
621
|
+
// This handles cases like: KEY=value # comment or KEY=value#comment
|
|
622
|
+
// Find first # and remove everything after it (including the #)
|
|
623
|
+
const commentIndex = value.indexOf('#');
|
|
624
|
+
if (commentIndex !== -1) {
|
|
625
|
+
// Remove everything from # onwards, then trim trailing whitespace
|
|
626
|
+
const beforeComment = value.substring(0, commentIndex);
|
|
627
|
+
value = beforeComment.trim();
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
value = value.trim();
|
|
631
|
+
}
|
|
632
|
+
// Parse value: remove quotes and trim
|
|
633
|
+
let unquotedValue = value.trim();
|
|
634
|
+
unquotedValue = unquotedValue.replace(/^["']+|["']+$/g, '').trim();
|
|
635
|
+
// URLs from .env files are expected to be clean - just use as-is
|
|
636
|
+
if (key === 'SAP_URL') {
|
|
637
|
+
// No special processing needed
|
|
638
|
+
// Debug logging for Windows
|
|
639
|
+
if (process.platform === 'win32' && !isStdio) {
|
|
640
|
+
process.stderr.write(`[MCP-ENV] Parsed SAP_URL: "${unquotedValue}" (length: ${unquotedValue.length})\n`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// Only set if not already in process.env (don't override launcher's cleaned values)
|
|
644
|
+
if (key && !process.env[key]) {
|
|
645
|
+
process.env[key] = unquotedValue;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
process.stderr.write(`[MCP-ENV] ✓ Successfully loaded: ${envFilePath}\n`);
|
|
649
|
+
// Debug: log SAP_URL if loaded (for troubleshooting on Windows)
|
|
650
|
+
if (process.env.SAP_URL) {
|
|
651
|
+
const urlHex = Buffer.from(process.env.SAP_URL, 'utf8').toString('hex');
|
|
652
|
+
process.stderr.write(`[MCP-ENV] SAP_URL loaded: "${process.env.SAP_URL}" (length: ${process.env.SAP_URL.length})\n`);
|
|
653
|
+
if (process.platform === 'win32') {
|
|
654
|
+
process.stderr.write(`[MCP-ENV] SAP_URL (hex): ${urlHex.substring(0, 60)}...\n`);
|
|
655
|
+
// Check for comments
|
|
656
|
+
if (process.env.SAP_URL.includes('#')) {
|
|
657
|
+
process.stderr.write(`[MCP-ENV] ⚠ WARNING: SAP_URL contains # character (comment not removed?)\n`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
process.stderr.write(`[MCP-ENV] ⚠ WARNING: SAP_URL not found in .env file\n`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
process.stderr.write(`[MCP-ENV] ✗ Failed to load: ${envFilePath}\n`);
|
|
667
|
+
if (error instanceof Error) {
|
|
668
|
+
process.stderr.write(`[MCP-ENV] Error: ${error.message}\n`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// .env file specified but not found
|
|
675
|
+
if (isEnvMandatory) {
|
|
676
|
+
// Always write error to stderr (stderr is safe even in stdio mode, unlike stdout)
|
|
677
|
+
logger_1.logger?.error(".env file not found", { path: envFilePath });
|
|
678
|
+
process.stderr.write(`[MCP-ENV] ✗ ERROR: .env file not found at: ${envFilePath}\n`);
|
|
679
|
+
process.stderr.write(`[MCP-ENV] Current working directory: ${process.cwd()}\n`);
|
|
680
|
+
process.stderr.write(`[MCP-ENV] Transport mode '${transportType}' requires .env file.\n`);
|
|
681
|
+
process.stderr.write(`[MCP-ENV] Use --env=/path/to/.env to specify custom location\n`);
|
|
682
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
683
|
+
if (process.platform === 'win32') {
|
|
684
|
+
setTimeout(() => process.exit(1), 100);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
// Always write error to stderr (stderr is safe even in stdio mode)
|
|
692
|
+
process.stderr.write(`[MCP-ENV] ✗ ERROR: .env file not found at: ${envFilePath}\n`);
|
|
693
|
+
process.stderr.write(`[MCP-ENV] Transport mode '${transportType}' was explicitly specified but .env file is missing.\n`);
|
|
694
|
+
process.stderr.write(`[MCP-ENV] Use --env=/path/to/.env to specify custom location\n`);
|
|
695
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
696
|
+
if (process.platform === 'win32') {
|
|
697
|
+
setTimeout(() => process.exit(1), 100);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// No .env file found and none specified
|
|
707
|
+
if (isEnvMandatory) {
|
|
708
|
+
// Transport explicitly set to stdio/sse but no .env found
|
|
709
|
+
const cwdEnvPath = path_1.default.resolve(process.cwd(), ".env");
|
|
710
|
+
// Always write error to stderr (stderr is safe even in stdio mode)
|
|
711
|
+
logger_1.logger?.error(".env file not found", { path: cwdEnvPath });
|
|
712
|
+
process.stderr.write(`[MCP-ENV] ✗ ERROR: .env file not found in current directory: ${process.cwd()}\n`);
|
|
713
|
+
process.stderr.write(`[MCP-ENV] Transport mode '${transportType}' requires .env file.\n`);
|
|
714
|
+
process.stderr.write(`[MCP-ENV] Use --env=/path/to/.env to specify custom location\n`);
|
|
715
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
716
|
+
if (process.platform === 'win32') {
|
|
717
|
+
setTimeout(() => process.exit(1), 100);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
process.exit(1);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
// No .env found, but transport is stdio (default) - this is OK for HTTP/SSE, but stdio requires .env or --mcp
|
|
725
|
+
if (explicitTransportType === null) {
|
|
726
|
+
// Transport not specified, using default (stdio)
|
|
727
|
+
// For stdio mode, don't write to stderr
|
|
728
|
+
if (!isStdio) {
|
|
729
|
+
process.stderr.write(`[MCP-ENV] NOTE: No .env file found in current directory: ${process.cwd()}\n`);
|
|
730
|
+
process.stderr.write(`[MCP-ENV] Starting in HTTP mode (no .env file required)\n`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
// Transport explicitly set to HTTP - this is OK
|
|
735
|
+
// For stdio mode, don't write to stderr
|
|
736
|
+
if (!isStdio) {
|
|
737
|
+
process.stderr.write(`[MCP-ENV] NOTE: No .env file found, continuing in ${transportType} mode\n`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else if (envFilePath) {
|
|
744
|
+
if (!path_1.default.isAbsolute(envFilePath)) {
|
|
745
|
+
envFilePath = path_1.default.resolve(process.cwd(), envFilePath);
|
|
746
|
+
}
|
|
747
|
+
// For stdio mode, don't write to stderr
|
|
748
|
+
if (!isStdio) {
|
|
749
|
+
process.stderr.write(`[MCP-ENV] Environment autoload skipped; using provided path reference: ${envFilePath}\n`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
// For stdio mode, don't write to stderr
|
|
754
|
+
if (!isStdio) {
|
|
755
|
+
process.stderr.write(`[MCP-ENV] Environment autoload skipped (MCP_SKIP_ENV_LOAD=true).\n`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// --- END ENV FILE LOADING LOGIC ---
|
|
759
|
+
// Debug: Log loaded SAP_URL and SAP_CLIENT using the MCP-compatible logger
|
|
760
|
+
// Skip logging in stdio mode (MCP protocol requires clean JSON only)
|
|
761
|
+
if (!isStdio) {
|
|
762
|
+
const envLogPath = envFilePath ?? "(skipped)";
|
|
763
|
+
logger_1.logger?.info("SAP configuration loaded", {
|
|
764
|
+
type: "CONFIG_INFO",
|
|
765
|
+
SAP_URL: process.env.SAP_URL,
|
|
766
|
+
SAP_CLIENT: process.env.SAP_CLIENT || "(not set)",
|
|
767
|
+
SAP_AUTH_TYPE: process.env.SAP_AUTH_TYPE || "(not set)",
|
|
768
|
+
SAP_JWT_TOKEN: process.env.SAP_JWT_TOKEN ? "[set]" : "(not set)",
|
|
769
|
+
ENV_PATH: envLogPath,
|
|
770
|
+
CWD: process.cwd(),
|
|
771
|
+
DIRNAME: __dirname,
|
|
772
|
+
TRANSPORT: transportType
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function getArgValue(name) {
|
|
776
|
+
const args = process.argv;
|
|
777
|
+
for (let i = 0; i < args.length; i++) {
|
|
778
|
+
const arg = args[i];
|
|
779
|
+
if (arg.startsWith(`${name}=`)) {
|
|
780
|
+
return arg.slice(name.length + 1);
|
|
781
|
+
}
|
|
782
|
+
if (arg === name && i + 1 < args.length) {
|
|
783
|
+
return args[i + 1];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return undefined;
|
|
787
|
+
}
|
|
788
|
+
function hasFlag(name) {
|
|
789
|
+
return process.argv.includes(name);
|
|
790
|
+
}
|
|
791
|
+
function parseBoolean(value) {
|
|
792
|
+
if (!value) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
const normalized = value.trim().toLowerCase();
|
|
796
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
797
|
+
}
|
|
798
|
+
function resolvePortOption(argName, envName, defaultValue) {
|
|
799
|
+
const rawValue = getArgValue(argName) ?? process.env[envName];
|
|
800
|
+
if (!rawValue) {
|
|
801
|
+
return defaultValue;
|
|
802
|
+
}
|
|
803
|
+
const port = Number.parseInt(rawValue, 10);
|
|
804
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
805
|
+
throw new Error(`Invalid port value for ${argName}: ${rawValue}`);
|
|
806
|
+
}
|
|
807
|
+
return port;
|
|
808
|
+
}
|
|
809
|
+
function resolveBooleanOption(argName, envName, defaultValue) {
|
|
810
|
+
const argValue = getArgValue(argName);
|
|
811
|
+
if (argValue !== undefined) {
|
|
812
|
+
return parseBoolean(argValue);
|
|
813
|
+
}
|
|
814
|
+
if (hasFlag(argName)) {
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
const envValue = process.env[envName];
|
|
818
|
+
if (envValue !== undefined) {
|
|
819
|
+
return parseBoolean(envValue);
|
|
820
|
+
}
|
|
821
|
+
return defaultValue;
|
|
822
|
+
}
|
|
823
|
+
function resolveListOption(argName, envName) {
|
|
824
|
+
const rawValue = getArgValue(argName) ?? process.env[envName];
|
|
825
|
+
if (!rawValue) {
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
const items = rawValue
|
|
829
|
+
.split(",")
|
|
830
|
+
.map((entry) => entry.trim())
|
|
831
|
+
.filter((entry) => entry.length > 0);
|
|
832
|
+
return items.length > 0 ? items : undefined;
|
|
833
|
+
}
|
|
834
|
+
function parseTransportConfig() {
|
|
835
|
+
// Use the transport type we already determined (handles explicit args, env vars, and defaults)
|
|
836
|
+
const normalized = transportType;
|
|
837
|
+
if (normalized &&
|
|
838
|
+
normalized !== "stdio" &&
|
|
839
|
+
normalized !== "http" &&
|
|
840
|
+
normalized !== "streamable-http" &&
|
|
841
|
+
normalized !== "server" &&
|
|
842
|
+
normalized !== "sse") {
|
|
843
|
+
throw new Error(`Unsupported transport: ${normalized}`);
|
|
844
|
+
}
|
|
845
|
+
const sseRequested = normalized === "sse" ||
|
|
846
|
+
hasFlag("--sse");
|
|
847
|
+
if (sseRequested) {
|
|
848
|
+
const port = resolvePortOption("--sse-port", "MCP_SSE_PORT", 3001);
|
|
849
|
+
// Default to localhost (127.0.0.1) for security - only accepts local connections
|
|
850
|
+
// Use 0.0.0.0 to accept connections from all interfaces (less secure)
|
|
851
|
+
const host = getArgValue("--sse-host") ?? process.env.MCP_SSE_HOST ?? "127.0.0.1";
|
|
852
|
+
const allowedOrigins = resolveListOption("--sse-allowed-origins", "MCP_SSE_ALLOWED_ORIGINS");
|
|
853
|
+
const allowedHosts = resolveListOption("--sse-allowed-hosts", "MCP_SSE_ALLOWED_HOSTS");
|
|
854
|
+
const enableDnsRebindingProtection = resolveBooleanOption("--sse-enable-dns-protection", "MCP_SSE_ENABLE_DNS_PROTECTION", false);
|
|
855
|
+
return {
|
|
856
|
+
type: "sse",
|
|
857
|
+
host,
|
|
858
|
+
port,
|
|
859
|
+
allowedOrigins,
|
|
860
|
+
allowedHosts,
|
|
861
|
+
enableDnsRebindingProtection,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const httpRequested = normalized === "http" ||
|
|
865
|
+
normalized === "streamable-http" ||
|
|
866
|
+
normalized === "server" ||
|
|
867
|
+
hasFlag("--http") ||
|
|
868
|
+
// Note: Default is stdio (set in runtimeConfig), so this only applies if explicitly requested
|
|
869
|
+
(!sseRequested && normalized !== "stdio");
|
|
870
|
+
if (httpRequested) {
|
|
871
|
+
const port = resolvePortOption("--http-port", "MCP_HTTP_PORT", 3000);
|
|
872
|
+
// Default to localhost (127.0.0.1) for security - only accepts local connections
|
|
873
|
+
// Use 0.0.0.0 to accept connections from all interfaces (less secure)
|
|
874
|
+
const host = getArgValue("--http-host") ?? process.env.MCP_HTTP_HOST ?? "127.0.0.1";
|
|
875
|
+
const enableJsonResponse = resolveBooleanOption("--http-json-response", "MCP_HTTP_ENABLE_JSON_RESPONSE", false);
|
|
876
|
+
const allowedOrigins = resolveListOption("--http-allowed-origins", "MCP_HTTP_ALLOWED_ORIGINS");
|
|
877
|
+
const allowedHosts = resolveListOption("--http-allowed-hosts", "MCP_HTTP_ALLOWED_HOSTS");
|
|
878
|
+
const enableDnsRebindingProtection = resolveBooleanOption("--http-enable-dns-protection", "MCP_HTTP_ENABLE_DNS_PROTECTION", false);
|
|
879
|
+
return {
|
|
880
|
+
type: "streamable-http",
|
|
881
|
+
host,
|
|
882
|
+
port,
|
|
883
|
+
enableJsonResponse,
|
|
884
|
+
allowedOrigins,
|
|
885
|
+
allowedHosts,
|
|
886
|
+
enableDnsRebindingProtection,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
return { type: "stdio" };
|
|
890
|
+
}
|
|
891
|
+
function setAbapConnectionOverride(connection) {
|
|
892
|
+
(0, utils_1.setConnectionOverride)(connection);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Retrieves SAP configuration from environment variables.
|
|
896
|
+
* Reads configuration from process.env (caller is responsible for loading .env file if needed).
|
|
897
|
+
*
|
|
898
|
+
* @returns {SapConfig} The SAP configuration object.
|
|
899
|
+
* @throws {Error} If any required environment variable is missing.
|
|
900
|
+
*/
|
|
901
|
+
// Helper function for Windows-compatible logging
|
|
902
|
+
// Only logs when DEBUG_CONNECTORS, DEBUG_TESTS, or DEBUG_ADT_TESTS is enabled
|
|
903
|
+
function debugLog(message) {
|
|
904
|
+
const debugEnabled = process.env.DEBUG_CONNECTORS === 'true' ||
|
|
905
|
+
process.env.DEBUG_TESTS === 'true' ||
|
|
906
|
+
process.env.DEBUG_ADT_TESTS === 'true';
|
|
907
|
+
if (!debugEnabled) {
|
|
908
|
+
return; // Suppress debug logs when not in debug mode
|
|
909
|
+
}
|
|
910
|
+
// Try stderr first
|
|
911
|
+
try {
|
|
912
|
+
process.stderr.write(message);
|
|
913
|
+
}
|
|
914
|
+
catch (e) {
|
|
915
|
+
// Fallback to console.error for Windows
|
|
916
|
+
console.error(message.trim());
|
|
917
|
+
}
|
|
918
|
+
// Also try to write to a debug file on Windows
|
|
919
|
+
if (process.platform === 'win32') {
|
|
920
|
+
try {
|
|
921
|
+
const fs = require('fs');
|
|
922
|
+
const path = require('path');
|
|
923
|
+
const debugFile = path.join(process.cwd(), 'mcp-debug.log');
|
|
924
|
+
fs.appendFileSync(debugFile, `${new Date().toISOString()} ${message}`, 'utf8');
|
|
925
|
+
}
|
|
926
|
+
catch (e) {
|
|
927
|
+
// Ignore file write errors
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
// Re-export header constants from interfaces package
|
|
932
|
+
__exportStar(require("@mcp-abap-adt/interfaces"), exports);
|
|
933
|
+
/**
|
|
934
|
+
* Server class for interacting with ABAP systems via ADT.
|
|
935
|
+
*/
|
|
936
|
+
class mcp_abap_adt_server {
|
|
937
|
+
allowProcessExit;
|
|
938
|
+
registerSignalHandlers;
|
|
939
|
+
mcpServer; // MCP server for all transports
|
|
940
|
+
sapConfig; // SAP configuration
|
|
941
|
+
hasEnvFile; // Track if .env file was found at startup
|
|
942
|
+
transportConfig;
|
|
943
|
+
httpServer;
|
|
944
|
+
shuttingDown = false;
|
|
945
|
+
defaultMcpDestination; // Default MCP destination from --mcp parameter
|
|
946
|
+
defaultDestination; // Default destination (from --mcp or .env, used when client doesn't specify)
|
|
947
|
+
mcpHandlers;
|
|
948
|
+
// Client session tracking for StreamableHTTP (like the example)
|
|
949
|
+
streamableHttpSessions = new Map();
|
|
950
|
+
// Map sessionId -> client key (streamable HTTP) for quick lookups
|
|
951
|
+
streamableSessionIndex = new Map();
|
|
952
|
+
// SSE session tracking (McpServer + SSEServerTransport per session)
|
|
953
|
+
sseSessions = new Map();
|
|
954
|
+
// AuthBroker factory for creating and managing AuthBroker instances
|
|
955
|
+
authBrokerFactory;
|
|
956
|
+
// Path to .env file (used to create SessionStore when --mcp is not specified)
|
|
957
|
+
envFilePath;
|
|
958
|
+
/**
|
|
959
|
+
* Initialize default broker on server startup (for stdio/SSE transports)
|
|
960
|
+
* Creates broker with default destination from --mcp parameter or .env file
|
|
961
|
+
*/
|
|
962
|
+
/**
|
|
963
|
+
* Initialize default broker using unified AuthBrokerFactory logic
|
|
964
|
+
* Sets defaultDestination based on what broker was created
|
|
965
|
+
*/
|
|
966
|
+
async initializeDefaultBroker() {
|
|
967
|
+
// Use unified AuthBrokerFactory logic to initialize default broker
|
|
968
|
+
await this.authBrokerFactory.initializeDefaultBroker();
|
|
969
|
+
// Get default broker to determine default destination
|
|
970
|
+
const defaultBroker = this.authBrokerFactory.getDefaultBroker();
|
|
971
|
+
if (defaultBroker) {
|
|
972
|
+
// If we have --mcp, use that as default destination
|
|
973
|
+
if (this.defaultMcpDestination) {
|
|
974
|
+
this.defaultDestination = this.defaultMcpDestination;
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
// Otherwise use 'default' as destination name
|
|
978
|
+
this.defaultDestination = 'default';
|
|
979
|
+
}
|
|
980
|
+
logger_1.logger?.info("Default broker initialized", {
|
|
981
|
+
type: "DEFAULT_BROKER_INITIALIZED",
|
|
982
|
+
destination: this.defaultDestination,
|
|
983
|
+
transport: this.transportConfig.type,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
logger_1.logger?.debug("No default broker created (no conditions met)", {
|
|
988
|
+
type: "NO_DEFAULT_BROKER",
|
|
989
|
+
transport: this.transportConfig.type,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Get or create AuthBroker for a specific destination (lazy initialization)
|
|
995
|
+
* If destination is not specified, returns default broker
|
|
996
|
+
*/
|
|
997
|
+
async getOrCreateAuthBroker(destination, clientKey) {
|
|
998
|
+
// clientKey is ignored in unified logic (one broker per destination)
|
|
999
|
+
return this.authBrokerFactory.getOrCreateAuthBroker(destination);
|
|
1000
|
+
}
|
|
1001
|
+
async applyAuthHeaders(headers, sessionId, clientKey) {
|
|
1002
|
+
// Security: If server is listening on non-local interface (0.0.0.0), don't use default destination
|
|
1003
|
+
// Client must provide all connection parameters in headers - server only proxies to ABAP
|
|
1004
|
+
const isNonLocalInterface = this.isListeningOnNonLocalInterface();
|
|
1005
|
+
if (isNonLocalInterface) {
|
|
1006
|
+
// Non-local interface: use only what client provides in headers, no default destination
|
|
1007
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
1008
|
+
logger_1.logger?.info("No headers provided - server listening on non-local interface requires all headers from client", {
|
|
1009
|
+
type: "NO_HEADERS_NON_LOCAL_INTERFACE",
|
|
1010
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1011
|
+
hint: `Server is listening on 0.0.0.0 - client must provide all connection parameters in headers. Server will not use default destination for security.`,
|
|
1012
|
+
});
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
// Use headers as-is, no default destination
|
|
1016
|
+
logger_1.logger?.debug("Non-local interface: using only client-provided headers", {
|
|
1017
|
+
type: "NON_LOCAL_INTERFACE_HEADERS_ONLY",
|
|
1018
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
// Local interface (127.0.0.1): can use default destination if no headers
|
|
1023
|
+
// If no headers but defaultDestination is set, use it
|
|
1024
|
+
// Client values have priority (Q10), but if no headers at all, use default
|
|
1025
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
1026
|
+
if (this.defaultDestination) {
|
|
1027
|
+
// Use default destination to get connection config from broker
|
|
1028
|
+
const authBroker = await this.getOrCreateAuthBroker(this.defaultDestination, clientKey || sessionId);
|
|
1029
|
+
if (authBroker) {
|
|
1030
|
+
try {
|
|
1031
|
+
const connConfig = await authBroker.getConnectionConfig(this.defaultDestination);
|
|
1032
|
+
if (connConfig?.serviceUrl) {
|
|
1033
|
+
// Check authType from connection config BEFORE calling getToken()
|
|
1034
|
+
const isBasicAuth = connConfig.authType === 'basic' || (connConfig.username && connConfig.password);
|
|
1035
|
+
if (isBasicAuth) {
|
|
1036
|
+
// Basic auth for on-premise
|
|
1037
|
+
if (connConfig.username && connConfig.password) {
|
|
1038
|
+
this.processBasicAuthConfigUpdate(connConfig.serviceUrl, connConfig.username, connConfig.password, sessionId);
|
|
1039
|
+
logger_1.logger?.info("No headers provided, using default destination with basic auth", {
|
|
1040
|
+
type: "NO_HEADERS_DEFAULT_DESTINATION_BASIC_AUTH",
|
|
1041
|
+
destination: this.defaultDestination,
|
|
1042
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
// JWT auth for cloud
|
|
1048
|
+
const jwtToken = await authBroker.getToken(this.defaultDestination);
|
|
1049
|
+
if (jwtToken) {
|
|
1050
|
+
// Create headers object with default destination
|
|
1051
|
+
headers = {
|
|
1052
|
+
[interfaces_1.HEADER_MCP_DESTINATION]: this.defaultDestination,
|
|
1053
|
+
[interfaces_1.HEADER_SAP_URL]: connConfig.serviceUrl,
|
|
1054
|
+
[interfaces_1.HEADER_SAP_JWT_TOKEN]: jwtToken,
|
|
1055
|
+
};
|
|
1056
|
+
logger_1.logger?.info("No headers provided, using default destination", {
|
|
1057
|
+
type: "NO_HEADERS_DEFAULT_DESTINATION_USED",
|
|
1058
|
+
destination: this.defaultDestination,
|
|
1059
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
logger_1.logger?.warn("Failed to get connection config from default destination", {
|
|
1067
|
+
type: "DEFAULT_DESTINATION_CONFIG_FAILED",
|
|
1068
|
+
destination: this.defaultDestination,
|
|
1069
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1070
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// If still no headers after trying default destination, log and return
|
|
1076
|
+
if (!headers || Object.keys(headers).length === 0) {
|
|
1077
|
+
logger_1.logger?.info("No headers provided in request and no default destination available", {
|
|
1078
|
+
type: "NO_HEADERS_PROVIDED",
|
|
1079
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1080
|
+
hint: `Provide authentication headers (${interfaces_1.HEADER_SAP_DESTINATION_SERVICE}, ${interfaces_1.HEADER_SAP_URL}, ${interfaces_1.HEADER_SAP_AUTH_TYPE}, etc.) or use --mcp parameter or .env file`,
|
|
1081
|
+
});
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// Apply default destination if not provided in headers (client values have priority)
|
|
1086
|
+
// Only for local interface
|
|
1087
|
+
if (this.defaultDestination && !headers[interfaces_1.HEADER_MCP_DESTINATION] && !headers['X-MCP-Destination']) {
|
|
1088
|
+
headers[interfaces_1.HEADER_MCP_DESTINATION] = this.defaultDestination;
|
|
1089
|
+
logger_1.logger?.info("Using default destination (client didn't specify)", {
|
|
1090
|
+
type: "DEFAULT_DESTINATION_APPLIED",
|
|
1091
|
+
destination: this.defaultDestination,
|
|
1092
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// Ensure headers is set for processing below
|
|
1097
|
+
if (!headers) {
|
|
1098
|
+
headers = {};
|
|
1099
|
+
}
|
|
1100
|
+
const headersWithDefault = { ...headers };
|
|
1101
|
+
// Log which auth headers are present (for debugging)
|
|
1102
|
+
// Check headers in both cases (Node.js normalizes to lowercase, but check both for safety)
|
|
1103
|
+
const authHeaders = {
|
|
1104
|
+
[interfaces_1.HEADER_SAP_DESTINATION_SERVICE]: headersWithDefault[interfaces_1.HEADER_SAP_DESTINATION_SERVICE] || headersWithDefault['X-SAP-Destination'],
|
|
1105
|
+
[interfaces_1.HEADER_MCP_DESTINATION]: headersWithDefault[interfaces_1.HEADER_MCP_DESTINATION] || headersWithDefault['X-MCP-Destination'],
|
|
1106
|
+
[interfaces_1.HEADER_SAP_URL]: (headersWithDefault[interfaces_1.HEADER_SAP_URL] || headersWithDefault['X-SAP-URL']) ? '[present]' : undefined,
|
|
1107
|
+
[interfaces_1.HEADER_SAP_AUTH_TYPE]: headersWithDefault[interfaces_1.HEADER_SAP_AUTH_TYPE] || headersWithDefault['X-SAP-Auth-Type'],
|
|
1108
|
+
[interfaces_1.HEADER_SAP_JWT_TOKEN]: (headersWithDefault[interfaces_1.HEADER_SAP_JWT_TOKEN] || headersWithDefault['X-SAP-JWT-Token']) ? '[present]' : undefined,
|
|
1109
|
+
[interfaces_1.HEADER_SAP_LOGIN]: (headersWithDefault[interfaces_1.HEADER_SAP_LOGIN] || headersWithDefault['X-SAP-Login']) ? '[present]' : undefined,
|
|
1110
|
+
};
|
|
1111
|
+
logger_1.logger?.info("Processing authentication headers", {
|
|
1112
|
+
type: "AUTH_HEADERS_PROCESSING",
|
|
1113
|
+
headers: authHeaders,
|
|
1114
|
+
platform: process.platform,
|
|
1115
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1116
|
+
allHeaderKeys: Object.keys(headersWithDefault).filter(k => {
|
|
1117
|
+
const lowerKey = k.toLowerCase();
|
|
1118
|
+
return lowerKey.startsWith('x-sap') || lowerKey.startsWith('x-mcp');
|
|
1119
|
+
}),
|
|
1120
|
+
});
|
|
1121
|
+
// Use header validator to validate and prioritize authentication methods
|
|
1122
|
+
const validationResult = (0, header_validator_1.validateAuthHeaders)(headersWithDefault);
|
|
1123
|
+
// Log warnings if any
|
|
1124
|
+
if (validationResult.warnings.length > 0) {
|
|
1125
|
+
logger_1.logger?.debug("Header validation warnings", {
|
|
1126
|
+
type: "HEADER_VALIDATION_WARNINGS",
|
|
1127
|
+
warnings: validationResult.warnings,
|
|
1128
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
// If validation failed, log info (not error) and return
|
|
1132
|
+
// This is not an error - user may be using .env file or base config
|
|
1133
|
+
if (!validationResult.isValid || !validationResult.config) {
|
|
1134
|
+
if (validationResult.errors.length > 0) {
|
|
1135
|
+
logger_1.logger?.debug("Header validation failed - will use .env file or base config", {
|
|
1136
|
+
type: "HEADER_VALIDATION_FAILED",
|
|
1137
|
+
errors: validationResult.errors,
|
|
1138
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1139
|
+
hint: "No valid headers found. Will use .env file or base config if available.",
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
logger_1.logger?.debug("No valid authentication headers found - will use .env file or base config", {
|
|
1144
|
+
type: "NO_VALID_AUTH_HEADERS",
|
|
1145
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1146
|
+
hint: "No headers provided. Will use .env file or base config if available.",
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const config = validationResult.config;
|
|
1152
|
+
// Process based on priority (highest to lowest)
|
|
1153
|
+
switch (config.priority) {
|
|
1154
|
+
case header_validator_1.AuthMethodPriority.SAP_DESTINATION: {
|
|
1155
|
+
// Priority 4: x-sap-destination (uses AuthBroker, URL from destination)
|
|
1156
|
+
if (!config.destination) {
|
|
1157
|
+
logger_1.logger?.warn("SAP destination auth requires destination name", {
|
|
1158
|
+
type: "SAP_DESTINATION_AUTH_MISSING",
|
|
1159
|
+
destination: config.destination,
|
|
1160
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1161
|
+
});
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
// Get or create AuthBroker for this destination (lazy initialization)
|
|
1165
|
+
const authBroker = await this.getOrCreateAuthBroker(config.destination, clientKey || sessionId);
|
|
1166
|
+
if (!authBroker) {
|
|
1167
|
+
const errorMessage = `Failed to initialize AuthBroker for destination "${config.destination}". Auth-broker is only available for HTTP/streamable-http transport.`;
|
|
1168
|
+
logger_1.logger?.error(errorMessage, {
|
|
1169
|
+
type: "AUTH_BROKER_NOT_INITIALIZED",
|
|
1170
|
+
destination: config.destination,
|
|
1171
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1172
|
+
transport: this.transportConfig.type,
|
|
1173
|
+
});
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
try {
|
|
1177
|
+
// Get connection config from AuthBroker (loads from .env or service key)
|
|
1178
|
+
// Note: destination name must exactly match service key filename (case-sensitive)
|
|
1179
|
+
// Example: if file is "sk.json", destination must be "sk" (not "SK")
|
|
1180
|
+
const connConfig = await authBroker.getConnectionConfig(config.destination);
|
|
1181
|
+
if (!connConfig || !connConfig.serviceUrl) {
|
|
1182
|
+
logger_1.logger?.error("Failed to get SAP URL from destination", {
|
|
1183
|
+
type: "SAP_DESTINATION_URL_NOT_FOUND",
|
|
1184
|
+
destination: config.destination,
|
|
1185
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1186
|
+
hint: `Service key file "${config.destination}.json" not found or missing URL. Check file name matches destination exactly (case-sensitive).`,
|
|
1187
|
+
});
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const sapUrl = connConfig.serviceUrl;
|
|
1191
|
+
logger_1.logger?.info("SAP URL retrieved from destination", {
|
|
1192
|
+
type: "SAP_URL_RETRIEVED",
|
|
1193
|
+
destination: config.destination,
|
|
1194
|
+
url: sapUrl,
|
|
1195
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1196
|
+
});
|
|
1197
|
+
// Check authType from connection config BEFORE calling getToken()
|
|
1198
|
+
const isBasicAuth = connConfig.authType === 'basic' || (connConfig.username && connConfig.password);
|
|
1199
|
+
if (isBasicAuth) {
|
|
1200
|
+
// Basic auth for on-premise
|
|
1201
|
+
if (connConfig.username && connConfig.password) {
|
|
1202
|
+
this.processBasicAuthConfigUpdate(sapUrl, connConfig.username, connConfig.password, sessionId);
|
|
1203
|
+
logger_1.logger?.info("Updated SAP configuration using SAP destination with basic auth", {
|
|
1204
|
+
type: "SAP_CONFIG_UPDATED_SAP_DESTINATION_BASIC_AUTH",
|
|
1205
|
+
destination: config.destination,
|
|
1206
|
+
url: sapUrl,
|
|
1207
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
logger_1.logger?.error("Basic auth requires username and password", {
|
|
1212
|
+
type: "BASIC_AUTH_MISSING_CREDENTIALS",
|
|
1213
|
+
destination: config.destination,
|
|
1214
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
// JWT auth for cloud
|
|
1220
|
+
// Get token from AuthBroker
|
|
1221
|
+
const jwtToken = await authBroker.getToken(config.destination);
|
|
1222
|
+
// Register AuthBroker in global registry for connection to use during token refresh
|
|
1223
|
+
const { registerAuthBroker } = require('./lib/utils');
|
|
1224
|
+
registerAuthBroker(config.destination, authBroker);
|
|
1225
|
+
this.processJwtConfigUpdate(sapUrl, jwtToken, undefined, config.destination, sessionId);
|
|
1226
|
+
logger_1.logger?.info("Updated SAP configuration using SAP destination (AuthBroker)", {
|
|
1227
|
+
type: "SAP_CONFIG_UPDATED_SAP_DESTINATION",
|
|
1228
|
+
destination: config.destination,
|
|
1229
|
+
url: sapUrl,
|
|
1230
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch (error) {
|
|
1235
|
+
logger_1.logger?.error("Failed to get token from AuthBroker for SAP destination", {
|
|
1236
|
+
type: "AUTH_BROKER_ERROR_SAP_DESTINATION",
|
|
1237
|
+
destination: config.destination,
|
|
1238
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1239
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
case header_validator_1.AuthMethodPriority.MCP_DESTINATION: {
|
|
1245
|
+
// Priority 3: x-mcp-destination (uses AuthBroker, URL from destination or x-sap-url header)
|
|
1246
|
+
if (!config.destination) {
|
|
1247
|
+
logger_1.logger?.warn("MCP destination auth requires destination", {
|
|
1248
|
+
type: "MCP_DESTINATION_AUTH_MISSING",
|
|
1249
|
+
destination: config.destination,
|
|
1250
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
// Get or create AuthBroker for this destination (lazy initialization)
|
|
1255
|
+
const authBroker = await this.getOrCreateAuthBroker(config.destination, clientKey || sessionId);
|
|
1256
|
+
if (!authBroker) {
|
|
1257
|
+
logger_1.logger?.error("Failed to initialize AuthBroker for MCP destination", {
|
|
1258
|
+
type: "AUTH_BROKER_NOT_INITIALIZED",
|
|
1259
|
+
destination: config.destination,
|
|
1260
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1261
|
+
transport: this.transportConfig.type,
|
|
1262
|
+
});
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
// Get connection config from AuthBroker (loads from .env or service key)
|
|
1267
|
+
// If x-sap-url was provided in headers, it will be ignored (warning already issued by validator)
|
|
1268
|
+
// Note: destination name must exactly match service key filename (case-sensitive)
|
|
1269
|
+
const connConfig = await authBroker.getConnectionConfig(config.destination);
|
|
1270
|
+
if (!connConfig || !connConfig.serviceUrl) {
|
|
1271
|
+
logger_1.logger?.error("Failed to get SAP URL from destination", {
|
|
1272
|
+
type: "MCP_DESTINATION_URL_NOT_FOUND",
|
|
1273
|
+
destination: config.destination,
|
|
1274
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1275
|
+
hint: `Service key file "${config.destination}.json" not found or missing URL. Check file name matches destination exactly (case-sensitive).`,
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const sapUrl = connConfig.serviceUrl;
|
|
1280
|
+
logger_1.logger?.info("SAP URL retrieved from destination", {
|
|
1281
|
+
type: "SAP_URL_RETRIEVED",
|
|
1282
|
+
destination: config.destination,
|
|
1283
|
+
url: sapUrl,
|
|
1284
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1285
|
+
});
|
|
1286
|
+
// Check authType from connection config BEFORE calling getToken()
|
|
1287
|
+
const isBasicAuth = connConfig.authType === 'basic' || (connConfig.username && connConfig.password);
|
|
1288
|
+
if (isBasicAuth) {
|
|
1289
|
+
// Basic auth for on-premise
|
|
1290
|
+
if (connConfig.username && connConfig.password) {
|
|
1291
|
+
this.processBasicAuthConfigUpdate(sapUrl, connConfig.username, connConfig.password, sessionId);
|
|
1292
|
+
logger_1.logger?.info("Updated SAP configuration using MCP destination with basic auth", {
|
|
1293
|
+
type: "SAP_CONFIG_UPDATED_MCP_DESTINATION_BASIC_AUTH",
|
|
1294
|
+
destination: config.destination,
|
|
1295
|
+
url: sapUrl,
|
|
1296
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
logger_1.logger?.error("Basic auth requires username and password", {
|
|
1301
|
+
type: "BASIC_AUTH_MISSING_CREDENTIALS",
|
|
1302
|
+
destination: config.destination,
|
|
1303
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
// JWT auth for cloud
|
|
1309
|
+
// Get token from AuthBroker
|
|
1310
|
+
const jwtToken = await authBroker.getToken(config.destination);
|
|
1311
|
+
// Register AuthBroker in global registry for connection to use during token refresh
|
|
1312
|
+
const { registerAuthBroker } = require('./lib/utils');
|
|
1313
|
+
registerAuthBroker(config.destination, authBroker);
|
|
1314
|
+
this.processJwtConfigUpdate(sapUrl, jwtToken, undefined, config.destination, sessionId);
|
|
1315
|
+
logger_1.logger?.info("Updated SAP configuration using MCP destination (AuthBroker)", {
|
|
1316
|
+
type: "SAP_CONFIG_UPDATED_MCP_DESTINATION",
|
|
1317
|
+
destination: config.destination,
|
|
1318
|
+
url: sapUrl,
|
|
1319
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
logger_1.logger?.error("Failed to get token from AuthBroker for MCP destination", {
|
|
1325
|
+
type: "AUTH_BROKER_ERROR_MCP_DESTINATION",
|
|
1326
|
+
destination: config.destination,
|
|
1327
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1328
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
case header_validator_1.AuthMethodPriority.DIRECT_JWT: {
|
|
1334
|
+
// Priority 2: x-sap-jwt-token (direct JWT token)
|
|
1335
|
+
if (!config.sapUrl || !config.jwtToken) {
|
|
1336
|
+
logger_1.logger?.warn("Direct JWT auth requires URL and token", {
|
|
1337
|
+
type: "DIRECT_JWT_AUTH_MISSING",
|
|
1338
|
+
sapUrl: config.sapUrl,
|
|
1339
|
+
hasToken: !!config.jwtToken,
|
|
1340
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1341
|
+
});
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
this.processJwtConfigUpdate(config.sapUrl, config.jwtToken, config.refreshToken, sessionId);
|
|
1345
|
+
logger_1.logger?.info("Updated SAP configuration using direct JWT token", {
|
|
1346
|
+
type: "SAP_CONFIG_UPDATED_DIRECT_JWT",
|
|
1347
|
+
url: config.sapUrl,
|
|
1348
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1349
|
+
});
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
case header_validator_1.AuthMethodPriority.BASIC: {
|
|
1353
|
+
// Priority 1: x-sap-login + x-sap-password (basic auth)
|
|
1354
|
+
if (!config.sapUrl || !config.username || !config.password) {
|
|
1355
|
+
logger_1.logger?.warn("Basic auth requires URL, username, and password", {
|
|
1356
|
+
type: "BASIC_AUTH_MISSING",
|
|
1357
|
+
sapUrl: config.sapUrl,
|
|
1358
|
+
hasUsername: !!config.username,
|
|
1359
|
+
hasPassword: !!config.password,
|
|
1360
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1361
|
+
});
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
this.processBasicAuthConfigUpdate(config.sapUrl, config.username, config.password, sessionId);
|
|
1365
|
+
logger_1.logger?.info("Updated SAP configuration using basic auth", {
|
|
1366
|
+
type: "SAP_CONFIG_UPDATED_BASIC",
|
|
1367
|
+
url: config.sapUrl,
|
|
1368
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1369
|
+
});
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
default: {
|
|
1373
|
+
logger_1.logger?.warn("Unknown authentication method priority", {
|
|
1374
|
+
type: "UNKNOWN_AUTH_PRIORITY",
|
|
1375
|
+
priority: config.priority,
|
|
1376
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
processJwtConfigUpdate(sapUrl, jwtToken, refreshToken, destination, sessionId) {
|
|
1383
|
+
const sanitizeToken = (token) => token.length <= 10 ? token : `${token.substring(0, 6)}…${token.substring(token.length - 4)}`;
|
|
1384
|
+
// URL from auth-broker/service key is already clean and correct, no need to clean it
|
|
1385
|
+
// Only validate format
|
|
1386
|
+
let cleanedUrl = sapUrl.trim();
|
|
1387
|
+
// Ensure URL has protocol
|
|
1388
|
+
if (!/^https?:\/\//.test(cleanedUrl)) {
|
|
1389
|
+
logger_1.logger?.error("Invalid URL format in processJwtConfigUpdate", {
|
|
1390
|
+
type: "INVALID_URL_FORMAT",
|
|
1391
|
+
originalUrl: sapUrl,
|
|
1392
|
+
cleanedUrl: cleanedUrl,
|
|
1393
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1394
|
+
});
|
|
1395
|
+
throw new Error(`Invalid URL format: "${sapUrl}". Expected format: https://your-system.sap.com`);
|
|
1396
|
+
}
|
|
1397
|
+
// Normalize URL using URL object
|
|
1398
|
+
try {
|
|
1399
|
+
const urlObj = new URL(cleanedUrl);
|
|
1400
|
+
cleanedUrl = urlObj.href.replace(/\/$/, ''); // Remove trailing slash
|
|
1401
|
+
}
|
|
1402
|
+
catch (urlError) {
|
|
1403
|
+
logger_1.logger?.error("Failed to parse URL in processJwtConfigUpdate", {
|
|
1404
|
+
type: "URL_PARSE_ERROR",
|
|
1405
|
+
url: cleanedUrl,
|
|
1406
|
+
error: urlError instanceof Error ? urlError.message : String(urlError),
|
|
1407
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1408
|
+
});
|
|
1409
|
+
throw new Error(`Invalid URL: "${sapUrl}". Error: ${urlError instanceof Error ? urlError.message : urlError}`);
|
|
1410
|
+
}
|
|
1411
|
+
// Use cleaned URL
|
|
1412
|
+
sapUrl = cleanedUrl;
|
|
1413
|
+
let baseConfig = this.sapConfig;
|
|
1414
|
+
// Don't load from .env if using auth-broker (--mcp or --auth-broker)
|
|
1415
|
+
// This prevents .env from being used when destination-based auth is specified
|
|
1416
|
+
const isUsingAuthBroker = this.defaultMcpDestination || useAuthBroker;
|
|
1417
|
+
if ((!baseConfig || baseConfig.url === "http://placeholder") && !isUsingAuthBroker) {
|
|
1418
|
+
try {
|
|
1419
|
+
baseConfig = (0, config_js_1.getConfig)();
|
|
1420
|
+
}
|
|
1421
|
+
catch (error) {
|
|
1422
|
+
logger_1.logger?.warn("Failed to load base SAP config when applying headers", {
|
|
1423
|
+
type: "SAP_CONFIG_HEADER_APPLY_FAILED",
|
|
1424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1425
|
+
});
|
|
1426
|
+
baseConfig = {
|
|
1427
|
+
url: sapUrl,
|
|
1428
|
+
authType: "jwt",
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
else if (!baseConfig || baseConfig.url === "http://placeholder") {
|
|
1433
|
+
// Using auth-broker, don't load from .env
|
|
1434
|
+
baseConfig = {
|
|
1435
|
+
url: sapUrl,
|
|
1436
|
+
authType: "jwt",
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
// Check if any configuration changed
|
|
1440
|
+
const urlChanged = sapUrl !== baseConfig.url;
|
|
1441
|
+
const authTypeChanged = "jwt" !== baseConfig.authType;
|
|
1442
|
+
const tokenChanged = baseConfig.jwtToken !== jwtToken ||
|
|
1443
|
+
(!!refreshToken && refreshToken.trim() !== baseConfig.refreshToken);
|
|
1444
|
+
if (!urlChanged && !authTypeChanged && !tokenChanged) {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const newConfig = {
|
|
1448
|
+
...baseConfig,
|
|
1449
|
+
url: sapUrl,
|
|
1450
|
+
authType: "jwt",
|
|
1451
|
+
jwtToken,
|
|
1452
|
+
};
|
|
1453
|
+
if (refreshToken && refreshToken.trim()) {
|
|
1454
|
+
newConfig.refreshToken = refreshToken.trim();
|
|
1455
|
+
}
|
|
1456
|
+
(0, config_js_1.setSapConfigOverride)(newConfig);
|
|
1457
|
+
this.sapConfig = newConfig;
|
|
1458
|
+
// Store config and destination in session if sessionId is provided
|
|
1459
|
+
if (sessionId) {
|
|
1460
|
+
const clientKeyForSession = this.streamableSessionIndex.get(sessionId);
|
|
1461
|
+
const session = clientKeyForSession ? this.streamableHttpSessions.get(clientKeyForSession) : undefined;
|
|
1462
|
+
if (session) {
|
|
1463
|
+
session.sapConfig = newConfig;
|
|
1464
|
+
if (destination) {
|
|
1465
|
+
session.destination = destination;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
// Force connection cache invalidation (for backward compatibility)
|
|
1470
|
+
const { invalidateConnectionCache } = require('./lib/utils');
|
|
1471
|
+
try {
|
|
1472
|
+
invalidateConnectionCache();
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
logger_1.logger?.debug("Connection cache invalidation failed", {
|
|
1476
|
+
type: "CONNECTION_CACHE_INVALIDATION_FAILED",
|
|
1477
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
logger_1.logger?.info("Updated SAP configuration from HTTP headers (JWT)", {
|
|
1481
|
+
type: "SAP_CONFIG_UPDATED",
|
|
1482
|
+
urlChanged: Boolean(urlChanged),
|
|
1483
|
+
authTypeChanged: Boolean(authTypeChanged),
|
|
1484
|
+
tokenChanged: Boolean(tokenChanged),
|
|
1485
|
+
hasRefreshToken: Boolean(refreshToken),
|
|
1486
|
+
jwtPreview: sanitizeToken(jwtToken),
|
|
1487
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Process JWT config update using AuthBroker for destination-based authentication
|
|
1492
|
+
* @private
|
|
1493
|
+
*/
|
|
1494
|
+
async processJwtConfigUpdateWithAuthBroker(sapUrl, destination, sessionId) {
|
|
1495
|
+
// Get or create AuthBroker for this destination (lazy initialization)
|
|
1496
|
+
const authBroker = await this.getOrCreateAuthBroker(destination, sessionId);
|
|
1497
|
+
if (!authBroker) {
|
|
1498
|
+
logger_1.logger?.warn("AuthBroker not available, falling back to direct token", {
|
|
1499
|
+
type: "AUTH_BROKER_NOT_AVAILABLE",
|
|
1500
|
+
destination,
|
|
1501
|
+
transport: this.transportConfig.type,
|
|
1502
|
+
});
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
// Get token from AuthBroker (will load from .env, validate, and refresh if needed)
|
|
1507
|
+
const jwtToken = await authBroker.getToken(destination);
|
|
1508
|
+
// Load refresh token from .env if available (AuthBroker doesn't return it, but we can load it)
|
|
1509
|
+
// For now, we'll just use the access token - refresh will be handled by AuthBroker automatically
|
|
1510
|
+
const refreshToken = undefined; // AuthBroker handles refresh internally
|
|
1511
|
+
// Process JWT config update with the token from AuthBroker
|
|
1512
|
+
this.processJwtConfigUpdate(sapUrl, jwtToken, refreshToken, destination, sessionId);
|
|
1513
|
+
logger_1.logger?.info("Updated SAP configuration using AuthBroker (destination-based)", {
|
|
1514
|
+
type: "SAP_CONFIG_UPDATED_AUTH_BROKER",
|
|
1515
|
+
destination,
|
|
1516
|
+
url: sapUrl,
|
|
1517
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
catch (error) {
|
|
1521
|
+
logger_1.logger?.error("Failed to get token from AuthBroker", {
|
|
1522
|
+
type: "AUTH_BROKER_ERROR",
|
|
1523
|
+
destination,
|
|
1524
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1525
|
+
});
|
|
1526
|
+
// Don't throw - let the request continue with existing config or fail later
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
processBasicAuthConfigUpdate(sapUrl, username, password, sessionId) {
|
|
1530
|
+
let baseConfig = this.sapConfig;
|
|
1531
|
+
if (!baseConfig || baseConfig.url === "http://placeholder") {
|
|
1532
|
+
try {
|
|
1533
|
+
baseConfig = (0, config_js_1.getConfig)();
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
logger_1.logger?.warn("Failed to load base SAP config when applying headers", {
|
|
1537
|
+
type: "SAP_CONFIG_HEADER_APPLY_FAILED",
|
|
1538
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1539
|
+
});
|
|
1540
|
+
baseConfig = {
|
|
1541
|
+
url: sapUrl,
|
|
1542
|
+
authType: "basic",
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
// Check if any configuration changed
|
|
1547
|
+
const urlChanged = sapUrl !== baseConfig.url;
|
|
1548
|
+
const authTypeChanged = "basic" !== baseConfig.authType;
|
|
1549
|
+
const credentialsChanged = baseConfig.username !== username ||
|
|
1550
|
+
baseConfig.password !== password;
|
|
1551
|
+
if (!urlChanged && !authTypeChanged && !credentialsChanged) {
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const newConfig = {
|
|
1555
|
+
...baseConfig,
|
|
1556
|
+
url: sapUrl,
|
|
1557
|
+
authType: "basic",
|
|
1558
|
+
username,
|
|
1559
|
+
password,
|
|
1560
|
+
};
|
|
1561
|
+
(0, config_js_1.setSapConfigOverride)(newConfig);
|
|
1562
|
+
this.sapConfig = newConfig;
|
|
1563
|
+
// Store config in session if sessionId is provided
|
|
1564
|
+
if (sessionId) {
|
|
1565
|
+
const clientKeyForSession = this.streamableSessionIndex.get(sessionId);
|
|
1566
|
+
const session = clientKeyForSession ? this.streamableHttpSessions.get(clientKeyForSession) : undefined;
|
|
1567
|
+
if (session) {
|
|
1568
|
+
session.sapConfig = newConfig;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
// Force connection cache invalidation (for backward compatibility)
|
|
1572
|
+
const { invalidateConnectionCache } = require('./lib/utils');
|
|
1573
|
+
try {
|
|
1574
|
+
invalidateConnectionCache();
|
|
1575
|
+
}
|
|
1576
|
+
catch (error) {
|
|
1577
|
+
logger_1.logger?.debug("Connection cache invalidation failed", {
|
|
1578
|
+
type: "CONNECTION_CACHE_INVALIDATION_FAILED",
|
|
1579
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
logger_1.logger?.info("Updated SAP configuration from HTTP headers (Basic)", {
|
|
1583
|
+
type: "SAP_CONFIG_UPDATED",
|
|
1584
|
+
urlChanged: Boolean(urlChanged),
|
|
1585
|
+
authTypeChanged: Boolean(authTypeChanged),
|
|
1586
|
+
credentialsChanged: Boolean(credentialsChanged),
|
|
1587
|
+
hasUsername: Boolean(username),
|
|
1588
|
+
sessionId: sessionId?.substring(0, 8),
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Check if connection is from localhost
|
|
1593
|
+
*/
|
|
1594
|
+
isLocalConnection(remoteAddress) {
|
|
1595
|
+
if (!remoteAddress) {
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
// Check for IPv4 localhost
|
|
1599
|
+
if (remoteAddress === "127.0.0.1" || remoteAddress === "localhost") {
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
// Check for IPv6 localhost
|
|
1603
|
+
if (remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1") {
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
// Check if it's a loopback interface
|
|
1607
|
+
if (remoteAddress.startsWith("127.") || remoteAddress.startsWith("::1")) {
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Check if server is listening on non-local interface (0.0.0.0)
|
|
1614
|
+
* When listening on 0.0.0.0, we don't use default destination - client must provide all headers
|
|
1615
|
+
*/
|
|
1616
|
+
isListeningOnNonLocalInterface() {
|
|
1617
|
+
if (this.transportConfig.type === "streamable-http" || this.transportConfig.type === "sse") {
|
|
1618
|
+
const host = this.transportConfig.host;
|
|
1619
|
+
// If host is 0.0.0.0, ::, or empty, it accepts connections from all interfaces
|
|
1620
|
+
return host === "0.0.0.0" || host === "::" || host === "" || !host;
|
|
1621
|
+
}
|
|
1622
|
+
// stdio is always local
|
|
1623
|
+
return false;
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Check if request has SAP connection headers
|
|
1627
|
+
*/
|
|
1628
|
+
hasSapHeaders(headers) {
|
|
1629
|
+
if (!headers) {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
// Check for destination-based auth headers
|
|
1633
|
+
if (headers[interfaces_1.HEADER_SAP_DESTINATION_SERVICE] || headers[interfaces_1.HEADER_MCP_DESTINATION]) {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
// Check for direct auth headers
|
|
1637
|
+
const sapUrl = headers[interfaces_1.HEADER_SAP_URL];
|
|
1638
|
+
const sapAuthType = headers[interfaces_1.HEADER_SAP_AUTH_TYPE];
|
|
1639
|
+
return !!(sapUrl && sapAuthType);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Constructor for the mcp_abap_adt_server class.
|
|
1643
|
+
*/
|
|
1644
|
+
constructor(options) {
|
|
1645
|
+
this.allowProcessExit = options?.allowProcessExit ?? true;
|
|
1646
|
+
this.registerSignalHandlers = options?.registerSignalHandlers ?? true;
|
|
1647
|
+
this.defaultMcpDestination = defaultMcpDestination;
|
|
1648
|
+
this.envFilePath = envFilePath; // Store .env file path for SessionStore creation
|
|
1649
|
+
// Initialize AuthBroker factory
|
|
1650
|
+
// Pass useAuthBroker to ensure .env is not used when --auth-broker is specified
|
|
1651
|
+
this.authBrokerFactory = new authBrokerFactory_1.AuthBrokerFactory({
|
|
1652
|
+
defaultMcpDestination: this.defaultMcpDestination,
|
|
1653
|
+
defaultDestination: this.defaultDestination,
|
|
1654
|
+
envFilePath: this.envFilePath,
|
|
1655
|
+
authBrokerPath,
|
|
1656
|
+
unsafe,
|
|
1657
|
+
transportType: transportType,
|
|
1658
|
+
useAuthBroker: useAuthBroker, // Important: prevents .env from being used when --auth-broker is specified
|
|
1659
|
+
logger: // Important: prevents .env from being used when --auth-broker is specified
|
|
1660
|
+
logger_1.logger,
|
|
1661
|
+
});
|
|
1662
|
+
this.mcpHandlers = new mcp_handlers_1.McpHandlers();
|
|
1663
|
+
// Check if .env file exists (was loaded at startup)
|
|
1664
|
+
// This is used to determine if we should restrict non-local connections
|
|
1665
|
+
this.hasEnvFile = fs_1.default.existsSync(envFilePath || path_1.default.resolve(process.cwd(), ".env"));
|
|
1666
|
+
if (options?.connection) {
|
|
1667
|
+
setAbapConnectionOverride(options.connection);
|
|
1668
|
+
}
|
|
1669
|
+
else {
|
|
1670
|
+
setAbapConnectionOverride(undefined);
|
|
1671
|
+
}
|
|
1672
|
+
if (!options?.connection) {
|
|
1673
|
+
(0, config_js_1.setSapConfigOverride)(options?.sapConfig);
|
|
1674
|
+
}
|
|
1675
|
+
// CHANGED: Don't validate config in constructor - will validate on actual ABAP requests
|
|
1676
|
+
// This allows creating server instance without .env file when using runtime config (e.g., from HTTP headers)
|
|
1677
|
+
try {
|
|
1678
|
+
if (options?.sapConfig) {
|
|
1679
|
+
this.sapConfig = options.sapConfig;
|
|
1680
|
+
}
|
|
1681
|
+
else if (!options?.connection) {
|
|
1682
|
+
// Don't load .env config if using auth-broker (--mcp or --auth-broker)
|
|
1683
|
+
// Connection will be created via auth-broker instead
|
|
1684
|
+
if (this.defaultMcpDestination || useAuthBroker) {
|
|
1685
|
+
// Using auth-broker, don't load .env config
|
|
1686
|
+
this.sapConfig = {
|
|
1687
|
+
url: "http://placeholder",
|
|
1688
|
+
authType: "basic",
|
|
1689
|
+
};
|
|
1690
|
+
logger_1.logger?.debug("Skipping .env config load (using auth-broker)", {
|
|
1691
|
+
type: "SKIP_ENV_CONFIG_FOR_AUTH_BROKER",
|
|
1692
|
+
defaultMcpDestination: this.defaultMcpDestination,
|
|
1693
|
+
useAuthBroker,
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
else {
|
|
1697
|
+
// Try to get config from .env, but don't fail if it's invalid - server should still initialize
|
|
1698
|
+
// Invalid config will be caught when handlers try to use it
|
|
1699
|
+
try {
|
|
1700
|
+
this.sapConfig = (0, config_js_1.getConfig)();
|
|
1701
|
+
}
|
|
1702
|
+
catch (configError) {
|
|
1703
|
+
// For stdio mode, we want the server to initialize even with invalid config
|
|
1704
|
+
// The error will be shown when user tries to use a tool
|
|
1705
|
+
// Check stdio mode using environment variable or stdin check (transportConfig not set yet)
|
|
1706
|
+
const isStdioMode = process.env.MCP_TRANSPORT === "stdio" || !process.stdin.isTTY;
|
|
1707
|
+
if (isStdioMode) {
|
|
1708
|
+
// In stdio mode, write error to stderr (safe for MCP protocol)
|
|
1709
|
+
process.stderr.write(`[MCP] ⚠ WARNING: Invalid SAP configuration: ${configError instanceof Error ? configError.message : String(configError)}\n`);
|
|
1710
|
+
process.stderr.write(`[MCP] Server will start, but tools will fail until configuration is fixed.\n`);
|
|
1711
|
+
}
|
|
1712
|
+
logger_1.logger?.warn("SAP config invalid at initialization, will use placeholder", {
|
|
1713
|
+
type: "CONFIG_INVALID",
|
|
1714
|
+
error: configError instanceof Error ? configError.message : String(configError),
|
|
1715
|
+
});
|
|
1716
|
+
// Set a placeholder that will be replaced when valid config is provided
|
|
1717
|
+
this.sapConfig = { url: "http://placeholder", authType: "jwt", jwtToken: "placeholder" };
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
else {
|
|
1722
|
+
this.sapConfig = { url: "http://injected-connection", authType: "jwt", jwtToken: "injected" };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
catch (error) {
|
|
1726
|
+
// If config is not available yet, that's OK - it will be provided later via setSapConfigOverride or DI
|
|
1727
|
+
logger_1.logger?.warn("SAP config not available at initialization, will use runtime config", {
|
|
1728
|
+
type: "CONFIG_DEFERRED",
|
|
1729
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1730
|
+
});
|
|
1731
|
+
// Set a placeholder that will be replaced
|
|
1732
|
+
this.sapConfig = { url: "http://placeholder", authType: "jwt", jwtToken: "placeholder" };
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
this.transportConfig = options?.transportConfig ?? parseTransportConfig();
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1739
|
+
// Always write error to stderr (stderr is safe even in stdio mode)
|
|
1740
|
+
logger_1.logger?.error("Failed to parse transport configuration", {
|
|
1741
|
+
type: "TRANSPORT_CONFIG_ERROR",
|
|
1742
|
+
error: message,
|
|
1743
|
+
});
|
|
1744
|
+
process.stderr.write(`[MCP] ✗ ERROR: Failed to parse transport configuration: ${message}\n`);
|
|
1745
|
+
if (this.allowProcessExit) {
|
|
1746
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
1747
|
+
if (process.platform === 'win32') {
|
|
1748
|
+
setTimeout(() => process.exit(1), 100);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
process.exit(1);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
throw error instanceof Error ? error : new Error(message);
|
|
1755
|
+
}
|
|
1756
|
+
// Create McpServer (for all transports)
|
|
1757
|
+
this.mcpServer = new mcp_js_1.McpServer({
|
|
1758
|
+
name: "mcp-abap-adt",
|
|
1759
|
+
version: "0.1.0"
|
|
1760
|
+
});
|
|
1761
|
+
// AuthBroker will be initialized lazily when needed (per destination)
|
|
1762
|
+
// Only for HTTP/streamable-http transport (not for stdio or SSE)
|
|
1763
|
+
const isHttpTransport = this.transportConfig.type === "streamable-http";
|
|
1764
|
+
if (isHttpTransport && !useAuthBroker) {
|
|
1765
|
+
// Only initialize if --auth-broker flag is NOT set (for stdio/SSE, ignore the flag)
|
|
1766
|
+
// For stdio/SSE, --auth-broker flag is ignored
|
|
1767
|
+
}
|
|
1768
|
+
else if (isHttpTransport && useAuthBroker) {
|
|
1769
|
+
// Support DEBUG_AUTH_BROKER as alias for DEBUG_AUTH_LOG
|
|
1770
|
+
// If DEBUG_AUTH_BROKER is set, ensure DEBUG_AUTH_LOG is also set for auth-broker package
|
|
1771
|
+
if (process.env.DEBUG_AUTH_BROKER === "true" && !process.env.DEBUG_AUTH_LOG) {
|
|
1772
|
+
process.env.DEBUG_AUTH_LOG = "true";
|
|
1773
|
+
}
|
|
1774
|
+
// Get paths for service keys and sessions
|
|
1775
|
+
// Use authBrokerPath if provided, otherwise use default platform paths
|
|
1776
|
+
// If authBrokerPath is provided, it's the base path - service-keys and sessions will be subdirectories
|
|
1777
|
+
const customPath = authBrokerPath ? path_1.default.resolve(authBrokerPath.replace(/^~/, os.homedir())) : undefined;
|
|
1778
|
+
const serviceKeysPaths = (0, platformPaths_1.getPlatformPaths)(customPath, 'service-keys');
|
|
1779
|
+
const sessionsPaths = (0, platformPaths_1.getPlatformPaths)(customPath, 'sessions');
|
|
1780
|
+
// Create directories if they don't exist
|
|
1781
|
+
const fs = require('fs');
|
|
1782
|
+
const serviceKeysDir = serviceKeysPaths[0]; // First path is where we save
|
|
1783
|
+
const sessionsDir = sessionsPaths[0]; // First path is where we save
|
|
1784
|
+
if (!fs.existsSync(serviceKeysDir)) {
|
|
1785
|
+
fs.mkdirSync(serviceKeysDir, { recursive: true });
|
|
1786
|
+
logger_1.logger?.info("Created service keys directory", {
|
|
1787
|
+
type: "SERVICE_KEYS_DIR_CREATED",
|
|
1788
|
+
path: serviceKeysDir,
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
1792
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
1793
|
+
logger_1.logger?.info("Created sessions directory", {
|
|
1794
|
+
type: "SESSIONS_DIR_CREATED",
|
|
1795
|
+
path: sessionsDir,
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
logger_1.logger?.info("AuthBroker will be initialized lazily when destination is needed", {
|
|
1799
|
+
type: "AUTH_BROKER_LAZY_INIT",
|
|
1800
|
+
transport: this.transportConfig.type,
|
|
1801
|
+
useAuthBrokerFlag: useAuthBroker,
|
|
1802
|
+
hasEnvFile: !!envFilePath,
|
|
1803
|
+
authBrokerPath: customPath || 'default',
|
|
1804
|
+
});
|
|
1805
|
+
// Print paths information with stars and empty lines
|
|
1806
|
+
console.log('');
|
|
1807
|
+
console.log('********************************************************************************');
|
|
1808
|
+
console.log('* AuthBroker Storage Paths:');
|
|
1809
|
+
console.log('*');
|
|
1810
|
+
console.log('* Service Keys (searched in order):');
|
|
1811
|
+
serviceKeysPaths.forEach((p, i) => {
|
|
1812
|
+
console.log(`* ${i + 1}. ${p}`);
|
|
1813
|
+
});
|
|
1814
|
+
console.log('*');
|
|
1815
|
+
console.log('* Sessions (saved to):');
|
|
1816
|
+
sessionsPaths.forEach((p, i) => {
|
|
1817
|
+
console.log(`* ${i + 1}. ${p}`);
|
|
1818
|
+
});
|
|
1819
|
+
console.log('********************************************************************************');
|
|
1820
|
+
console.log('');
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
logger_1.logger?.info("AuthBroker not available - not needed for this transport type", {
|
|
1824
|
+
type: "AUTH_BROKER_SKIPPED",
|
|
1825
|
+
transport: this.transportConfig.type,
|
|
1826
|
+
reason: "AuthBroker is only used for HTTP/streamable-http transport. For stdio/SSE, use .env file instead.",
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
if (this.transportConfig.type === "streamable-http") {
|
|
1830
|
+
logger_1.logger?.info("Transport configured", {
|
|
1831
|
+
type: "TRANSPORT_CONFIG",
|
|
1832
|
+
transport: this.transportConfig.type,
|
|
1833
|
+
host: this.transportConfig.host,
|
|
1834
|
+
port: this.transportConfig.port,
|
|
1835
|
+
enableJsonResponse: this.transportConfig.enableJsonResponse,
|
|
1836
|
+
allowedOrigins: this.transportConfig.allowedOrigins ?? [],
|
|
1837
|
+
allowedHosts: this.transportConfig.allowedHosts ?? [],
|
|
1838
|
+
enableDnsRebindingProtection: this.transportConfig.enableDnsRebindingProtection,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
else if (this.transportConfig.type === "sse") {
|
|
1842
|
+
logger_1.logger?.info("Transport configured", {
|
|
1843
|
+
type: "TRANSPORT_CONFIG",
|
|
1844
|
+
transport: this.transportConfig.type,
|
|
1845
|
+
host: this.transportConfig.host,
|
|
1846
|
+
port: this.transportConfig.port,
|
|
1847
|
+
allowedOrigins: this.transportConfig.allowedOrigins ?? [],
|
|
1848
|
+
allowedHosts: this.transportConfig.allowedHosts ?? [],
|
|
1849
|
+
enableDnsRebindingProtection: this.transportConfig.enableDnsRebindingProtection,
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
else {
|
|
1853
|
+
logger_1.logger?.info("Transport configured", {
|
|
1854
|
+
type: "TRANSPORT_CONFIG",
|
|
1855
|
+
transport: this.transportConfig.type,
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Creates AbapConnection from available sources (headers, broker, or .env)
|
|
1861
|
+
* @param headers - Optional HTTP headers containing connection info
|
|
1862
|
+
* @param sessionId - Optional session ID
|
|
1863
|
+
* @param destination - Optional destination name for broker-based auth
|
|
1864
|
+
* @returns AbapConnection instance
|
|
1865
|
+
* @private
|
|
1866
|
+
*/
|
|
1867
|
+
async getOrCreateConnectionForServer(headers, sessionId, destination) {
|
|
1868
|
+
// Extract destination from headers if not provided
|
|
1869
|
+
let actualDestination = destination;
|
|
1870
|
+
if (!actualDestination && headers) {
|
|
1871
|
+
actualDestination = headers[interfaces_1.HEADER_MCP_DESTINATION] ||
|
|
1872
|
+
headers['X-MCP-Destination'] ||
|
|
1873
|
+
headers['x-mcp-destination'];
|
|
1874
|
+
}
|
|
1875
|
+
// Use default destination if still not set
|
|
1876
|
+
if (!actualDestination && this.defaultDestination) {
|
|
1877
|
+
actualDestination = this.defaultDestination;
|
|
1878
|
+
logger_1.logger?.debug('Using default destination for connection', {
|
|
1879
|
+
destination: actualDestination,
|
|
1880
|
+
type: 'DEFAULT_DESTINATION_USED',
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
logger_1.logger?.info("Creating connection for server", {
|
|
1884
|
+
type: "CONNECTION_CREATION_START",
|
|
1885
|
+
hasHeaders: !!headers,
|
|
1886
|
+
sessionId: sessionId || 'not-provided',
|
|
1887
|
+
destination: actualDestination || 'not-provided',
|
|
1888
|
+
});
|
|
1889
|
+
// Try to get connection from session context first
|
|
1890
|
+
const context = utils_1.sessionContext.getStore();
|
|
1891
|
+
if (context?.sapConfig) {
|
|
1892
|
+
logger_1.logger?.info("Using connection from session context", {
|
|
1893
|
+
type: "CONNECTION_FROM_SESSION_CONTEXT",
|
|
1894
|
+
sessionId: sessionId || 'not-provided',
|
|
1895
|
+
url: context.sapConfig.url,
|
|
1896
|
+
authType: context.sapConfig.authType,
|
|
1897
|
+
});
|
|
1898
|
+
const sessionConnection = (0, connection_1.createAbapConnection)(context.sapConfig, loggerAdapter_1.loggerAdapter, sessionId || `mcp-server-${(0, crypto_1.randomUUID)()}`);
|
|
1899
|
+
return sessionConnection;
|
|
1900
|
+
}
|
|
1901
|
+
// If headers provided, create connection from headers
|
|
1902
|
+
if (headers && this.hasSapHeaders(headers)) {
|
|
1903
|
+
// Get config from headers (already processed by applyAuthHeaders)
|
|
1904
|
+
const config = this.sapConfig;
|
|
1905
|
+
if (config && config.url !== "http://placeholder" && config.url !== "http://injected-connection") {
|
|
1906
|
+
// If destination is known, try to refresh token via auth broker before creating connection
|
|
1907
|
+
if (actualDestination) {
|
|
1908
|
+
try {
|
|
1909
|
+
const brokerForHeaders = await this.getOrCreateAuthBroker(actualDestination, sessionId || 'global');
|
|
1910
|
+
if (brokerForHeaders) {
|
|
1911
|
+
const freshToken = await brokerForHeaders.getToken(actualDestination);
|
|
1912
|
+
if (freshToken && config) {
|
|
1913
|
+
config.authorizationToken = freshToken;
|
|
1914
|
+
}
|
|
1915
|
+
const { registerAuthBroker } = require('./lib/utils');
|
|
1916
|
+
registerAuthBroker(actualDestination, brokerForHeaders);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
catch (e) {
|
|
1920
|
+
logger_1.logger?.warn?.("Auth broker refresh failed for header connection", {
|
|
1921
|
+
type: "CONNECTION_FROM_HEADERS_BROKER_WARN",
|
|
1922
|
+
error: e instanceof Error ? e.message : String(e),
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
logger_1.logger?.info("Using connection from headers", {
|
|
1927
|
+
type: "CONNECTION_FROM_HEADERS",
|
|
1928
|
+
sessionId: sessionId || 'not-provided',
|
|
1929
|
+
url: config.url,
|
|
1930
|
+
authType: config.authType,
|
|
1931
|
+
});
|
|
1932
|
+
let connection = (0, connection_1.createAbapConnection)(config, loggerAdapter_1.loggerAdapter, sessionId || `mcp-server-${(0, crypto_1.randomUUID)()}`);
|
|
1933
|
+
if (actualDestination) {
|
|
1934
|
+
const { createDestinationAwareConnection } = require('./lib/utils');
|
|
1935
|
+
connection = createDestinationAwareConnection(connection, actualDestination);
|
|
1936
|
+
}
|
|
1937
|
+
return connection;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
// If destination provided, create connection via broker
|
|
1941
|
+
if (actualDestination) {
|
|
1942
|
+
try {
|
|
1943
|
+
logger_1.logger?.info("Attempting to create connection via auth broker", {
|
|
1944
|
+
type: "CONNECTION_VIA_BROKER_ATTEMPT",
|
|
1945
|
+
destination: actualDestination,
|
|
1946
|
+
sessionId: sessionId || 'not-provided',
|
|
1947
|
+
});
|
|
1948
|
+
const authBroker = await this.getOrCreateAuthBroker(actualDestination, sessionId || 'global');
|
|
1949
|
+
if (authBroker) {
|
|
1950
|
+
const connConfig = await authBroker.getConnectionConfig(actualDestination);
|
|
1951
|
+
if (connConfig?.serviceUrl) {
|
|
1952
|
+
// Get fresh token now to ensure connection can be created
|
|
1953
|
+
const jwtToken = await authBroker.getToken(actualDestination);
|
|
1954
|
+
if (jwtToken) {
|
|
1955
|
+
// Register AuthBroker in global registry for connection to use during token refresh
|
|
1956
|
+
const { registerAuthBroker } = require('./lib/utils');
|
|
1957
|
+
registerAuthBroker(actualDestination, authBroker);
|
|
1958
|
+
const config = {
|
|
1959
|
+
url: connConfig.serviceUrl,
|
|
1960
|
+
authType: "jwt",
|
|
1961
|
+
jwtToken,
|
|
1962
|
+
client: connConfig.sapClient,
|
|
1963
|
+
};
|
|
1964
|
+
logger_1.logger?.info("Using connection from auth broker", {
|
|
1965
|
+
type: "CONNECTION_FROM_BROKER",
|
|
1966
|
+
destination: actualDestination,
|
|
1967
|
+
sessionId: sessionId || 'not-provided',
|
|
1968
|
+
url: config.url,
|
|
1969
|
+
authType: config.authType,
|
|
1970
|
+
});
|
|
1971
|
+
// Create connection and wrap it to intercept refreshToken()/makeAdtRequest for destination-based authentication
|
|
1972
|
+
let connection = (0, connection_1.createAbapConnection)(config, loggerAdapter_1.loggerAdapter, sessionId || `mcp-server-${(0, crypto_1.randomUUID)()}`);
|
|
1973
|
+
// Wrap connection to intercept refreshToken()/makeAdtRequest
|
|
1974
|
+
const { createDestinationAwareConnection } = require('./lib/utils');
|
|
1975
|
+
connection = createDestinationAwareConnection(connection, actualDestination);
|
|
1976
|
+
return connection;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
catch (error) {
|
|
1982
|
+
logger_1.logger?.warn("Failed to create connection from destination", {
|
|
1983
|
+
type: "CONNECTION_FROM_DESTINATION_FAILED",
|
|
1984
|
+
destination: actualDestination,
|
|
1985
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
// Fallback to default config (from .env or constructor)
|
|
1990
|
+
// BUT: Don't use .env fallback if we have defaultDestination (means we're using auth-broker)
|
|
1991
|
+
// This prevents .env from being used when --mcp is specified
|
|
1992
|
+
if (this.sapConfig &&
|
|
1993
|
+
this.sapConfig.url !== "http://placeholder" &&
|
|
1994
|
+
this.sapConfig.url !== "http://injected-connection" &&
|
|
1995
|
+
!this.defaultDestination) { // Don't fallback to .env if using auth-broker
|
|
1996
|
+
logger_1.logger?.info("Using connection from default config (.env or constructor)", {
|
|
1997
|
+
type: "CONNECTION_FROM_DEFAULT_CONFIG",
|
|
1998
|
+
sessionId: sessionId || 'not-provided',
|
|
1999
|
+
url: this.sapConfig.url,
|
|
2000
|
+
authType: this.sapConfig.authType,
|
|
2001
|
+
});
|
|
2002
|
+
return (0, connection_1.createAbapConnection)(this.sapConfig, loggerAdapter_1.loggerAdapter, sessionId || `mcp-server-${(0, crypto_1.randomUUID)()}`);
|
|
2003
|
+
}
|
|
2004
|
+
logger_1.logger?.error("Unable to create connection: no valid configuration available", {
|
|
2005
|
+
type: "CONNECTION_CREATION_FAILED",
|
|
2006
|
+
hasHeaders: !!headers,
|
|
2007
|
+
sessionId: sessionId || 'not-provided',
|
|
2008
|
+
destination: actualDestination || 'not-provided',
|
|
2009
|
+
defaultDestination: this.defaultDestination || 'not-set',
|
|
2010
|
+
hasSapConfig: !!this.sapConfig,
|
|
2011
|
+
sapConfigUrl: this.sapConfig?.url || 'not-set',
|
|
2012
|
+
});
|
|
2013
|
+
throw new Error("Unable to create connection: no valid configuration available");
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Creates a new McpServer instance with all handlers registered
|
|
2017
|
+
* Used for SSE sessions where each session needs its own server instance
|
|
2018
|
+
* @param context - HandlerContext instance to use for handlers
|
|
2019
|
+
* @private
|
|
2020
|
+
*/
|
|
2021
|
+
createMcpServerForSession(context) {
|
|
2022
|
+
const server = new mcp_js_1.McpServer({
|
|
2023
|
+
name: "mcp-abap-adt",
|
|
2024
|
+
version: "0.1.0"
|
|
2025
|
+
});
|
|
2026
|
+
// Register all tools using McpHandlers
|
|
2027
|
+
const handlers = new mcp_handlers_1.McpHandlers();
|
|
2028
|
+
handlers.RegisterAllToolsOnServer(server, context);
|
|
2029
|
+
return server;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Sets up handlers for new McpServer using registerTool (recommended API)
|
|
2033
|
+
* @param context - HandlerContext with connection and logger
|
|
2034
|
+
* @private
|
|
2035
|
+
*/
|
|
2036
|
+
setupMcpServerHandlers(context) {
|
|
2037
|
+
// Connection is already wrapped with token refresh in run() method
|
|
2038
|
+
// Just register handlers with the provided context
|
|
2039
|
+
this.mcpHandlers.RegisterAllToolsOnServer(this.mcpServer, context);
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Creates a wrapper connection that refreshes token before each request
|
|
2043
|
+
* @private
|
|
2044
|
+
*/
|
|
2045
|
+
createConnectionWithTokenRefresh(connection, destination) {
|
|
2046
|
+
const connectionWithRefresh = connection;
|
|
2047
|
+
// Wrap makeAdtRequest to refresh token before each request
|
|
2048
|
+
if (connectionWithRefresh.makeAdtRequest) {
|
|
2049
|
+
const originalMakeAdtRequest = connectionWithRefresh.makeAdtRequest.bind(connection);
|
|
2050
|
+
connectionWithRefresh.makeAdtRequest = async function (options) {
|
|
2051
|
+
// Always refresh token via AuthBroker before request (AuthBroker will refresh if needed)
|
|
2052
|
+
const { getAuthBroker } = require('./lib/utils');
|
|
2053
|
+
const authBroker = getAuthBroker(destination);
|
|
2054
|
+
logger_1.logger?.debug('makeAdtRequest called, checking AuthBroker', {
|
|
2055
|
+
destination,
|
|
2056
|
+
hasAuthBroker: !!authBroker,
|
|
2057
|
+
method: options?.method,
|
|
2058
|
+
url: options?.url,
|
|
2059
|
+
});
|
|
2060
|
+
if (!authBroker) {
|
|
2061
|
+
logger_1.logger?.warn('AuthBroker not found for destination, cannot refresh token', {
|
|
2062
|
+
destination,
|
|
2063
|
+
type: 'AUTH_BROKER_NOT_FOUND',
|
|
2064
|
+
});
|
|
2065
|
+
// Continue without refresh - will use existing token
|
|
2066
|
+
return await originalMakeAdtRequest(options);
|
|
2067
|
+
}
|
|
2068
|
+
try {
|
|
2069
|
+
// Get fresh token from AuthBroker (will refresh automatically if expired)
|
|
2070
|
+
logger_1.logger?.debug('Requesting fresh token from AuthBroker', {
|
|
2071
|
+
destination,
|
|
2072
|
+
type: 'TOKEN_REFRESH_REQUEST',
|
|
2073
|
+
});
|
|
2074
|
+
const freshToken = await authBroker.getToken(destination);
|
|
2075
|
+
const config = connectionWithRefresh.getConfig();
|
|
2076
|
+
if (config) {
|
|
2077
|
+
// Always update token (AuthBroker.getToken() returns fresh token)
|
|
2078
|
+
const tokenChanged = config.jwtToken !== freshToken;
|
|
2079
|
+
const oldToken = config.jwtToken;
|
|
2080
|
+
logger_1.logger?.debug('Updating connection config with fresh token', {
|
|
2081
|
+
destination,
|
|
2082
|
+
tokenChanged,
|
|
2083
|
+
oldTokenPreview: oldToken ? oldToken.substring(0, 20) + '...' : 'none',
|
|
2084
|
+
newTokenPreview: freshToken.substring(0, 20) + '...',
|
|
2085
|
+
type: 'TOKEN_UPDATE',
|
|
2086
|
+
});
|
|
2087
|
+
config.jwtToken = freshToken;
|
|
2088
|
+
// Verify token was updated
|
|
2089
|
+
const verifyConfig = connectionWithRefresh.getConfig();
|
|
2090
|
+
if (verifyConfig.jwtToken !== freshToken) {
|
|
2091
|
+
logger_1.logger?.error('Token update failed - config.jwtToken does not match fresh token', {
|
|
2092
|
+
destination,
|
|
2093
|
+
expectedPreview: freshToken.substring(0, 20) + '...',
|
|
2094
|
+
actualPreview: verifyConfig.jwtToken ? verifyConfig.jwtToken.substring(0, 20) + '...' : 'none',
|
|
2095
|
+
type: 'TOKEN_UPDATE_FAILED',
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
// Get refresh token from session store if available
|
|
2099
|
+
try {
|
|
2100
|
+
const { authorizationConfig } = await authBroker.getConnectionConfig(destination);
|
|
2101
|
+
if (authorizationConfig?.refreshToken) {
|
|
2102
|
+
config.refreshToken = authorizationConfig.refreshToken;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
catch (configError) {
|
|
2106
|
+
// Ignore errors getting refresh token (not critical)
|
|
2107
|
+
logger_1.logger?.debug('Could not get refresh token from AuthBroker', {
|
|
2108
|
+
destination,
|
|
2109
|
+
error: configError instanceof Error ? configError.message : String(configError),
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
// Reset connection only if token changed to ensure fresh token is used
|
|
2113
|
+
// This ensures that buildAuthorizationHeader() will use the new token
|
|
2114
|
+
// Don't reset if token didn't change to preserve CSRF token and cookies
|
|
2115
|
+
if (tokenChanged) {
|
|
2116
|
+
connectionWithRefresh.reset();
|
|
2117
|
+
logger_1.logger?.info('Token refreshed via AuthBroker before request', {
|
|
2118
|
+
destination,
|
|
2119
|
+
tokenPreview: freshToken.substring(0, 20) + '...',
|
|
2120
|
+
oldTokenPreview: oldToken ? oldToken.substring(0, 20) + '...' : 'none',
|
|
2121
|
+
type: 'TOKEN_REFRESHED_BEFORE_REQUEST',
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
else {
|
|
2125
|
+
// Token didn't change - no need to reset, connection can reuse CSRF token and cookies
|
|
2126
|
+
logger_1.logger?.debug('Token verified via AuthBroker before request (no change, keeping session)', {
|
|
2127
|
+
destination,
|
|
2128
|
+
tokenPreview: freshToken.substring(0, 20) + '...',
|
|
2129
|
+
type: 'TOKEN_VERIFIED_BEFORE_REQUEST',
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
logger_1.logger?.warn('Connection config not available, cannot update token', {
|
|
2135
|
+
destination,
|
|
2136
|
+
type: 'CONNECTION_CONFIG_NOT_AVAILABLE',
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
catch (error) {
|
|
2141
|
+
logger_1.logger?.error('Failed to refresh token before request', {
|
|
2142
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2143
|
+
destination,
|
|
2144
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
2145
|
+
type: 'TOKEN_REFRESH_FAILED',
|
|
2146
|
+
});
|
|
2147
|
+
// Continue with existing token - will try to use it, but may fail if expired
|
|
2148
|
+
}
|
|
2149
|
+
// Call original makeAdtRequest (with potentially refreshed token)
|
|
2150
|
+
try {
|
|
2151
|
+
return await originalMakeAdtRequest(options);
|
|
2152
|
+
}
|
|
2153
|
+
catch (error) {
|
|
2154
|
+
// If we still get 401/403 or "JWT token has expired" error, try to refresh token again
|
|
2155
|
+
const isAuthError = error?.response?.status === 401 || error?.response?.status === 403;
|
|
2156
|
+
const isExpiredTokenError = error?.message?.includes('JWT token has expired') ||
|
|
2157
|
+
error?.message?.includes('Please re-authenticate');
|
|
2158
|
+
if ((isAuthError || isExpiredTokenError) && authBroker) {
|
|
2159
|
+
// Check if this is a permissions error, not an auth error
|
|
2160
|
+
const responseData = error?.response?.data;
|
|
2161
|
+
const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
|
|
2162
|
+
if (responseText.includes("ExceptionResourceNoAccess") ||
|
|
2163
|
+
responseText.includes("No authorization") ||
|
|
2164
|
+
responseText.includes("Missing authorization")) {
|
|
2165
|
+
// Not an auth token issue, re-throw
|
|
2166
|
+
throw error;
|
|
2167
|
+
}
|
|
2168
|
+
// Try to refresh token again and retry
|
|
2169
|
+
try {
|
|
2170
|
+
logger_1.logger?.info('Got 401/403 after token refresh, attempting to refresh token again', {
|
|
2171
|
+
destination,
|
|
2172
|
+
error: error?.message,
|
|
2173
|
+
type: 'TOKEN_REFRESH_RETRY',
|
|
2174
|
+
});
|
|
2175
|
+
const freshToken = await authBroker.getToken(destination);
|
|
2176
|
+
const config = connectionWithRefresh.getConfig();
|
|
2177
|
+
if (config) {
|
|
2178
|
+
config.jwtToken = freshToken;
|
|
2179
|
+
// Get refresh token from session store if available
|
|
2180
|
+
try {
|
|
2181
|
+
const { authorizationConfig } = await authBroker.getConnectionConfig(destination);
|
|
2182
|
+
if (authorizationConfig?.refreshToken) {
|
|
2183
|
+
config.refreshToken = authorizationConfig.refreshToken;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
catch (configError) {
|
|
2187
|
+
// Ignore errors getting refresh token
|
|
2188
|
+
}
|
|
2189
|
+
// Reset connection to use new token
|
|
2190
|
+
connectionWithRefresh.reset();
|
|
2191
|
+
logger_1.logger?.info('Token refreshed again, retrying request', {
|
|
2192
|
+
destination,
|
|
2193
|
+
tokenPreview: freshToken.substring(0, 20) + '...',
|
|
2194
|
+
type: 'TOKEN_REFRESHED_RETRY',
|
|
2195
|
+
});
|
|
2196
|
+
// Retry the request with new token
|
|
2197
|
+
return await originalMakeAdtRequest(options);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
catch (refreshError) {
|
|
2201
|
+
logger_1.logger?.error('Failed to refresh token on retry', {
|
|
2202
|
+
destination,
|
|
2203
|
+
error: refreshError instanceof Error ? refreshError.message : String(refreshError),
|
|
2204
|
+
type: 'TOKEN_REFRESH_RETRY_FAILED',
|
|
2205
|
+
});
|
|
2206
|
+
// Re-throw original error if refresh fails
|
|
2207
|
+
throw error;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
// Re-throw error if not handled
|
|
2211
|
+
throw error;
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
return connectionWithRefresh;
|
|
2216
|
+
}
|
|
2217
|
+
setupSignalHandlers() {
|
|
2218
|
+
const signals = ["SIGINT", "SIGTERM"];
|
|
2219
|
+
for (const signal of signals) {
|
|
2220
|
+
process.on(signal, () => {
|
|
2221
|
+
if (this.shuttingDown) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
this.shuttingDown = true;
|
|
2225
|
+
logger_1.logger?.info("Received shutdown signal", {
|
|
2226
|
+
type: "SERVER_SHUTDOWN_SIGNAL",
|
|
2227
|
+
signal,
|
|
2228
|
+
transport: this.transportConfig.type,
|
|
2229
|
+
});
|
|
2230
|
+
void this.shutdown().finally(() => {
|
|
2231
|
+
if (this.allowProcessExit) {
|
|
2232
|
+
process.exit(0);
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
async shutdown() {
|
|
2239
|
+
try {
|
|
2240
|
+
await this.mcpServer.close();
|
|
2241
|
+
}
|
|
2242
|
+
catch (error) {
|
|
2243
|
+
logger_1.logger?.error("Failed to close MCP server", {
|
|
2244
|
+
type: "SERVER_SHUTDOWN_ERROR",
|
|
2245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
// Close all SSE sessions
|
|
2249
|
+
for (const [sessionId, session] of this.sseSessions.entries()) {
|
|
2250
|
+
try {
|
|
2251
|
+
await session.transport.close();
|
|
2252
|
+
session.server.server.close();
|
|
2253
|
+
logger_1.logger?.debug("SSE session closed during shutdown", {
|
|
2254
|
+
type: "SSE_SESSION_SHUTDOWN",
|
|
2255
|
+
sessionId,
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
catch (error) {
|
|
2259
|
+
logger_1.logger?.error("Failed to close SSE session", {
|
|
2260
|
+
type: "SSE_SHUTDOWN_ERROR",
|
|
2261
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2262
|
+
sessionId,
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
this.sseSessions.clear();
|
|
2267
|
+
if (this.httpServer) {
|
|
2268
|
+
await new Promise((resolve) => {
|
|
2269
|
+
this.httpServer?.close((closeError) => {
|
|
2270
|
+
if (closeError) {
|
|
2271
|
+
logger_1.logger?.error("Failed to close HTTP server", {
|
|
2272
|
+
type: "HTTP_SERVER_SHUTDOWN_ERROR",
|
|
2273
|
+
error: closeError instanceof Error ? closeError.message : String(closeError),
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
resolve();
|
|
2277
|
+
});
|
|
2278
|
+
});
|
|
2279
|
+
this.httpServer = undefined;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Starts the MCP server and connects it to the transport.
|
|
2284
|
+
*/
|
|
2285
|
+
async run() {
|
|
2286
|
+
if (this.transportConfig.type === "stdio") {
|
|
2287
|
+
// For stdio: must have destination or .env file (error if neither exists)
|
|
2288
|
+
if (!this.defaultMcpDestination && !this.hasEnvFile) {
|
|
2289
|
+
const errorMsg = "stdio transport requires either --mcp parameter or .env file";
|
|
2290
|
+
logger_1.logger?.error(errorMsg, {
|
|
2291
|
+
type: "STDIO_NO_DESTINATION_ERROR",
|
|
2292
|
+
});
|
|
2293
|
+
process.stderr.write(`[MCP] ✗ ERROR: ${errorMsg}\n`);
|
|
2294
|
+
process.stderr.write(`[MCP] Please provide --mcp=<destination> parameter or ensure .env file exists in current directory.\n`);
|
|
2295
|
+
if (this.allowProcessExit) {
|
|
2296
|
+
process.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
throw new Error(errorMsg);
|
|
2299
|
+
}
|
|
2300
|
+
// Initialize default broker (creates broker with default destination from --mcp or .env)
|
|
2301
|
+
await this.initializeDefaultBroker();
|
|
2302
|
+
// Create connection for stdio mode
|
|
2303
|
+
// Note: getOrCreateConnectionForServer already wraps connection via createDestinationAwareConnection
|
|
2304
|
+
// which handles token refresh on 401/403 errors. We also wrap it to refresh token before each request.
|
|
2305
|
+
const connection = await this.getOrCreateConnectionForServer(undefined, // no headers in stdio
|
|
2306
|
+
'stdio-session', this.defaultDestination);
|
|
2307
|
+
// Additional wrapper to refresh token BEFORE each request (not just on error)
|
|
2308
|
+
// This ensures token is always fresh, preventing 401/403 errors
|
|
2309
|
+
const wrappedConnection = this.defaultDestination
|
|
2310
|
+
? this.createConnectionWithTokenRefresh(connection, this.defaultDestination)
|
|
2311
|
+
: connection;
|
|
2312
|
+
logger_1.logger?.debug('Connection created for stdio', {
|
|
2313
|
+
destination: this.defaultDestination || 'none',
|
|
2314
|
+
hasTokenRefresh: !!this.defaultDestination,
|
|
2315
|
+
});
|
|
2316
|
+
// Setup handlers with connection
|
|
2317
|
+
this.setupMcpServerHandlers({ connection: wrappedConnection, logger: loggerAdapter_1.loggerAdapter });
|
|
2318
|
+
// Simple stdio setup like reference implementation
|
|
2319
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
2320
|
+
await this.mcpServer.server.connect(transport);
|
|
2321
|
+
// Process stays alive waiting for messages from stdin
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
if (this.transportConfig.type === "streamable-http") {
|
|
2325
|
+
const httpConfig = this.transportConfig;
|
|
2326
|
+
// Initialize default broker for HTTP transport (creates broker with default destination from --mcp)
|
|
2327
|
+
await this.initializeDefaultBroker();
|
|
2328
|
+
// HTTP Server wrapper for StreamableHTTP transport (like the SDK example)
|
|
2329
|
+
const httpServer = (0, http_1.createServer)(async (req, res) => {
|
|
2330
|
+
// Log incoming HTTP request (if debug enabled)
|
|
2331
|
+
const debugHttpEnabled = process.env.DEBUG_HTTP_REQUESTS === "true" || process.env.DEBUG_CONNECTORS === "true";
|
|
2332
|
+
if (debugHttpEnabled) {
|
|
2333
|
+
const sanitizedHeaders = {};
|
|
2334
|
+
const sensitiveKeys = [interfaces_1.HEADER_AUTHORIZATION, interfaces_1.HEADER_SAP_JWT_TOKEN, interfaces_1.HEADER_SAP_REFRESH_TOKEN, interfaces_1.HEADER_SAP_PASSWORD, interfaces_1.HEADER_SAP_UAA_CLIENT_SECRET];
|
|
2335
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
2336
|
+
if (sensitiveKeys.includes(key.toLowerCase())) {
|
|
2337
|
+
sanitizedHeaders[key] = '[REDACTED]';
|
|
2338
|
+
}
|
|
2339
|
+
else {
|
|
2340
|
+
sanitizedHeaders[key] = Array.isArray(value) ? value.join(', ') : (value || '');
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
logger_1.logger?.info("HTTP Request received", {
|
|
2344
|
+
type: "HTTP_REQUEST",
|
|
2345
|
+
method: req.method,
|
|
2346
|
+
url: req.url,
|
|
2347
|
+
headers: sanitizedHeaders,
|
|
2348
|
+
remoteAddress: req.socket.remoteAddress,
|
|
2349
|
+
remotePort: req.socket.remotePort,
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
// Only handle POST requests (like the example)
|
|
2353
|
+
if (req.method !== "POST") {
|
|
2354
|
+
if (debugHttpEnabled) {
|
|
2355
|
+
logger_1.logger?.warn("HTTP Method not allowed", {
|
|
2356
|
+
type: "HTTP_METHOD_NOT_ALLOWED",
|
|
2357
|
+
method: req.method,
|
|
2358
|
+
url: req.url,
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
2362
|
+
res.end("Method not allowed");
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
// HTTP: Restrict non-local connections if .env file exists and no SAP headers provided
|
|
2366
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
2367
|
+
if (this.hasEnvFile && !this.hasSapHeaders(req.headers)) {
|
|
2368
|
+
if (!this.isLocalConnection(remoteAddress)) {
|
|
2369
|
+
logger_1.logger?.warn("HTTP: Non-local connection rejected (has .env but no SAP headers)", {
|
|
2370
|
+
type: "HTTP_NON_LOCAL_REJECTED",
|
|
2371
|
+
remoteAddress,
|
|
2372
|
+
hasEnvFile: this.hasEnvFile,
|
|
2373
|
+
});
|
|
2374
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2375
|
+
res.end(`Forbidden: Non-local connections require SAP connection headers (${interfaces_1.HEADER_SAP_URL}, ${interfaces_1.HEADER_SAP_AUTH_TYPE})`);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
// Track client (like the example)
|
|
2380
|
+
const clientID = `${req.socket.remoteAddress}:${req.socket.remotePort}`;
|
|
2381
|
+
logger_1.logger?.debug("Client connected", {
|
|
2382
|
+
type: "STREAMABLE_HTTP_CLIENT_CONNECTED",
|
|
2383
|
+
clientID,
|
|
2384
|
+
});
|
|
2385
|
+
// Extract session ID from headers (like the example)
|
|
2386
|
+
const clientSessionId = (req.headers["x-session-id"] || req.headers["mcp-session-id"]);
|
|
2387
|
+
let session = this.streamableHttpSessions.get(clientID);
|
|
2388
|
+
if (session && !this.streamableSessionIndex.has(session.sessionId)) {
|
|
2389
|
+
this.streamableSessionIndex.set(session.sessionId, clientID);
|
|
2390
|
+
}
|
|
2391
|
+
// If client sent session ID, try to find existing session
|
|
2392
|
+
if (clientSessionId && !session) {
|
|
2393
|
+
// Search for existing session by sessionId (client might have new IP:PORT)
|
|
2394
|
+
for (const [key, sess] of this.streamableHttpSessions.entries()) {
|
|
2395
|
+
if (sess.sessionId === clientSessionId) {
|
|
2396
|
+
session = sess;
|
|
2397
|
+
// Update clientID (port might have changed)
|
|
2398
|
+
this.streamableHttpSessions.delete(key);
|
|
2399
|
+
this.streamableHttpSessions.set(clientID, session);
|
|
2400
|
+
this.streamableSessionIndex.set(session.sessionId, clientID);
|
|
2401
|
+
logger_1.logger?.debug("Existing session restored", {
|
|
2402
|
+
type: "STREAMABLE_HTTP_SESSION_RESTORED",
|
|
2403
|
+
sessionId: session.sessionId,
|
|
2404
|
+
clientID,
|
|
2405
|
+
});
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
// If no session found, create new one
|
|
2411
|
+
if (!session) {
|
|
2412
|
+
session = {
|
|
2413
|
+
sessionId: (0, crypto_1.randomUUID)(),
|
|
2414
|
+
clientIP: req.socket.remoteAddress || "unknown",
|
|
2415
|
+
connectedAt: new Date(),
|
|
2416
|
+
requestCount: 0,
|
|
2417
|
+
};
|
|
2418
|
+
this.streamableHttpSessions.set(clientID, session);
|
|
2419
|
+
this.streamableSessionIndex.set(session.sessionId, clientID);
|
|
2420
|
+
logger_1.logger?.debug("New session created", {
|
|
2421
|
+
type: "STREAMABLE_HTTP_SESSION_CREATED",
|
|
2422
|
+
sessionId: session.sessionId,
|
|
2423
|
+
clientID,
|
|
2424
|
+
totalSessions: this.streamableHttpSessions.size,
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
session.requestCount++;
|
|
2428
|
+
logger_1.logger?.debug("Request received", {
|
|
2429
|
+
type: "STREAMABLE_HTTP_REQUEST",
|
|
2430
|
+
sessionId: session.sessionId,
|
|
2431
|
+
requestNumber: session.requestCount,
|
|
2432
|
+
clientID,
|
|
2433
|
+
});
|
|
2434
|
+
// Handle client disconnect (like the example)
|
|
2435
|
+
req.on("close", () => {
|
|
2436
|
+
const closedSession = this.streamableHttpSessions.get(clientID);
|
|
2437
|
+
if (closedSession) {
|
|
2438
|
+
// Clean up connection cache for this session
|
|
2439
|
+
// Include destination to ensure correct connection is removed
|
|
2440
|
+
if (closedSession.sapConfig) {
|
|
2441
|
+
(0, utils_1.removeConnectionForSession)(closedSession.sessionId, closedSession.sapConfig, closedSession.destination);
|
|
2442
|
+
}
|
|
2443
|
+
this.streamableHttpSessions.delete(clientID);
|
|
2444
|
+
this.streamableSessionIndex.delete(closedSession.sessionId);
|
|
2445
|
+
logger_1.logger?.debug("Session closed", {
|
|
2446
|
+
type: "STREAMABLE_HTTP_SESSION_CLOSED",
|
|
2447
|
+
sessionId: closedSession.sessionId,
|
|
2448
|
+
requestCount: closedSession.requestCount,
|
|
2449
|
+
totalSessions: this.streamableHttpSessions.size,
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
try {
|
|
2454
|
+
// Read request body first to check if this request requires SAP config
|
|
2455
|
+
let body = null;
|
|
2456
|
+
const chunks = [];
|
|
2457
|
+
for await (const chunk of req) {
|
|
2458
|
+
chunks.push(chunk);
|
|
2459
|
+
}
|
|
2460
|
+
if (chunks.length > 0) {
|
|
2461
|
+
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
2462
|
+
try {
|
|
2463
|
+
body = JSON.parse(bodyString);
|
|
2464
|
+
// Log MCP request (if debug enabled)
|
|
2465
|
+
const debugHttpEnabled = process.env.DEBUG_HTTP_REQUESTS === "true" || process.env.DEBUG_CONNECTORS === "true";
|
|
2466
|
+
if (debugHttpEnabled && body && typeof body === 'object') {
|
|
2467
|
+
const mcpMethod = body.method || body.jsonrpc ? 'JSON-RPC' : 'Unknown';
|
|
2468
|
+
const sanitizedParams = {};
|
|
2469
|
+
if (body.params && typeof body.params === 'object') {
|
|
2470
|
+
// Sanitize sensitive params
|
|
2471
|
+
for (const [key, value] of Object.entries(body.params)) {
|
|
2472
|
+
const lowerKey = key.toLowerCase();
|
|
2473
|
+
if (lowerKey.includes('password') || lowerKey.includes('token') || lowerKey.includes('secret')) {
|
|
2474
|
+
sanitizedParams[key] = '[REDACTED]';
|
|
2475
|
+
}
|
|
2476
|
+
else {
|
|
2477
|
+
sanitizedParams[key] = value;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
logger_1.logger?.info("MCP Request", {
|
|
2482
|
+
type: "MCP_REQUEST",
|
|
2483
|
+
method: mcpMethod,
|
|
2484
|
+
jsonrpc: body.jsonrpc,
|
|
2485
|
+
id: body.id,
|
|
2486
|
+
params: sanitizedParams,
|
|
2487
|
+
sessionId: session.sessionId,
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
catch (parseError) {
|
|
2492
|
+
// If body is not JSON, pass as string or null
|
|
2493
|
+
body = bodyString || null;
|
|
2494
|
+
if (debugHttpEnabled) {
|
|
2495
|
+
logger_1.logger?.warn("Failed to parse request body as JSON", {
|
|
2496
|
+
type: "HTTP_BODY_PARSE_ERROR",
|
|
2497
|
+
error: parseError instanceof Error ? parseError.message : String(parseError),
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
// Check if this is a request that requires SAP connection
|
|
2503
|
+
// Only tools/call requires SAP config - all other methods (tools/list, tools/get, initialize, ping, etc.) don't
|
|
2504
|
+
const isMethodRequest = body && typeof body === 'object' && body.method;
|
|
2505
|
+
const methodName = isMethodRequest ? String(body.method) : '';
|
|
2506
|
+
const requiresSapConfig = methodName === 'tools/call';
|
|
2507
|
+
// Apply auth headers before processing and store config in session
|
|
2508
|
+
// Apply headers if:
|
|
2509
|
+
// 1. This request requires SAP config (tools/call)
|
|
2510
|
+
// 2. AND (auth headers are present OR defaultMcpDestination is set)
|
|
2511
|
+
// If no headers are present and no defaultMcpDestination, fall back to .env file or base config
|
|
2512
|
+
if (requiresSapConfig && (this.hasSapHeaders(req.headers) || this.defaultMcpDestination)) {
|
|
2513
|
+
await this.applyAuthHeaders(req.headers, session.sessionId, clientID);
|
|
2514
|
+
}
|
|
2515
|
+
// Get SAP config for this session (from headers or existing session)
|
|
2516
|
+
// Re-read session config after applyAuthHeaders (it may have been updated)
|
|
2517
|
+
const updatedSession = this.streamableHttpSessions.get(clientID);
|
|
2518
|
+
let sessionSapConfig = updatedSession?.sapConfig || session.sapConfig || this.sapConfig;
|
|
2519
|
+
// If no headers provided and no session config, try to reload from .env file
|
|
2520
|
+
// This handles the case when .env file exists but config wasn't loaded properly
|
|
2521
|
+
// Skip .env reload if defaultMcpDestination is set (use auth-broker instead)
|
|
2522
|
+
if (requiresSapConfig && !this.hasSapHeaders(req.headers) && !this.defaultMcpDestination && (!sessionSapConfig || sessionSapConfig.url === "http://placeholder" || sessionSapConfig.url === "http://injected-connection")) {
|
|
2523
|
+
try {
|
|
2524
|
+
// Try to reload config from .env file
|
|
2525
|
+
const envConfig = (0, config_js_1.getConfig)();
|
|
2526
|
+
if (envConfig && envConfig.url && envConfig.url !== "http://placeholder" && envConfig.url !== "http://injected-connection") {
|
|
2527
|
+
// URL from getConfig() is already cleaned (it uses cleanUrl internally)
|
|
2528
|
+
// Just use it as-is
|
|
2529
|
+
const cleanedUrl = envConfig.url;
|
|
2530
|
+
// Validate URL format
|
|
2531
|
+
if (/^https?:\/\//.test(cleanedUrl)) {
|
|
2532
|
+
// Update config with cleaned URL
|
|
2533
|
+
const cleanedConfig = { ...envConfig, url: cleanedUrl };
|
|
2534
|
+
this.sapConfig = cleanedConfig;
|
|
2535
|
+
sessionSapConfig = cleanedConfig;
|
|
2536
|
+
logger_1.logger?.debug("Reloaded SAP config from .env file for HTTP request", {
|
|
2537
|
+
type: "SAP_CONFIG_RELOADED_FROM_ENV",
|
|
2538
|
+
sessionId: session.sessionId,
|
|
2539
|
+
url: cleanedUrl,
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
else {
|
|
2543
|
+
logger_1.logger?.warn("Invalid URL format after cleaning", {
|
|
2544
|
+
type: "INVALID_URL_AFTER_CLEANING",
|
|
2545
|
+
url: cleanedUrl,
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
catch (configError) {
|
|
2551
|
+
// Config reload failed, continue with existing config
|
|
2552
|
+
logger_1.logger?.debug("Failed to reload config from .env file", {
|
|
2553
|
+
type: "SAP_CONFIG_RELOAD_FAILED",
|
|
2554
|
+
error: configError instanceof Error ? configError.message : String(configError),
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
// Validate that we have a real config (not placeholder) only for requests that need it
|
|
2559
|
+
if (requiresSapConfig && (!sessionSapConfig || sessionSapConfig.url === "http://placeholder" || sessionSapConfig.url === "http://injected-connection")) {
|
|
2560
|
+
// Check what headers were provided
|
|
2561
|
+
const providedHeaders = {};
|
|
2562
|
+
if (req.headers[interfaces_1.HEADER_SAP_DESTINATION_SERVICE])
|
|
2563
|
+
providedHeaders[interfaces_1.HEADER_SAP_DESTINATION_SERVICE] = String(req.headers[interfaces_1.HEADER_SAP_DESTINATION_SERVICE]);
|
|
2564
|
+
if (req.headers[interfaces_1.HEADER_MCP_DESTINATION])
|
|
2565
|
+
providedHeaders[interfaces_1.HEADER_MCP_DESTINATION] = String(req.headers[interfaces_1.HEADER_MCP_DESTINATION]);
|
|
2566
|
+
if (req.headers[interfaces_1.HEADER_SAP_URL])
|
|
2567
|
+
providedHeaders[interfaces_1.HEADER_SAP_URL] = '[present]';
|
|
2568
|
+
if (req.headers[interfaces_1.HEADER_SAP_AUTH_TYPE])
|
|
2569
|
+
providedHeaders[interfaces_1.HEADER_SAP_AUTH_TYPE] = String(req.headers[interfaces_1.HEADER_SAP_AUTH_TYPE]);
|
|
2570
|
+
const errorMessage = `No valid SAP configuration available. Please provide authentication headers (${interfaces_1.HEADER_SAP_URL}, ${interfaces_1.HEADER_SAP_AUTH_TYPE}, etc.) or ensure destination is configured correctly.`;
|
|
2571
|
+
logger_1.logger?.error("No valid SAP configuration available for request", {
|
|
2572
|
+
type: "NO_VALID_SAP_CONFIG",
|
|
2573
|
+
sessionId: session.sessionId,
|
|
2574
|
+
method: methodName,
|
|
2575
|
+
hasSessionConfig: !!updatedSession?.sapConfig,
|
|
2576
|
+
hasBaseConfig: !!this.sapConfig,
|
|
2577
|
+
baseConfigUrl: this.sapConfig?.url,
|
|
2578
|
+
providedHeaders: Object.keys(providedHeaders).length > 0 ? providedHeaders : 'none',
|
|
2579
|
+
hasAuthBroker: this.transportConfig.type === "streamable-http",
|
|
2580
|
+
hint: "For destination-based auth, ensure service key file exists and destination name matches exactly (case-sensitive). For direct auth, provide x-sap-url and x-sap-auth-type headers.",
|
|
2581
|
+
});
|
|
2582
|
+
// Return error response to client
|
|
2583
|
+
if (!res.headersSent) {
|
|
2584
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2585
|
+
res.end(JSON.stringify({
|
|
2586
|
+
jsonrpc: "2.0",
|
|
2587
|
+
id: null,
|
|
2588
|
+
error: {
|
|
2589
|
+
code: -32603,
|
|
2590
|
+
message: errorMessage,
|
|
2591
|
+
data: {
|
|
2592
|
+
type: "NO_VALID_SAP_CONFIG",
|
|
2593
|
+
hint: "Provide authentication headers or configure destination correctly",
|
|
2594
|
+
},
|
|
2595
|
+
},
|
|
2596
|
+
}));
|
|
2597
|
+
}
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
// Create connection for this request
|
|
2601
|
+
const connection = await this.getOrCreateConnectionForServer(req.headers, session.sessionId, updatedSession?.destination || this.defaultDestination);
|
|
2602
|
+
// Create new McpServer instance for this request
|
|
2603
|
+
const requestServer = new mcp_js_1.McpServer({
|
|
2604
|
+
name: "mcp-abap-adt",
|
|
2605
|
+
version: "0.1.0"
|
|
2606
|
+
});
|
|
2607
|
+
// Register all tools using McpHandlers
|
|
2608
|
+
const handlers = new mcp_handlers_1.McpHandlers();
|
|
2609
|
+
handlers.RegisterAllToolsOnServer(requestServer, { connection, logger: loggerAdapter_1.loggerAdapter });
|
|
2610
|
+
// KEY MOMENT: Create new StreamableHTTP transport for each request (like the SDK example)
|
|
2611
|
+
// SDK automatically handles:
|
|
2612
|
+
// - Chunked transfer encoding
|
|
2613
|
+
// - Session tracking
|
|
2614
|
+
// - JSON-RPC protocol
|
|
2615
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
2616
|
+
sessionIdGenerator: undefined, // Stateless mode (like the SDK example)
|
|
2617
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2618
|
+
allowedOrigins: httpConfig.allowedOrigins,
|
|
2619
|
+
allowedHosts: httpConfig.allowedHosts,
|
|
2620
|
+
enableDnsRebindingProtection: httpConfig.enableDnsRebindingProtection,
|
|
2621
|
+
});
|
|
2622
|
+
// Close transport when response closes (like the SDK example)
|
|
2623
|
+
res.on("close", () => {
|
|
2624
|
+
transport.close();
|
|
2625
|
+
});
|
|
2626
|
+
// Connect transport to new McpServer (like the SDK example)
|
|
2627
|
+
await requestServer.connect(transport);
|
|
2628
|
+
logger_1.logger?.debug("Transport connected", {
|
|
2629
|
+
type: "STREAMABLE_HTTP_TRANSPORT_CONNECTED",
|
|
2630
|
+
sessionId: session.sessionId,
|
|
2631
|
+
clientID,
|
|
2632
|
+
});
|
|
2633
|
+
// Run handlers in AsyncLocalStorage context with session info
|
|
2634
|
+
// This allows getManagedConnection() to access sessionId, config, and destination
|
|
2635
|
+
const sessionDestination = updatedSession?.destination;
|
|
2636
|
+
await utils_1.sessionContext.run({
|
|
2637
|
+
sessionId: session.sessionId,
|
|
2638
|
+
sapConfig: sessionSapConfig,
|
|
2639
|
+
destination: sessionDestination,
|
|
2640
|
+
}, async () => {
|
|
2641
|
+
// Handle HTTP request through transport (like the SDK example)
|
|
2642
|
+
// Pass body as third parameter if available (like the SDK example)
|
|
2643
|
+
await transport.handleRequest(req, res, body);
|
|
2644
|
+
});
|
|
2645
|
+
logger_1.logger?.debug("Request completed", {
|
|
2646
|
+
type: "STREAMABLE_HTTP_REQUEST_COMPLETED",
|
|
2647
|
+
sessionId: session.sessionId,
|
|
2648
|
+
clientID,
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
catch (error) {
|
|
2652
|
+
logger_1.logger?.error("Failed to handle HTTP request", {
|
|
2653
|
+
type: "HTTP_REQUEST_ERROR",
|
|
2654
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2655
|
+
sessionId: session.sessionId,
|
|
2656
|
+
clientID,
|
|
2657
|
+
});
|
|
2658
|
+
if (!res.headersSent) {
|
|
2659
|
+
res.writeHead(500).end("Internal Server Error");
|
|
2660
|
+
}
|
|
2661
|
+
else {
|
|
2662
|
+
res.end();
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
});
|
|
2666
|
+
httpServer.on("clientError", (err, socket) => {
|
|
2667
|
+
logger_1.logger?.error("HTTP client error", {
|
|
2668
|
+
type: "HTTP_CLIENT_ERROR",
|
|
2669
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2670
|
+
});
|
|
2671
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
2672
|
+
});
|
|
2673
|
+
await new Promise((resolve, reject) => {
|
|
2674
|
+
const onError = (error) => {
|
|
2675
|
+
logger_1.logger?.error("HTTP server failed to start", {
|
|
2676
|
+
type: "HTTP_SERVER_ERROR",
|
|
2677
|
+
error: error.message,
|
|
2678
|
+
});
|
|
2679
|
+
httpServer.off("error", onError);
|
|
2680
|
+
reject(error);
|
|
2681
|
+
};
|
|
2682
|
+
httpServer.once("error", onError);
|
|
2683
|
+
httpServer.listen(httpConfig.port, httpConfig.host, () => {
|
|
2684
|
+
httpServer.off("error", onError);
|
|
2685
|
+
logger_1.logger?.info("HTTP server listening", {
|
|
2686
|
+
type: "HTTP_SERVER_LISTENING",
|
|
2687
|
+
host: httpConfig.host,
|
|
2688
|
+
port: httpConfig.port,
|
|
2689
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
2690
|
+
});
|
|
2691
|
+
resolve();
|
|
2692
|
+
});
|
|
2693
|
+
});
|
|
2694
|
+
this.httpServer = httpServer;
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const sseConfig = this.transportConfig;
|
|
2698
|
+
// Initialize default broker if destination/.env exists (for SSE: optional, wait for headers if not)
|
|
2699
|
+
if (this.defaultMcpDestination || this.hasEnvFile) {
|
|
2700
|
+
await this.initializeDefaultBroker();
|
|
2701
|
+
// If default destination exists, connect to ABAP automatically
|
|
2702
|
+
if (this.defaultDestination) {
|
|
2703
|
+
try {
|
|
2704
|
+
const authBroker = await this.getOrCreateAuthBroker(this.defaultDestination, 'global');
|
|
2705
|
+
if (authBroker) {
|
|
2706
|
+
const connConfig = await authBroker.getConnectionConfig(this.defaultDestination);
|
|
2707
|
+
if (connConfig?.serviceUrl) {
|
|
2708
|
+
const jwtToken = await authBroker.getToken(this.defaultDestination);
|
|
2709
|
+
if (jwtToken) {
|
|
2710
|
+
// Register AuthBroker in global registry for connection to use during token refresh
|
|
2711
|
+
const { registerAuthBroker } = require('./lib/utils');
|
|
2712
|
+
registerAuthBroker(this.defaultDestination, authBroker);
|
|
2713
|
+
this.processJwtConfigUpdate(connConfig.serviceUrl, jwtToken, undefined, this.defaultDestination);
|
|
2714
|
+
logger_1.logger?.info("SAP configuration initialized for SSE transport", {
|
|
2715
|
+
type: "SSE_DESTINATION_INIT",
|
|
2716
|
+
destination: this.defaultDestination,
|
|
2717
|
+
url: connConfig.serviceUrl,
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
catch (error) {
|
|
2724
|
+
logger_1.logger?.warn("Failed to initialize connection with default destination for SSE transport", {
|
|
2725
|
+
type: "SSE_DESTINATION_INIT_FAILED",
|
|
2726
|
+
destination: this.defaultDestination,
|
|
2727
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
else {
|
|
2733
|
+
logger_1.logger?.debug("No default destination for SSE transport, will wait for headers", {
|
|
2734
|
+
type: "SSE_NO_DEFAULT_DESTINATION",
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
const streamPathMap = new Map([
|
|
2738
|
+
["/", "/messages"],
|
|
2739
|
+
["/mcp/events", "/mcp/messages"],
|
|
2740
|
+
["/sse", "/messages"],
|
|
2741
|
+
]);
|
|
2742
|
+
const streamPaths = Array.from(streamPathMap.keys());
|
|
2743
|
+
const postPathSet = new Set(streamPathMap.values());
|
|
2744
|
+
postPathSet.add("/messages");
|
|
2745
|
+
postPathSet.add("/mcp/messages");
|
|
2746
|
+
const httpServer = (0, http_1.createServer)(async (req, res) => {
|
|
2747
|
+
// SSE: Always restrict to local connections only
|
|
2748
|
+
const remoteAddress = req.socket.remoteAddress;
|
|
2749
|
+
if (!this.isLocalConnection(remoteAddress)) {
|
|
2750
|
+
logger_1.logger?.warn("SSE: Non-local connection rejected", {
|
|
2751
|
+
type: "SSE_NON_LOCAL_REJECTED",
|
|
2752
|
+
remoteAddress,
|
|
2753
|
+
});
|
|
2754
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2755
|
+
res.end("Forbidden: SSE transport only accepts local connections");
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
const requestUrl = req.url ? new URL(req.url, `http://${req.headers.host ?? `${sseConfig.host}:${sseConfig.port}`}`) : undefined;
|
|
2759
|
+
let pathname = requestUrl?.pathname ?? "/";
|
|
2760
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
2761
|
+
pathname = pathname.slice(0, -1);
|
|
2762
|
+
}
|
|
2763
|
+
await this.applyAuthHeaders(req.headers);
|
|
2764
|
+
logger_1.logger?.debug("SSE request received", {
|
|
2765
|
+
type: "SSE_HTTP_REQUEST",
|
|
2766
|
+
method: req.method,
|
|
2767
|
+
pathname,
|
|
2768
|
+
originalUrl: req.url,
|
|
2769
|
+
headers: {
|
|
2770
|
+
accept: req.headers.accept,
|
|
2771
|
+
"content-type": req.headers["content-type"],
|
|
2772
|
+
},
|
|
2773
|
+
});
|
|
2774
|
+
// GET /sse, /mcp/events, or / - establish SSE connection
|
|
2775
|
+
if (req.method === "GET" && streamPathMap.has(pathname)) {
|
|
2776
|
+
const postEndpoint = streamPathMap.get(pathname) ?? "/messages";
|
|
2777
|
+
logger_1.logger?.debug("SSE client connecting", {
|
|
2778
|
+
type: "SSE_CLIENT_CONNECTING",
|
|
2779
|
+
pathname,
|
|
2780
|
+
postEndpoint,
|
|
2781
|
+
});
|
|
2782
|
+
// Create connection for this session
|
|
2783
|
+
const connection = await this.getOrCreateConnectionForServer(req.headers, undefined, // sessionId will be set after transport creation
|
|
2784
|
+
this.defaultDestination);
|
|
2785
|
+
// Create new McpServer instance for this session (like the working example)
|
|
2786
|
+
const server = this.createMcpServerForSession({ connection, logger: loggerAdapter_1.loggerAdapter });
|
|
2787
|
+
// Create SSE transport
|
|
2788
|
+
const transport = new sse_js_1.SSEServerTransport(postEndpoint, res, {
|
|
2789
|
+
allowedHosts: sseConfig.allowedHosts,
|
|
2790
|
+
allowedOrigins: sseConfig.allowedOrigins,
|
|
2791
|
+
enableDnsRebindingProtection: sseConfig.enableDnsRebindingProtection,
|
|
2792
|
+
});
|
|
2793
|
+
const sessionId = transport.sessionId;
|
|
2794
|
+
logger_1.logger?.info("New SSE session created", {
|
|
2795
|
+
type: "SSE_SESSION_CREATED",
|
|
2796
|
+
sessionId,
|
|
2797
|
+
pathname,
|
|
2798
|
+
});
|
|
2799
|
+
// Store transport and server for this session
|
|
2800
|
+
this.sseSessions.set(sessionId, {
|
|
2801
|
+
server,
|
|
2802
|
+
transport,
|
|
2803
|
+
});
|
|
2804
|
+
// Connect transport to server (using server.server like in the example)
|
|
2805
|
+
try {
|
|
2806
|
+
await server.server.connect(transport);
|
|
2807
|
+
logger_1.logger?.info("SSE transport connected", {
|
|
2808
|
+
type: "SSE_CONNECTION_READY",
|
|
2809
|
+
sessionId,
|
|
2810
|
+
pathname,
|
|
2811
|
+
postEndpoint,
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
catch (error) {
|
|
2815
|
+
logger_1.logger?.error("Failed to connect SSE transport", {
|
|
2816
|
+
type: "SSE_CONNECT_ERROR",
|
|
2817
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2818
|
+
sessionId,
|
|
2819
|
+
});
|
|
2820
|
+
this.sseSessions.delete(sessionId);
|
|
2821
|
+
if (!res.headersSent) {
|
|
2822
|
+
res.writeHead(500).end("Internal Server Error");
|
|
2823
|
+
}
|
|
2824
|
+
else {
|
|
2825
|
+
res.end();
|
|
2826
|
+
}
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
// Cleanup on connection close
|
|
2830
|
+
res.on("close", () => {
|
|
2831
|
+
logger_1.logger?.info("SSE connection closed", {
|
|
2832
|
+
type: "SSE_CONNECTION_CLOSED",
|
|
2833
|
+
sessionId,
|
|
2834
|
+
pathname,
|
|
2835
|
+
});
|
|
2836
|
+
this.sseSessions.delete(sessionId);
|
|
2837
|
+
server.server.close();
|
|
2838
|
+
});
|
|
2839
|
+
transport.onerror = (error) => {
|
|
2840
|
+
logger_1.logger?.error("SSE transport error", {
|
|
2841
|
+
type: "SSE_TRANSPORT_ERROR",
|
|
2842
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2843
|
+
sessionId,
|
|
2844
|
+
});
|
|
2845
|
+
};
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
// POST /messages or /mcp/messages - handle client messages
|
|
2849
|
+
if (req.method === "POST" && postPathSet.has(pathname)) {
|
|
2850
|
+
// Extract sessionId from query string or header
|
|
2851
|
+
let sessionId;
|
|
2852
|
+
if (requestUrl) {
|
|
2853
|
+
sessionId = requestUrl.searchParams.get("sessionId") || undefined;
|
|
2854
|
+
}
|
|
2855
|
+
if (!sessionId) {
|
|
2856
|
+
sessionId = req.headers["x-session-id"];
|
|
2857
|
+
}
|
|
2858
|
+
logger_1.logger?.debug("SSE POST request received", {
|
|
2859
|
+
type: "SSE_POST_REQUEST",
|
|
2860
|
+
sessionId,
|
|
2861
|
+
pathname,
|
|
2862
|
+
});
|
|
2863
|
+
if (!sessionId || !this.sseSessions.has(sessionId)) {
|
|
2864
|
+
logger_1.logger?.error("Invalid or missing SSE session", {
|
|
2865
|
+
type: "SSE_INVALID_SESSION",
|
|
2866
|
+
sessionId,
|
|
2867
|
+
});
|
|
2868
|
+
res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
2869
|
+
jsonrpc: "2.0",
|
|
2870
|
+
error: {
|
|
2871
|
+
code: -32000,
|
|
2872
|
+
message: "Invalid or missing sessionId",
|
|
2873
|
+
},
|
|
2874
|
+
id: null,
|
|
2875
|
+
}));
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const session = this.sseSessions.get(sessionId);
|
|
2879
|
+
const { transport } = session;
|
|
2880
|
+
try {
|
|
2881
|
+
// Read request body
|
|
2882
|
+
let body = null;
|
|
2883
|
+
const chunks = [];
|
|
2884
|
+
for await (const chunk of req) {
|
|
2885
|
+
chunks.push(chunk);
|
|
2886
|
+
}
|
|
2887
|
+
if (chunks.length > 0) {
|
|
2888
|
+
const bodyString = Buffer.concat(chunks).toString('utf-8');
|
|
2889
|
+
try {
|
|
2890
|
+
body = JSON.parse(bodyString);
|
|
2891
|
+
}
|
|
2892
|
+
catch (parseError) {
|
|
2893
|
+
body = bodyString || null;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
// Handle POST message through transport (like the working example)
|
|
2897
|
+
await transport.handlePostMessage(req, res, body);
|
|
2898
|
+
logger_1.logger?.debug("SSE POST request processed", {
|
|
2899
|
+
type: "SSE_POST_PROCESSED",
|
|
2900
|
+
sessionId,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
catch (error) {
|
|
2904
|
+
logger_1.logger?.error("Failed to handle SSE POST message", {
|
|
2905
|
+
type: "SSE_POST_ERROR",
|
|
2906
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2907
|
+
sessionId,
|
|
2908
|
+
});
|
|
2909
|
+
if (!res.headersSent) {
|
|
2910
|
+
res.writeHead(500).end("Internal Server Error");
|
|
2911
|
+
}
|
|
2912
|
+
else {
|
|
2913
|
+
res.end();
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
// OPTIONS - CORS preflight
|
|
2919
|
+
if (req.method === "OPTIONS" && (streamPathMap.has(pathname) || postPathSet.has(pathname))) {
|
|
2920
|
+
res.writeHead(204, {
|
|
2921
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
2922
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
2923
|
+
}).end();
|
|
2924
|
+
return;
|
|
2925
|
+
}
|
|
2926
|
+
res.writeHead(404, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "Not Found" }));
|
|
2927
|
+
});
|
|
2928
|
+
httpServer.on("clientError", (err, socket) => {
|
|
2929
|
+
logger_1.logger?.error("SSE HTTP client error", {
|
|
2930
|
+
type: "SSE_HTTP_CLIENT_ERROR",
|
|
2931
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2932
|
+
});
|
|
2933
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
2934
|
+
});
|
|
2935
|
+
await new Promise((resolve, reject) => {
|
|
2936
|
+
const onError = (error) => {
|
|
2937
|
+
logger_1.logger?.error("SSE HTTP server failed to start", {
|
|
2938
|
+
type: "SSE_HTTP_SERVER_ERROR",
|
|
2939
|
+
error: error.message,
|
|
2940
|
+
});
|
|
2941
|
+
httpServer.off("error", onError);
|
|
2942
|
+
reject(error);
|
|
2943
|
+
};
|
|
2944
|
+
httpServer.once("error", onError);
|
|
2945
|
+
httpServer.listen(sseConfig.port, sseConfig.host, () => {
|
|
2946
|
+
httpServer.off("error", onError);
|
|
2947
|
+
logger_1.logger?.info("SSE HTTP server listening", {
|
|
2948
|
+
type: "SSE_HTTP_SERVER_LISTENING",
|
|
2949
|
+
host: sseConfig.host,
|
|
2950
|
+
port: sseConfig.port,
|
|
2951
|
+
streamPaths,
|
|
2952
|
+
postPaths: Array.from(postPathSet.values()),
|
|
2953
|
+
});
|
|
2954
|
+
resolve();
|
|
2955
|
+
});
|
|
2956
|
+
});
|
|
2957
|
+
this.httpServer = httpServer;
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
exports.mcp_abap_adt_server = mcp_abap_adt_server;
|
|
2961
|
+
if (process.env.MCP_SKIP_AUTO_START !== "true") {
|
|
2962
|
+
const server = new mcp_abap_adt_server();
|
|
2963
|
+
server.run().catch((error) => {
|
|
2964
|
+
logger_1.logger?.error("Fatal error while running MCP server", {
|
|
2965
|
+
type: "SERVER_FATAL_ERROR",
|
|
2966
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2967
|
+
});
|
|
2968
|
+
// Always write to stderr (safe even in stdio mode)
|
|
2969
|
+
process.stderr.write(`[MCP] ✗ Fatal error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2970
|
+
// On Windows, add a small delay before exit to allow error message to be visible
|
|
2971
|
+
if (process.platform === 'win32') {
|
|
2972
|
+
setTimeout(() => process.exit(1), 100);
|
|
2973
|
+
}
|
|
2974
|
+
else {
|
|
2975
|
+
process.exit(1);
|
|
2976
|
+
}
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
//# sourceMappingURL=index.js.map
|