Files
Orchestrator/bahn/db-planet-mcp-server/src/server.ts
T
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

165 lines
5.5 KiB
TypeScript

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express, { Request, Response, NextFunction } from 'express';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
import getToolAPIServer from "./getToolServer.js";
import bodyParser from 'body-parser';
import { Logger } from "tslog";
import cors from 'cors';
// Load .env file manually (no dotenv dependency)
try {
const __dirname = dirname(fileURLToPath(import.meta.url));
const envPath = resolve(__dirname, '..', '.env');
const envContent = readFileSync(envPath, 'utf-8');
for (const line of envContent.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
const value = trimmed.slice(eqIndex + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
} catch (e) {
// .env file not found — rely on environment variables
}
const log = new Logger({stylePrettyLogs: false});
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors<Request>());
function getCredentials(req: Request): {
username?: string;
password?: string;
clientId?: string;
clientSecret?: string;
baseUrl?: string
} {
return {
username: (req.headers["x-dbplanet-username"] as string) || process.env.DBPLANET_USERNAME,
password: (req.headers["x-dbplanet-password"] as string) || process.env.DBPLANET_PASSWORD,
clientId: (req.headers["x-dbplanet-client-id"] as string) || process.env.DBPLANET_CLIENT_ID,
clientSecret: (req.headers["x-dbplanet-client-secret"] as string) || process.env.DBPLANET_CLIENT_SECRET,
baseUrl: (req.headers["x-dbplanet-base-url"] as string) || process.env.DBPLANET_BASE_URL
};
}
const transports: Record<string, StreamableHTTPServerTransport> = {};
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'ok' });
});
app.all('/servers/dbplanet/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
log.info(`Request to /mcp, session=${sessionId ?? "n/a"}, method=${req.method}`);
try {
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore,
onsessioninitialized: (sessionId) => {
log.info('StreamableHTTP session initialized, session=', sessionId);
transports[sessionId] = transport;
}
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
log.info(`Transport closed, session=${sessionId}`);
delete transports[sid];
}
};
const credentials = getCredentials(req);
const server = await getToolAPIServer(credentials);
await server.connect(transport);
} else {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// For GET requests (SSE streams), send periodic keep-alive pings
// to prevent client-side read timeouts on idle connections.
let keepAliveInterval: ReturnType<typeof setInterval> | undefined;
if (req.method === 'GET') {
keepAliveInterval = setInterval(() => {
if (!res.writableEnded) {
res.write(':ping\n\n');
} else {
clearInterval(keepAliveInterval);
}
}, 15_000);
res.on('close', () => {
if (keepAliveInterval) clearInterval(keepAliveInterval);
});
}
await transport.handleRequest(req, res, req.body);
} catch (error) {
log.error('Error handling MCP request:', (error as Error).message);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
});
}
}
});
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
log.error('Server error:', err.message);
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'production' ? undefined : err.message
});
});
const PORT = 4000;
app.listen(PORT, () => {
log.info(`DB Planet MCP server listening on port ${PORT}`);
log.info(`\nEndpoint: /servers/dbplanet/mcp\nMethods: GET, POST, DELETE\n`);
});
process.on('SIGINT', async () => {
log.info('Shutting down server...');
for (const sessionId in transports) {
try {
log.info(`Closing transport, session=${sessionId}`);
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
log.error(`Error closing transport, session=${sessionId}:`, (error as Error).message);
}
}
log.info('Server shutdown complete');
process.exit(0);
});