fix
This commit is contained in:
@@ -10,6 +10,13 @@ MCP_SERVER_VERSION=1.0.0
|
|||||||
MCP_SERVER_PORT=3000
|
MCP_SERVER_PORT=3000
|
||||||
MCP_SERVER_HOST=localhost
|
MCP_SERVER_HOST=localhost
|
||||||
|
|
||||||
|
# MCP Transport Configuration
|
||||||
|
# Options: "stdio", "http", or "both"
|
||||||
|
# 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
|
||||||
|
|
||||||
# Authentication Configuration - Admin
|
# Authentication Configuration - Admin
|
||||||
ADMIN_USERNAME=admin@healthcare.com
|
ADMIN_USERNAME=admin@healthcare.com
|
||||||
ADMIN_PASSWORD=your_admin_password
|
ADMIN_PASSWORD=your_admin_password
|
||||||
|
112
README.md
112
README.md
@@ -107,24 +107,62 @@ ENABLE_DETAILED_ERRORS=false
|
|||||||
|
|
||||||
### Starting the Server
|
### Starting the Server
|
||||||
|
|
||||||
|
The server supports three transport modes:
|
||||||
|
|
||||||
|
**1. Both Modes (Recommended - stdio + HTTP simultaneously):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# MCP Server only (stdio protocol)
|
# Default mode - both transports running simultaneously
|
||||||
npm start
|
npm start
|
||||||
|
|
||||||
# HTTP Server (full features with MCP tools)
|
# Explicitly set both modes
|
||||||
npm run start
|
npm run start:both
|
||||||
|
|
||||||
|
|
||||||
# Both MCP and HTTP servers simultaneously
|
|
||||||
npm run start
|
|
||||||
|
|
||||||
|
# Development mode with auto-restart
|
||||||
|
npm run dev:both
|
||||||
```
|
```
|
||||||
|
|
||||||
### Server URLs
|
**2. Stdio Mode Only (for MCP clients like Claude Desktop):**
|
||||||
|
|
||||||
When running the HTTP server, you can access:
|
```bash
|
||||||
|
# Stdio transport only
|
||||||
|
npm run start:stdio
|
||||||
|
|
||||||
|
# Development mode with auto-restart
|
||||||
|
npm run dev:stdio
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. HTTP Mode Only (for web-based clients):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HTTP transport only
|
||||||
|
npm run start:http
|
||||||
|
|
||||||
|
# Development mode with auto-restart
|
||||||
|
npm run dev:http
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transport Configuration
|
||||||
|
|
||||||
|
Set the transport mode in your `.env` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For both MCP clients and HTTP/web clients (recommended)
|
||||||
|
MCP_TRANSPORT=both
|
||||||
|
|
||||||
|
# For MCP clients only (Claude Desktop, VS Code MCP extensions)
|
||||||
|
MCP_TRANSPORT=stdio
|
||||||
|
|
||||||
|
# For HTTP/web clients only
|
||||||
|
MCP_TRANSPORT=http
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server URLs (HTTP Mode)
|
||||||
|
|
||||||
|
When running in HTTP mode, you can access:
|
||||||
|
|
||||||
- **Health Check**: `http://localhost:3000/health`
|
- **Health Check**: `http://localhost:3000/health`
|
||||||
|
- **MCP Protocol**: `POST http://localhost:3000/mcp` (JSON-RPC 2.0)
|
||||||
- **Tools List**: `http://localhost:3000/tools`
|
- **Tools List**: `http://localhost:3000/tools`
|
||||||
- **Server Stats**: `http://localhost:3000/stats`
|
- **Server Stats**: `http://localhost:3000/stats`
|
||||||
- **Tool Execution**: `POST http://localhost:3000/tools/{toolName}/execute`
|
- **Tool Execution**: `POST http://localhost:3000/tools/{toolName}/execute`
|
||||||
@@ -132,6 +170,62 @@ When running the HTTP server, you can access:
|
|||||||
|
|
||||||
### Using with MCP Clients
|
### Using with MCP Clients
|
||||||
|
|
||||||
|
**Claude Desktop Configuration:**
|
||||||
|
|
||||||
|
Add to your Claude Desktop configuration file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"laravel-healthcare": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/laravel-healthcare-mcp-server/index.js"],
|
||||||
|
"env": {
|
||||||
|
"LARAVEL_API_BASE_URL": "https://your-api.com",
|
||||||
|
"MCP_TRANSPORT": "stdio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VS Code MCP Extension:**
|
||||||
|
|
||||||
|
Configure the MCP extension to use:
|
||||||
|
|
||||||
|
- **Command**: `node`
|
||||||
|
- **Args**: `["/path/to/laravel-healthcare-mcp-server/index.js"]`
|
||||||
|
- **Environment**: Set `MCP_TRANSPORT=stdio`
|
||||||
|
|
||||||
|
### MCP Protocol Testing
|
||||||
|
|
||||||
|
**Test tool listing:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test tool execution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "public_manage_login",
|
||||||
|
"arguments": {
|
||||||
|
"email": "test@example.com",
|
||||||
|
"password": "password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 2
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
The server implements the Model Context Protocol and can be used with any MCP-compatible client:
|
The server implements the Model Context Protocol and can be used with any MCP-compatible client:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
@@ -1,12 +1,23 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP server with MCP tool execution support
|
* @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 dotenv from "dotenv";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import cors from "cors";
|
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
|
// Load environment variables from .env file
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
@@ -17,59 +28,452 @@ console.log(
|
|||||||
);
|
);
|
||||||
console.log(` MCP_SERVER_PORT: ${process.env.MCP_SERVER_PORT || "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_SERVER_HOST: ${process.env.MCP_SERVER_HOST || "NOT SET"}`);
|
||||||
|
console.log(` MCP_TRANSPORT: ${process.env.MCP_TRANSPORT || "http"}`);
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
||||||
// Set default values if not provided
|
// Set default values if not provided
|
||||||
process.env.LARAVEL_API_BASE_URL =
|
process.env.LARAVEL_API_BASE_URL =
|
||||||
process.env.LARAVEL_API_BASE_URL || "https://example.com";
|
process.env.LARAVEL_API_BASE_URL || "https://example.com";
|
||||||
|
|
||||||
console.log("🚀 Starting Laravel Healthcare MCP HTTP Server with Tools...");
|
const transportMode = process.env.MCP_TRANSPORT || "both";
|
||||||
|
console.log(
|
||||||
|
`🚀 Starting Laravel Healthcare MCP Server (${transportMode} mode)...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// HTTP server setup (for HTTP transport mode)
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.MCP_SERVER_PORT || 3000;
|
const port = process.env.MCP_SERVER_PORT || 3000;
|
||||||
const host = process.env.MCP_SERVER_HOST || "0.0.0.0";
|
const host = process.env.MCP_SERVER_HOST || "0.0.0.0";
|
||||||
|
|
||||||
// Middleware
|
// Middleware for HTTP mode
|
||||||
app.use(cors());
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(express.json({ limit: "10mb" }));
|
app.use(express.json({ limit: "10mb" }));
|
||||||
|
|
||||||
// Initialize MCP components
|
// MCP Server and components
|
||||||
|
let mcpServer = null;
|
||||||
let toolGenerator = null;
|
let toolGenerator = null;
|
||||||
let authManager = null;
|
let authManager = null;
|
||||||
|
let configManager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize MCP components and create MCP server instance
|
||||||
|
* @returns {Promise<boolean>} Success status
|
||||||
|
*/
|
||||||
async function initializeMCP() {
|
async function initializeMCP() {
|
||||||
try {
|
try {
|
||||||
console.log("📋 Initializing MCP components...");
|
console.log("📋 Initializing MCP components...");
|
||||||
|
|
||||||
|
// Import required modules
|
||||||
const { ConfigManager } = await import("./src/config/ConfigManager.js");
|
const { ConfigManager } = await import("./src/config/ConfigManager.js");
|
||||||
const { AuthManager } = await import("./src/auth/AuthManager.js");
|
const { AuthManager } = await import("./src/auth/AuthManager.js");
|
||||||
const { ApiClient } = await import("./src/proxy/ApiClient.js");
|
const { ApiClient } = await import("./src/proxy/ApiClient.js");
|
||||||
const { ToolGenerator } = await import("./src/tools/ToolGenerator.js");
|
const { ToolGenerator } = await import("./src/tools/ToolGenerator.js");
|
||||||
|
const { logger } = await import("./src/utils/logger.js");
|
||||||
|
|
||||||
const config = new ConfigManager();
|
// Initialize components
|
||||||
authManager = new AuthManager(null, config.getAll(true));
|
configManager = new ConfigManager();
|
||||||
const apiClient = new ApiClient(config.getAll(), authManager);
|
const config = configManager.getAll(true);
|
||||||
|
authManager = new AuthManager(null, config);
|
||||||
|
const apiClient = new ApiClient(config, authManager);
|
||||||
toolGenerator = new ToolGenerator(apiClient);
|
toolGenerator = new ToolGenerator(apiClient);
|
||||||
|
|
||||||
console.log("✅ MCP components initialized");
|
// 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;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to initialize MCP:", error.message);
|
console.error("❌ Failed to initialize MCP:", error.message);
|
||||||
|
console.error(error.stack);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health endpoint
|
/**
|
||||||
|
* 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) => {
|
app.get("/health", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: "healthy",
|
status: "healthy",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
server: "Laravel Healthcare MCP Server",
|
server: "Laravel Healthcare MCP Server",
|
||||||
|
version: "1.0.0",
|
||||||
|
transport: transportMode,
|
||||||
port: port,
|
port: port,
|
||||||
mcpInitialized: toolGenerator !== null,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// List all tools
|
// List all tools
|
||||||
app.get("/tools", (req, res) => {
|
app.get("/tools", (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -361,10 +765,45 @@ app.use((error, req, res, next) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize and start server
|
/**
|
||||||
async function startServer() {
|
* Start MCP server in stdio mode (for desktop clients like Claude Desktop)
|
||||||
const mcpReady = await initializeMCP();
|
*/
|
||||||
|
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) {
|
if (!mcpReady) {
|
||||||
console.error("❌ Cannot start server without MCP initialization");
|
console.error("❌ Cannot start server without MCP initialization");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -374,36 +813,40 @@ async function startServer() {
|
|||||||
const serverUrl = `http://${
|
const serverUrl = `http://${
|
||||||
host === "0.0.0.0" ? "localhost" : host
|
host === "0.0.0.0" ? "localhost" : host
|
||||||
}:${port}`;
|
}:${port}`;
|
||||||
|
const toolCount = toolGenerator.generateAllTools().length;
|
||||||
|
|
||||||
console.log("\n" + "=".repeat(60));
|
console.log("\n" + "=".repeat(70));
|
||||||
console.log("🚀 LARAVEL HEALTHCARE MCP SERVER - HTTP MODE");
|
console.log("🚀 LARAVEL HEALTHCARE MCP SERVER - HTTP MODE");
|
||||||
console.log("=".repeat(60));
|
console.log("=".repeat(70));
|
||||||
console.log(`📡 Server URL: ${serverUrl}`);
|
console.log(`📡 Server URL: ${serverUrl}`);
|
||||||
console.log(`🌐 Host: ${host}`);
|
console.log(`🌐 Host: ${host}`);
|
||||||
console.log(`🔌 Port: ${port}`);
|
console.log(`🔌 Port: ${port}`);
|
||||||
console.log(`🔗 API URL: ${process.env.LARAVEL_API_BASE_URL}`);
|
console.log(`🔗 API URL: ${process.env.LARAVEL_API_BASE_URL}`);
|
||||||
console.log("=".repeat(60));
|
console.log(`📊 Available Tools: ${toolCount}`);
|
||||||
console.log("📋 Available Endpoints:");
|
console.log(`🔄 Protocol Version: 2024-11-05`);
|
||||||
|
console.log("=".repeat(70));
|
||||||
|
console.log("📋 MCP Endpoints:");
|
||||||
console.log(` • Health Check: ${serverUrl}/health`);
|
console.log(` • Health Check: ${serverUrl}/health`);
|
||||||
|
console.log(` • MCP Protocol: POST ${serverUrl}/mcp`);
|
||||||
console.log(` • Tools List: ${serverUrl}/tools`);
|
console.log(` • Tools List: ${serverUrl}/tools`);
|
||||||
console.log(` • Server Stats: ${serverUrl}/stats`);
|
console.log(` • Server Stats: ${serverUrl}/stats`);
|
||||||
console.log(
|
console.log("=".repeat(70));
|
||||||
` • Tool Execute: POST ${serverUrl}/tools/{toolName}/execute`
|
console.log("🧪 Test MCP Protocol:");
|
||||||
);
|
console.log(` curl -X POST ${serverUrl}/mcp \\`);
|
||||||
console.log("=".repeat(60));
|
console.log(` -H "Content-Type: application/json" \\`);
|
||||||
console.log("📊 Server Status: READY");
|
console.log(` -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'`);
|
||||||
console.log(`⏰ Started at: ${new Date().toLocaleString()}`);
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log("💡 Press Ctrl+C to stop the server");
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("🧪 Test login tool:");
|
console.log("🔑 Test Login Tool:");
|
||||||
console.log(
|
console.log(` curl -X POST ${serverUrl}/mcp \\`);
|
||||||
` curl -X POST ${serverUrl}/tools/public_manage_login/execute \\`
|
|
||||||
);
|
|
||||||
console.log(` -H "Content-Type: application/json" \\`);
|
console.log(` -H "Content-Type: application/json" \\`);
|
||||||
console.log(
|
console.log(
|
||||||
` -d '{"email": "test@example.com", "password": "password"}'`
|
` -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("");
|
console.log("");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -418,9 +861,132 @@ async function startServer() {
|
|||||||
|
|
||||||
process.on("SIGINT", () => shutdown("SIGINT"));
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to start HTTP server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startServer().catch((error) => {
|
/**
|
||||||
console.error("❌ Failed to start server:", error);
|
* 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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
console.error(`❌ Invalid transport mode: ${transportMode}`);
|
||||||
|
console.log("💡 Valid modes: 'stdio', 'http', or 'both'");
|
||||||
|
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();
|
||||||
|
21
index.js
Normal file
21
index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @fileoverview Main entry point for Laravel Healthcare MCP Server
|
||||||
|
* Automatically detects transport mode and starts appropriate server
|
||||||
|
* Default: stdio mode for MCP clients like Claude Desktop
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
|
// Load environment variables
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// Set default transport to stdio for this entry point
|
||||||
|
process.env.MCP_TRANSPORT = "stdio";
|
||||||
|
|
||||||
|
// Import and start the server
|
||||||
|
import("./http-tools-server.js").catch((error) => {
|
||||||
|
console.error("❌ Failed to start MCP server:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
16
package.json
16
package.json
@@ -6,17 +6,13 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node http-tools-server.js",
|
"start": "node http-tools-server.js",
|
||||||
|
"start:stdio": "node index.js",
|
||||||
|
"start:http": "node http-tools-server.js",
|
||||||
|
"start:both": "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:http": "node --watch http-tools-server.js",
|
||||||
"test": "node run-tests-simple.js all",
|
"dev:both": "node --watch http-tools-server.js"
|
||||||
"test:quick": "node run-tests-simple.js quick",
|
|
||||||
"test:coverage": "node run-tests-simple.js coverage",
|
|
||||||
"test:watch": "node run-tests-simple.js all --watch",
|
|
||||||
"test:public": "node run-tests-simple.js suite public",
|
|
||||||
"test:provider": "node run-tests-simple.js suite provider",
|
|
||||||
"test:patient": "node run-tests-simple.js suite patient",
|
|
||||||
"test:business": "node run-tests-simple.js suite business",
|
|
||||||
"test:healthcare": "node run-tests-simple.js suite healthcare",
|
|
||||||
"test:errors": "node run-tests-simple.js suite errors"
|
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
|
Reference in New Issue
Block a user