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.
165 lines
5.5 KiB
TypeScript
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);
|
|
});
|