Files
html/api/mcp.php
2026-04-12 22:57:03 +02:00

350 lines
15 KiB
PHP

<?php
/**
* WEVIA MCP Layer v1.0 — Model Context Protocol
* Pattern #17 from Claude Code architecture
*
* Dual mode:
* SERVER: Exposes WEVIA tools as MCP endpoints (JSON-RPC 2.0)
* CLIENT: Consumes external MCP servers (tool discovery + invocation)
*
* Spec: https://modelcontextprotocol.io
*
* SERVER endpoint: /api/mcp.php (this file)
* CLIENT: wevia_mcp_discover() + wevia_mcp_invoke()
*
* Usage: require_once __DIR__ . '/wevia-mcp-layer.php';
*/
define('MCP_VERSION', '1.0.0');
define('MCP_PROTOCOL', '2024-11-05'); // MCP spec version
// ═══════════════════════════════════════════════════════════════
// MCP SERVER — Expose WEVIA tools as MCP endpoints
// ═══════════════════════════════════════════════════════════════
function mcp_server_handle($request) {
$method = $request['method'] ?? '';
$params = $request['params'] ?? [];
$id = $request['id'] ?? null;
switch ($method) {
case 'initialize':
return mcp_response($id, [
'protocolVersion' => MCP_PROTOCOL,
'capabilities' => [
'tools' => ['listChanged' => false],
'resources' => ['subscribe' => false, 'listChanged' => false],
],
'serverInfo' => [
'name' => 'wevia-mcp-server',
'version' => MCP_VERSION,
'description' => 'WEVIA Sovereign AI — Tool Server'
]
]);
case 'tools/list':
return mcp_response($id, ['tools' => mcp_list_tools()]);
case 'tools/call':
$toolName = $params['name'] ?? '';
$toolArgs = $params['arguments'] ?? [];
return mcp_response($id, mcp_call_tool($toolName, $toolArgs));
case 'resources/list':
return mcp_response($id, ['resources' => mcp_list_resources()]);
case 'resources/read':
$uri = $params['uri'] ?? '';
return mcp_response($id, mcp_read_resource($uri));
case 'ping':
return mcp_response($id, []);
default:
return mcp_error($id, -32601, "Method not found: $method");
}
}
function mcp_list_tools() {
return [
[
'name' => 'wevia_chat',
'description' => 'Send a message to WEVIA sovereign AI chatbot',
'inputSchema' => [
'type' => 'object',
'properties' => [
'message' => ['type' => 'string', 'description' => 'User message'],
'mode' => ['type' => 'string', 'enum' => ['fast', 'balanced', 'verified'], 'default' => 'balanced'],
'lang' => ['type' => 'string', 'default' => 'fr'],
],
'required' => ['message']
]
],
[
'name' => 'wevia_kb_search',
'description' => 'Search WEVIA knowledge base (32 tables, 2490+ entries)',
'inputSchema' => [
'type' => 'object',
'properties' => [
'query' => ['type' => 'string', 'description' => 'Search query'],
'limit' => ['type' => 'integer', 'default' => 5],
],
'required' => ['query']
]
],
[
'name' => 'wevia_pdf_generate',
'description' => 'Generate a professional PDF document on any topic',
'inputSchema' => [
'type' => 'object',
'properties' => [
'topic' => ['type' => 'string', 'description' => 'Document topic/title'],
'sections' => ['type' => 'integer', 'default' => 8],
],
'required' => ['topic']
]
],
[
'name' => 'wevia_mermaid',
'description' => 'Generate a Mermaid diagram (flowchart, sequence, class, etc.)',
'inputSchema' => [
'type' => 'object',
'properties' => [
'description' => ['type' => 'string', 'description' => 'What to diagram'],
'type' => ['type' => 'string', 'enum' => ['flowchart', 'sequence', 'class', 'state', 'er', 'gantt'], 'default' => 'flowchart'],
],
'required' => ['description']
]
],
[
'name' => 'wevia_web_search',
'description' => 'Sovereign web search via SearXNG',
'inputSchema' => [
'type' => 'object',
'properties' => [
'query' => ['type' => 'string'],
'max_results' => ['type' => 'integer', 'default' => 5],
],
'required' => ['query']
]
],
[
'name' => 'wevia_benchmark',
'description' => 'Get AI ecosystem benchmark data (66 AIs, 222 skills)',
'inputSchema' => [
'type' => 'object',
'properties' => [
'ai_name' => ['type' => 'string', 'description' => 'Specific AI name or "all"'],
],
'required' => []
]
],
];
}
function mcp_call_tool($name, $args) {
switch ($name) {
case 'wevia_chat':
$ch = curl_init('http://127.0.0.1/wevia-ia/weval-chatbot-api.php');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'message' => $args['message'] ?? '',
'mode' => $args['mode'] ?? 'balanced',
'lang' => $args['lang'] ?? 'fr',
])
]);
$r = curl_exec($ch); curl_close($ch);
$d = json_decode($r, true);
return ['content' => [['type' => 'text', 'text' => $d['response'] ?? 'No response']]];
case 'wevia_kb_search':
$query = $args['query'] ?? '';
$limit = $args['limit'] ?? 5;
// Direct KB query
try {
@require_once('/opt/wevads/vault/credentials.php');
$pdo = new PDO("pgsql:host=10.1.0.3;dbname=adx_system", "admin", "admin123");
$st = $pdo->prepare("SELECT title, content, category FROM admin.wevia_kb WHERE LOWER(content) LIKE LOWER(?) LIMIT ?");
$st->execute(["%$query%", $limit]);
$results = $st->fetchAll(PDO::FETCH_ASSOC);
return ['content' => [['type' => 'text', 'text' => json_encode($results, JSON_UNESCAPED_UNICODE)]]];
} catch (Exception $e) {
return ['content' => [['type' => 'text', 'text' => 'KB search error: ' . $e->getMessage()]]];
}
case 'wevia_benchmark':
$cache = @json_decode(@file_get_contents('/var/www/html/api/ai-benchmark-cache.json'), true);
$aiName = $args['ai_name'] ?? 'all';
if ($aiName === 'all') {
$lb = $cache['leaderboard'] ?? [];
arsort($lb);
return ['content' => [['type' => 'text', 'text' => json_encode(array_slice($lb, 0, 20, true))]]];
}
$ai = $cache['all_ais'][$aiName] ?? null;
return ['content' => [['type' => 'text', 'text' => $ai ? json_encode($ai) : "AI '$aiName' not found"]]];
case 'wevia_web_search':
$ch = curl_init('http://127.0.0.1:8888/search?q=' . urlencode($args['query'] ?? '') . '&format=json&categories=general');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10]);
$r = curl_exec($ch); curl_close($ch);
$d = json_decode($r, true);
$results = array_slice($d['results'] ?? [], 0, $args['max_results'] ?? 5);
$text = implode("\n\n", array_map(fn($r) => "**{$r['title']}**\n{$r['url']}\n{$r['content']}", $results));
return ['content' => [['type' => 'text', 'text' => $text ?: 'No results']]];
default:
return ['content' => [['type' => 'text', 'text' => "Unknown tool: $name"]], 'isError' => true];
}
}
function mcp_list_resources() {
return [
['uri' => 'wevia://benchmark/leaderboard', 'name' => 'AI Benchmark Leaderboard', 'mimeType' => 'application/json'],
['uri' => 'wevia://config/providers', 'name' => 'Active Providers', 'mimeType' => 'application/json'],
['uri' => 'wevia://status/health', 'name' => 'System Health', 'mimeType' => 'application/json'],
];
}
function mcp_read_resource($uri) {
switch ($uri) {
case 'wevia://benchmark/leaderboard':
$cache = @json_decode(@file_get_contents('/var/www/html/api/ai-benchmark-cache.json'), true);
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode($cache['leaderboard'] ?? [])]]];
case 'wevia://config/providers':
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode(['chain' => 'Groq→Cerebras→SambaNova→Mistral→Alibaba', 'sovereign' => 'Local→EU→Free→Paid'])]]];
case 'wevia://status/health':
return ['contents' => [['uri' => $uri, 'mimeType' => 'application/json', 'text' => json_encode(['status' => 'ok', 'modules' => ['WCP' => 'v1.0', 'WSI' => 'v1.0', 'Dream' => 'active', 'MCP' => 'v1.0'], 'uptime' => exec('uptime -p 2>/dev/null')])]]];
default:
return ['contents' => []];
}
}
// ═══════════════════════════════════════════════════════════════
// MCP CLIENT — Consume external MCP servers
// ═══════════════════════════════════════════════════════════════
function wevia_mcp_discover($serverUrl, $timeout = 10) {
/**
* Discover tools from an external MCP server.
* Returns array of available tools with schemas.
*/
// Initialize
$init = wevia_mcp_call($serverUrl, 'initialize', [
'protocolVersion' => MCP_PROTOCOL,
'capabilities' => [],
'clientInfo' => ['name' => 'wevia-mcp-client', 'version' => MCP_VERSION]
], $timeout);
if (!$init) return ['error' => 'init_failed'];
// List tools
$tools = wevia_mcp_call($serverUrl, 'tools/list', [], $timeout);
return $tools['tools'] ?? [];
}
function wevia_mcp_invoke($serverUrl, $toolName, $arguments = [], $timeout = 30) {
/**
* Invoke a tool on an external MCP server.
*/
$result = wevia_mcp_call($serverUrl, 'tools/call', [
'name' => $toolName,
'arguments' => $arguments,
], $timeout);
return $result;
}
function wevia_mcp_call($serverUrl, $method, $params = [], $timeout = 10) {
$payload = json_encode([
'jsonrpc' => '2.0',
'id' => uniqid('mcp_'),
'method' => $method,
'params' => $params,
]);
$ch = curl_init($serverUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS => $payload,
]);
$r = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || !$r) return null;
$d = json_decode($r, true);
return $d['result'] ?? null;
}
// ═══════════════════════════════════════════════════════════════
// MCP REGISTRY — Track connected MCP servers
// ═══════════════════════════════════════════════════════════════
function wevia_mcp_registry() {
/**
* Registry of known MCP servers.
* Auto-populated by OSS Discovery + manual config.
*/
$registryFile = '/opt/wevads/vault/mcp-registry.json';
if (!file_exists($registryFile)) {
$default = [
'servers' => [
['name' => 'wevia-local', 'url' => 'http://127.0.0.1/api/mcp.php', 'type' => 'local', 'active' => true],
],
'last_scan' => null,
];
@file_put_contents($registryFile, json_encode($default, JSON_PRETTY_PRINT));
return $default;
}
return json_decode(file_get_contents($registryFile), true) ?: [];
}
function wevia_mcp_register_server($name, $url, $type = 'external') {
$registry = wevia_mcp_registry();
$registry['servers'][] = ['name' => $name, 'url' => $url, 'type' => $type, 'active' => true, 'added' => date('c')];
@file_put_contents('/opt/wevads/vault/mcp-registry.json', json_encode($registry, JSON_PRETTY_PRINT));
return true;
}
// ═══════════════════════════════════════════════════════════════
// JSON-RPC HELPERS
// ═══════════════════════════════════════════════════════════════
function mcp_response($id, $result) {
return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $result];
}
function mcp_error($id, $code, $message) {
return ['jsonrpc' => '2.0', 'id' => $id, 'error' => ['code' => $code, 'message' => $message]];
}
// ═══════════════════════════════════════════════════════════════
// STANDALONE SERVER MODE — if called directly as /api/mcp.php
// ═══════════════════════════════════════════════════════════════
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'wevia-mcp-layer.php' ||
basename($_SERVER['SCRIPT_FILENAME'] ?? '') === 'mcp.php') {
header('Content-Type: application/json');
// Auth check
$key = $_GET['k'] ?? $_SERVER['HTTP_X_MCP_KEY'] ?? '';
if ($key !== 'WEVADS2026' && $key !== 'MCP2026') {
http_response_code(403);
die(json_encode(mcp_error(null, -32000, 'Unauthorized')));
}
$body = json_decode(file_get_contents('php://input'), true);
if (!$body || !isset($body['method'])) {
die(json_encode(mcp_error(null, -32700, 'Parse error')));
}
die(json_encode(mcp_server_handle($body)));
}
error_log("MCP: wevia-mcp-layer v" . MCP_VERSION . " loaded (server+client)");