Files
mcp-tool/http-tools-server.js
nasir@endelospay.com 82d922e8bf fix
2025-07-22 22:11:18 +05:00

1799 lines
54 KiB
JavaScript

#!/usr/bin/env node
/**
* @fileoverview MCP-compliant HTTP and stdio server for Laravel Healthcare API
* Implements full MCP protocol specification with dual transport support
* Based on @modelcontextprotocol/sdk v1.0.4
*/
import dotenv from "dotenv";
import express from "express";
import cors from "cors";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
InitializeRequestSchema,
PingRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// Load environment variables from .env file
dotenv.config();
console.log("🔧 Environment variables loaded:");
console.log(
` LARAVEL_API_BASE_URL: ${process.env.LARAVEL_API_BASE_URL || "NOT SET"}`
);
console.log(` MCP_SERVER_PORT: ${process.env.MCP_SERVER_PORT || "NOT SET"}`);
console.log(` MCP_SERVER_HOST: ${process.env.MCP_SERVER_HOST || "NOT SET"}`);
console.log(` MCP_TRANSPORT: ${process.env.MCP_TRANSPORT || "http"}`);
console.log("");
// Set default values if not provided
process.env.LARAVEL_API_BASE_URL =
process.env.LARAVEL_API_BASE_URL || "https://example.com";
const transportMode = process.env.MCP_TRANSPORT || "all";
console.log(
`🚀 Starting Laravel Healthcare MCP Server (${transportMode} mode)...`
);
// HTTP server setup (for HTTP transport mode)
const app = express();
const port = process.env.MCP_SERVER_PORT || 3000;
const host = process.env.MCP_SERVER_HOST || "0.0.0.0";
// Middleware for HTTP mode
app.use(
cors({
origin: "*",
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
})
);
app.use(express.json({ limit: "10mb" }));
// MCP Server and components
let mcpServer = null;
let toolGenerator = null;
let authManager = null;
let configManager = null;
/**
* Initialize MCP components and create MCP server instance
* @returns {Promise<boolean>} Success status
*/
async function initializeMCP() {
try {
console.log("📋 Initializing MCP components...");
// Import required modules
const { ConfigManager } = await import("./src/config/ConfigManager.js");
const { AuthManager } = await import("./src/auth/AuthManager.js");
const { ApiClient } = await import("./src/proxy/ApiClient.js");
const { ToolGenerator } = await import("./src/tools/ToolGenerator.js");
const { logger } = await import("./src/utils/logger.js");
// Initialize components
configManager = new ConfigManager();
const config = configManager.getAll(true);
authManager = new AuthManager(null, config);
const apiClient = new ApiClient(config, authManager);
toolGenerator = new ToolGenerator(apiClient);
// Create MCP server instance
mcpServer = new Server(
{
name: config.MCP_SERVER_NAME || "laravel-healthcare-mcp-server",
version: config.MCP_SERVER_VERSION || "1.0.0",
},
{
capabilities: {
tools: {},
logging: {},
},
}
);
// Setup MCP request handlers
await setupMcpHandlers();
console.log("✅ MCP components and server initialized");
logger.info(
`MCP Server initialized with ${
toolGenerator.generateAllTools().length
} tools`
);
return true;
} catch (error) {
console.error("❌ Failed to initialize MCP:", error.message);
console.error(error.stack);
return false;
}
}
/**
* Setup MCP protocol request handlers
* Implements all required MCP methods according to specification
*/
async function setupMcpHandlers() {
// Handle initialization requests
mcpServer.setRequestHandler(InitializeRequestSchema, async (request) => {
const { clientInfo } = request.params;
console.log(
`🤝 MCP client connected: ${clientInfo?.name || "Unknown"} v${
clientInfo?.version || "Unknown"
}`
);
return {
protocolVersion: "2024-11-05",
capabilities: {
tools: {
listChanged: true,
},
logging: {},
},
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
};
});
// Handle ping requests
mcpServer.setRequestHandler(PingRequestSchema, async () => {
return {};
});
// Handle list tools requests
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
try {
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,
},
},
};
});
console.log(`📋 Listed ${tools.length} tools for MCP client`);
return { tools };
} catch (error) {
console.error("Error listing tools:", error);
throw new Error(`Failed to list tools: ${error.message}`);
}
});
// Handle call tool requests
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name: toolName, arguments: toolArgs } = request.params;
const startTime = Date.now();
try {
console.log(`🔧 MCP tool execution: ${toolName}`);
console.log(`📝 Parameters:`, JSON.stringify(toolArgs, null, 2));
// Get tool implementation
const tool = toolGenerator.getTool(toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
// Execute tool
const result = await tool.execute(toolArgs || {});
// Handle login token extraction for provider authentication
if (toolName === "public_manage_login" && result && authManager) {
await handleLoginTokenExtraction(result);
}
const duration = Date.now() - startTime;
console.log(`✅ Tool ${toolName} executed successfully (${duration}ms)`);
// Return MCP-compliant response
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(
`❌ Tool execution failed for ${toolName} (${duration}ms):`,
error.message
);
// Return MCP-compliant error response
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: true,
message: error.message,
toolName: toolName,
timestamp: new Date().toISOString(),
},
null,
2
),
},
],
isError: true,
};
}
});
}
/**
* Extract and categorize tool by name pattern
* @param {string} toolName - Tool name
* @returns {string} Category
*/
function getToolCategory(toolName) {
if (toolName.startsWith("public_")) return "authentication";
if (toolName.startsWith("provider_")) return "clinical";
if (toolName.startsWith("patient_")) return "patient-portal";
if (toolName.startsWith("partner_")) return "business";
if (toolName.startsWith("affiliate_")) return "affiliate";
if (toolName.startsWith("network_")) return "network";
return "general";
}
/**
* Handle token extraction from login responses
* @param {Object} result - Login response result
*/
async function handleLoginTokenExtraction(result) {
try {
let token = null;
let expiresIn = 3600; // Default 1 hour
let userData = null;
// Extract token from different possible response formats
if (result.accessToken || result.access_token || result.token) {
token = result.accessToken || result.access_token || result.token;
expiresIn = result.expiresIn || result.expires_in || 3600;
userData = result.userData || result.user || result.data || null;
} else if (result.data) {
// Token might be nested in data object
token =
result.data.accessToken ||
result.data.access_token ||
result.data.token;
expiresIn = result.data.expiresIn || result.data.expires_in || 3600;
userData = result.data.userData || result.data.user || null;
}
if (token) {
// Store token for provider auth type
authManager.setToken("provider", token, expiresIn, userData);
console.log(
`🔑 Stored bearer token for provider authentication (expires in ${expiresIn}s)`
);
// Add token info to response
result._tokenInfo = {
stored: true,
authType: "provider",
expiresIn: expiresIn,
message: "Token automatically stored for provider endpoints",
};
} else {
console.log(`⚠️ No token found in login response`);
}
} catch (error) {
console.error(
`❌ Failed to store token from login response:`,
error.message
);
}
}
// Health endpoint for HTTP mode
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
server: "Laravel Healthcare MCP Server",
version: "1.0.0",
transport: transportMode,
port: port,
mcpInitialized: mcpServer !== null && toolGenerator !== null,
protocolVersion: "2024-11-05",
capabilities: {
tools: true,
logging: true,
},
});
});
// MCP protocol endpoint for HTTP transport
app.post("/mcp", async (req, res) => {
try {
if (!mcpServer) {
return res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "MCP server not initialized",
},
id: req.body.id || null,
});
}
// Handle JSON-RPC 2.0 requests
const request = req.body;
// 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(`📨 MCP HTTP request: ${request.method}`);
// Route to appropriate handler based on method
let result;
try {
switch (request.method) {
case "initialize":
// Handle initialization directly
result = {
protocolVersion: "2024-11-05",
capabilities: {
tools: {
listChanged: true,
},
logging: {},
},
serverInfo: {
name: "laravel-healthcare-mcp-server",
version: "1.0.0",
},
};
break;
case "tools/list":
// Handle 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,
};
});
result = { tools };
break;
case "tools/call":
// Handle tool execution
const { name: toolName, arguments: toolArgs } = request.params;
const tool = toolGenerator.getTool(toolName);
if (!tool) {
throw new Error(`Tool not found: ${toolName}`);
}
const toolResult = await tool.execute(toolArgs || {});
// Handle login token extraction
if (toolName === "public_manage_login" && toolResult && authManager) {
await handleLoginTokenExtraction(toolResult);
}
result = {
content: [
{
type: "text",
text: JSON.stringify(toolResult, null, 2),
},
],
};
break;
case "ping":
result = {};
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
res.json({
jsonrpc: "2.0",
result: result,
id: request.id || null,
});
} catch (error) {
console.error(`❌ MCP request error:`, error);
// Return JSON-RPC error response
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: error.message || "Internal error",
data: {
method: request.method,
timestamp: new Date().toISOString(),
},
},
id: request.id || null,
});
}
} catch (error) {
console.error("❌ MCP endpoint error:", error);
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32700,
message: "Parse error",
},
id: null,
});
}
});
// 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 {
if (!toolGenerator) {
return res.status(500).json({ error: "MCP not initialized" });
}
const tools = toolGenerator.generateAllTools();
const toolsWithDetails = tools.map((tool) => {
const toolDef = toolGenerator.getTool(tool.name);
return {
name: tool.name,
description: tool.description,
authType: toolDef?.authType,
method: toolDef?.endpoint?.method,
path: toolDef?.endpoint?.path,
inputSchema: tool.inputSchema,
};
});
res.json({
total: toolsWithDetails.length,
tools: toolsWithDetails,
});
} catch (error) {
console.error("Failed to list tools:", error);
res.status(500).json({ error: error.message });
}
});
// Get specific tool
app.get("/tools/:toolName", (req, res) => {
try {
if (!toolGenerator) {
return res.status(500).json({ error: "MCP not initialized" });
}
const { toolName } = req.params;
const tool = toolGenerator.getTool(toolName);
if (!tool) {
return res.status(404).json({ error: "Tool not found" });
}
res.json({
name: tool.name,
description: tool.description,
authType: tool.authType,
endpoint: tool.endpoint,
inputSchema: tool.inputSchema,
});
} catch (error) {
console.error("Failed to get tool:", error);
res.status(500).json({ error: error.message });
}
});
// Execute MCP tool
app.post("/tools/:toolName/execute", async (req, res) => {
const { toolName } = req.params;
const parameters = req.body;
try {
if (!toolGenerator) {
return res.status(500).json({ error: "MCP not initialized" });
}
console.log(`🔧 Executing tool: ${toolName}`);
console.log(`📝 Parameters:`, JSON.stringify(parameters, null, 2));
// Get all tools and find the one we want
const allTools = toolGenerator.generateAllTools();
const toolDef = allTools.find((tool) => tool.name === toolName);
if (!toolDef) {
console.log(`❌ Tool ${toolName} not found in generated tools`);
return res.status(404).json({ error: "Tool not found" });
}
console.log(`🔍 Found tool: ${toolDef.name}`);
// Get the actual tool implementation
const tool = toolGenerator.getTool(toolName);
console.log(tool);
if (!tool || !tool.execute) {
console.log(`❌ Tool ${toolName} has no execute method`);
return res
.status(500)
.json({ error: "Tool execution method not available" });
}
console.log(`🚀 Executing tool...`);
const result = await tool.execute(parameters);
console.log(`✅ Tool ${toolName} executed successfully`);
console.log(`📊 Result:`, JSON.stringify(result, null, 2));
// Special handling for login tools - extract and store token
if (toolName === "public_manage_login" && result && authManager) {
try {
let token = null;
let expiresIn = 3600; // Default 1 hour
let userData = null;
// Extract token from different possible response formats
if (result.accessToken || result.access_token || result.token) {
token = result.accessToken || result.access_token || result.token;
expiresIn = result.expiresIn || result.expires_in || 3600;
userData = result.userData || result.user || result.data || null;
} else if (result.data) {
// Token might be nested in data object
token =
result.data.accessToken ||
result.data.access_token ||
result.data.token;
expiresIn = result.data.expiresIn || result.data.expires_in || 3600;
userData = result.data.userData || result.data.user || null;
}
if (token) {
// Store token for provider auth type
authManager.setToken("provider", token, expiresIn, userData);
console.log(
`🔑 Stored bearer token for provider authentication (expires in ${expiresIn}s)`
);
// Add token info to response
result._tokenInfo = {
stored: true,
authType: "provider",
expiresIn: expiresIn,
message: "Token automatically stored for provider endpoints",
};
} else {
console.log(`⚠️ No token found in login response`);
}
} catch (error) {
console.error(
`❌ Failed to store token from login response:`,
error.message
);
}
}
res.json({
success: true,
toolName,
result,
});
} catch (error) {
console.error(`❌ Tool execution failed for ${toolName}:`, error.message);
console.error(`📋 Error stack:`, error.stack);
res.status(error.status || 500).json({
success: false,
toolName,
error: error.message,
details: error.details || null,
stack: error.stack,
});
}
});
// Server stats
app.get("/stats", (req, res) => {
try {
if (!toolGenerator) {
return res.status(500).json({ error: "MCP not initialized" });
}
const tools = toolGenerator.generateAllTools();
const publicTools = tools.filter((tool) => {
const toolDef = toolGenerator.getTool(tool.name);
return toolDef?.authType === "public";
});
const providerTools = tools.filter((tool) => {
const toolDef = toolGenerator.getTool(tool.name);
return toolDef?.authType === "provider";
});
res.json({
server: {
name: "Laravel Healthcare MCP Server",
version: "1.0.0",
uptime: process.uptime(),
memory: process.memoryUsage(),
},
tools: {
total: tools.length,
public: publicTools.length,
provider: providerTools.length,
},
config: {
port: port,
host: host,
apiUrl: process.env.LARAVEL_API_BASE_URL,
},
authentication: {
hasProviderToken: authManager
? authManager.getCacheStats().keys?.includes("token_provider") ||
false
: false,
},
});
} catch (error) {
console.error("Failed to get stats:", error);
res.status(500).json({ error: error.message });
}
});
// Set bearer token manually
app.post("/auth/set-token", (req, res) => {
try {
if (!authManager) {
return res.status(500).json({ error: "Auth manager not initialized" });
}
const {
authType = "provider",
token,
expiresIn = 3600,
userData = null,
} = req.body;
if (!token) {
return res.status(400).json({ error: "Token is required" });
}
authManager.setToken(authType, token, expiresIn, userData);
res.json({
success: true,
message: `Token set for ${authType}`,
authType,
expiresIn,
});
} catch (error) {
console.error("Failed to set token:", error);
res.status(500).json({ error: error.message });
}
});
// Get auth status
app.get("/auth/status", (req, res) => {
try {
if (!authManager) {
return res.status(500).json({ error: "Auth manager not initialized" });
}
const cacheStats = authManager.getCacheStats();
const hasProviderToken =
cacheStats.keys?.includes("token_provider") || false;
res.json({
authManager: "initialized",
cacheStats,
tokens: {
provider: hasProviderToken ? "present" : "missing",
},
});
} catch (error) {
console.error("Failed to get auth status:", error);
res.status(500).json({ error: error.message });
}
});
// 404 handler
app.use("*", (req, res) => {
res.status(404).json({
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",
"GET /stats",
"POST /auth/set-token",
"GET /auth/status",
],
});
});
// Error handler
app.use((error, req, res, next) => {
console.error("HTTP server error:", error);
res.status(500).json({
error: "Internal server error",
message: error.message,
});
});
/**
* Start MCP server in stdio mode (for desktop clients like Claude Desktop)
*/
async function startStdioMode() {
try {
console.log("🔌 Starting MCP server in stdio mode...");
const mcpReady = await initializeMCP();
if (!mcpReady) {
console.error("❌ Cannot start server without MCP initialization");
process.exit(1);
}
// Create stdio transport
const transport = new StdioServerTransport();
// Connect server to transport
await mcpServer.connect(transport);
console.log("✅ MCP Server started in stdio mode");
console.log(
`📊 Available tools: ${toolGenerator.generateAllTools().length}`
);
console.log("🔗 Connected to stdio transport");
console.log("💡 Server ready for MCP client connections");
} catch (error) {
console.error("❌ Failed to start stdio server:", error);
process.exit(1);
}
}
/**
* Start MCP server in HTTP mode (for web-based clients)
*/
async function startHttpMode() {
try {
console.log("🌐 Starting MCP server in HTTP mode...");
const mcpReady = await initializeMCP();
if (!mcpReady) {
console.error("❌ Cannot start server without MCP initialization");
process.exit(1);
}
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(70));
console.log("🚀 LARAVEL HEALTHCARE MCP SERVER - HTTP MODE");
console.log("=".repeat(70));
console.log(`📡 Server URL: ${serverUrl}`);
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(70));
console.log("📋 MCP Endpoints:");
console.log(` • Health Check: ${serverUrl}/health`);
console.log(` • MCP Protocol: POST ${serverUrl}/mcp`);
console.log(` • Tools List: ${serverUrl}/tools`);
console.log(` • Server Stats: ${serverUrl}/stats`);
console.log("=".repeat(70));
console.log("🧪 Test MCP Protocol:");
console.log(` curl -X POST ${serverUrl}/mcp \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(` -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'`);
console.log("");
console.log("🔑 Test Login Tool:");
console.log(` curl -X POST ${serverUrl}/mcp \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(
` -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"public_manage_login","arguments":{"email":"test@example.com","password":"password"}},"id":2}'`
);
console.log("=".repeat(70));
console.log("📊 Server Status: READY");
console.log(`⏰ Started at: ${new Date().toLocaleString()}`);
console.log("=".repeat(70));
console.log("💡 Press Ctrl+C to stop the server");
console.log("");
});
// Graceful shutdown
const shutdown = (signal) => {
console.log(`\n🛑 Received ${signal}, shutting down HTTP server...`);
server.close(() => {
console.log("✅ HTTP server stopped");
process.exit(0);
});
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
} catch (error) {
console.error("❌ Failed to start HTTP server:", error);
process.exit(1);
}
}
/**
* Start MCP server in both stdio and HTTP modes simultaneously
*/
async function startBothModes() {
try {
console.log(
"🔄 Starting MCP server in dual transport mode (stdio + HTTP)..."
);
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 - HTTP mode can still work
}
})();
// 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(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(` • HTTP: ${serverUrl}`);
console.log("=".repeat(70));
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(70));
console.log("📋 HTTP Endpoints:");
console.log(` • Health Check: ${serverUrl}/health`);
console.log(` • MCP Protocol: POST ${serverUrl}/mcp`);
console.log(` • Tools List: ${serverUrl}/tools`);
console.log(` • Server Stats: ${serverUrl}/stats`);
console.log("=".repeat(70));
console.log("🧪 Test MCP Protocol (HTTP):");
console.log(` curl -X POST ${serverUrl}/mcp \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(` -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'`);
console.log("");
console.log("🔑 Test Login Tool (HTTP):");
console.log(` curl -X POST ${serverUrl}/mcp \\`);
console.log(` -H "Content-Type: application/json" \\`);
console.log(
` -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"public_manage_login","arguments":{"email":"test@example.com","password":"password"}},"id":2}'`
);
console.log("=".repeat(70));
console.log("📊 Server Status: READY (Both Transports)");
console.log(`⏰ Started at: ${new Date().toLocaleString()}`);
console.log("=".repeat(70));
console.log("💡 Press Ctrl+C to stop the server");
console.log("");
resolve(server);
});
server.on("error", reject);
});
// Wait for both transports to initialize
const [, server] = await Promise.all([stdioPromise, httpPromise]);
// Graceful shutdown for both transports
const shutdown = (signal) => {
console.log(`\n🛑 Received ${signal}, shutting down both transports...`);
server.close(() => {
console.log("✅ HTTP server 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 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
*/
async function main() {
try {
console.log(`🎯 Transport mode: ${transportMode}`);
if (transportMode === "stdio") {
await startStdioMode();
} else if (transportMode === "http") {
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', 'both', or 'all'");
console.log("💡 Set MCP_TRANSPORT environment variable");
process.exit(1);
}
} catch (error) {
console.error("❌ Failed to start server:", error);
console.error(error.stack);
process.exit(1);
}
}
// Start the server
main();