feat: Add MCP server mode (--mcp-server)
AISSIA can now run as an MCP server, exposing tools via JSON-RPC over stdio. This allows external clients (like Claude Code) to use AISSIA's tools. - Add MCPServer.hpp/cpp implementing MCP protocol - Add --mcp-server flag to main.cpp - Currently exposes 6 FileSystem tools - Update documentation (CLAUDE.md, SUCCESSION.md) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
09fc32386e
commit
ac230b195d
@ -52,6 +52,9 @@ cmake -B build && cmake --build build -j4
|
||||
|
||||
# Hot-reload: rebuild modules seulement
|
||||
cmake --build build --target modules
|
||||
|
||||
# Mode MCP Server (expose tools via JSON-RPC stdio)
|
||||
./build/aissia --mcp-server
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@ -73,12 +73,13 @@ if(OPENSSL_FOUND)
|
||||
target_compile_definitions(AissiaLLM PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
endif()
|
||||
|
||||
# Tools Library (Internal tools + FileSystem tools + MCP client)
|
||||
# Tools Library (Internal tools + FileSystem tools + MCP client + MCP server)
|
||||
add_library(AissiaTools STATIC
|
||||
src/shared/tools/InternalTools.cpp
|
||||
src/shared/tools/FileSystemTools.cpp
|
||||
src/shared/mcp/StdioTransport.cpp
|
||||
src/shared/mcp/MCPClient.cpp
|
||||
src/shared/mcp/MCPServer.cpp
|
||||
)
|
||||
target_include_directories(AissiaTools PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
|
||||
@ -28,6 +28,9 @@ cmake -B build && cmake --build build -j4
|
||||
# Run (depuis racine ou build/)
|
||||
./build/aissia
|
||||
|
||||
# Mode MCP Server (expose les tools via JSON-RPC stdio)
|
||||
./build/aissia --mcp-server
|
||||
|
||||
# Tests
|
||||
cmake -B build -DBUILD_TESTING=ON
|
||||
./build/tests/aissia_tests "[scheduler],[notification]" # Modules
|
||||
@ -123,7 +126,8 @@ src/modules/StorageModule.*
|
||||
### MCP
|
||||
```
|
||||
src/shared/mcp/MCPTypes.hpp
|
||||
src/shared/mcp/MCPClient.*
|
||||
src/shared/mcp/MCPClient.* # Client MCP (consomme des serveurs externes)
|
||||
src/shared/mcp/MCPServer.* # Serveur MCP (expose AISSIA comme serveur)
|
||||
src/shared/mcp/StdioTransport.*
|
||||
config/mcp.json
|
||||
```
|
||||
@ -164,6 +168,50 @@ cmake -B build -DBUILD_TESTING=ON && cmake --build build -j4
|
||||
7. **CI/CD** - GitHub Actions
|
||||
8. **Documentation API** - Doxygen
|
||||
|
||||
## MCP Server Mode
|
||||
|
||||
AISSIA peut fonctionner comme **serveur MCP**, exposant ses tools à des clients externes via JSON-RPC sur stdio.
|
||||
|
||||
```bash
|
||||
./build/aissia --mcp-server
|
||||
```
|
||||
|
||||
### Protocole
|
||||
|
||||
Communication JSON-RPC 2.0 sur stdin/stdout :
|
||||
|
||||
```json
|
||||
// Client → AISSIA
|
||||
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"client","version":"1.0"}}}
|
||||
|
||||
// AISSIA → Client
|
||||
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"aissia","version":"0.2.0"},...}}
|
||||
|
||||
// Lister les tools
|
||||
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
|
||||
|
||||
// Appeler un tool
|
||||
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_directory","arguments":{"path":"."}}}
|
||||
```
|
||||
|
||||
### Utilisation avec Claude Code
|
||||
|
||||
Ajouter dans la config MCP :
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"aissia": {
|
||||
"command": "/chemin/vers/build/aissia",
|
||||
"args": ["--mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tools Exposés (actuellement)
|
||||
|
||||
6 FileSystem tools. TODO: exposer les tools internes (scheduler, voice, etc.).
|
||||
|
||||
## Notes Techniques
|
||||
|
||||
### WSL
|
||||
|
||||
47
src/main.cpp
47
src/main.cpp
@ -6,6 +6,9 @@
|
||||
#include "services/StorageService.hpp"
|
||||
#include "services/PlatformService.hpp"
|
||||
#include "services/VoiceService.hpp"
|
||||
#include "shared/mcp/MCPServer.hpp"
|
||||
#include "shared/tools/FileSystemTools.hpp"
|
||||
#include "shared/llm/ToolRegistry.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
@ -16,6 +19,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <cstring>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@ -155,7 +159,49 @@ private:
|
||||
std::map<std::string, grove::IIO*> m_serviceIOs;
|
||||
};
|
||||
|
||||
// Run AISSIA as MCP server (stdio mode)
|
||||
int runMCPServer() {
|
||||
// Log to stderr so stdout stays clean for JSON-RPC
|
||||
// Use stderr_color_sink from stdout_color_sinks.h
|
||||
auto logger = spdlog::stderr_color_mt("MCPServer");
|
||||
spdlog::set_default_logger(logger);
|
||||
spdlog::set_level(spdlog::level::info);
|
||||
|
||||
spdlog::info("AISSIA MCP Server starting...");
|
||||
|
||||
// Create tool registry with FileSystem tools
|
||||
aissia::ToolRegistry registry;
|
||||
|
||||
// Register FileSystem tools
|
||||
for (const auto& toolDef : aissia::tools::FileSystemTools::getToolDefinitions()) {
|
||||
std::string toolName = toolDef["name"].get<std::string>();
|
||||
registry.registerTool(
|
||||
toolName,
|
||||
toolDef["description"].get<std::string>(),
|
||||
toolDef["input_schema"],
|
||||
[toolName](const nlohmann::json& input) -> nlohmann::json {
|
||||
return aissia::tools::FileSystemTools::execute(toolName, input);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
spdlog::info("Registered {} tools", registry.size());
|
||||
|
||||
// Create and run MCP server
|
||||
aissia::mcp::MCPServer server(registry);
|
||||
server.setServerInfo("aissia", "0.2.0");
|
||||
server.run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
// Check for MCP server mode
|
||||
if (argc > 1 && (std::strcmp(argv[1], "--mcp-server") == 0 ||
|
||||
std::strcmp(argv[1], "--mcp") == 0)) {
|
||||
return runMCPServer();
|
||||
}
|
||||
|
||||
// Setup logging
|
||||
auto console = spdlog::stdout_color_mt("Aissia");
|
||||
spdlog::set_default_logger(console);
|
||||
@ -166,6 +212,7 @@ int main(int argc, char* argv[]) {
|
||||
spdlog::info(" AISSIA - Assistant Personnel IA");
|
||||
spdlog::info(" Powered by GroveEngine");
|
||||
spdlog::info(" Architecture: Services + Hot-Reload Modules");
|
||||
spdlog::info(" (Use --mcp-server to run as MCP server)");
|
||||
spdlog::info("========================================");
|
||||
|
||||
// Signal handling
|
||||
|
||||
276
src/shared/mcp/MCPServer.cpp
Normal file
276
src/shared/mcp/MCPServer.cpp
Normal file
@ -0,0 +1,276 @@
|
||||
#include "MCPServer.hpp"
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
namespace aissia::mcp {
|
||||
|
||||
MCPServer::MCPServer(aissia::ToolRegistry& registry) : m_registry(registry) {
|
||||
m_logger = spdlog::get("MCPServer");
|
||||
if (!m_logger) {
|
||||
// Log to stderr so stdout stays clean for JSON-RPC
|
||||
m_logger = spdlog::stderr_color_mt("MCPServer");
|
||||
}
|
||||
|
||||
// Default capabilities
|
||||
m_capabilities = {
|
||||
{"tools", {{"listChanged", false}}}
|
||||
};
|
||||
}
|
||||
|
||||
MCPServer::~MCPServer() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void MCPServer::setServerInfo(const std::string& name, const std::string& version) {
|
||||
m_serverName = name;
|
||||
m_serverVersion = version;
|
||||
}
|
||||
|
||||
void MCPServer::setCapabilities(const json& capabilities) {
|
||||
m_capabilities = capabilities;
|
||||
}
|
||||
|
||||
void MCPServer::run() {
|
||||
m_running = true;
|
||||
m_logger->info("MCP Server starting (stdio mode)");
|
||||
|
||||
while (m_running) {
|
||||
std::string line = readLine();
|
||||
if (line.empty()) {
|
||||
if (m_running) {
|
||||
m_logger->debug("EOF received, stopping server");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
processLine(line);
|
||||
}
|
||||
|
||||
m_logger->info("MCP Server stopped");
|
||||
}
|
||||
|
||||
void MCPServer::stop() {
|
||||
m_running = false;
|
||||
}
|
||||
|
||||
std::string MCPServer::readLine() {
|
||||
std::string line;
|
||||
if (!std::getline(std::cin, line)) {
|
||||
return "";
|
||||
}
|
||||
// Remove trailing \r if present (Windows line endings)
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
void MCPServer::writeLine(const std::string& line) {
|
||||
std::cout << line << std::endl;
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
void MCPServer::processLine(const std::string& line) {
|
||||
m_logger->debug("Received: {}", line.substr(0, 200));
|
||||
|
||||
try {
|
||||
json message = json::parse(line);
|
||||
|
||||
// Validate JSON-RPC
|
||||
if (!message.contains("jsonrpc") || message["jsonrpc"] != "2.0") {
|
||||
sendError(-1, -32600, "Invalid JSON-RPC: missing or invalid jsonrpc field");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a request (has method) or notification (no id)
|
||||
if (!message.contains("method")) {
|
||||
sendError(message.value("id", -1), -32600, "Invalid Request: missing method");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonRpcRequest request;
|
||||
request.method = message["method"].get<std::string>();
|
||||
request.params = message.value("params", json::object());
|
||||
request.id = message.value("id", 0);
|
||||
|
||||
bool isNotification = !message.contains("id");
|
||||
|
||||
json result = handleRequest(request);
|
||||
|
||||
// Only send response for requests, not notifications
|
||||
if (!isNotification) {
|
||||
if (result.contains("error")) {
|
||||
sendError(request.id, result["error"]["code"].get<int>(),
|
||||
result["error"]["message"].get<std::string>());
|
||||
} else {
|
||||
sendResponse(request.id, result);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (const json::exception& e) {
|
||||
m_logger->error("JSON parse error: {}", e.what());
|
||||
sendError(-1, -32700, std::string("Parse error: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
json MCPServer::handleRequest(const JsonRpcRequest& request) {
|
||||
m_logger->debug("Handling method: {}", request.method);
|
||||
|
||||
// MCP methods
|
||||
if (request.method == "initialize") {
|
||||
return handleInitialize(request.params);
|
||||
} else if (request.method == "ping") {
|
||||
return handlePing(request.params);
|
||||
} else if (request.method == "tools/list") {
|
||||
return handleToolsList(request.params);
|
||||
} else if (request.method == "tools/call") {
|
||||
return handleToolsCall(request.params);
|
||||
} else if (request.method == "notifications/initialized") {
|
||||
// Client notification that initialization is complete
|
||||
m_logger->info("Client initialized");
|
||||
return json::object(); // No response for notifications
|
||||
}
|
||||
|
||||
// Unknown method
|
||||
return {{"error", {{"code", -32601}, {"message", "Method not found: " + request.method}}}};
|
||||
}
|
||||
|
||||
json MCPServer::handleInitialize(const json& params) {
|
||||
if (m_initialized) {
|
||||
return {{"error", {{"code", -32600}, {"message", "Already initialized"}}}};
|
||||
}
|
||||
|
||||
// Extract client info
|
||||
if (params.contains("clientInfo")) {
|
||||
m_clientName = params["clientInfo"].value("name", "unknown");
|
||||
m_clientVersion = params["clientInfo"].value("version", "unknown");
|
||||
m_logger->info("Client connected: {} v{}", m_clientName, m_clientVersion);
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
|
||||
// Return server info and capabilities
|
||||
return {
|
||||
{"protocolVersion", "2024-11-05"},
|
||||
{"serverInfo", {
|
||||
{"name", m_serverName},
|
||||
{"version", m_serverVersion}
|
||||
}},
|
||||
{"capabilities", m_capabilities}
|
||||
};
|
||||
}
|
||||
|
||||
json MCPServer::handleToolsList(const json& params) {
|
||||
if (!m_initialized) {
|
||||
return {{"error", {{"code", -32600}, {"message", "Not initialized"}}}};
|
||||
}
|
||||
|
||||
// Get tool definitions from registry and convert to MCP format
|
||||
json registryTools = m_registry.getToolDefinitions();
|
||||
json tools = json::array();
|
||||
|
||||
for (const auto& tool : registryTools) {
|
||||
json toolEntry;
|
||||
toolEntry["name"] = tool["name"];
|
||||
toolEntry["description"] = tool["description"];
|
||||
toolEntry["inputSchema"] = tool["input_schema"];
|
||||
tools.push_back(toolEntry);
|
||||
}
|
||||
|
||||
return {{"tools", tools}};
|
||||
}
|
||||
|
||||
json MCPServer::handleToolsCall(const json& params) {
|
||||
if (!m_initialized) {
|
||||
return {{"error", {{"code", -32600}, {"message", "Not initialized"}}}};
|
||||
}
|
||||
|
||||
if (!params.contains("name")) {
|
||||
return {{"error", {{"code", -32602}, {"message", "Missing tool name"}}}};
|
||||
}
|
||||
|
||||
std::string toolName = params["name"].get<std::string>();
|
||||
json arguments = params.value("arguments", json::object());
|
||||
|
||||
m_logger->info("Calling tool: {}", toolName);
|
||||
|
||||
try {
|
||||
json result = m_registry.execute(toolName, arguments);
|
||||
|
||||
// Format result as MCP content
|
||||
json content = json::array();
|
||||
|
||||
// Convert result to text content
|
||||
std::string textContent;
|
||||
if (result.is_string()) {
|
||||
textContent = result.get<std::string>();
|
||||
} else {
|
||||
textContent = result.dump(2);
|
||||
}
|
||||
|
||||
json contentEntry;
|
||||
contentEntry["type"] = "text";
|
||||
contentEntry["text"] = textContent;
|
||||
content.push_back(contentEntry);
|
||||
|
||||
return {
|
||||
{"content", content},
|
||||
{"isError", false}
|
||||
};
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
m_logger->error("Tool execution failed: {}", e.what());
|
||||
json errorContent = json::array();
|
||||
json errorEntry;
|
||||
errorEntry["type"] = "text";
|
||||
errorEntry["text"] = e.what();
|
||||
errorContent.push_back(errorEntry);
|
||||
|
||||
json errorResult;
|
||||
errorResult["content"] = errorContent;
|
||||
errorResult["isError"] = true;
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
json MCPServer::handlePing(const json& params) {
|
||||
return json::object(); // Empty response = pong
|
||||
}
|
||||
|
||||
void MCPServer::sendResponse(int id, const json& result) {
|
||||
json response = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", id},
|
||||
{"result", result}
|
||||
};
|
||||
std::string line = response.dump();
|
||||
m_logger->debug("Sending: {}", line.substr(0, 200));
|
||||
writeLine(line);
|
||||
}
|
||||
|
||||
void MCPServer::sendError(int id, int code, const std::string& message) {
|
||||
json response = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", id},
|
||||
{"error", {
|
||||
{"code", code},
|
||||
{"message", message}
|
||||
}}
|
||||
};
|
||||
writeLine(response.dump());
|
||||
}
|
||||
|
||||
void MCPServer::sendNotification(const std::string& method, const json& params) {
|
||||
json notification = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"method", method}
|
||||
};
|
||||
if (!params.empty()) {
|
||||
notification["params"] = params;
|
||||
}
|
||||
writeLine(notification.dump());
|
||||
}
|
||||
|
||||
} // namespace aissia::mcp
|
||||
85
src/shared/mcp/MCPServer.hpp
Normal file
85
src/shared/mcp/MCPServer.hpp
Normal file
@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "MCPTypes.hpp"
|
||||
#include "../llm/ToolRegistry.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace aissia::mcp {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
/**
|
||||
* @brief MCP Server - Exposes AISSIA tools via MCP protocol
|
||||
*
|
||||
* Allows external clients (like Claude Code) to connect to AISSIA
|
||||
* and use its tools via JSON-RPC over stdio.
|
||||
*
|
||||
* Usage:
|
||||
* MCPServer server(toolRegistry);
|
||||
* server.setServerInfo("aissia", "1.0.0");
|
||||
* server.run(); // Blocking - reads stdin, writes stdout
|
||||
*
|
||||
* Or in a thread:
|
||||
* std::thread serverThread([&]() { server.run(); });
|
||||
*/
|
||||
class MCPServer {
|
||||
public:
|
||||
explicit MCPServer(aissia::ToolRegistry& registry);
|
||||
~MCPServer();
|
||||
|
||||
// Server configuration
|
||||
void setServerInfo(const std::string& name, const std::string& version);
|
||||
void setCapabilities(const json& capabilities);
|
||||
|
||||
// Run the server (blocking)
|
||||
void run();
|
||||
|
||||
// Stop the server
|
||||
void stop();
|
||||
|
||||
// Check if running
|
||||
bool isRunning() const { return m_running; }
|
||||
|
||||
private:
|
||||
// JSON-RPC handling
|
||||
void processLine(const std::string& line);
|
||||
json handleRequest(const JsonRpcRequest& request);
|
||||
|
||||
// MCP method handlers
|
||||
json handleInitialize(const json& params);
|
||||
json handleToolsList(const json& params);
|
||||
json handleToolsCall(const json& params);
|
||||
json handlePing(const json& params);
|
||||
|
||||
// IO
|
||||
std::string readLine();
|
||||
void writeLine(const std::string& line);
|
||||
void sendResponse(int id, const json& result);
|
||||
void sendError(int id, int code, const std::string& message);
|
||||
void sendNotification(const std::string& method, const json& params = json::object());
|
||||
|
||||
// State
|
||||
aissia::ToolRegistry& m_registry;
|
||||
std::atomic<bool> m_running{false};
|
||||
bool m_initialized{false};
|
||||
|
||||
// Server info
|
||||
std::string m_serverName{"aissia"};
|
||||
std::string m_serverVersion{"1.0.0"};
|
||||
json m_capabilities;
|
||||
|
||||
// Client info (from initialize)
|
||||
std::string m_clientName;
|
||||
std::string m_clientVersion;
|
||||
|
||||
std::shared_ptr<spdlog::logger> m_logger;
|
||||
};
|
||||
|
||||
} // namespace aissia::mcp
|
||||
Loading…
Reference in New Issue
Block a user