This commit is contained in:
nasir@endelospay.com
2025-07-22 22:11:18 +05:00
parent e503b3c670
commit 82d922e8bf
11 changed files with 2061 additions and 11 deletions

View File

@@ -35,7 +35,7 @@ console.log("");
process.env.LARAVEL_API_BASE_URL =
process.env.LARAVEL_API_BASE_URL || "https://example.com";
const transportMode = process.env.MCP_TRANSPORT || "both";
const transportMode = process.env.MCP_TRANSPORT || "all";
console.log(
`🚀 Starting Laravel Healthcare MCP Server (${transportMode} mode)...`
);
@@ -474,6 +474,384 @@ app.post("/mcp", async (req, res) => {
}
});
// n8n MCP Server Information endpoint
app.get("/n8n/info", (req, res) => {
try {
if (!toolGenerator) {
return res.status(500).json({ error: "MCP not initialized" });
}
const tools = toolGenerator.generateAllTools();
const toolsByCategory = {};
tools.forEach((tool) => {
const category = getToolCategory(tool.name);
if (!toolsByCategory[category]) {
toolsByCategory[category] = [];
}
toolsByCategory[category].push({
name: tool.name,
description: tool.description,
authType: toolGenerator.getTool(tool.name)?.authType || "public",
});
});
res.json({
server: {
name: "Laravel Healthcare MCP Server",
version: "1.0.0",
description:
"MCP server providing access to 470+ Laravel Healthcare API endpoints",
protocol: "2024-11-05",
transport: "http",
},
endpoints: {
mcp: "/mcp",
health: "/health",
tools: "/tools",
stats: "/stats",
},
capabilities: {
tools: true,
logging: true,
authentication: true,
},
tools: {
total: tools.length,
categories: toolsByCategory,
},
examples: {
initialize: {
url: "POST /mcp",
body: {
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
clientInfo: { name: "n8n", version: "1.0.0" },
},
id: 1,
},
},
listTools: {
url: "POST /mcp",
body: {
jsonrpc: "2.0",
method: "tools/list",
params: {},
id: 2,
},
},
callTool: {
url: "POST /mcp",
body: {
jsonrpc: "2.0",
method: "tools/call",
params: {
name: "public_manage_login",
arguments: { email: "user@example.com", password: "password" },
},
id: 3,
},
},
},
});
} catch (error) {
console.error("Failed to get n8n info:", error);
res.status(500).json({ error: error.message });
}
});
// Store active SSE connections globally
const sseConnections = new Set();
// SSE endpoint for MCP protocol
app.get("/sse", (req, res) => {
console.log("🔌 New SSE client connecting...");
// Set SSE headers
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control, Content-Type",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
});
// Add to active connections
sseConnections.add(res);
// Send initial MCP connection event
const connectionEvent = {
jsonrpc: "2.0",
method: "notification/initialized",
params: {
protocolVersion: "2024-11-05",
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
capabilities: {
tools: { listChanged: true },
logging: {},
},
message: "MCP SSE transport connected successfully",
timestamp: new Date().toISOString(),
},
};
res.write(`event: mcp-initialized\n`);
res.write(`data: ${JSON.stringify(connectionEvent)}\n\n`);
// Send periodic heartbeat
const heartbeat = setInterval(() => {
if (res.writableEnded) {
clearInterval(heartbeat);
sseConnections.delete(res);
return;
}
try {
const pingEvent = {
jsonrpc: "2.0",
method: "notification/ping",
params: {
timestamp: new Date().toISOString(),
connections: sseConnections.size,
},
};
res.write(`event: ping\n`);
res.write(`data: ${JSON.stringify(pingEvent)}\n\n`);
} catch (error) {
console.log("❌ SSE heartbeat failed, removing connection");
clearInterval(heartbeat);
sseConnections.delete(res);
}
}, 15000); // Every 15 seconds
// Handle client disconnect
req.on("close", () => {
console.log("🔌 SSE client disconnected");
clearInterval(heartbeat);
sseConnections.delete(res);
});
req.on("error", (error) => {
console.log("❌ SSE connection error:", error.message);
clearInterval(heartbeat);
sseConnections.delete(res);
});
console.log(
`✅ SSE client connected (${sseConnections.size} total connections)`
);
});
// SSE MCP message endpoint with proper protocol handling
app.post("/sse/message", async (req, res) => {
try {
const request = req.body;
const startTime = Date.now();
// Validate JSON-RPC format
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request - missing or invalid jsonrpc field",
},
id: request.id || null,
});
}
if (!request.method) {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request - missing method field",
},
id: request.id || null,
});
}
console.log(`📨 SSE MCP request: ${request.method} (ID: ${request.id})`);
// Handle MCP methods with proper error handling
let result;
try {
switch (request.method) {
case "initialize":
const { clientInfo } = request.params || {};
console.log(
`🤝 SSE MCP client connected: ${clientInfo?.name || "Unknown"} v${
clientInfo?.version || "Unknown"
}`
);
result = {
protocolVersion: "2024-11-05",
capabilities: {
tools: {
listChanged: true,
},
logging: {},
},
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
};
break;
case "tools/list":
const allTools = toolGenerator.generateAllTools();
const tools = allTools.map((tool) => {
const toolDef = toolGenerator.getTool(tool.name);
return {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
// Add metadata for better client understanding
metadata: {
authType: toolDef?.authType || "public",
category: getToolCategory(tool.name),
endpoint: {
method: toolDef?.endpoint?.method,
path: toolDef?.endpoint?.path,
},
},
};
});
result = { tools };
console.log(`📋 Listed ${tools.length} tools for SSE MCP client`);
break;
case "tools/call":
const { name: toolName, arguments: toolArgs } = request.params;
console.log(`🔧 SSE MCP tool execution: ${toolName}`);
console.log(`📝 Parameters:`, JSON.stringify(toolArgs, null, 2));
const tool = toolGenerator.getTool(toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
const toolResult = await tool.execute(toolArgs || {});
// Handle login token extraction for provider authentication
if (toolName === "public_manage_login" && toolResult && authManager) {
await handleLoginTokenExtraction(toolResult);
}
result = {
content: [
{
type: "text",
text: JSON.stringify(toolResult, null, 2),
},
],
};
const duration = Date.now() - startTime;
console.log(
`✅ SSE Tool ${toolName} executed successfully (${duration}ms)`
);
break;
case "ping":
result = {
timestamp: new Date().toISOString(),
server: "laravel-healthcare-mcp-server",
transport: "sse",
};
break;
default:
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32601,
message: `Method not found: ${request.method}`,
},
id: request.id || null,
});
}
// Return successful JSON-RPC response
const response = {
jsonrpc: "2.0",
result: result,
id: request.id || null,
};
res.json(response);
// Broadcast to SSE connections if needed
if (sseConnections.size > 0) {
const notification = {
jsonrpc: "2.0",
method: "notification/method_executed",
params: {
method: request.method,
success: true,
timestamp: new Date().toISOString(),
duration: Date.now() - startTime,
},
};
sseConnections.forEach((connection) => {
try {
if (!connection.writableEnded) {
connection.write(`event: method-executed\n`);
connection.write(`data: ${JSON.stringify(notification)}\n\n`);
}
} catch (error) {
console.log(
"Failed to broadcast to SSE connection:",
error.message
);
}
});
}
} catch (methodError) {
const duration = Date.now() - startTime;
console.error(
`❌ SSE Tool execution failed (${duration}ms):`,
methodError.message
);
// Return MCP-compliant error response
return res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: methodError.message || "Internal error",
data: {
method: request.method,
timestamp: new Date().toISOString(),
duration: duration,
},
},
id: request.id || null,
});
}
} catch (error) {
console.error(`❌ SSE MCP request error:`, error);
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32700,
message: "Parse error",
},
id: null,
});
}
});
// List all tools
app.get("/tools", (req, res) => {
try {
@@ -746,6 +1124,10 @@ app.use("*", (req, res) => {
error: "Endpoint not found",
availableEndpoints: [
"GET /health",
"POST /mcp",
"GET /sse",
"POST /sse/message",
"GET /n8n/info",
"GET /tools",
"GET /tools/:toolName",
"POST /tools/:toolName/execute",
@@ -872,7 +1254,9 @@ async function startHttpMode() {
*/
async function startBothModes() {
try {
console.log("🔄 Starting MCP server in dual transport mode (stdio + HTTP)...");
console.log(
"🔄 Starting MCP server in dual transport mode (stdio + HTTP)..."
);
const mcpReady = await initializeMCP();
if (!mcpReady) {
@@ -895,14 +1279,18 @@ async function startBothModes() {
// Start HTTP server
const httpPromise = new Promise((resolve, reject) => {
const server = app.listen(port, host, () => {
const serverUrl = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
const serverUrl = `http://${
host === "0.0.0.0" ? "localhost" : host
}:${port}`;
const toolCount = toolGenerator.generateAllTools().length;
console.log("\n" + "=".repeat(70));
console.log("🚀 LARAVEL HEALTHCARE MCP SERVER - DUAL MODE");
console.log("=".repeat(70));
console.log("📡 Transports:");
console.log(` • Stdio: Ready for MCP clients (Claude Desktop, VS Code)`);
console.log(
` • Stdio: Ready for MCP clients (Claude Desktop, VS Code)`
);
console.log(` • HTTP: ${serverUrl}`);
console.log("=".repeat(70));
console.log(`🌐 Host: ${host}`);
@@ -937,7 +1325,7 @@ async function startBothModes() {
resolve(server);
});
server.on('error', reject);
server.on("error", reject);
});
// Wait for both transports to initialize
@@ -955,13 +1343,429 @@ async function startBothModes() {
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
} catch (error) {
console.error("❌ Failed to start dual transport server:", error);
process.exit(1);
}
}
/**
* Start MCP server with all transports (stdio + HTTP + SSE)
*/
async function startAllTransports() {
try {
console.log(
"🔄 Starting MCP server with all transports (stdio + HTTP + SSE)..."
);
const mcpReady = await initializeMCP();
if (!mcpReady) {
console.error("❌ Cannot start server without MCP initialization");
process.exit(1);
}
// Start stdio transport in background
const stdioPromise = (async () => {
try {
const transport = new StdioServerTransport();
await mcpServer.connect(transport);
console.log("✅ Stdio transport connected and ready");
} catch (error) {
console.error("❌ Failed to start stdio transport:", error);
// Don't exit - other transports can still work
}
})();
// Start SSE transport
const ssePromise = (async () => {
try {
// Store active SSE connections
const sseConnections = new Set();
// Add SSE endpoint for MCP
app.get("/sse", (req, res) => {
console.log("🔌 New SSE client connecting...");
// Set SSE headers
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control, Content-Type",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
});
// Add to active connections
sseConnections.add(res);
// Send initial MCP connection event
const connectionEvent = {
jsonrpc: "2.0",
method: "notification/initialized",
params: {
protocolVersion: "2024-11-05",
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
capabilities: {
tools: { listChanged: true },
logging: {},
},
message: "MCP SSE transport connected successfully",
timestamp: new Date().toISOString(),
},
};
res.write(`event: mcp-initialized\n`);
res.write(`data: ${JSON.stringify(connectionEvent)}\n\n`);
// Send periodic heartbeat
const heartbeat = setInterval(() => {
if (res.writableEnded) {
clearInterval(heartbeat);
sseConnections.delete(res);
return;
}
try {
const pingEvent = {
jsonrpc: "2.0",
method: "notification/ping",
params: {
timestamp: new Date().toISOString(),
connections: sseConnections.size,
},
};
res.write(`event: ping\n`);
res.write(`data: ${JSON.stringify(pingEvent)}\n\n`);
} catch (error) {
console.log("❌ SSE heartbeat failed, removing connection");
clearInterval(heartbeat);
sseConnections.delete(res);
}
}, 15000); // Every 15 seconds
// Handle client disconnect
req.on("close", () => {
console.log("🔌 SSE client disconnected");
clearInterval(heartbeat);
sseConnections.delete(res);
});
req.on("error", (error) => {
console.log("❌ SSE connection error:", error.message);
clearInterval(heartbeat);
sseConnections.delete(res);
});
console.log(
`✅ SSE client connected (${sseConnections.size} total connections)`
);
});
// SSE MCP message endpoint with proper protocol handling
app.post("/sse/message", async (req, res) => {
try {
const request = req.body;
const startTime = Date.now();
// Validate JSON-RPC format
if (!request.jsonrpc || request.jsonrpc !== "2.0") {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request - missing or invalid jsonrpc field",
},
id: request.id || null,
});
}
if (!request.method) {
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32600,
message: "Invalid Request - missing method field",
},
id: request.id || null,
});
}
console.log(
`📨 SSE MCP request: ${request.method} (ID: ${request.id})`
);
// Handle MCP methods with proper error handling
let result;
try {
switch (request.method) {
case "initialize":
const { clientInfo } = request.params || {};
console.log(
`🤝 SSE MCP client connected: ${
clientInfo?.name || "Unknown"
} v${clientInfo?.version || "Unknown"}`
);
result = {
protocolVersion: "2024-11-05",
capabilities: {
tools: {
listChanged: true,
},
logging: {},
},
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
};
break;
case "tools/list":
const allTools = toolGenerator.generateAllTools();
const tools = allTools.map((tool) => {
const toolDef = toolGenerator.getTool(tool.name);
return {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
// Add metadata for better client understanding
metadata: {
authType: toolDef?.authType || "public",
category: getToolCategory(tool.name),
endpoint: {
method: toolDef?.endpoint?.method,
path: toolDef?.endpoint?.path,
},
},
};
});
result = { tools };
console.log(
`📋 Listed ${tools.length} tools for SSE MCP client`
);
break;
case "tools/call":
const { name: toolName, arguments: toolArgs } =
request.params;
console.log(`🔧 SSE MCP tool execution: ${toolName}`);
console.log(
`📝 Parameters:`,
JSON.stringify(toolArgs, null, 2)
);
const tool = toolGenerator.getTool(toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
const toolResult = await tool.execute(toolArgs || {});
// Handle login token extraction for provider authentication
if (
toolName === "public_manage_login" &&
toolResult &&
authManager
) {
await handleLoginTokenExtraction(toolResult);
}
result = {
content: [
{
type: "text",
text: JSON.stringify(toolResult, null, 2),
},
],
};
const duration = Date.now() - startTime;
console.log(
`✅ SSE Tool ${toolName} executed successfully (${duration}ms)`
);
break;
case "ping":
result = {
timestamp: new Date().toISOString(),
server: "laravel-healthcare-mcp-server",
transport: "sse",
};
break;
default:
return res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32601,
message: `Method not found: ${request.method}`,
},
id: request.id || null,
});
}
// Return successful JSON-RPC response
const response = {
jsonrpc: "2.0",
result: result,
id: request.id || null,
};
res.json(response);
// Broadcast to SSE connections if needed
if (sseConnections.size > 0) {
const notification = {
jsonrpc: "2.0",
method: "notification/method_executed",
params: {
method: request.method,
success: true,
timestamp: new Date().toISOString(),
duration: Date.now() - startTime,
},
};
sseConnections.forEach((connection) => {
try {
if (!connection.writableEnded) {
connection.write(`event: method-executed\n`);
connection.write(
`data: ${JSON.stringify(notification)}\n\n`
);
}
} catch (error) {
console.log(
"Failed to broadcast to SSE connection:",
error.message
);
}
});
}
} catch (methodError) {
const duration = Date.now() - startTime;
console.error(
`❌ SSE Tool execution failed (${duration}ms):`,
methodError.message
);
// Return MCP-compliant error response
return res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: methodError.message || "Internal error",
data: {
method: request.method,
timestamp: new Date().toISOString(),
duration: duration,
},
},
id: request.id || null,
});
}
} catch (error) {
console.error(`❌ SSE MCP request error:`, error);
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32700,
message: "Parse error",
},
id: null,
});
}
});
console.log("✅ SSE transport configured and ready");
} catch (error) {
console.error("❌ Failed to configure SSE transport:", error);
}
})();
// Start HTTP server
const httpPromise = new Promise((resolve, reject) => {
const server = app.listen(port, host, () => {
const serverUrl = `http://${
host === "0.0.0.0" ? "localhost" : host
}:${port}`;
const toolCount = toolGenerator.generateAllTools().length;
console.log("\n" + "=".repeat(80));
console.log("🚀 LARAVEL HEALTHCARE MCP SERVER - ALL TRANSPORTS");
console.log("=".repeat(80));
console.log("📡 Available Transports:");
console.log(
` • Stdio: Ready for MCP clients (Claude Desktop, VS Code)`
);
console.log(` • HTTP: ${serverUrl}/mcp (JSON-RPC 2.0)`);
console.log(` • SSE: ${serverUrl}/sse (Server-Sent Events)`);
console.log("=".repeat(80));
console.log(`🌐 Host: ${host}`);
console.log(`🔌 Port: ${port}`);
console.log(`🔗 API URL: ${process.env.LARAVEL_API_BASE_URL}`);
console.log(`📊 Available Tools: ${toolCount}`);
console.log(`🔄 Protocol Version: 2024-11-05`);
console.log("=".repeat(80));
console.log("📋 Connection URLs:");
console.log(` • HTTP MCP: POST ${serverUrl}/mcp`);
console.log(` • SSE Stream: GET ${serverUrl}/sse`);
console.log(` • SSE Messages: POST ${serverUrl}/sse/message`);
console.log(` • Health: GET ${serverUrl}/health`);
console.log(` • n8n Info: GET ${serverUrl}/n8n/info`);
console.log("=".repeat(80));
console.log("🧪 Test Commands:");
console.log(" # Test SSE connection");
console.log(` curl -N ${serverUrl}/sse`);
console.log("");
console.log(" # Test SSE MCP message");
console.log(` curl -X POST ${serverUrl}/sse/message \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(` -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'`);
console.log("=".repeat(80));
console.log("📊 Server Status: READY (All Transports)");
console.log(`⏰ Started at: ${new Date().toLocaleString()}`);
console.log("=".repeat(80));
console.log("💡 Press Ctrl+C to stop the server");
console.log("");
resolve(server);
});
server.on("error", reject);
});
// Wait for all transports to initialize
const [, , server] = await Promise.all([
stdioPromise,
ssePromise,
httpPromise,
]);
// Graceful shutdown for all transports
const shutdown = (signal) => {
console.log(`\n🛑 Received ${signal}, shutting down all transports...`);
server.close(() => {
console.log("✅ HTTP server stopped");
console.log("✅ SSE transport stopped");
console.log("✅ Stdio transport stopped");
process.exit(0);
});
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
} catch (error) {
console.error("❌ Failed to start all transports server:", error);
process.exit(1);
}
}
/**
* Main entry point - start server based on transport mode
*/
@@ -975,9 +1779,11 @@ async function main() {
await startHttpMode();
} else if (transportMode === "both") {
await startBothModes();
} else if (transportMode === "all") {
await startAllTransports();
} else {
console.error(`❌ Invalid transport mode: ${transportMode}`);
console.log("💡 Valid modes: 'stdio', 'http', or 'both'");
console.log("💡 Valid modes: 'stdio', 'http', 'both', or 'all'");
console.log("💡 Set MCP_TRANSPORT environment variable");
process.exit(1);
}