#!/usr/bin/env node
import { config } from 'dotenv';
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
McpError,
ErrorCode
} from "@modelcontextprotocol/sdk/types.js";
import { ADTClient, session_types } from "abap-adt-api";
import path from 'path';
import express from 'express';
import cors from 'cors';
import { v4 as uuidv4 } from 'uuid';
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { AuthHandlers } from './handlers/AuthHandlers.js';
import { TransportHandlers } from './handlers/TransportHandlers.js';
import { ObjectHandlers } from './handlers/ObjectHandlers.js';
import { ClassHandlers } from './handlers/ClassHandlers.js';
import { CodeAnalysisHandlers } from './handlers/CodeAnalysisHandlers.js';
import { ObjectLockHandlers } from './handlers/ObjectLockHandlers.js';
import { ObjectSourceHandlers } from './handlers/ObjectSourceHandlers.js';
import { ObjectDeletionHandlers } from './handlers/ObjectDeletionHandlers.js';
import { ObjectManagementHandlers } from './handlers/ObjectManagementHandlers.js';
import { ObjectRegistrationHandlers } from './handlers/ObjectRegistrationHandlers.js';
import { NodeHandlers } from './handlers/NodeHandlers.js';
import { DiscoveryHandlers } from './handlers/DiscoveryHandlers.js';
import { UnitTestHandlers } from './handlers/UnitTestHandlers.js';
import { PrettyPrinterHandlers } from './handlers/PrettyPrinterHandlers.js';
import { GitHandlers } from './handlers/GitHandlers.js';
import { DdicHandlers } from './handlers/DdicHandlers.js';
import { ServiceBindingHandlers } from './handlers/ServiceBindingHandlers.js';
import { QueryHandlers } from './handlers/QueryHandlers.js';
import { FeedHandlers } from './handlers/FeedHandlers.js';
import { DebugHandlers } from './handlers/DebugHandlers.js';
import { RenameHandlers } from './handlers/RenameHandlers.js';
import { AtcHandlers } from './handlers/AtcHandlers.js';
import { TraceHandlers } from './handlers/TraceHandlers.js';
import { RefactorHandlers } from './handlers/RefactorHandlers.js';
import { RevisionHandlers } from './handlers/RevisionHandlers.js';
process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1,hc1srv02,.bosch.com';
process.env.no_proxy = process.env.NO_PROXY;
process.env.HTTP_PROXY = '';
process.env.HTTPS_PROXY = '';
config({ path: path.resolve(__dirname, '../.env') });
export class AbapAdtServer extends Server {
private adtClient: ADTClient;
private proxyAgent: any; // Legacy/Fallback
private httpProxyAgent: any;
private httpsProxyAgent: any;
private proxyHeaders: any; // Cache headers for reLogin
private handlers: any[] = []; // Assuming BaseHandler is not defined, using any[] for now
private authHandlers!: AuthHandlers;
private transportHandlers!: TransportHandlers;
private objectHandlers!: ObjectHandlers;
private classHandlers!: ClassHandlers;
private codeAnalysisHandlers!: CodeAnalysisHandlers;
private objectLockHandlers!: ObjectLockHandlers;
private objectSourceHandlers!: ObjectSourceHandlers;
private objectDeletionHandlers!: ObjectDeletionHandlers;
private objectManagementHandlers!: ObjectManagementHandlers;
private objectRegistrationHandlers!: ObjectRegistrationHandlers;
private nodeHandlers!: NodeHandlers;
private discoveryHandlers!: DiscoveryHandlers;
private unitTestHandlers!: UnitTestHandlers;
private prettyPrinterHandlers!: PrettyPrinterHandlers;
private gitHandlers!: GitHandlers;
private ddicHandlers!: DdicHandlers;
private serviceBindingHandlers!: ServiceBindingHandlers;
private queryHandlers!: QueryHandlers;
private feedHandlers!: FeedHandlers;
private debugHandlers!: DebugHandlers;
private renameHandlers!: RenameHandlers;
private atcHandlers!: AtcHandlers;
private traceHandlers!: TraceHandlers;
private refactorHandlers!: RefactorHandlers;
private revisionHandlers!: RevisionHandlers;
constructor() {
super(
{
name: "mcp-abap-abap-adt-api",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
// Initial config from environment (default empty to force manual login)
const config = {
SAP_URL: '',
SAP_USER: '',
SAP_PASSWORD: '',
SAP_CLIENT: '',
SAP_LANGUAGE: ''
};
// Environment variables checking is now optional to allow login via parameters
// We no longer rely on specific env vars for auto-login to ensure security and explicit connection
this.adtClient = new ADTClient(
config.SAP_URL || 'https://example.com', // Dummy URL to satisfy constructor
config.SAP_USER || 'dummy', // Dummy User
config.SAP_PASSWORD || 'SSO_PLACEHOLDER', // Dummy Password to satisfy ADTClient validation
config.SAP_CLIENT,
config.SAP_LANGUAGE
);
this.adtClient.stateful = session_types.stateful
this.initializeHandlers(this.adtClient);
}
// Method to probe common SAP endpoints to find whitelisted paths
async runConnectivityProbe(baseUrl: string, agent: any) {
console.log(`[PROBE] Starting connectivity probe to ${baseUrl}...`);
const paths = [
'/sap/public/ping',
'/sap/bc/ping',
'/sap/bc/adt/discovery',
'/sap/bc/adt/compatibility/graph',
'/sap/bc/adt/core/discovery',
'/sap/bc/adt/oo/classes',
'/sap/bc/adt/programs/programs',
'/sap/public/info',
];
const axios = require('axios');
for (const path of paths) {
try {
const url = `${baseUrl}${path}`;
console.log(`[PROBE] Checking GET ${url} ...`);
await axios.get(url, {
httpsAgent: agent, // Use the BTP proxy
httpAgent: agent,
timeout: 5000, // Short timeout
validateStatus: () => true // Don't throw on error status
}).then((res: any) => {
console.log(`[PROBE] GET ${path} -> Status: ${res.status} ${res.statusText}`);
});
} catch (err: any) {
console.log(`[PROBE] GET ${path} -> FAILED: ${err.message}`);
}
}
console.log(`[PROBE] Probe completed.`);
}
// New method to initialize BTP Connectivity with async token fetch
async initBTPConnection() {
console.log("[DEBUG] initBTPConnection called. Checking for VCAP_SERVICES...");
if (!process.env.VCAP_SERVICES) {
console.warn("[DEBUG] VCAP_SERVICES environment variable is NOT defined.");
return;
}
try {
console.log("[DEBUG] VCAP_SERVICES found. Parsing...");
const vcapServices = JSON.parse(process.env.VCAP_SERVICES);
console.log("[DEBUG] Available services in VCAP_SERVICES:", Object.keys(vcapServices));
const connectivityService = vcapServices.connectivity ? vcapServices.connectivity[0] : null;
if (!connectivityService) {
console.warn("[DEBUG] 'connectivity' service NOT found in VCAP_SERVICES.");
}
if (connectivityService) {
console.log("BTP Connectivity Service detected. Configuring proxy with token...");
const credentials = connectivityService.credentials;
const proxyHost = credentials.onpremise_proxy_host;
const proxyPort = credentials.onpremise_proxy_port;
const tokenServiceUrl = credentials.token_service_url;
const clientId = credentials.clientid;
const clientSecret = credentials.clientsecret;
let httpsProxyAgent;
let httpProxyAgent;
if (tokenServiceUrl && clientId && clientSecret) {
try {
// Dynamic require to avoid build issues if axios not top-level
const axios = require('axios');
const tokenResponse = await axios.post(`${tokenServiceUrl}/oauth/token?grant_type=client_credentials`, null, {
headers: {
'Authorization': 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
}
});
const connectivityToken = tokenResponse.data.access_token;
const HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent;
const HttpProxyAgent = require('http-proxy-agent').HttpProxyAgent;
const proxyHeaders = {
'Proxy-Authorization': `Bearer ${connectivityToken}`
};
const proxyOptions = {
host: proxyHost,
port: proxyPort,
headers: proxyHeaders
};
httpsProxyAgent = new HttpsProxyAgent(proxyOptions);
httpProxyAgent = new HttpProxyAgent(proxyOptions);
console.log(`BTP Connectivity Proxy configured successfully. Host: ${proxyHost}, Port: ${proxyPort}`);
// Verify token presence (do not log full token)
if (connectivityToken) {
console.log("Proxy-Authorization token retrieved successfully.");
} else {
console.warn("WARNING: Proxy-Authorization token is empty!");
}
// Note: Probe moved to AuthHandlers to use user-provided URL
// Pass the proxy agent to AuthHandlers so the probe works
if (this.authHandlers) {
this.proxyAgent = httpsProxyAgent; // Cache main for legacy compat
this.httpProxyAgent = httpProxyAgent; // Cache http agent
this.httpsProxyAgent = httpsProxyAgent; // Cache https agent
this.proxyHeaders = proxyHeaders; // Cache headers
// Pass both agents AND headers to AuthHandlers
this.authHandlers.setProxyAgents(httpProxyAgent, httpsProxyAgent, proxyHeaders);
console.log("Passed proxy agents and headers to AuthHandlers.");
}
} catch (tokenError: any) {
console.error("Failed to fetch Connectivity Service token:", tokenError.message);
}
} else {
const HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent;
const HttpProxyAgent = require('http-proxy-agent').HttpProxyAgent;
httpsProxyAgent = new HttpsProxyAgent(`http://${proxyHost}:${proxyPort}`);
httpProxyAgent = new HttpProxyAgent(`http://${proxyHost}:${proxyPort}`);
console.log("Configured basic proxy (no token service found)");
}
if (httpsProxyAgent || httpProxyAgent) {
// Re-create ADTClient with the proxy
// We reuse the existing dummy config, the real one comes from reLogin anyway
// But importantly, we set the DEFAULT proxy for the instance
const clientOptions = {
httpsAgent: httpsProxyAgent,
httpAgent: httpProxyAgent
};
const config = {
SAP_URL: process.env.SAP_URL || 'https://example.com',
SAP_USER: process.env.SAP_USER || 'dummy',
SAP_PASSWORD: process.env.SAP_PASSWORD || 'SSO_PLACEHOLDER',
SAP_CLIENT: process.env.SAP_CLIENT,
SAP_LANGUAGE: process.env.SAP_LANGUAGE
};
this.adtClient = new ADTClient(
config.SAP_URL,
config.SAP_USER,
config.SAP_PASSWORD,
config.SAP_CLIENT,
config.SAP_LANGUAGE,
clientOptions
);
this.adtClient.stateful = session_types.stateful;
// Brute-force: Manually inject proxy agent into the internal axios instance
// This ensures that even if AdtHTTP/toAxiosConfig drops it, it's restored on the defaults.
try {
// @ts-ignore - reaching into internals
const adtHttp = this.adtClient.httpClient;
// @ts-ignore
const axiosHttpClient = adtHttp.httpclient;
// @ts-ignore
if (axiosHttpClient && axiosHttpClient.axios) {
// @ts-ignore
axiosHttpClient.axios.defaults.httpAgent = httpProxyAgent;
// @ts-ignore
axiosHttpClient.axios.defaults.httpsAgent = httpsProxyAgent;
// Also inject Proxy-Authorization for HTTP (non-tunnel) connectivity
if (this.proxyHeaders) {
// @ts-ignore
Object.assign(axiosHttpClient.axios.defaults.headers.common, this.proxyHeaders);
}
console.log("Forcibly injected proxy agents into internal ADTClient axios defaults.");
}
} catch (injectError) {
console.warn("Failed to force inject proxy agent:", injectError);
}
this.initializeHandlers(this.adtClient);
}
}
} catch (error) {
console.error("Failed to parse VCAP_SERVICES or configure proxy:", error);
}
}
private isLoggedIn: boolean = false;
private initializeHandlers(client: ADTClient) {
// Initialize handlers
this.authHandlers = new AuthHandlers(client, this.reLogin.bind(this));
this.transportHandlers = new TransportHandlers(client);
this.objectHandlers = new ObjectHandlers(client);
this.classHandlers = new ClassHandlers(client);
this.codeAnalysisHandlers = new CodeAnalysisHandlers(client);
this.objectLockHandlers = new ObjectLockHandlers(client);
this.objectSourceHandlers = new ObjectSourceHandlers(client);
this.objectDeletionHandlers = new ObjectDeletionHandlers(client);
this.objectManagementHandlers = new ObjectManagementHandlers(client);
this.objectRegistrationHandlers = new ObjectRegistrationHandlers(client);
this.nodeHandlers = new NodeHandlers(client);
this.discoveryHandlers = new DiscoveryHandlers(client);
this.unitTestHandlers = new UnitTestHandlers(client);
this.prettyPrinterHandlers = new PrettyPrinterHandlers(client);
this.gitHandlers = new GitHandlers(client);
this.ddicHandlers = new DdicHandlers(client);
this.serviceBindingHandlers = new ServiceBindingHandlers(client);
this.queryHandlers = new QueryHandlers(client);
this.feedHandlers = new FeedHandlers(client);
this.debugHandlers = new DebugHandlers(client);
this.renameHandlers = new RenameHandlers(client);
this.atcHandlers = new AtcHandlers(client);
this.traceHandlers = new TraceHandlers(client);
this.refactorHandlers = new RefactorHandlers(client);
this.revisionHandlers = new RevisionHandlers(client);
// Setup tool handlers
this.setupToolHandlers();
// [CRITICAL FIX] Restore proxy agents if they exist
// When re-initializing handlers (e.g. after BTP connection setup or reLogin),
// we must ensure the new AuthHandlers instance gets the proxy configuration.
if (this.httpProxyAgent || this.httpsProxyAgent) {
console.log("[initializeHandlers] Restoring proxy agents to new AuthHandlers instance.");
this.authHandlers.setProxyAgents(this.httpProxyAgent, this.httpsProxyAgent, this.proxyHeaders);
}
}
private serializeResult(result: any) {
// If result is already formatted as MCP result (has content array), return it directly
if (result && typeof result === 'object' && Array.isArray(result.content)) {
return result;
}
try {
return {
content: [{
type: 'text',
text: JSON.stringify(result, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
)
}]
};
} catch (error) {
return this.handleError(new McpError(
ErrorCode.InternalError,
'Failed to serialize result'
));
}
}
private handleError(error: unknown) {
if (!(error instanceof Error)) {
error = new Error(String(error));
}
if (error instanceof McpError) {
console.error(`[MCP Tool Error] ${error.message}`, error.stack);
return {
content: [{
type: 'text',
text: JSON.stringify({
error: error.message,
code: error.code
})
}],
isError: true
};
}
console.error(`[MCP Internal Error]`, error);
return {
content: [{
type: 'text',
text: JSON.stringify({
error: 'Internal server error',
code: ErrorCode.InternalError,
details: String(error)
})
}],
isError: true
};
}
private setupToolHandlers() {
this.setRequestHandler(ListToolsRequestSchema, async () => {
// Always list tools, but they might return error if not logged in
return {
tools: [
...this.authHandlers.getTools(),
...this.transportHandlers.getTools(),
...this.objectHandlers.getTools(),
...this.classHandlers.getTools(),
...this.codeAnalysisHandlers.getTools(),
...this.objectLockHandlers.getTools(),
...this.objectSourceHandlers.getTools(),
...this.objectDeletionHandlers.getTools(),
...this.objectManagementHandlers.getTools(),
...this.objectRegistrationHandlers.getTools(),
...this.nodeHandlers.getTools(),
...this.discoveryHandlers.getTools(),
...this.unitTestHandlers.getTools(),
...this.prettyPrinterHandlers.getTools(),
...this.gitHandlers.getTools(),
...this.ddicHandlers.getTools(),
...this.serviceBindingHandlers.getTools(),
...this.queryHandlers.getTools(),
...this.feedHandlers.getTools(),
...this.debugHandlers.getTools(),
...this.renameHandlers.getTools(),
...this.atcHandlers.getTools(),
...this.traceHandlers.getTools(),
...this.refactorHandlers.getTools(),
...this.revisionHandlers.getTools(),
{
name: 'healthcheck',
description: 'Check server health and connectivity',
inputSchema: {
type: 'object',
properties: {}
}
}
]
};
});
this.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const toolName = request.params.name;
// Check login status for all tools except 'login' and 'healthcheck'
// 'logout' and 'dropSession' are allowed to ensure we can clear state even if confused
if (!this.isLoggedIn && toolName !== 'login' && toolName !== 'healthcheck') {
throw new McpError(ErrorCode.InvalidRequest, "Please log in first using the 'login' tool.");
}
let result: any;
switch (request.params.name) {
case 'login':
case 'logout':
case 'dropSession':
result = await this.authHandlers.handle(request.params.name, request.params.arguments);
// Update login state based on the action
if (request.params.name === 'logout' || request.params.name === 'dropSession') {
this.isLoggedIn = false;
}
break;
case 'transportInfo':
case 'createTransport':
case 'hasTransportConfig':
case 'transportConfigurations':
case 'getTransportConfiguration':
case 'setTransportsConfig':
case 'createTransportsConfig':
case 'userTransports':
case 'transportsByConfig':
case 'transportDelete':
case 'transportRelease':
case 'transportSetOwner':
case 'transportAddUser':
case 'systemUsers':
case 'transportReference':
result = await this.transportHandlers.handle(request.params.name, request.params.arguments);
break;
case 'lock':
case 'unLock':
result = await this.objectLockHandlers.handle(request.params.name, request.params.arguments);
break;
case 'objectStructure':
case 'searchObject':
case 'findObjectPath':
case 'objectTypes':
case 'reentranceTicket':
case 'searchPackage':
result = await this.objectHandlers.handle(request.params.name, request.params.arguments);
break;
case 'classIncludes':
case 'classComponents':
result = await this.classHandlers.handle(request.params.name, request.params.arguments);
break;
case 'syntaxCheckCode':
case 'syntaxCheckCdsUrl':
case 'codeCompletion':
case 'findDefinition':
case 'usageReferences':
case 'syntaxCheckTypes':
case 'codeCompletionFull':
case 'runClass':
case 'codeCompletionElement':
case 'usageReferenceSnippets':
case 'fixProposals':
case 'fixEdits':
case 'fragmentMappings':
case 'abapDocumentation':
result = await this.codeAnalysisHandlers.handle(request.params.name, request.params.arguments);
break;
case 'getObjectSource':
case 'setObjectSource':
result = await this.objectSourceHandlers.handle(request.params.name, request.params.arguments);
break;
case 'deleteObject':
result = await this.objectDeletionHandlers.handle(request.params.name, request.params.arguments);
break;
case 'activateObjects':
case 'activateByName':
case 'inactiveObjects':
result = await this.objectManagementHandlers.handle(request.params.name, request.params.arguments);
break;
case 'objectRegistrationInfo':
case 'validateNewObject':
case 'createObject':
result = await this.objectRegistrationHandlers.handle(request.params.name, request.params.arguments);
break;
case 'nodeContents':
case 'mainPrograms':
result = await this.nodeHandlers.handle(request.params.name, request.params.arguments);
break;
case 'featureDetails':
case 'collectionFeatureDetails':
case 'findCollectionByUrl':
case 'loadTypes':
case 'adtDiscovery':
case 'adtCoreDiscovery':
case 'adtCompatibiliyGraph':
result = await this.discoveryHandlers.handle(request.params.name, request.params.arguments);
break;
case 'unitTestRun':
case 'unitTestEvaluation':
case 'unitTestOccurrenceMarkers':
case 'createTestInclude':
result = await this.unitTestHandlers.handle(request.params.name, request.params.arguments);
break;
case 'prettyPrinterSetting':
case 'setPrettyPrinterSetting':
case 'prettyPrinter':
result = await this.prettyPrinterHandlers.handle(request.params.name, request.params.arguments);
break;
case 'gitRepos':
case 'gitExternalRepoInfo':
case 'gitCreateRepo':
case 'gitPullRepo':
case 'gitUnlinkRepo':
case 'stageRepo':
case 'pushRepo':
case 'checkRepo':
case 'remoteRepoInfo':
case 'switchRepoBranch':
result = await this.gitHandlers.handle(request.params.name, request.params.arguments);
break;
case 'annotationDefinitions':
case 'ddicElement':
case 'ddicRepositoryAccess':
case 'packageSearchHelp':
result = await this.ddicHandlers.handle(request.params.name, request.params.arguments);
break;
case 'publishServiceBinding':
case 'unPublishServiceBinding':
case 'bindingDetails':
result = await this.serviceBindingHandlers.handle(request.params.name, request.params.arguments);
break;
case 'tableContents':
case 'runQuery':
result = await this.queryHandlers.handle(request.params.name, request.params.arguments);
break;
case 'feeds':
case 'dumps':
result = await this.feedHandlers.handle(request.params.name, request.params.arguments);
break;
case 'debuggerListeners':
case 'debuggerListen':
case 'debuggerDeleteListener':
case 'debuggerSetBreakpoints':
case 'debuggerDeleteBreakpoints':
case 'debuggerAttach':
case 'debuggerSaveSettings':
case 'debuggerStackTrace':
case 'debuggerVariables':
case 'debuggerChildVariables':
case 'debuggerStep':
case 'debuggerGoToStack':
case 'debuggerSetVariableValue':
result = await this.debugHandlers.handle(request.params.name, request.params.arguments);
break;
case 'renameEvaluate':
case 'renamePreview':
case 'renameExecute':
result = await this.renameHandlers.handle(request.params.name, request.params.arguments);
break;
case 'atcCustomizing':
case 'atcCheckVariant':
case 'createAtcRun':
case 'atcWorklists':
case 'atcUsers':
case 'atcExemptProposal':
case 'atcRequestExemption':
case 'isProposalMessage':
case 'atcContactUri':
case 'atcChangeContact':
result = await this.atcHandlers.handle(request.params.name, request.params.arguments);
break;
case 'tracesList':
case 'tracesListRequests':
case 'tracesHitList':
case 'tracesDbAccess':
case 'tracesStatements':
case 'tracesSetParameters':
case 'tracesCreateConfiguration':
case 'tracesDeleteConfiguration':
case 'tracesDelete':
result = await this.traceHandlers.handle(request.params.name, request.params.arguments);
break;
case 'extractMethodEvaluate':
case 'extractMethodPreview':
case 'extractMethodExecute':
result = await this.refactorHandlers.handle(request.params.name, request.params.arguments);
break;
case 'revisions':
result = await this.revisionHandlers.handle(request.params.name, request.params.arguments);
break;
case 'healthcheck':
result = { status: 'healthy', timestamp: new Date().toISOString(), isLoggedIn: this.isLoggedIn };
break;
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
return this.serializeResult(result);
} catch (error) {
return this.handleError(error);
}
});
}
async reLogin(config: any) {
const newConfig = {
SAP_URL: config.SAP_URL || process.env.SAP_URL || '',
SAP_USER: config.SAP_USER || process.env.SAP_USER || '',
SAP_PASSWORD: config.SAP_PASSWORD || process.env.SAP_PASSWORD || 'SSO_PLACEHOLDER',
SAP_CLIENT: config.SAP_CLIENT || process.env.SAP_CLIENT || '',
SAP_LANGUAGE: config.SAP_LANGUAGE || process.env.SAP_LANGUAGE || ''
};
if (config.NODE_TLS_REJECT_UNAUTHORIZED) process.env.NODE_TLS_REJECT_UNAUTHORIZED = config.NODE_TLS_REJECT_UNAUTHORIZED;
if (config.NO_PROXY) {
process.env.NO_PROXY = config.NO_PROXY;
process.env.no_proxy = config.NO_PROXY;
}
if (config.HTTP_PROXY) {
process.env.HTTP_PROXY = config.HTTP_PROXY;
process.env.http_proxy = config.HTTP_PROXY;
}
if (config.HTTPS_PROXY) {
process.env.HTTPS_PROXY = config.HTTPS_PROXY;
process.env.https_proxy = config.HTTPS_PROXY;
}
// Check for BTP Connectivity Proxy (re-login)
// Reuse existing proxy if already configured in initBTPConnection
let httpsProxyAgent: any = this.httpsProxyAgent;
let httpProxyAgent: any = this.httpProxyAgent;
// Fallback logic if cache empty but env present (mostly for dev/tests, likely skipped if initBTPConnection worked)
// ... (abbreviated, relying on initBTPConnection primarily)
const clientOptions: any = {};
if (httpsProxyAgent || httpProxyAgent) {
clientOptions.httpsAgent = httpsProxyAgent;
clientOptions.httpAgent = httpProxyAgent;
}
this.adtClient = new ADTClient(
newConfig.SAP_URL,
newConfig.SAP_USER,
newConfig.SAP_PASSWORD,
newConfig.SAP_CLIENT,
newConfig.SAP_LANGUAGE,
clientOptions
);
this.adtClient.stateful = session_types.stateful;
// Re-initialize handlers with new client
this.initializeHandlers(this.adtClient);
// Update AuthHandlers with agents explicitly again to be safe
if (this.authHandlers && (httpProxyAgent || httpsProxyAgent)) {
this.authHandlers.setProxyAgents(httpProxyAgent, httpsProxyAgent, this.proxyHeaders);
}
// Brute-force: Manually inject proxy agent into the internal axios instance (for reLogin)
try {
if (httpsProxyAgent || httpProxyAgent) {
// @ts-ignore
const adtHttp = this.adtClient.httpClient;
// @ts-ignore
const axiosHttpClient = adtHttp.httpclient;
// @ts-ignore
if (axiosHttpClient && axiosHttpClient.axios) {
// @ts-ignore
axiosHttpClient.axios.defaults.httpAgent = httpProxyAgent;
// @ts-ignore
axiosHttpClient.axios.defaults.httpsAgent = httpsProxyAgent;
if (this.proxyHeaders) {
// @ts-ignore
Object.assign(axiosHttpClient.axios.defaults.headers.common, this.proxyHeaders);
}
console.log("Forcibly injected proxy agents and headers into internal ADTClient axios defaults (reLogin).");
}
}
} catch (injectError) {
console.warn("Failed to force inject proxy agent in reLogin:", injectError);
}
// Attempt login with new client to verify and establish session
const result = await this.adtClient.login();
// If successful, mark as logged in
this.isLoggedIn = true;
return result;
}
async run() {
const args = process.argv.slice(2);
const forceStdio = args.includes('--stdio');
await this.initBTPConnection();
if (process.env.PORT && !forceStdio) {
const app = express();
app.use(cors());
// Serve static files from 'client/dist' directory (Vite build)
app.use(express.static(path.join(__dirname, '../client/dist')));
const port = process.env.PORT;
const sessions = new Map<string, { server: AbapAdtServer, transport: SSEServerTransport }>();
app.get('/sse', async (req, res) => {
const sessionId = uuidv4();
const transport = new SSEServerTransport(`/messages?sessionId=${sessionId}`, res);
const server = new AbapAdtServer();
// [CRITICAL FIX] Initialize BTP connection for this session!
// This ensures the correct proxy agent (with token) is loaded for this instance.
// Otherwise, the new instance has no proxy configuration.
await server.initBTPConnection();
sessions.set(sessionId, { server, transport });
console.error(`New session: ${sessionId}`);
res.on('close', () => {
console.error(`Session closed: ${sessionId}`);
sessions.delete(sessionId);
});
await server.connect(transport);
});
app.post('/messages', async (req, res) => {
let sessionId = req.query.sessionId as string;
if (!sessionId) {
res.status(400).send("Missing sessionId");
return;
}
// Fix for potential double query params
if (sessionId.includes('?')) {
sessionId = sessionId.split('?')[0];
}
console.error(`Received message for session: ${sessionId}`);
const session = sessions.get(sessionId);
if (!session) {
console.error(`Session not found: ${sessionId}`);
console.error(`Active sessions: ${Array.from(sessions.keys()).join(', ')}`);
res.status(404).send("Session not found");
return;
}
await session.transport.handlePostMessage(req, res);
});
app.listen(port, () => {
console.error(`MCP Server running on port ${port}`);
});
} else {
const transport = new StdioServerTransport();
await this.connect(transport);
console.error('MCP ABAP ADT API server running on stdio');
// Handle shutdown
process.on('SIGINT', async () => {
await this.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
await this.close();
process.exit(0);
});
// Handle errors
this.onerror = (error) => {
console.error('[MCP Error]', error);
};
}
}
}
// Create and run server instance
const server = new AbapAdtServer();
server.run().catch((error) => {
console.error('Failed to start MCP server:', error);
process.exit(1);
});
// Force git verification change