diff --git a/.env.example b/.env.example index 96793a7..916fe40 100644 --- a/.env.example +++ b/.env.example @@ -11,11 +11,12 @@ MCP_SERVER_PORT=3000 MCP_SERVER_HOST=localhost # MCP Transport Configuration -# Options: "stdio", "http", or "both" +# Options: "stdio", "http", "both", or "all" # stdio: Uses stdin/stdout for communication (for MCP clients like Claude Desktop) # http: Uses HTTP server with JSON-RPC 2.0 over HTTP POST (for web clients) -# both: Runs both stdio and HTTP transports simultaneously (recommended) -MCP_TRANSPORT=both +# both: Runs both stdio and HTTP transports simultaneously +# all: Runs all transports (stdio + HTTP + SSE) simultaneously (recommended) +MCP_TRANSPORT=all # Authentication Configuration - Admin ADMIN_USERNAME=admin@healthcare.com diff --git a/.gitignore b/.gitignore index e819331..9f15d40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules logs +.env \ No newline at end of file diff --git a/N8N-SETUP-GUIDE.md b/N8N-SETUP-GUIDE.md new file mode 100644 index 0000000..82d372a --- /dev/null +++ b/N8N-SETUP-GUIDE.md @@ -0,0 +1,245 @@ +# ๐Ÿ”ง n8n MCP Client Setup Guide + +This guide shows you how to connect n8n to the Laravel Healthcare MCP Server via HTTP. + +## ๐Ÿ“‹ Prerequisites + +1. **MCP Server Running**: Ensure your Laravel Healthcare MCP Server is running +2. **n8n Installed**: Have n8n installed and accessible +3. **Network Access**: n8n can reach the MCP server (usually `http://localhost:3000`) + +## ๐Ÿš€ Quick Start + +### 1. Start the MCP Server + +```bash +cd laravel-healthcare-mcp-server +npm start +``` + +The server will start in dual mode (stdio + HTTP) and be available at: +- **HTTP Endpoint**: `http://localhost:3000/mcp` +- **n8n Info**: `http://localhost:3000/n8n/info` + +### 2. Test Server Connectivity + +```bash +# Test server health +curl http://localhost:3000/health + +# Get n8n-specific information +curl http://localhost:3000/n8n/info +``` + +## ๐Ÿ”Œ n8n Integration Methods + +### Method 1: HTTP Request Node (Recommended) + +**Step 1: Add HTTP Request Node** +- Add an "HTTP Request" node to your n8n workflow +- Set the following parameters: + +**Basic Configuration:** +- **Method**: `POST` +- **URL**: `http://localhost:3000/mcp` +- **Headers**: + ```json + { + "Content-Type": "application/json", + "Accept": "application/json" + } + ``` + +**Step 2: Initialize MCP Connection** +```json +{ + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "clientInfo": { + "name": "n8n", + "version": "1.0.0" + } + }, + "id": 1 +} +``` + +**Step 3: List Available Tools** +```json +{ + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 +} +``` + +**Step 4: Execute Healthcare Tools** +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "public_manage_login", + "arguments": { + "email": "provider@example.com", + "password": "your_password" + } + }, + "id": 3 +} +``` + +### Method 2: Custom n8n Node (Advanced) + +For advanced users, you can create a custom n8n node specifically for MCP: + +1. **Create Node Structure**: + ``` + nodes/ + โ”œโ”€โ”€ McpClient/ + โ”‚ โ”œโ”€โ”€ McpClient.node.ts + โ”‚ โ””โ”€โ”€ mcp-client.svg + ``` + +2. **Node Configuration**: + - Server URL: `http://localhost:3000/mcp` + - Protocol: JSON-RPC 2.0 + - Methods: initialize, tools/list, tools/call, ping + +## ๐Ÿ“Š Available Healthcare Tools + +The MCP server provides 470+ healthcare API tools organized by category: + +### Authentication Tools +- `public_manage_login` - Provider login +- `patient_login` - Patient portal login +- `partner_login` - Partner authentication + +### Clinical Tools (Provider Auth Required) +- `provider_get_patients` - Get patient list +- `provider_create_patient` - Create new patient +- `provider_update_patient` - Update patient data +- `provider_get_appointments` - Get appointments + +### Patient Portal Tools +- `patient_get_profile` - Get patient profile +- `patient_get_appointments` - Get patient appointments +- `patient_book_appointment` - Book new appointment + +### Business Tools +- `partner_get_analytics` - Get business analytics +- `affiliate_get_commissions` - Get affiliate data +- `network_get_providers` - Get network providers + +## ๐Ÿ” Authentication Flow + +### 1. Login First +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "public_manage_login", + "arguments": { + "email": "your-email@example.com", + "password": "your-password" + } + }, + "id": 1 +} +``` + +### 2. Token Auto-Storage +The server automatically extracts and stores authentication tokens from login responses. + +### 3. Use Protected Tools +After login, you can use provider-authenticated tools without additional auth. + +## ๐Ÿงช Testing Examples + +### Example 1: Get Patient List +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "provider_get_patients", + "arguments": { + "page": 1, + "limit": 10 + } + }, + "id": 4 +} +``` + +### Example 2: Create Patient +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "provider_create_patient", + "arguments": { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "dateOfBirth": "1990-01-01", + "phone": "+1234567890" + } + }, + "id": 5 +} +``` + +## ๐Ÿ”ง Troubleshooting + +### Common Issues + +1. **Connection Refused** + - Ensure MCP server is running: `npm start` + - Check server URL: `http://localhost:3000/mcp` + - Verify port 3000 is not blocked + +2. **Authentication Errors** + - Login first using `public_manage_login` + - Check credentials are correct + - Verify token storage in server logs + +3. **Tool Not Found** + - List available tools: `tools/list` + - Check tool name spelling + - Verify tool exists in current server version + +### Debug Commands + +```bash +# Check server status +curl http://localhost:3000/health + +# Get server info for n8n +curl http://localhost:3000/n8n/info + +# List all tools +curl -X POST http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' +``` + +## ๐Ÿ“š Additional Resources + +- **Server Documentation**: `README.md` +- **API Reference**: `MCP-TOOLS-REFERENCE.md` +- **Example Workflow**: `n8n-workflow-template.json` +- **Configuration**: `n8n-mcp-config.json` + +## ๐Ÿ†˜ Support + +If you encounter issues: +1. Check server logs for errors +2. Verify n8n can reach the server URL +3. Test with curl commands first +4. Check authentication flow diff --git a/http-client-example.js b/http-client-example.js new file mode 100644 index 0000000..71e34b6 --- /dev/null +++ b/http-client-example.js @@ -0,0 +1,141 @@ +#!/usr/bin/env node + +/** + * @fileoverview Example HTTP MCP Client for Laravel Healthcare MCP Server + * Demonstrates how to connect to MCP server via HTTP transport + */ + +import axios from 'axios'; + +class HttpMcpClient { + constructor(serverUrl = 'http://localhost:3000/mcp') { + this.serverUrl = serverUrl; + this.requestId = 1; + } + + /** + * Send MCP request via HTTP + * @param {string} method - MCP method name + * @param {Object} params - Method parameters + * @returns {Promise} Response + */ + async request(method, params = {}) { + const request = { + jsonrpc: "2.0", + method: method, + params: params, + id: this.requestId++ + }; + + try { + console.log(`๐Ÿ“ค Sending MCP request: ${method}`); + console.log(`๐Ÿ“ Request:`, JSON.stringify(request, null, 2)); + + const response = await axios.post(this.serverUrl, request, { + headers: { + 'Content-Type': 'application/json' + }, + timeout: 30000 + }); + + console.log(`๐Ÿ“ฅ Response:`, JSON.stringify(response.data, null, 2)); + return response.data; + + } catch (error) { + console.error(`โŒ Request failed:`, error.message); + if (error.response) { + console.error(`๐Ÿ“‹ Response data:`, error.response.data); + } + throw error; + } + } + + /** + * Initialize MCP connection + */ + async initialize() { + return await this.request('initialize', { + protocolVersion: "2024-11-05", + clientInfo: { + name: "http-mcp-client", + version: "1.0.0" + } + }); + } + + /** + * List available tools + */ + async listTools() { + return await this.request('tools/list'); + } + + /** + * Execute a tool + * @param {string} toolName - Tool name + * @param {Object} args - Tool arguments + */ + async callTool(toolName, args = {}) { + return await this.request('tools/call', { + name: toolName, + arguments: args + }); + } + + /** + * Ping server + */ + async ping() { + return await this.request('ping'); + } +} + +// Example usage +async function main() { + const client = new HttpMcpClient(); + + try { + console.log("๐Ÿš€ Starting HTTP MCP Client..."); + console.log("=".repeat(50)); + + // Test connection + console.log("1๏ธโƒฃ Testing connection..."); + await client.initialize(); + + // List tools + console.log("\n2๏ธโƒฃ Listing available tools..."); + const toolsResponse = await client.listTools(); + const tools = toolsResponse.result?.tools || []; + console.log(`๐Ÿ“Š Found ${tools.length} tools`); + + // Show first 5 tools + console.log("\n๐Ÿ“‹ First 5 tools:"); + tools.slice(0, 5).forEach((tool, index) => { + console.log(` ${index + 1}. ${tool.name}: ${tool.description}`); + }); + + // Test tool execution (login) + console.log("\n3๏ธโƒฃ Testing tool execution (login)..."); + await client.callTool('public_manage_login', { + email: 'test@example.com', + password: 'password' + }); + + // Test ping + console.log("\n4๏ธโƒฃ Testing ping..."); + await client.ping(); + + console.log("\nโœ… All tests completed successfully!"); + + } catch (error) { + console.error("\nโŒ Client test failed:", error.message); + process.exit(1); + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} + +export { HttpMcpClient }; diff --git a/http-client-example.py b/http-client-example.py new file mode 100644 index 0000000..37ef7b9 --- /dev/null +++ b/http-client-example.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +""" +Example HTTP MCP Client for Laravel Healthcare MCP Server +Demonstrates how to connect to MCP server via HTTP transport +""" + +import json +import requests +from typing import Dict, Any, Optional + +class HttpMcpClient: + def __init__(self, server_url: str = "http://localhost:3000/mcp"): + self.server_url = server_url + self.request_id = 1 + self.session = requests.Session() + self.session.headers.update({ + 'Content-Type': 'application/json' + }) + + def request(self, method: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Send MCP request via HTTP""" + if params is None: + params = {} + + request_data = { + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": self.request_id + } + self.request_id += 1 + + print(f"๐Ÿ“ค Sending MCP request: {method}") + print(f"๐Ÿ“ Request: {json.dumps(request_data, indent=2)}") + + try: + response = self.session.post( + self.server_url, + json=request_data, + timeout=30 + ) + response.raise_for_status() + + result = response.json() + print(f"๐Ÿ“ฅ Response: {json.dumps(result, indent=2)}") + return result + + except requests.exceptions.RequestException as e: + print(f"โŒ Request failed: {e}") + raise + + def initialize(self) -> Dict[str, Any]: + """Initialize MCP connection""" + return self.request('initialize', { + "protocolVersion": "2024-11-05", + "clientInfo": { + "name": "python-http-mcp-client", + "version": "1.0.0" + } + }) + + def list_tools(self) -> Dict[str, Any]: + """List available tools""" + return self.request('tools/list') + + def call_tool(self, tool_name: str, args: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Execute a tool""" + if args is None: + args = {} + + return self.request('tools/call', { + "name": tool_name, + "arguments": args + }) + + def ping(self) -> Dict[str, Any]: + """Ping server""" + return self.request('ping') + +def main(): + """Example usage""" + client = HttpMcpClient() + + try: + print("๐Ÿš€ Starting HTTP MCP Client...") + print("=" * 50) + + # Test connection + print("1๏ธโƒฃ Testing connection...") + client.initialize() + + # List tools + print("\n2๏ธโƒฃ Listing available tools...") + tools_response = client.list_tools() + tools = tools_response.get('result', {}).get('tools', []) + print(f"๐Ÿ“Š Found {len(tools)} tools") + + # Show first 5 tools + print("\n๐Ÿ“‹ First 5 tools:") + for i, tool in enumerate(tools[:5], 1): + print(f" {i}. {tool['name']}: {tool['description']}") + + # Test tool execution (login) + print("\n3๏ธโƒฃ Testing tool execution (login)...") + client.call_tool('public_manage_login', { + 'email': 'test@example.com', + 'password': 'password' + }) + + # Test ping + print("\n4๏ธโƒฃ Testing ping...") + client.ping() + + print("\nโœ… All tests completed successfully!") + + except Exception as e: + print(f"\nโŒ Client test failed: {e}") + return 1 + + return 0 + +if __name__ == "__main__": + exit(main()) diff --git a/http-tools-server.js b/http-tools-server.js index 1d3385f..5ecf239 100644 --- a/http-tools-server.js +++ b/http-tools-server.js @@ -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); } diff --git a/n8n-mcp-config.json b/n8n-mcp-config.json new file mode 100644 index 0000000..4a44441 --- /dev/null +++ b/n8n-mcp-config.json @@ -0,0 +1,96 @@ +{ + "name": "Laravel Healthcare MCP Server", + "description": "MCP server providing access to 470+ Laravel Healthcare API endpoints", + "version": "1.0.0", + "server": { + "url": "http://localhost:3000/mcp", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Accept": "application/json" + } + }, + "protocol": { + "version": "2024-11-05", + "transport": "http", + "format": "json-rpc-2.0" + }, + "capabilities": { + "tools": true, + "logging": true, + "authentication": true + }, + "endpoints": { + "initialize": { + "method": "initialize", + "description": "Initialize MCP connection" + }, + "tools/list": { + "method": "tools/list", + "description": "List all available healthcare API tools" + }, + "tools/call": { + "method": "tools/call", + "description": "Execute a healthcare API tool" + }, + "ping": { + "method": "ping", + "description": "Test server connectivity" + } + }, + "authentication": { + "types": [ + "public", + "provider", + "patient", + "partner", + "affiliate", + "network" + ], + "login_tools": { + "provider": "public_manage_login", + "patient": "patient_login", + "partner": "partner_login" + } + }, + "categories": { + "authentication": "Login and authentication tools", + "clinical": "Provider/clinical data management", + "patient-portal": "Patient portal and records", + "business": "Partner and business operations", + "affiliate": "Affiliate management", + "network": "Network operations" + }, + "examples": { + "initialize": { + "jsonrpc": "2.0", + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "clientInfo": { + "name": "n8n", + "version": "1.0.0" + } + }, + "id": 1 + }, + "list_tools": { + "jsonrpc": "2.0", + "method": "tools/list", + "params": {}, + "id": 2 + }, + "login": { + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "public_manage_login", + "arguments": { + "email": "provider@example.com", + "password": "your_password" + } + }, + "id": 3 + } + } +} diff --git a/n8n-workflow-template.json b/n8n-workflow-template.json new file mode 100644 index 0000000..0bd420a --- /dev/null +++ b/n8n-workflow-template.json @@ -0,0 +1,174 @@ +{ + "name": "Laravel Healthcare MCP Integration", + "nodes": [ + { + "parameters": {}, + "id": "start", + "name": "Start", + "type": "n8n-nodes-base.start", + "typeVersion": 1, + "position": [240, 300] + }, + { + "parameters": { + "url": "http://localhost:3000/mcp", + "options": { + "headers": { + "Content-Type": "application/json" + } + }, + "bodyParametersUi": { + "parameter": [ + { + "name": "jsonrpc", + "value": "2.0" + }, + { + "name": "method", + "value": "initialize" + }, + { + "name": "params", + "value": { + "protocolVersion": "2024-11-05", + "clientInfo": { + "name": "n8n", + "version": "1.0.0" + } + } + }, + { + "name": "id", + "value": 1 + } + ] + } + }, + "id": "initialize", + "name": "Initialize MCP", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [460, 300] + }, + { + "parameters": { + "url": "http://localhost:3000/mcp", + "options": { + "headers": { + "Content-Type": "application/json" + } + }, + "bodyParametersUi": { + "parameter": [ + { + "name": "jsonrpc", + "value": "2.0" + }, + { + "name": "method", + "value": "tools/list" + }, + { + "name": "params", + "value": {} + }, + { + "name": "id", + "value": 2 + } + ] + } + }, + "id": "list_tools", + "name": "List Tools", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [680, 300] + }, + { + "parameters": { + "url": "http://localhost:3000/mcp", + "options": { + "headers": { + "Content-Type": "application/json" + } + }, + "bodyParametersUi": { + "parameter": [ + { + "name": "jsonrpc", + "value": "2.0" + }, + { + "name": "method", + "value": "tools/call" + }, + { + "name": "params", + "value": { + "name": "public_manage_login", + "arguments": { + "email": "{{ $json.email }}", + "password": "{{ $json.password }}" + } + } + }, + { + "name": "id", + "value": 3 + } + ] + } + }, + "id": "login", + "name": "Login Tool", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [900, 300] + } + ], + "connections": { + "Start": { + "main": [ + [ + { + "node": "Initialize MCP", + "type": "main", + "index": 0 + } + ] + ] + }, + "Initialize MCP": { + "main": [ + [ + { + "node": "List Tools", + "type": "main", + "index": 0 + } + ] + ] + }, + "List Tools": { + "main": [ + [ + { + "node": "Login Tool", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": ["mcp", "healthcare", "laravel"], + "triggerCount": 0, + "updatedAt": "2025-01-22T00:00:00.000Z", + "versionId": "1" +} diff --git a/package.json b/package.json index 3f6f3fd..324f0db 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,14 @@ "start:stdio": "node index.js", "start:http": "node http-tools-server.js", "start:both": "node http-tools-server.js", + "start:all": "node http-tools-server.js", + "start:n8n": "node http-tools-server.js", "dev": "node --watch http-tools-server.js", "dev:stdio": "node --watch index.js", "dev:http": "node --watch http-tools-server.js", - "dev:both": "node --watch http-tools-server.js" + "dev:both": "node --watch http-tools-server.js", + "dev:all": "node --watch http-tools-server.js", + "dev:n8n": "node --watch http-tools-server.js" }, "keywords": [ "mcp", diff --git a/sse-client-example.html b/sse-client-example.html new file mode 100644 index 0000000..8216104 --- /dev/null +++ b/sse-client-example.html @@ -0,0 +1,347 @@ + + + + + + Laravel Healthcare MCP SSE Client + + + +
+

๐Ÿฅ Laravel Healthcare MCP SSE Client

+ +
+

SSE Connection

+ + + + +
+ Disconnected +
+
+ +
+

MCP Commands

+ + + +

+ + + +
+ +
+

SSE Events Log

+ +
SSE events will appear here...
+
+ +
+

MCP Response

+
+ MCP responses will appear here... +
+
+
+ + + + diff --git a/web-client-example.html b/web-client-example.html new file mode 100644 index 0000000..dfbfee2 --- /dev/null +++ b/web-client-example.html @@ -0,0 +1,112 @@ + + + + + + Laravel Healthcare MCP Web Client + + + +
+

๐Ÿฅ Laravel Healthcare MCP Web Client

+ +
+

Server Connection

+ + + +
+ +
+

Tool Execution

+ + + +
+ +
+

Response

+
Ready to connect...
+
+ +
+

Available Tools

+
Click "List Tools" to load...
+
+
+ + + +