fix
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user