fix: Enable HTTPS for Claude API and fix interactive mode IIO routing
- HttpClient: Use full URL with scheme (https://) for proper SSL support - main.cpp: Create separate InteractiveClient IO to avoid self-delivery skip - main.cpp: Process llm:response messages in main loop for terminal display - ClaudeProvider: Add debug logging for request details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ac230b195d
commit
58d7ca4355
105
PROMPT_SUCCESSEUR.md
Normal file
105
PROMPT_SUCCESSEUR.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# Prompt Successeur - AISSIA
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Tu reprends le développement d'**AISSIA**, un assistant vocal agentique en C++17 basé sur GroveEngine.
|
||||||
|
|
||||||
|
**Architecture** : Services (non hot-reload) + Modules (.so hot-reload) + MCP (client et serveur)
|
||||||
|
|
||||||
|
## État Actuel
|
||||||
|
|
||||||
|
✅ Build OK : `cmake -B build && cmake --build build -j4`
|
||||||
|
✅ 6 modules hot-reload fonctionnels
|
||||||
|
✅ 4 services infrastructure (LLM, Storage, Platform, Voice)
|
||||||
|
✅ 17 tools pour l'agent LLM
|
||||||
|
✅ Mode MCP Server : `./build/aissia --mcp-server`
|
||||||
|
✅ Mode interactif : `./build/aissia --interactive` ou `-i`
|
||||||
|
✅ **Tests MCP : 50/50 passent** (transport + client)
|
||||||
|
✅ **Tests totaux : 102/110** (8 échecs dans tests modules)
|
||||||
|
|
||||||
|
## Fichiers Clés
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `src/main.cpp` | Entry point, charge modules, route messages, mode interactif |
|
||||||
|
| `src/services/LLMService.*` | Boucle agentique, ToolRegistry, appels Claude |
|
||||||
|
| `src/shared/mcp/MCPServer.*` | AISSIA comme serveur MCP (stdio JSON-RPC) |
|
||||||
|
| `src/shared/mcp/MCPClient.*` | Consomme serveurs MCP externes |
|
||||||
|
| `src/shared/mcp/StdioTransport.*` | Transport stdio pour MCP (spawne process enfant) |
|
||||||
|
| `src/shared/tools/FileSystemTools.*` | 6 tools fichiers (read/write/edit/glob/grep) |
|
||||||
|
| `src/shared/tools/InternalTools.*` | 11 tools internes (scheduler, voice, storage) |
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ IIO pub/sub ┌─────────────┐
|
||||||
|
│ Modules │ ◄──────────────────► │ Services │
|
||||||
|
│ (.so hot) │ JsonDataNode │ (static) │
|
||||||
|
└─────────────┘ └─────────────┘
|
||||||
|
│
|
||||||
|
HTTP │
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ Claude API │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cmake -B build && cmake --build build -j4
|
||||||
|
|
||||||
|
# Run normal (boucle principale sans interaction)
|
||||||
|
./build/aissia
|
||||||
|
|
||||||
|
# Run avec interface stdin interactive
|
||||||
|
./build/aissia --interactive # ou -i
|
||||||
|
# Tape "quit" ou "q" pour quitter
|
||||||
|
|
||||||
|
# Run comme serveur MCP (pour Claude Code)
|
||||||
|
./build/aissia --mcp-server
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
cp tests/fixtures/mock_mcp.json build/tests/fixtures/
|
||||||
|
./build/tests/aissia_tests # Tous (102/110)
|
||||||
|
./build/tests/aissia_tests "[mcp]" # MCP (50/50)
|
||||||
|
./build/tests/aissia_tests "[transport]" # Transport (20/20)
|
||||||
|
./build/tests/aissia_tests "[client]" # Client (15/15)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests qui échouent (8)
|
||||||
|
|
||||||
|
Tous dans les modules, pas critiques :
|
||||||
|
- `MonitoringModuleTests.cpp` : 4 tests (monitoring:app_changed non publié)
|
||||||
|
- `AIModuleTests.cpp` : 2 tests (ai:suggestion non publié)
|
||||||
|
- `VoiceModuleTests.cpp` : 1 test (voice:speak assertion)
|
||||||
|
- `StorageModuleTests.cpp` : 1 test (durationMinutes != 45)
|
||||||
|
|
||||||
|
## Prochaines Étapes Suggérées
|
||||||
|
|
||||||
|
1. **Tester la boucle agentique** - `export ANTHROPIC_API_KEY=sk-...` puis `./build/aissia -i`
|
||||||
|
- Essayer "Quelle heure est-il ?" (tool get_current_time)
|
||||||
|
- Essayer "Liste les fichiers dans src/" (tool glob)
|
||||||
|
|
||||||
|
2. **Fixer les 8 tests modules** - Problèmes de publication IIO dans les mocks
|
||||||
|
|
||||||
|
3. **Exposer InternalTools via MCP Server** - Actuellement seuls FileSystem + get_current_time sont exposés. Les InternalTools (scheduler, voice, storage) nécessitent que les modules tournent (IIO).
|
||||||
|
|
||||||
|
## Notes Techniques
|
||||||
|
|
||||||
|
- **API key** : `ANTHROPIC_API_KEY` dans env ou `.env`
|
||||||
|
- **WSL** : window tracker et TTS utilisent des stubs
|
||||||
|
- **GroveEngine** : symlink vers `../GroveEngine`
|
||||||
|
- **Hot-reload** : modifier un .so dans `build/modules/` → rechargé automatiquement
|
||||||
|
- **Tests fixtures** : toujours copier `mock_mcp.json` avant tests client
|
||||||
|
|
||||||
|
## Changements Récents (cette session)
|
||||||
|
|
||||||
|
1. **Fix python → python3** dans configs et tests MCP
|
||||||
|
2. **Fix StdioTransport** :
|
||||||
|
- Préserve l'ID de requête fourni par l'utilisateur
|
||||||
|
- Détecte les commandes invalides (waitpid après 100ms)
|
||||||
|
- Stop non-bloquant (ferme stdout avant join reader)
|
||||||
|
- Ignore les messages avec id=null (notifications)
|
||||||
|
3. **Ajout mode interactif** (`--interactive`) pour tester la boucle LLM
|
||||||
134
src/main.cpp
134
src/main.cpp
@ -20,6 +20,9 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
@ -105,6 +108,8 @@ struct ModuleEntry {
|
|||||||
// Message router between modules and services
|
// Message router between modules and services
|
||||||
class MessageRouter {
|
class MessageRouter {
|
||||||
public:
|
public:
|
||||||
|
using MessageCallback = std::function<void(const std::string& topic, grove::IDataNode* data)>;
|
||||||
|
|
||||||
void addModuleIO(const std::string& name, grove::IIO* io) {
|
void addModuleIO(const std::string& name, grove::IIO* io) {
|
||||||
m_moduleIOs[name] = io;
|
m_moduleIOs[name] = io;
|
||||||
}
|
}
|
||||||
@ -113,6 +118,10 @@ public:
|
|||||||
m_serviceIOs[name] = io;
|
m_serviceIOs[name] = io;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMessageCallback(MessageCallback callback) {
|
||||||
|
m_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
void routeMessages() {
|
void routeMessages() {
|
||||||
// Collect all messages from modules and services
|
// Collect all messages from modules and services
|
||||||
std::vector<grove::Message> messages;
|
std::vector<grove::Message> messages;
|
||||||
@ -133,6 +142,11 @@ public:
|
|||||||
|
|
||||||
// Route messages to appropriate destinations
|
// Route messages to appropriate destinations
|
||||||
for (auto& msg : messages) {
|
for (auto& msg : messages) {
|
||||||
|
// Call callback if set
|
||||||
|
if (m_callback && msg.data) {
|
||||||
|
m_callback(msg.topic, msg.data.get());
|
||||||
|
}
|
||||||
|
|
||||||
// Determine destination based on topic prefix
|
// Determine destination based on topic prefix
|
||||||
std::string prefix = msg.topic.substr(0, msg.topic.find(':'));
|
std::string prefix = msg.topic.substr(0, msg.topic.find(':'));
|
||||||
|
|
||||||
@ -157,6 +171,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::map<std::string, grove::IIO*> m_moduleIOs;
|
std::map<std::string, grove::IIO*> m_moduleIOs;
|
||||||
std::map<std::string, grove::IIO*> m_serviceIOs;
|
std::map<std::string, grove::IIO*> m_serviceIOs;
|
||||||
|
MessageCallback m_callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run AISSIA as MCP server (stdio mode)
|
// Run AISSIA as MCP server (stdio mode)
|
||||||
@ -172,6 +187,20 @@ int runMCPServer() {
|
|||||||
// Create tool registry with FileSystem tools
|
// Create tool registry with FileSystem tools
|
||||||
aissia::ToolRegistry registry;
|
aissia::ToolRegistry registry;
|
||||||
|
|
||||||
|
// Register get_current_time tool
|
||||||
|
registry.registerTool(
|
||||||
|
"get_current_time",
|
||||||
|
"Get the current date and time",
|
||||||
|
{{"type", "object"}, {"properties", nlohmann::json::object()}},
|
||||||
|
[](const nlohmann::json& input) -> nlohmann::json {
|
||||||
|
std::time_t now = std::time(nullptr);
|
||||||
|
std::tm* tm = std::localtime(&now);
|
||||||
|
char buffer[64];
|
||||||
|
std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tm);
|
||||||
|
return {{"time", buffer}};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Register FileSystem tools
|
// Register FileSystem tools
|
||||||
for (const auto& toolDef : aissia::tools::FileSystemTools::getToolDefinitions()) {
|
for (const auto& toolDef : aissia::tools::FileSystemTools::getToolDefinitions()) {
|
||||||
std::string toolName = toolDef["name"].get<std::string>();
|
std::string toolName = toolDef["name"].get<std::string>();
|
||||||
@ -195,6 +224,39 @@ int runMCPServer() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flag for interactive mode
|
||||||
|
static std::atomic<bool> g_interactive{false};
|
||||||
|
static grove::IIO* g_interactiveIO = nullptr;
|
||||||
|
|
||||||
|
// Stdin reader thread for interactive mode
|
||||||
|
void stdinReaderThread() {
|
||||||
|
std::string line;
|
||||||
|
std::cout << "\n[AISSIA] Mode interactif active. Tapez vos messages (quit pour quitter):\n> " << std::flush;
|
||||||
|
|
||||||
|
while (g_running && std::getline(std::cin, line)) {
|
||||||
|
if (line == "quit" || line == "exit" || line == "q") {
|
||||||
|
g_running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.empty()) {
|
||||||
|
std::cout << "> " << std::flush;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to LLM service via IIO (from InteractiveClient, not LLMService)
|
||||||
|
if (g_interactiveIO) {
|
||||||
|
auto request = std::make_unique<grove::JsonDataNode>("request");
|
||||||
|
request->setString("query", line);
|
||||||
|
request->setString("conversation_id", "interactive");
|
||||||
|
g_interactiveIO->publish("llm:request", std::move(request));
|
||||||
|
spdlog::info("Message envoye: {}", line);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "> " << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
// Check for MCP server mode
|
// Check for MCP server mode
|
||||||
if (argc > 1 && (std::strcmp(argv[1], "--mcp-server") == 0 ||
|
if (argc > 1 && (std::strcmp(argv[1], "--mcp-server") == 0 ||
|
||||||
@ -202,6 +264,14 @@ int main(int argc, char* argv[]) {
|
|||||||
return runMCPServer();
|
return runMCPServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for interactive mode
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (std::strcmp(argv[i], "--interactive") == 0 ||
|
||||||
|
std::strcmp(argv[i], "-i") == 0) {
|
||||||
|
g_interactive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup logging
|
// Setup logging
|
||||||
auto console = spdlog::stdout_color_mt("Aissia");
|
auto console = spdlog::stdout_color_mt("Aissia");
|
||||||
spdlog::set_default_logger(console);
|
spdlog::set_default_logger(console);
|
||||||
@ -308,6 +378,22 @@ int main(int argc, char* argv[]) {
|
|||||||
platformService.isHealthy() ? "OK" : "FAIL",
|
platformService.isHealthy() ? "OK" : "FAIL",
|
||||||
voiceService.isHealthy() ? "OK" : "FAIL");
|
voiceService.isHealthy() ? "OK" : "FAIL");
|
||||||
|
|
||||||
|
// Create separate IO for interactive mode (to avoid self-delivery skip)
|
||||||
|
std::unique_ptr<grove::IIO> interactiveIO;
|
||||||
|
std::unique_ptr<std::thread> stdinThread;
|
||||||
|
if (g_interactive) {
|
||||||
|
spdlog::info("Mode interactif active (--interactive)");
|
||||||
|
interactiveIO = grove::IOFactory::create("intra", "InteractiveClient");
|
||||||
|
g_interactiveIO = interactiveIO.get();
|
||||||
|
|
||||||
|
// Subscribe to LLM responses to display them
|
||||||
|
grove::SubscriptionConfig subConfig;
|
||||||
|
interactiveIO->subscribe("llm:response", subConfig);
|
||||||
|
interactiveIO->subscribe("llm:error", subConfig);
|
||||||
|
|
||||||
|
stdinThread = std::make_unique<std::thread>(stdinReaderThread);
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Hot-Reloadable Modules
|
// Hot-Reloadable Modules
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@ -321,6 +407,27 @@ int main(int argc, char* argv[]) {
|
|||||||
router.addServiceIO("PlatformService", platformIO.get());
|
router.addServiceIO("PlatformService", platformIO.get());
|
||||||
router.addServiceIO("VoiceService", voiceIO.get());
|
router.addServiceIO("VoiceService", voiceIO.get());
|
||||||
|
|
||||||
|
// Setup interactive mode callback to display LLM responses
|
||||||
|
if (g_interactive) {
|
||||||
|
router.setMessageCallback([](const std::string& topic, grove::IDataNode* data) {
|
||||||
|
if (topic == "llm:response" && data) {
|
||||||
|
auto* jsonNode = dynamic_cast<grove::JsonDataNode*>(data);
|
||||||
|
if (jsonNode) {
|
||||||
|
std::string text = jsonNode->getString("text", "");
|
||||||
|
if (!text.empty()) {
|
||||||
|
std::cout << "\n[AISSIA] " << text << "\n> " << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (topic == "llm:error" && data) {
|
||||||
|
auto* jsonNode = dynamic_cast<grove::JsonDataNode*>(data);
|
||||||
|
if (jsonNode) {
|
||||||
|
std::string message = jsonNode->getString("message", "Unknown error");
|
||||||
|
std::cout << "\n[ERREUR] " << message << "\n> " << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Liste des modules a charger (sans infrastructure)
|
// Liste des modules a charger (sans infrastructure)
|
||||||
std::vector<std::pair<std::string, std::string>> moduleList = {
|
std::vector<std::pair<std::string, std::string>> moduleList = {
|
||||||
{"SchedulerModule", "scheduler.json"},
|
{"SchedulerModule", "scheduler.json"},
|
||||||
@ -451,6 +558,24 @@ int main(int argc, char* argv[]) {
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
router.routeMessages();
|
router.routeMessages();
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Process interactive mode responses
|
||||||
|
// =====================================================================
|
||||||
|
if (g_interactive && interactiveIO) {
|
||||||
|
while (interactiveIO->hasMessages() > 0) {
|
||||||
|
auto msg = interactiveIO->pullMessage();
|
||||||
|
if (msg.topic == "llm:response" && msg.data) {
|
||||||
|
std::string text = msg.data->getString("text", "");
|
||||||
|
if (!text.empty()) {
|
||||||
|
std::cout << "\n[AISSIA] " << text << "\n> " << std::flush;
|
||||||
|
}
|
||||||
|
} else if (msg.topic == "llm:error" && msg.data) {
|
||||||
|
std::string error = msg.data->getString("message", "Unknown error");
|
||||||
|
std::cout << "\n[ERREUR] " << error << "\n> " << std::flush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Frame timing
|
// Frame timing
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@ -477,6 +602,13 @@ int main(int argc, char* argv[]) {
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
spdlog::info("Arret en cours...");
|
spdlog::info("Arret en cours...");
|
||||||
|
|
||||||
|
// Stop interactive thread
|
||||||
|
if (stdinThread && stdinThread->joinable()) {
|
||||||
|
// Thread will exit when g_running becomes false or on EOF
|
||||||
|
// We can't cleanly interrupt stdin reading, so just detach
|
||||||
|
stdinThread->detach();
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown modules first
|
// Shutdown modules first
|
||||||
for (auto& [name, entry] : modules) {
|
for (auto& [name, entry] : modules) {
|
||||||
if (entry.module) {
|
if (entry.module) {
|
||||||
@ -491,6 +623,8 @@ int main(int argc, char* argv[]) {
|
|||||||
storageService.shutdown();
|
storageService.shutdown();
|
||||||
llmService.shutdown();
|
llmService.shutdown();
|
||||||
|
|
||||||
|
g_interactiveIO = nullptr;
|
||||||
|
|
||||||
spdlog::info("A bientot!");
|
spdlog::info("A bientot!");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,18 +68,17 @@ public:
|
|||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::unique_ptr<httplib::Client> client;
|
// Create client with full URL scheme for proper HTTPS support
|
||||||
|
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
|
||||||
|
httplib::Client client(url);
|
||||||
|
|
||||||
if (m_useSSL) {
|
if (m_useSSL) {
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
client.enable_server_certificate_verification(true);
|
||||||
client->enable_server_certificate_verification(true);
|
|
||||||
} else {
|
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client->set_connection_timeout(m_timeoutSeconds);
|
client.set_connection_timeout(m_timeoutSeconds);
|
||||||
client->set_read_timeout(m_timeoutSeconds);
|
client.set_read_timeout(m_timeoutSeconds);
|
||||||
client->set_write_timeout(m_timeoutSeconds);
|
client.set_write_timeout(m_timeoutSeconds);
|
||||||
|
|
||||||
httplib::Headers headers;
|
httplib::Headers headers;
|
||||||
headers.emplace("Content-Type", "application/json");
|
headers.emplace("Content-Type", "application/json");
|
||||||
@ -90,7 +89,7 @@ public:
|
|||||||
std::string bodyStr = body.dump();
|
std::string bodyStr = body.dump();
|
||||||
m_logger->debug("POST {} ({} bytes)", path, bodyStr.size());
|
m_logger->debug("POST {} ({} bytes)", path, bodyStr.size());
|
||||||
|
|
||||||
auto result = client->Post(path, headers, bodyStr, "application/json");
|
auto result = client.Post(path, headers, bodyStr, "application/json");
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
response.status = result->status;
|
response.status = result->status;
|
||||||
@ -120,24 +119,22 @@ public:
|
|||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::unique_ptr<httplib::Client> client;
|
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
|
||||||
|
httplib::Client client(url);
|
||||||
|
|
||||||
if (m_useSSL) {
|
if (m_useSSL) {
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
client.enable_server_certificate_verification(true);
|
||||||
client->enable_server_certificate_verification(true);
|
|
||||||
} else {
|
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client->set_connection_timeout(m_timeoutSeconds);
|
client.set_connection_timeout(m_timeoutSeconds);
|
||||||
client->set_read_timeout(m_timeoutSeconds);
|
client.set_read_timeout(m_timeoutSeconds);
|
||||||
|
|
||||||
httplib::Headers headers;
|
httplib::Headers headers;
|
||||||
for (const auto& [key, value] : m_headers) {
|
for (const auto& [key, value] : m_headers) {
|
||||||
headers.emplace(key, value);
|
headers.emplace(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = client->Post(path, headers, items);
|
auto result = client.Post(path, headers, items);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
response.status = result->status;
|
response.status = result->status;
|
||||||
@ -158,23 +155,22 @@ public:
|
|||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
std::unique_ptr<httplib::Client> client;
|
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
|
||||||
|
httplib::Client client(url);
|
||||||
|
|
||||||
if (m_useSSL) {
|
if (m_useSSL) {
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
client.enable_server_certificate_verification(true);
|
||||||
} else {
|
|
||||||
client = std::make_unique<httplib::Client>(m_host);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client->set_connection_timeout(m_timeoutSeconds);
|
client.set_connection_timeout(m_timeoutSeconds);
|
||||||
client->set_read_timeout(m_timeoutSeconds);
|
client.set_read_timeout(m_timeoutSeconds);
|
||||||
|
|
||||||
httplib::Headers headers;
|
httplib::Headers headers;
|
||||||
for (const auto& [key, value] : m_headers) {
|
for (const auto& [key, value] : m_headers) {
|
||||||
headers.emplace(key, value);
|
headers.emplace(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = client->Get(path, headers);
|
auto result = client.Get(path, headers);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
response.status = result->status;
|
response.status = result->status;
|
||||||
|
|||||||
@ -45,7 +45,8 @@ LLMResponse ClaudeProvider::chat(const std::string& systemPrompt,
|
|||||||
request["tools"] = convertTools(tools);
|
request["tools"] = convertTools(tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_logger->debug("Sending request to Claude: {} messages", messages.size());
|
m_logger->debug("Sending request to Claude: {} messages, {} tools", messages.size(), tools.size());
|
||||||
|
m_logger->trace("Request body: {}", request.dump(2));
|
||||||
|
|
||||||
auto response = m_client->post("/v1/messages", request);
|
auto response = m_client->post("/v1/messages", request);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace aissia::mcp {
|
namespace aissia::mcp {
|
||||||
|
|
||||||
@ -50,26 +54,12 @@ void StdioTransport::stop() {
|
|||||||
|
|
||||||
m_running = false;
|
m_running = false;
|
||||||
|
|
||||||
// Close write pipe to signal EOF to child
|
// Close pipes and terminate process first, so reader thread can exit
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
if (m_stdinWrite) {
|
if (m_stdinWrite) {
|
||||||
CloseHandle(m_stdinWrite);
|
CloseHandle(m_stdinWrite);
|
||||||
m_stdinWrite = nullptr;
|
m_stdinWrite = nullptr;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
if (m_stdinFd >= 0) {
|
|
||||||
close(m_stdinFd);
|
|
||||||
m_stdinFd = -1;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Wait for reader thread
|
|
||||||
if (m_readerThread.joinable()) {
|
|
||||||
m_readerThread.join();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminate process
|
|
||||||
#ifdef _WIN32
|
|
||||||
if (m_processHandle) {
|
if (m_processHandle) {
|
||||||
TerminateProcess(m_processHandle, 0);
|
TerminateProcess(m_processHandle, 0);
|
||||||
CloseHandle(m_processHandle);
|
CloseHandle(m_processHandle);
|
||||||
@ -80,10 +70,13 @@ void StdioTransport::stop() {
|
|||||||
m_stdoutRead = nullptr;
|
m_stdoutRead = nullptr;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
if (m_stdinFd >= 0) {
|
||||||
|
close(m_stdinFd);
|
||||||
|
m_stdinFd = -1;
|
||||||
|
}
|
||||||
if (m_pid > 0) {
|
if (m_pid > 0) {
|
||||||
kill(m_pid, SIGTERM);
|
kill(m_pid, SIGTERM);
|
||||||
waitpid(m_pid, nullptr, 0);
|
// Don't wait here - let the reader thread see EOF first
|
||||||
m_pid = -1;
|
|
||||||
}
|
}
|
||||||
if (m_stdoutFd >= 0) {
|
if (m_stdoutFd >= 0) {
|
||||||
close(m_stdoutFd);
|
close(m_stdoutFd);
|
||||||
@ -91,6 +84,19 @@ void StdioTransport::stop() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Now wait for reader thread (should exit quickly since stdout is closed)
|
||||||
|
if (m_readerThread.joinable()) {
|
||||||
|
m_readerThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final cleanup - wait for child process
|
||||||
|
#ifndef _WIN32
|
||||||
|
if (m_pid > 0) {
|
||||||
|
waitpid(m_pid, nullptr, 0);
|
||||||
|
m_pid = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
m_logger->info("MCP server stopped");
|
m_logger->info("MCP server stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +254,27 @@ bool StdioTransport::spawnProcess() {
|
|||||||
m_stdinFd = stdinPipe[1];
|
m_stdinFd = stdinPipe[1];
|
||||||
m_stdoutFd = stdoutPipe[0];
|
m_stdoutFd = stdoutPipe[0];
|
||||||
|
|
||||||
|
// Give the child a moment to fail if command doesn't exist
|
||||||
|
// 100ms should be enough for execvp to fail on invalid command
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
|
||||||
|
// Check if child is still alive
|
||||||
|
int status;
|
||||||
|
pid_t result = waitpid(m_pid, &status, WNOHANG);
|
||||||
|
if (result == m_pid) {
|
||||||
|
// Child has exited - command likely failed (exit code 1 from _exit)
|
||||||
|
m_logger->error("Child process exited immediately (command not found?)");
|
||||||
|
close(m_stdinFd);
|
||||||
|
close(m_stdoutFd);
|
||||||
|
m_stdinFd = -1;
|
||||||
|
m_stdoutFd = -1;
|
||||||
|
m_pid = -1;
|
||||||
|
return false;
|
||||||
|
} else if (result == -1) {
|
||||||
|
// Error in waitpid
|
||||||
|
m_logger->error("waitpid error: {}", strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -276,8 +303,9 @@ void StdioTransport::readerLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StdioTransport::handleMessage(const json& message) {
|
void StdioTransport::handleMessage(const json& message) {
|
||||||
// Check if this is a response
|
// Check if this is a response (id must be a number, not null)
|
||||||
if (message.contains("id") && (message.contains("result") || message.contains("error"))) {
|
if (message.contains("id") && message["id"].is_number() &&
|
||||||
|
(message.contains("result") || message.contains("error"))) {
|
||||||
int id = message["id"].get<int>();
|
int id = message["id"].get<int>();
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
@ -302,9 +330,11 @@ JsonRpcResponse StdioTransport::sendRequest(const JsonRpcRequest& request, int t
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a mutable copy with assigned ID
|
// Create a mutable copy, use provided ID if > 0, otherwise generate one
|
||||||
JsonRpcRequest req = request;
|
JsonRpcRequest req = request;
|
||||||
req.id = m_nextRequestId++;
|
if (req.id <= 0) {
|
||||||
|
req.id = m_nextRequestId++;
|
||||||
|
}
|
||||||
|
|
||||||
// Create pending request
|
// Create pending request
|
||||||
auto pending = std::make_shared<PendingRequest>();
|
auto pending = std::make_shared<PendingRequest>();
|
||||||
|
|||||||
4
tests/fixtures/mock_mcp.json
vendored
4
tests/fixtures/mock_mcp.json
vendored
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"servers": {
|
"servers": {
|
||||||
"mock_server": {
|
"mock_server": {
|
||||||
"command": "python",
|
"command": "python3",
|
||||||
"args": ["tests/fixtures/mock_mcp_server.py"],
|
"args": ["tests/fixtures/mock_mcp_server.py"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
@ -11,7 +11,7 @@
|
|||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"echo_server": {
|
"echo_server": {
|
||||||
"command": "python",
|
"command": "python3",
|
||||||
"args": ["tests/fixtures/echo_server.py"],
|
"args": ["tests/fixtures/echo_server.py"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"test_server", {
|
{"test_server", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"server.py"})},
|
{"args", json::array({"server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
@ -112,7 +112,7 @@ TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"enabled_server", {
|
{"enabled_server", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
@ -143,12 +143,12 @@ TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
{"server2", {
|
{"server2", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
@ -178,7 +178,7 @@ TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
@ -205,12 +205,12 @@ TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"server1", {
|
{"server1", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}},
|
}},
|
||||||
{"server2", {
|
{"server2", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
@ -366,7 +366,7 @@ TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
|
|||||||
json config = {
|
json config = {
|
||||||
{"servers", {
|
{"servers", {
|
||||||
{"test_server", {
|
{"test_server", {
|
||||||
{"command", "python"},
|
{"command", "python3"},
|
||||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||||
{"enabled", true}
|
{"enabled", true}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ using json = nlohmann::json;
|
|||||||
MCPServerConfig makeEchoServerConfig() {
|
MCPServerConfig makeEchoServerConfig() {
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "echo";
|
config.name = "echo";
|
||||||
config.command = "python";
|
config.command = "python3";
|
||||||
config.args = {"tests/fixtures/echo_server.py"};
|
config.args = {"tests/fixtures/echo_server.py"};
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
return config;
|
return config;
|
||||||
@ -29,7 +29,7 @@ MCPServerConfig makeEchoServerConfig() {
|
|||||||
MCPServerConfig makeMockMCPServerConfig() {
|
MCPServerConfig makeMockMCPServerConfig() {
|
||||||
MCPServerConfig config;
|
MCPServerConfig config;
|
||||||
config.name = "mock_mcp";
|
config.name = "mock_mcp";
|
||||||
config.command = "python";
|
config.command = "python3";
|
||||||
config.args = {"tests/fixtures/mock_mcp_server.py"};
|
config.args = {"tests/fixtures/mock_mcp_server.py"};
|
||||||
config.enabled = true;
|
config.enabled = true;
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user