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:
StillHammer 2025-11-27 19:38:32 +08:00
parent 09fc32386e
commit ac230b195d
6 changed files with 462 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View 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