McpController.php•11.9 kB
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cache;
use App\Models\SubjectConnection;
class McpController extends Controller
{
public function handle(Request $request)
{
if ($request->isMethod('GET')) {
return $this->handleSseStream($request);
}
if ($request->isMethod('POST')) {
return $this->handleJsonRpcMessage($request);
}
return response()->json(['error' => 'Method not allowed'], 405);
}
private function handleJsonRpcMessage(Request $request)
{
$jsonRpc = $request->json()->all();
// Valideer JSON-RPC format
if (!isset($jsonRpc['jsonrpc']) || $jsonRpc['jsonrpc'] !== '2.0') {
return $this->errorResponse(-32600, 'Invalid Request');
}
$method = $jsonRpc['method'] ?? null;
$id = $jsonRpc['id'] ?? null;
switch ($method) {
case 'initialize':
return $this->handleInitialize($jsonRpc);
case 'tools/list':
return $this->handleToolsList($jsonRpc);
case 'tools/call':
return $this->handleToolsCall($jsonRpc);
case 'resources/list':
return $this->handleResourcesList($jsonRpc);
case 'prompts/list':
return $this->handlePromptsList($jsonRpc);
case 'notifications/cancelled':
return $this->handleCancelled($jsonRpc);
default:
return $this->errorResponse(-32601, 'Method not found', $id);
}
}
private function handleInitialize($jsonRpc)
{
$sessionId = Str::uuid();
// Store session
$this->storeSession($sessionId);
// Use the same protocol version as the client
$clientProtocolVersion = $jsonRpc['params']['protocolVersion'] ?? '2024-11-05';
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'protocolVersion' => $clientProtocolVersion,
'capabilities' => [
'tools' => [
'listChanged' => true
],
'resources' => [
'subscribe' => false,
'listChanged' => false
],
'prompts' => [
'listChanged' => false
]
],
'serverInfo' => [
'name' => 'MCPDB Laravel Server',
'version' => '1.0.0'
]
]
];
return response()->json($response)
->header('Mcp-Session-Id', $sessionId);
}
private function handleToolsList($jsonRpc)
{
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'tools' => [
[
'name' => 'list_connections',
'description' => 'Lists all the database connections that can be queried using the MCPDB tool.',
'inputSchema' => [
'type' => 'object',
'properties' => (object)[],
'required' => []
]
],
[
'name' => 'execute_sql',
'description' => 'Executes SQL query on a specified database connection. the name should be coming from list_connections.',
'inputSchema' => [
'type' => 'object',
'properties' => [
'connection_name' => [
'type' => 'string',
'description' => 'Name of the database connection to use'
],
'sql' => [
'type' => 'string',
'description' => 'SQL query to execute'
]
],
'required' => ['connection_name', 'sql']
]
]
]
]
];
return response()->json($response);
}
private function handleToolsCall($jsonRpc)
{
$params = $jsonRpc['params'] ?? [];
$toolName = $params['name'] ?? null;
$arguments = $params['arguments'] ?? [];
switch ($toolName) {
case 'list_connections':
return $this->handleListConnections($jsonRpc);
case 'execute_sql':
return $this->handleExecuteSql($jsonRpc, $arguments);
default:
return $this->errorResponse(-32601, 'Tool not found', $jsonRpc['id']);
}
}
private function handleListConnections($jsonRpc)
{
try {
$connections = SubjectConnection::where('is_active', true)
->select('connection_name', 'driver', 'host', 'port', 'database', 'description')
->get();
$connectionList = $connections->map(function ($conn) {
return [
'name' => $conn->connection_name,
'driver' => $conn->driver,
'host' => $conn->host,
'port' => $conn->port,
'database' => $conn->database,
'description' => $conn->description
];
})->toArray();
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'content' => [
[
'type' => 'text',
'text' => json_encode($connectionList, JSON_PRETTY_PRINT)
]
],
'isError' => false
]
];
return response()->json($response);
} catch (\Exception $e) {
return $this->errorResponse(-32603, 'Internal error: ' . $e->getMessage(), $jsonRpc['id']);
}
}
private function handleExecuteSql($jsonRpc, $arguments)
{
try {
// Validate required parameters
if (!isset($arguments['connection_name']) || !isset($arguments['sql'])) {
return $this->errorResponse(-32602, 'Missing required parameters', $jsonRpc['id']);
}
$connectionName = $arguments['connection_name'];
$sql = $arguments['sql'];
// Find the connection
$connection = SubjectConnection::where('connection_name', $connectionName)
->where('is_active', true)
->first();
if (!$connection) {
return $this->errorResponse(-32602, "Connection '{$connectionName}' not found or inactive", $jsonRpc['id']);
}
// Execute the query
$result = $connection->executeQuery($sql);
// Format the result
if (is_array($result) && count($result) > 0) {
// SELECT-type query with results
$output = "Query executed successfully. Results:\n\n";
$output .= json_encode($result, JSON_PRETTY_PRINT);
} elseif (is_array($result) && count($result) === 0) {
// SELECT-type query with no results
$output = "Query executed successfully. No results returned.";
} else {
// Non-SELECT query (INSERT, UPDATE, DELETE, etc.)
$output = "Query executed successfully.";
}
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'content' => [
[
'type' => 'text',
'text' => $output
]
],
'isError' => false
]
];
return response()->json($response);
} catch (\Exception $e) {
return $this->errorResponse(-32603, 'SQL execution error: ' . $e->getMessage(), $jsonRpc['id']);
}
}
private function handleResourcesList($jsonRpc)
{
// Return empty resources list - we don't provide any resources currently
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'resources' => []
]
];
return response()->json($response);
}
private function handlePromptsList($jsonRpc)
{
// Return empty prompts list - we don't provide any prompts currently
$response = [
'jsonrpc' => '2.0',
'id' => $jsonRpc['id'],
'result' => [
'prompts' => []
]
];
return response()->json($response);
}
private function handleCancelled($jsonRpc)
{
// Handle client disconnect/cancellation
return response()->json(['status' => 'acknowledged'], 202);
}
private function handleSseStream(Request $request)
{
// Set SSE headers
$headers = [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => 'Cache-Control',
];
return response()->stream(function () {
// Send initial connection event
echo "event: connect\n";
echo "data: {\"type\":\"connect\",\"message\":\"MCP Server connected\"}\n\n";
ob_flush();
flush();
// Keep connection alive
$counter = 0;
while ($counter < 3600) { // Run for 1 hour max
echo "event: ping\n";
echo "data: {\"type\":\"ping\",\"timestamp\":\"" . now()->toISOString() . "\"}\n\n";
ob_flush();
flush();
sleep(30); // Ping every 30 seconds
$counter += 30;
// Check if client disconnected
if (connection_aborted()) {
break;
}
}
}, 200, $headers);
}
private function validateSession(Request $request)
{
$sessionId = $request->header('Mcp-Session-Id');
if (!$sessionId) {
return false;
}
// Check of session bestaat in cache
return Cache::has("mcp_session_{$sessionId}");
}
private function storeSession($sessionId)
{
// Sla session op voor 24 uur
Cache::put("mcp_session_{$sessionId}", [
'created_at' => now(),
'last_activity' => now()
], 86400);
}
private function errorResponse($code, $message, $id = null, $data = null)
{
$error = [
'code' => $code,
'message' => $message
];
if ($data !== null) {
$error['data'] = $data;
}
return response()->json([
'jsonrpc' => '2.0',
'id' => $id,
'error' => $error
]);
}
public function health()
{
return response()->json([
'status' => 'healthy',
'timestamp' => now()->toISOString(),
'server' => 'Laravel MCP Server',
'version' => '1.0.0'
]);
}
}