@iflow-mcp/ansvar-systems-german-law-mcp
Version:
German legal research MCP server with statutes, case law, and preparatory works from gesetze-im-internet.de
172 lines (171 loc) • 6.85 kB
JavaScript
/**
* HTTP Server Entry Point for Premium Docker Deployment
*
* Provides Streamable HTTP transport for remote MCP clients.
* Use src/index.ts for local stdio-based usage.
*/
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { randomUUID } from "node:crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { LawMcpShell } from "./shell/shell.js";
import { germanyAdapter } from "./adapters/de.js";
import { getCapabilities, getDb } from "./db/german-law-db.js";
import { getPremiumTools } from "./premium-tools.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PORT = parseInt(process.env.PORT || "3000", 10);
let pkgVersion = "0.0.0";
try {
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
pkgVersion = pkg.version;
}
catch {
// fallback
}
const SERVER_NAME = "german-law-mcp";
function createMcpServer() {
const enrichedAdapter = {
...germanyAdapter,
getDbCapabilities: () => getCapabilities(),
};
const shell = LawMcpShell.fromAdapters([enrichedAdapter]);
const server = new Server({ name: SERVER_NAME, version: pkgVersion }, { capabilities: { tools: {} } });
// Detect premium tools using the SDK's public API instead of wrapping
// internal _requestHandlers (which broke in SDK v1.27.1).
let premium = null;
try {
const db = getDb();
if (db) {
premium = getPremiumTools(db);
}
}
catch (err) {
console.warn(`[${SERVER_NAME}] Premium tools not available:`, err);
}
const premiumToolNames = new Set(premium?.handlers.keys() ?? []);
server.setRequestHandler(ListToolsRequestSchema, async () => {
const definitions = shell.getToolDefinitions();
const baseTools = definitions.map((def) => ({
name: def.name,
description: def.description,
inputSchema: def.inputSchema,
}));
if (!premium)
return { tools: baseTools };
// Deduplicate: premium tools override base tools with the same name
const filtered = baseTools.filter((t) => !premiumToolNames.has(t.name));
return { tools: [...filtered, ...premium.tools] };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const args = (request.params.arguments ?? {});
// Premium tool — handle directly
const premiumHandler = premium?.handlers.get(toolName);
if (premiumHandler) {
try {
const data = premiumHandler(args);
return {
content: [
{ type: "text", text: JSON.stringify(data, null, 2) },
],
};
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
content: [
{ type: "text", text: `Error executing ${toolName}: ${message}` },
],
isError: true,
};
}
}
// Base tool — delegate to shell
const result = await shell.handleToolCall({
name: toolName,
arguments: args,
});
if (result.ok) {
return {
content: [
{ type: "text", text: JSON.stringify(result.data, null, 2) },
],
};
}
return {
content: [
{ type: "text", text: JSON.stringify(result.error, null, 2) },
],
isError: true,
};
});
return { server, shell };
}
async function main() {
const sessions = new Map();
const httpServer = createServer((req, res) => {
handleRequest(req, res, sessions).catch((err) => {
console.error(`[${SERVER_NAME}] Unhandled error:`, err);
if (!res.headersSent) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal server error" }));
}
});
});
async function handleRequest(req, res, sessions) {
const url = new URL(req.url || "/", `http://localhost:${PORT}`);
if (url.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", server: SERVER_NAME }));
return;
}
if (url.pathname === "/mcp") {
const sessionId = req.headers["mcp-session-id"];
if (sessionId && sessions.has(sessionId)) {
const session = sessions.get(sessionId);
await session.transport.handleRequest(req, res);
return;
}
// New session — create a fresh MCP server instance per session
const { server: mcpServer } = createMcpServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK type mismatch with exactOptionalPropertyTypes
await mcpServer.connect(transport);
transport.onclose = () => {
if (transport.sessionId) {
sessions.delete(transport.sessionId);
}
mcpServer.close().catch(() => { });
};
await transport.handleRequest(req, res);
// Store AFTER handleRequest — sessionId is set during initialize
if (transport.sessionId) {
sessions.set(transport.sessionId, { transport, server: mcpServer });
}
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
}
httpServer.listen(PORT, () => {
console.error(`${SERVER_NAME} (HTTP) listening on port ${PORT}`);
console.error(`MCP endpoint: http://localhost:${PORT}/mcp`);
console.error(`Health check: http://localhost:${PORT}/health`);
});
process.on("SIGTERM", () => {
console.error("Received SIGTERM, shutting down...");
httpServer.close(() => process.exit(0));
});
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});