350 lines
15 KiB
PHP
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)");
|