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 <map>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@ -105,6 +108,8 @@ struct ModuleEntry {
|
||||
// Message router between modules and services
|
||||
class MessageRouter {
|
||||
public:
|
||||
using MessageCallback = std::function<void(const std::string& topic, grove::IDataNode* data)>;
|
||||
|
||||
void addModuleIO(const std::string& name, grove::IIO* io) {
|
||||
m_moduleIOs[name] = io;
|
||||
}
|
||||
@ -113,6 +118,10 @@ public:
|
||||
m_serviceIOs[name] = io;
|
||||
}
|
||||
|
||||
void setMessageCallback(MessageCallback callback) {
|
||||
m_callback = callback;
|
||||
}
|
||||
|
||||
void routeMessages() {
|
||||
// Collect all messages from modules and services
|
||||
std::vector<grove::Message> messages;
|
||||
@ -133,6 +142,11 @@ public:
|
||||
|
||||
// Route messages to appropriate destinations
|
||||
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
|
||||
std::string prefix = msg.topic.substr(0, msg.topic.find(':'));
|
||||
|
||||
@ -157,6 +171,7 @@ public:
|
||||
private:
|
||||
std::map<std::string, grove::IIO*> m_moduleIOs;
|
||||
std::map<std::string, grove::IIO*> m_serviceIOs;
|
||||
MessageCallback m_callback;
|
||||
};
|
||||
|
||||
// Run AISSIA as MCP server (stdio mode)
|
||||
@ -172,6 +187,20 @@ int runMCPServer() {
|
||||
// Create tool registry with FileSystem tools
|
||||
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
|
||||
for (const auto& toolDef : aissia::tools::FileSystemTools::getToolDefinitions()) {
|
||||
std::string toolName = toolDef["name"].get<std::string>();
|
||||
@ -195,6 +224,39 @@ int runMCPServer() {
|
||||
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[]) {
|
||||
// Check for MCP server mode
|
||||
if (argc > 1 && (std::strcmp(argv[1], "--mcp-server") == 0 ||
|
||||
@ -202,6 +264,14 @@ int main(int argc, char* argv[]) {
|
||||
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
|
||||
auto console = spdlog::stdout_color_mt("Aissia");
|
||||
spdlog::set_default_logger(console);
|
||||
@ -308,6 +378,22 @@ int main(int argc, char* argv[]) {
|
||||
platformService.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
|
||||
// =========================================================================
|
||||
@ -321,6 +407,27 @@ int main(int argc, char* argv[]) {
|
||||
router.addServiceIO("PlatformService", platformIO.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)
|
||||
std::vector<std::pair<std::string, std::string>> moduleList = {
|
||||
{"SchedulerModule", "scheduler.json"},
|
||||
@ -451,6 +558,24 @@ int main(int argc, char* argv[]) {
|
||||
// =====================================================================
|
||||
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
|
||||
// =====================================================================
|
||||
@ -477,6 +602,13 @@ int main(int argc, char* argv[]) {
|
||||
// =========================================================================
|
||||
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
|
||||
for (auto& [name, entry] : modules) {
|
||||
if (entry.module) {
|
||||
@ -491,6 +623,8 @@ int main(int argc, char* argv[]) {
|
||||
storageService.shutdown();
|
||||
llmService.shutdown();
|
||||
|
||||
g_interactiveIO = nullptr;
|
||||
|
||||
spdlog::info("A bientot!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -68,18 +68,17 @@ public:
|
||||
HttpResponse response;
|
||||
|
||||
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) {
|
||||
client = std::make_unique<httplib::Client>(m_host);
|
||||
client->enable_server_certificate_verification(true);
|
||||
} else {
|
||||
client = std::make_unique<httplib::Client>(m_host);
|
||||
client.enable_server_certificate_verification(true);
|
||||
}
|
||||
|
||||
client->set_connection_timeout(m_timeoutSeconds);
|
||||
client->set_read_timeout(m_timeoutSeconds);
|
||||
client->set_write_timeout(m_timeoutSeconds);
|
||||
client.set_connection_timeout(m_timeoutSeconds);
|
||||
client.set_read_timeout(m_timeoutSeconds);
|
||||
client.set_write_timeout(m_timeoutSeconds);
|
||||
|
||||
httplib::Headers headers;
|
||||
headers.emplace("Content-Type", "application/json");
|
||||
@ -90,7 +89,7 @@ public:
|
||||
std::string bodyStr = body.dump();
|
||||
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) {
|
||||
response.status = result->status;
|
||||
@ -120,24 +119,22 @@ public:
|
||||
HttpResponse response;
|
||||
|
||||
try {
|
||||
std::unique_ptr<httplib::Client> client;
|
||||
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
|
||||
httplib::Client client(url);
|
||||
|
||||
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.enable_server_certificate_verification(true);
|
||||
}
|
||||
|
||||
client->set_connection_timeout(m_timeoutSeconds);
|
||||
client->set_read_timeout(m_timeoutSeconds);
|
||||
client.set_connection_timeout(m_timeoutSeconds);
|
||||
client.set_read_timeout(m_timeoutSeconds);
|
||||
|
||||
httplib::Headers headers;
|
||||
for (const auto& [key, value] : m_headers) {
|
||||
headers.emplace(key, value);
|
||||
}
|
||||
|
||||
auto result = client->Post(path, headers, items);
|
||||
auto result = client.Post(path, headers, items);
|
||||
|
||||
if (result) {
|
||||
response.status = result->status;
|
||||
@ -158,23 +155,22 @@ public:
|
||||
HttpResponse response;
|
||||
|
||||
try {
|
||||
std::unique_ptr<httplib::Client> client;
|
||||
std::string url = (m_useSSL ? "https://" : "http://") + m_host;
|
||||
httplib::Client client(url);
|
||||
|
||||
if (m_useSSL) {
|
||||
client = std::make_unique<httplib::Client>(m_host);
|
||||
} else {
|
||||
client = std::make_unique<httplib::Client>(m_host);
|
||||
client.enable_server_certificate_verification(true);
|
||||
}
|
||||
|
||||
client->set_connection_timeout(m_timeoutSeconds);
|
||||
client->set_read_timeout(m_timeoutSeconds);
|
||||
client.set_connection_timeout(m_timeoutSeconds);
|
||||
client.set_read_timeout(m_timeoutSeconds);
|
||||
|
||||
httplib::Headers headers;
|
||||
for (const auto& [key, value] : m_headers) {
|
||||
headers.emplace(key, value);
|
||||
}
|
||||
|
||||
auto result = client->Get(path, headers);
|
||||
auto result = client.Get(path, headers);
|
||||
|
||||
if (result) {
|
||||
response.status = result->status;
|
||||
|
||||
@ -45,7 +45,8 @@ LLMResponse ClaudeProvider::chat(const std::string& systemPrompt,
|
||||
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);
|
||||
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
|
||||
#include <sstream>
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
namespace aissia::mcp {
|
||||
|
||||
@ -50,26 +54,12 @@ void StdioTransport::stop() {
|
||||
|
||||
m_running = false;
|
||||
|
||||
// Close write pipe to signal EOF to child
|
||||
// Close pipes and terminate process first, so reader thread can exit
|
||||
#ifdef _WIN32
|
||||
if (m_stdinWrite) {
|
||||
CloseHandle(m_stdinWrite);
|
||||
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) {
|
||||
TerminateProcess(m_processHandle, 0);
|
||||
CloseHandle(m_processHandle);
|
||||
@ -80,10 +70,13 @@ void StdioTransport::stop() {
|
||||
m_stdoutRead = nullptr;
|
||||
}
|
||||
#else
|
||||
if (m_stdinFd >= 0) {
|
||||
close(m_stdinFd);
|
||||
m_stdinFd = -1;
|
||||
}
|
||||
if (m_pid > 0) {
|
||||
kill(m_pid, SIGTERM);
|
||||
waitpid(m_pid, nullptr, 0);
|
||||
m_pid = -1;
|
||||
// Don't wait here - let the reader thread see EOF first
|
||||
}
|
||||
if (m_stdoutFd >= 0) {
|
||||
close(m_stdoutFd);
|
||||
@ -91,6 +84,19 @@ void StdioTransport::stop() {
|
||||
}
|
||||
#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");
|
||||
}
|
||||
|
||||
@ -248,6 +254,27 @@ bool StdioTransport::spawnProcess() {
|
||||
m_stdinFd = stdinPipe[1];
|
||||
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;
|
||||
#endif
|
||||
}
|
||||
@ -276,8 +303,9 @@ void StdioTransport::readerLoop() {
|
||||
}
|
||||
|
||||
void StdioTransport::handleMessage(const json& message) {
|
||||
// Check if this is a response
|
||||
if (message.contains("id") && (message.contains("result") || message.contains("error"))) {
|
||||
// Check if this is a response (id must be a number, not null)
|
||||
if (message.contains("id") && message["id"].is_number() &&
|
||||
(message.contains("result") || message.contains("error"))) {
|
||||
int id = message["id"].get<int>();
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@ -302,9 +330,11 @@ JsonRpcResponse StdioTransport::sendRequest(const JsonRpcRequest& request, int t
|
||||
return error;
|
||||
}
|
||||
|
||||
// Create a mutable copy with assigned ID
|
||||
// Create a mutable copy, use provided ID if > 0, otherwise generate one
|
||||
JsonRpcRequest req = request;
|
||||
req.id = m_nextRequestId++;
|
||||
if (req.id <= 0) {
|
||||
req.id = m_nextRequestId++;
|
||||
}
|
||||
|
||||
// Create pending request
|
||||
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": {
|
||||
"mock_server": {
|
||||
"command": "python",
|
||||
"command": "python3",
|
||||
"args": ["tests/fixtures/mock_mcp_server.py"],
|
||||
"enabled": true
|
||||
},
|
||||
@ -11,7 +11,7 @@
|
||||
"enabled": false
|
||||
},
|
||||
"echo_server": {
|
||||
"command": "python",
|
||||
"command": "python3",
|
||||
"args": ["tests/fixtures/echo_server.py"],
|
||||
"enabled": true
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"test_server", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"server.py"})},
|
||||
{"enabled", true}
|
||||
}}
|
||||
@ -112,7 +112,7 @@ TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"enabled_server", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}},
|
||||
@ -143,12 +143,12 @@ TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"server1", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}},
|
||||
{"server2", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}}
|
||||
@ -178,7 +178,7 @@ TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"server1", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}}
|
||||
@ -205,12 +205,12 @@ TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"server1", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}},
|
||||
{"server2", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}}
|
||||
@ -366,7 +366,7 @@ TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") {
|
||||
json config = {
|
||||
{"servers", {
|
||||
{"test_server", {
|
||||
{"command", "python"},
|
||||
{"command", "python3"},
|
||||
{"args", json::array({"tests/fixtures/echo_server.py"})},
|
||||
{"enabled", true}
|
||||
}}
|
||||
|
||||
@ -20,7 +20,7 @@ using json = nlohmann::json;
|
||||
MCPServerConfig makeEchoServerConfig() {
|
||||
MCPServerConfig config;
|
||||
config.name = "echo";
|
||||
config.command = "python";
|
||||
config.command = "python3";
|
||||
config.args = {"tests/fixtures/echo_server.py"};
|
||||
config.enabled = true;
|
||||
return config;
|
||||
@ -29,7 +29,7 @@ MCPServerConfig makeEchoServerConfig() {
|
||||
MCPServerConfig makeMockMCPServerConfig() {
|
||||
MCPServerConfig config;
|
||||
config.name = "mock_mcp";
|
||||
config.command = "python";
|
||||
config.command = "python3";
|
||||
config.args = {"tests/fixtures/mock_mcp_server.py"};
|
||||
config.enabled = true;
|
||||
return config;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user