CoolFace
Modelpublic

Felipe97/llama-cpp-compiled

sourceHugging Faceupdated 2d agoView on Hugging Face
0likes1.1kdownloads
server-mcp.cpp821 linesDownload Raw Back to server
1#include "server-mcp.h"2 3#include "subproc.h"4 5#include <atomic>6#include <chrono>7#include <cstdio>8#include <fstream>9#include <functional>10#include <sstream>11#include <thread>12 13#if defined(_WIN32)14#  include <io.h>15#  include <windows.h>16#else17#  include <errno.h>18#  include <fcntl.h>19#  include <poll.h>20#  include <unistd.h>21extern char ** environ;22#endif23 24// read NDJSON lines from a child pipe, calling on_line per line until `running` clears, EOF/error, or on_line returns false.25// polled, not blocking: a grandchild can inherit the pipe's write end and hold it open (terminate() kills only the direct child), so a blocking read would hang teardown on an EOF that never comes.26static void mcp_pump_ndjson(FILE * f, std::atomic<bool> & running,27                            const std::function<bool(std::string &&)> & on_line) {28    if (!f) {29        return;30    }31    const int    poll_ms  = 50;32    const size_t max_line = 8 * 1024 * 1024; // drop any single NDJSON line larger than this, so a child that never emits '\n' can't grow buf without bound33#if defined(_WIN32)34    HANDLE h = (HANDLE) _get_osfhandle(_fileno(f));35#else36    int fd = fileno(f);37    int fl = fcntl(fd, F_GETFL, 0);38    if (fl >= 0) {39        fcntl(fd, F_SETFL, fl | O_NONBLOCK);40    }41#endif42    std::string buf;43    bool        skipping = false; // discarding an over-long line until its terminating newline44    char        chunk[4096];45    while (running.load()) {46        size_t n = 0;47#if defined(_WIN32)48        DWORD avail = 0;49        if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) {50            break; // pipe broken / child gone51        }52        if (avail == 0) {53            std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms));54            continue;55        }56        DWORD to_read = avail < (DWORD) sizeof(chunk) ? avail : (DWORD) sizeof(chunk);57        DWORD got     = 0;58        if (!ReadFile(h, chunk, to_read, &got, NULL) || got == 0) {59            break;60        }61        n = (size_t) got;62#else63        struct pollfd pfd;64        pfd.fd      = fd;65        pfd.events  = POLLIN;66        pfd.revents = 0;67        int pr      = poll(&pfd, 1, poll_ms);68        if (pr < 0) {69            if (errno == EINTR) {70                continue;71            }72            break;73        }74        if (pr == 0) {75            continue; // timeout -> re-check running76        }77        if (pfd.revents & (POLLERR | POLLNVAL)) {78            break;79        }80        ssize_t r = read(fd, chunk, sizeof(chunk));81        if (r < 0) {82            if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) {83                continue;84            }85            break;86        }87        if (r == 0) {88            break; // EOF: child (and any pipe writers) closed the stream89        }90        n = (size_t) r;91#endif92        buf.append(chunk, n);93 94        // resync after an over-long, unterminated line: discard bytes until the next newline95        if (skipping) {96            size_t nl = buf.find('\n');97            if (nl == std::string::npos) {98                if (buf.size() > max_line) {99                    buf.clear(); // stay bounded while waiting for a terminator100                }101                continue;102            }103            buf.erase(0, nl + 1);104            skipping = false;105        }106 107        size_t pos;108        while ((pos = buf.find('\n')) != std::string::npos) {109            std::string line = buf.substr(0, pos);110            buf.erase(0, pos + 1);111            if (!line.empty() && line.back() == '\r') {112                line.pop_back();113            }114            if (line.empty()) {115                continue;116            }117            if (!on_line(std::move(line))) {118                return;119            }120        }121 122        // a partial line already larger than the cap and still no newline: drop it to avoid unbounded growth123        if (buf.size() > max_line) {124            SRV_WRN("MCP: dropping oversized line (> %zu bytes) from child pipe\n", max_line);125            buf.clear();126            skipping = true;127        }128    }129}130 131//132// server_mcp_server_config133//134 135std::vector<server_mcp_server_config> server_mcp_server_config::parse_from_json(const std::string & json_str) {136    return parse_cursor_format(json::parse(json_str));137}138 139std::vector<server_mcp_server_config> server_mcp_server_config::parse_cursor_format(const json & j) {140    std::vector<server_mcp_server_config> result;141 142    if (!j.contains("mcpServers") || !j.at("mcpServers").is_object()) {143        return result;144    }145 146    for (const auto & [name, cfg] : j.at("mcpServers").items()) {147        server_mcp_server_config sc;148        sc.name = name;149        sc.command = cfg.value("command", std::string());150        sc.cwd = cfg.value("cwd", std::string());151        sc.timeout_ms = cfg.value("timeout_ms", sc.timeout_ms);152 153        if (cfg.contains("args") && cfg.at("args").is_array()) {154            for (const auto & a : cfg.at("args")) {155                sc.args.push_back(a.get<std::string>());156            }157        }158        if (cfg.contains("env") && cfg.at("env").is_object()) {159            for (const auto & [k, v] : cfg.at("env").items()) {160                sc.env[k] = v.get<std::string>();161            }162        }163 164        if (sc.command.empty()) {165            SRV_WRN("MCP server '%s' has no command, skipping\n", name.c_str());166            continue;167        }168        result.push_back(std::move(sc));169    }170 171    return result;172}173 174 175//176// server_mcp_transport177//178 179static constexpr const char * MCP_PROTOCOL_VERSION = "2024-11-05";180 181static std::string rpc_error_message(const json & resp) {182    if (resp.contains("error")) {183        const json & e = resp.at("error");184        if (e.is_object()) {185            return e.value("message", "unknown error");186        }187        if (e.is_string()) {188            return e.get<std::string>();189        }190    }191    return "unknown error";192}193 194// normalize an MCP tools/call result to the /tools contract (see README-dev.md):195// concat text parts of result.content[], and surface an isError result196static json mcp_result_to_response(const json & result) {197    std::string text;198    if (result.contains("content") && result.at("content").is_array()) {199        for (const auto & part : result.at("content")) {200            if (part.is_object() && part.value("type", "") == "text") {201                if (!text.empty()) {202                    text += "\n";203                }204                text += part.value("text", "");205            }206        }207    }208    if (result.is_object() && result.value("isError", false)) {209        return {{"error", text.empty() ? "MCP tool returned an error" : text}};210    }211    return {{"plain_text_response", text}};212}213 214json server_mcp_transport::send_rpc(const json & request, const std::function<bool()> & should_stop) {215    if (!to_server.write(request.dump())) {216        return {{"error", {{"code", -32603}, {"message", "transport closed"}}}};217    }218 219    const bool has_id = request.contains("id");220    const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);221    auto stop = [&]() {222        return (should_stop && should_stop()) || std::chrono::steady_clock::now() >= deadline;223    };224 225    std::string frame;226    while (from_server.read(frame, stop, false)) {227        json reply;228        try {229            reply = json::parse(frame);230        } catch (...) {231            if (std::chrono::steady_clock::now() >= deadline) {232                break;233            }234            continue; // skip malformed frame235        }236        // no id: a notification. mismatched id: a stale reply from a timed-out request (ids are monotonic, never a future one)237        if (!has_id || (reply.contains("id") && reply.at("id") == request.at("id"))) {238            return reply;239        }240        if (std::chrono::steady_clock::now() >= deadline) {241            break; // a flood of notifications must not outrun the deadline242        }243    }244 245    if (should_stop && should_stop()) {246        return {{"error", {{"code", -32603}, {"message", "cancelled"}}}};247    }248    if (std::chrono::steady_clock::now() >= deadline) {249        return {{"error", {{"code", -32603}, {"message", "request timed out"}}}};250    }251    return {{"error", {{"code", -32603}, {"message", "transport closed"}}}};252}253 254bool server_mcp_transport::ensure_init(const std::function<bool()> & should_stop) {255    if (initialized) {256        return true;257    }258 259    json init_req = {260        {"jsonrpc", "2.0"},261        {"id", next_id++},262        {"method", "initialize"},263        {"params", {264            {"protocolVersion", MCP_PROTOCOL_VERSION},265            {"capabilities", json::object()},266            {"clientInfo", {{"name", "llama.cpp"}, {"version", "1.0"}}},267        }},268    };269    json resp = send_rpc(init_req, should_stop);270    if (!resp.contains("result")) {271        last_error = "initialize failed: " + rpc_error_message(resp);272        return false;273    }274 275    // notifications/initialized: no id, no reply expected276    json notif = {{"jsonrpc", "2.0"}, {"method", "notifications/initialized"}};277    to_server.write(notif.dump());278 279    initialized = true;280    return true;281}282 283std::vector<server_mcp_tool_def> server_mcp_transport::list_tools(const std::function<bool()> & should_stop) {284    std::lock_guard<std::mutex> lock(rpc_mutex);285    if (!ensure_init(should_stop)) {286        return {};287    }288    if (!tools.empty()) {289        return tools;290    }291 292    json req = {{"jsonrpc", "2.0"}, {"id", next_id++}, {"method", "tools/list"}};293    json resp = send_rpc(req, should_stop);294    if (!resp.contains("result")) {295        last_error = "tools/list failed: " + rpc_error_message(resp);296        return {};297    }298 299    const json & result = resp.at("result");300    if (result.contains("tools") && result.at("tools").is_array()) {301        for (const auto & t : result.at("tools")) {302            server_mcp_tool_def def;303            def.server_name = name;304            def.name = t.value("name", "");305            def.description = t.value("description", "");306            if (t.contains("inputSchema")) {307                def.input_schema = t.at("inputSchema");308            }309            tools.push_back(std::move(def));310        }311    }312    return tools;313}314 315json server_mcp_transport::call_tool(const std::string & tool_name,316                                     const json & arguments,317                                     const std::function<bool()> & should_stop) {318    std::lock_guard<std::mutex> lock(rpc_mutex);319    if (!ensure_init(should_stop)) {320        return {{"error", last_error}};321    }322 323    json req = {324        {"jsonrpc", "2.0"},325        {"id", next_id++},326        {"method", "tools/call"},327        {"params", {{"name", tool_name}, {"arguments", arguments}}},328    };329    json resp = send_rpc(req, should_stop);330    if (resp.contains("error")) {331        return {{"error", rpc_error_message(resp)}};332    }333    if (resp.contains("result")) {334        return mcp_result_to_response(resp.at("result"));335    }336    return {{"error", "invalid response from MCP server"}};337}338 339//340// server_mcp_stdio341//342 343struct server_mcp_stdio::process_handle {344    common_subproc sp;345    FILE * in  = nullptr; // child stdin346    FILE * out = nullptr; // child stdout347    FILE * err = nullptr; // child stderr348};349 350#if defined(_WIN32)351// config strings are UTF-8 (from JSON) and subprocess.h converts them with CP_UTF8, so inputs must be UTF-8, not the active code page352static std::wstring windows_utf8_to_wide(const std::string & s) {353    if (s.empty()) {354        return std::wstring();355    }356    int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int) s.size(), NULL, 0);357    if (n <= 0) {358        return std::wstring();359    }360    std::wstring w((size_t) n, L'\0');361    MultiByteToWideChar(CP_UTF8, 0, s.data(), (int) s.size(), &w[0], n);362    return w;363}364 365static std::string windows_wide_to_utf8(const wchar_t * s, int len /* -1 for NUL-terminated */) {366    int n = WideCharToMultiByte(CP_UTF8, 0, s, len, NULL, 0, NULL, NULL);367    if (n <= 0) {368        return std::string();369    }370    std::string out((size_t) n, '\0');371    WideCharToMultiByte(CP_UTF8, 0, s, len, &out[0], n, NULL, NULL);372    if (len == -1 && !out.empty() && out.back() == '\0') {373        out.pop_back(); // drop the terminator WideCharToMultiByte counts for -1374    }375    return out;376}377#endif378 379static std::string mcp_resolve_command(const std::string & command) {380#if defined(_WIN32)381    // For Windows: make sure we handle ".exe" correctly, as well as UTF-8382    std::wstring wcmd = windows_utf8_to_wide(command);383    wchar_t      buf[MAX_PATH * 4];384    const DWORD  cap = (DWORD) (sizeof(buf) / sizeof(buf[0]));385 386    auto search = [&](const wchar_t * ext) -> std::string {387        DWORD n = SearchPathW(NULL, wcmd.c_str(), ext, cap, buf, NULL);388        return (n > 0 && n < cap) ? windows_wide_to_utf8(buf, (int) n) : std::string();389    };390 391    std::string found = search(NULL); // exact path / already-extensioned / .exe on PATH392    if (!found.empty()) {393        return found;394    }395 396    std::wstring pathext;397    DWORD        need = GetEnvironmentVariableW(L"PATHEXT", NULL, 0);398    if (need > 0) {399        pathext.resize(need);400        DWORD got = GetEnvironmentVariableW(L"PATHEXT", &pathext[0], need);401        pathext.resize(got);402    }403    if (pathext.empty()) {404        pathext = L".COM;.EXE;.BAT;.CMD";405    }406    for (size_t start = 0; start <= pathext.size();) {407        size_t       sep = pathext.find(L';', start);408        std::wstring ext = pathext.substr(start, sep == std::wstring::npos ? std::wstring::npos : sep - start);409        if (!ext.empty()) {410            found = search(ext.c_str());411            if (!found.empty()) {412                return found;413            }414        }415        if (sep == std::wstring::npos) {416            break;417        }418        start = sep + 1;419    }420    return command; // give up and let subprocess.h report the spawn error421#else422    return command;423#endif // _WIN32424}425 426static std::vector<std::string> mcp_parent_env() {427    std::vector<std::string> env;428#if defined(_WIN32)429    LPWCH block = GetEnvironmentStringsW();430    if (block) {431        for (LPWCH e = block; *e; e += wcslen(e) + 1) {432            env.emplace_back(windows_wide_to_utf8(e, -1));433        }434        FreeEnvironmentStringsW(block);435    }436#else437    if (environ) {438        for (char ** e = environ; *e; ++e) {439            env.emplace_back(*e);440        }441    }442#endif443    return env;444}445 446// parent env with the config overrides applied, in "KEY=VALUE" form447static std::vector<std::string> mcp_build_env(const std::map<std::string, std::string> & overrides) {448    std::vector<std::string> env;449    for (auto & e : mcp_parent_env()) {450        size_t eq = e.find('=');451        std::string key = eq == std::string::npos ? e : e.substr(0, eq);452        if (overrides.find(key) == overrides.end()) {453            env.push_back(e);454        }455    }456    for (auto & [k, v] : overrides) {457        env.push_back(k + "=" + v);458    }459    return env;460}461 462server_mcp_stdio::server_mcp_stdio(const server_mcp_server_config & config) : config(config) {463    name = config.name;464    timeout_ms = config.timeout_ms;465    // bound the reply queue: send_rpc only drains during a call, so unsolicited notifications would otherwise grow it without limit466    from_server.max_size = 65536;467}468 469server_mcp_stdio::~server_mcp_stdio() {470    join_pumps();471}472 473bool server_mcp_stdio::start() {474    std::vector<std::string> argv_s;475    argv_s.push_back(mcp_resolve_command(config.command));476    argv_s.insert(argv_s.end(), config.args.begin(), config.args.end());477 478    int options = subprocess_option_no_window | subprocess_option_search_user_path;479    std::vector<std::string> envp_s;480    if (config.env.empty()) {481        options |= subprocess_option_inherit_environment;482    } else {483        envp_s = mcp_build_env(config.env);484    }485 486    auto handle = std::make_unique<process_handle>();487    bool ok = handle->sp.create(argv_s, options, envp_s, config.cwd.empty() ? nullptr : config.cwd.c_str());488    if (!ok) {489        SRV_WRN("MCP '%s': failed to spawn '%s'\n", config.name.c_str(), config.command.c_str());490        return false;491    }492    handle->in  = handle->sp.stdin_file();493    handle->out = handle->sp.stdout_file();494    handle->err = handle->sp.stderr_file();495 496    proc = std::move(handle);497    running.store(true);498    reader = std::thread([this] { reader_loop(); });499    writer = std::thread([this] { writer_loop(); });500    errlog = std::thread([this] { errlog_loop(); });501    return true;502}503 504void server_mcp_stdio::close() {505    join_pumps();506}507 508bool server_mcp_stdio::is_alive() const {509    return running.load();510}511 512std::string server_mcp_stdio::diagnostics() {513    std::string out;514    {515        std::lock_guard<std::mutex> lock(rpc_mutex); // last_error is written by send_rpc's callers516        out = last_error;517    }518    std::lock_guard<std::mutex> lk(err_mu);519    if (!err_tail.empty()) {520        if (!out.empty()) {521            out += "; ";522        }523        out += "last stderr: " + err_tail;524    }525    return out;526}527 528void server_mcp_stdio::reader_loop() {529    mcp_pump_ndjson(proc->out, running, [this](std::string && line) {530        return from_server.write(std::move(line)); // false => consumer gone, stop531    });532    running.store(false);533    to_server.close_write();   // stop the writer534    from_server.close_write(); // EOF to any waiting caller535}536 537// write all of `data` to child stdin, non-blocking and polled so teardown never hangs (a grandchild can hold the read end of a full pipe open). returns false on error/close/shutdown.538static bool mcp_write_all(FILE * f, const std::string & data, std::atomic<bool> & running) {539    if (!f) {540        return false;541    }542    size_t total = 0;543#if defined(_WIN32)544    HANDLE h      = (HANDLE) _get_osfhandle(_fileno(f));545    DWORD  nowait = PIPE_NOWAIT;546    SetNamedPipeHandleState(h, &nowait, NULL, NULL);547    while (total < data.size() && running.load()) {548        DWORD written = 0;549        BOOL  ok      = WriteFile(h, data.data() + total, (DWORD) (data.size() - total), &written, NULL);550        if (ok && written > 0) {551            total += written;552            continue;553        }554        if (!ok) {555            DWORD err = GetLastError();556            if (err != ERROR_NO_DATA && err != ERROR_PIPE_BUSY) {557                return false;558            }559        }560        // backpressure (pipe full) is rare for small JSON-RPC frames; sleep rather than spin.561        // no writable-wait exists for a PIPE_NOWAIT anonymous pipe, so this polls like the POSIX poll() path.562        std::this_thread::sleep_for(std::chrono::milliseconds(10));563    }564#else565    int fd = fileno(f);566    int fl = fcntl(fd, F_GETFL, 0);567    if (fl >= 0) {568        fcntl(fd, F_SETFL, fl | O_NONBLOCK);569    }570    while (total < data.size() && running.load()) {571        ssize_t n = write(fd, data.data() + total, data.size() - total);572        if (n > 0) {573            total += (size_t) n;574            continue;575        }576        if (n == 0) {577            return false;578        }579        if (errno == EINTR) {580            continue;581        }582        if (errno != EAGAIN && errno != EWOULDBLOCK) {583            return false;584        }585        struct pollfd pfd;586        pfd.fd      = fd;587        pfd.events  = POLLOUT;588        pfd.revents = 0;589        int pr      = poll(&pfd, 1, 50);590        if (pr < 0) {591            if (errno == EINTR) {592                continue;593            }594            return false;595        }596        if (pfd.revents & (POLLERR | POLLNVAL | POLLHUP)) {597            return false;598        }599    }600#endif601    return total == data.size();602}603 604void server_mcp_stdio::writer_loop() {605    auto should_stop = [this] { return !running.load(); };606    std::string msg;607    while (to_server.read(msg, should_stop)) {608        msg.push_back('\n');609        if (!mcp_write_all(proc->in, msg, running)) {610            break; // child gone or shutting down611        }612    }613    running.store(false);614    to_server.close_read();    // fail fast on any further send_rpc write615    from_server.close_write(); // wake any caller waiting for a reply616}617 618void server_mcp_stdio::errlog_loop() {619    static constexpr size_t ERR_TAIL_MAX = 4096;620    // drain stderr (an undrained pipe blocks the child):621    // log it, and keep a bounded tail for reporting when the server dies622    mcp_pump_ndjson(proc->err, running, [this](std::string && line) {623        SRV_DBG("MCP '%s' stderr: %s\n", name.c_str(), line.c_str());624        std::lock_guard<std::mutex> lk(err_mu);625        err_tail += line;626        err_tail += '\n';627        if (err_tail.size() > ERR_TAIL_MAX) {628            err_tail.erase(0, err_tail.size() - ERR_TAIL_MAX);629        }630        return true;631    });632}633 634void server_mcp_stdio::join_pumps() {635    if (!proc) {636        return;637    }638    running.store(false);639    to_server.close_write();   // wake the writer if it waits for a message640    from_server.close_write(); // wake any caller waiting for a reply641 642    proc->sp.terminate(); // child death unblocks the blocked fread/fwrite643 644    if (writer.joinable()) writer.join();645    if (reader.joinable()) reader.join();646    if (errlog.joinable()) errlog.join();647 648    proc->sp.join(); // reap the child: never waiting would leave the pid a zombie for the process lifetime649    proc.reset();650}651 652 653//654// server_mcp655//656 657static constexpr int MCP_COOLDOWN_SECONDS = 5;658static constexpr int MCP_WARMUP_TIMEOUT_SECONDS = 10; // cap per-server tool discovery at startup659 660server_mcp::~server_mcp() {661    shutdown();662 663    std::vector<std::shared_ptr<server_mcp_transport>> to_close;664    {665        std::lock_guard<std::mutex> lock(mutex);666        for (auto & [name, t] : transports) {667            to_close.push_back(std::move(t));668        }669        transports.clear();670    }671    for (auto & t : to_close) {672        t->close();673    }674}675 676std::shared_ptr<server_mcp_transport> server_mcp::create_transport(const server_mcp_server_config & cfg) {677    return std::make_shared<server_mcp_stdio>(cfg);678}679 680void server_mcp::shutdown() {681    stopping.store(true);682}683 684const server_mcp_server_config * server_mcp::find_config(const std::string & name) const {685    for (const auto & c : configs) {686        if (c.name == name) {687            return &c;688        }689    }690    return nullptr;691}692 693void server_mcp::start(const common_params & params) {694    auto append = [this](const std::string & json_str) {695        try {696            auto parsed = server_mcp_server_config::parse_from_json(json_str);697            if (parsed.empty()) {698                SRV_WRN("%s", "MCP config: no servers found in JSON\n");699            }700            for (auto & p : parsed) {701                // names must be unique across both config sources: get_or_create / find_config key on the name702                if (find_config(p.name)) {703                    SRV_WRN("MCP config: duplicate server name '%s', skipping\n", p.name.c_str());704                    continue;705                }706                configs.push_back(std::move(p));707            }708        } catch (const std::exception & e) {709            throw std::runtime_error(std::string("failed to parse MCP config JSON: ") + e.what());710        }711    };712    if (!params.mcp_servers_config.empty()) {713        std::ifstream f = fs_open_ifstream(params.mcp_servers_config, std::ios::in);714        if (!f) {715            throw std::runtime_error("failed to open MCP config file: " + params.mcp_servers_config);716        }717        std::stringstream ss;718        ss << f.rdbuf();719        append(ss.str());720    }721    if (!params.mcp_servers_json.empty()) {722        append(params.mcp_servers_json);723    }724 725    if (configs.empty()) {726        return;727    }728 729    std::vector<server_mcp_tool_def> discovered;730    for (const auto & cfg : configs) {731        auto t = create_transport(cfg);732        if (!t->start()) {733            SRV_WRN("MCP warmup: failed to spawn '%s': %s\n", cfg.name.c_str(), t->diagnostics().c_str());734            continue;735        }736        // bound warmup per server so an unresponsive one can't stall startup for the full per-call timeout737        const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(MCP_WARMUP_TIMEOUT_SECONDS);738        auto should_stop = [this, deadline]() {739            return stopping.load() || std::chrono::steady_clock::now() >= deadline;740        };741        auto tools = t->list_tools(should_stop);742        SRV_INF("MCP warmup: '%s' discovered %zu tools\n", cfg.name.c_str(), tools.size());743        discovered.insert(discovered.end(), tools.begin(), tools.end());744        t->close();745    }746 747    std::lock_guard<std::mutex> lock(mutex);748    registry.swap(discovered);749}750 751std::vector<server_mcp_tool_def> server_mcp::list_tools() const {752    std::lock_guard<std::mutex> lock(mutex);753    return registry;754}755 756json server_mcp::call_tool(const std::string & server_name,757                           const std::string & tool_name,758                           const json & arguments,759                           const std::function<bool()> & should_stop) {760    auto transport = get_or_create(server_name);761    if (!transport) {762        return {{"error", "MCP server unavailable: " + server_name}};763    }764 765    auto stop = [this, &should_stop]() {766        return stopping.load() || (should_stop && should_stop());767    };768    return transport->call_tool(tool_name, arguments, stop);769}770 771std::shared_ptr<server_mcp_transport> server_mcp::get_or_create(const std::string & name) {772    std::vector<std::shared_ptr<server_mcp_transport>> to_close; // closed after unlock773    std::shared_ptr<server_mcp_transport> result;774 775    {776        std::lock_guard<std::mutex> lock(mutex);777        if (stopping.load()) {778            return nullptr;779        }780 781        auto now = std::chrono::steady_clock::now();782        auto dead_it = dead_servers.find(name);783        if (dead_it != dead_servers.end()) {784            if (now < dead_it->second) {785                return nullptr;786            }787            dead_servers.erase(dead_it);788        }789 790        auto it = transports.find(name);791        if (it != transports.end()) {792            if (it->second->is_alive()) {793                return it->second;794            }795            SRV_WRN("MCP '%s' is no longer alive: %s\n", name.c_str(), it->second->diagnostics().c_str());796            to_close.push_back(std::move(it->second));797            transports.erase(it);798        }799 800        const server_mcp_server_config * cfg = find_config(name);801        if (cfg) {802            auto fresh = create_transport(*cfg);803            if (fresh->start() && fresh->is_alive()) {804                transports[name] = fresh;805                result = fresh;806            } else {807                SRV_WRN("MCP '%s': failed to start: %s\n", name.c_str(), fresh->diagnostics().c_str());808                to_close.push_back(std::move(fresh));809                dead_servers[name] = now + std::chrono::seconds(MCP_COOLDOWN_SECONDS);810            }811        }812    }813 814    for (auto & t : to_close) {815        t->close(); // blocking call, no leaks816    }817 818    return result;819}820 821