char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d2a818231
char/aliceai-t5-moe
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#else 17# include <errno.h> 18# include <fcntl.h> 19# include <poll.h> 20# include <unistd.h> 21extern char ** environ ; 22#endif 23 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 , 27const std::function < bool (std::string && )> & on_line ) { 28if (!f ) { 29return ; 30 } 31const int poll_ms = 50 ; 32const 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 bound 33#if defined(_WIN32 ) 34HANDLE h = (HANDLE )_get_osfhandle (_fileno (f )); 35#else 36int fd = fileno (f ); 37int fl = fcntl (fd ,F_GETFL ,0 ); 38if (fl >=0 ) { 39fcntl (fd ,F_SETFL ,fl |O_NONBLOCK ); 40 } 41#endif 42 std::string buf ; 43bool skipping = false;// discarding an over-long line until its terminating newline 44char chunk [4096 ]; 45while (running .load ()) { 46size_t n = 0 ; 47#if defined(_WIN32 ) 48DWORD avail = 0 ; 49if (!PeekNamedPipe (h ,NULL ,0 ,NULL ,& avail ,NULL )) { 50break ;// pipe broken / child gone 51 } 52if (avail == 0 ) { 53 std::this_thread::sleep_for (std::chrono::milliseconds (poll_ms )); 54continue ; 55 } 56DWORD to_read = avail < (DWORD )sizeof (chunk ) ?avail : (DWORD )sizeof (chunk ); 57DWORD got = 0 ; 58if (!ReadFile (h ,chunk ,to_read ,& got ,NULL )|| got == 0 ) { 59break ; 60 } 61n = (size_t )got ; 62#else 63struct pollfd pfd ; 64pfd .fd = fd ; 65pfd .events = POLLIN ; 66pfd .revents = 0 ; 67int pr = poll (& pfd ,1 ,poll_ms ); 68if (pr < 0 ) { 69if (errno == EINTR ) { 70continue ; 71 } 72break ; 73 } 74if (pr == 0 ) { 75continue ;// timeout -> re-check running 76 } 77if (pfd .revents & (POLLERR |POLLNVAL )) { 78break ; 79 } 80ssize_t r = read (fd ,chunk ,sizeof (chunk )); 81if (r < 0 ) { 82if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK ) { 83continue ; 84 } 85break ; 86 } 87if (r == 0 ) { 88break ;// EOF: child (and any pipe writers) closed the stream 89 } 90n = (size_t )r ; 91#endif 92buf .append (chunk ,n ); 93 94// resync after an over-long, unterminated line: discard bytes until the next newline 95if (skipping ) { 96size_t nl = buf .find ('\n' ); 97if (nl == std::string::npos ) { 98if (buf .size ()> max_line ) { 99buf .clear ();// stay bounded while waiting for a terminator 100 } 101continue ; 102 } 103buf .erase (0 ,nl + 1 ); 104skipping = false; 105 } 106 107size_t pos ; 108while ((pos = buf .find ('\n' ))!= std::string::npos ) { 109 std::string line = buf .substr (0 ,pos ); 110buf .erase (0 ,pos + 1 ); 111if (!line .empty ()&& line .back ()== '\r' ) { 112line .pop_back (); 113 } 114if (line .empty ()) { 115continue ; 116 } 117if (!on_line (std::move (line ))) { 118return ; 119 } 120 } 121 122// a partial line already larger than the cap and still no newline: drop it to avoid unbounded growth 123if (buf .size ()> max_line ) { 124SRV_WRN ("MCP: dropping oversized line (> %zu bytes) from child pipe\n" ,max_line ); 125buf .clear (); 126skipping = true; 127 } 128 } 129} 130 131// 132// server_mcp_server_config 133// 134 135std::vector < server_mcp_server_config > server_mcp_server_config::parse_from_json (const std::string & json_str ) { 136return 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 142if (!j .contains ("mcpServers" )|| !j .at ("mcpServers" ).is_object ()) { 143return result ; 144 } 145 146for (const auto & [name ,cfg ] :j .at ("mcpServers" ).items ()) { 147server_mcp_server_config sc ; 148sc .name = name ; 149sc .command = cfg .value ("command" , std::string ()); 150sc .cwd = cfg .value ("cwd" , std::string ()); 151sc .timeout_ms = cfg .value ("timeout_ms" ,sc .timeout_ms ); 152 153if (cfg .contains ("args" )&& cfg .at ("args" ).is_array ()) { 154for (const auto & a :cfg .at ("args" )) { 155sc .args .push_back (a .get < std::string > ()); 156 } 157 } 158if (cfg .contains ("env" )&& cfg .at ("env" ).is_object ()) { 159for (const auto & [k ,v ] :cfg .at ("env" ).items ()) { 160sc .env [k ]= v .get < std::string > (); 161 } 162 } 163 164if (sc .command .empty ()) { 165SRV_WRN ("MCP server '%s' has no command, skipping\n" ,name .c_str ()); 166continue ; 167 } 168result .push_back (std::move (sc )); 169 } 170 171return result ; 172} 173 174 175// 176// server_mcp_transport 177// 178 179static constexpr const char * MCP_PROTOCOL_VERSION = "2024-11-05" ; 180 181static std::string rpc_error_message (const json & resp ) { 182if (resp .contains ("error" )) { 183const json & e = resp .at ("error" ); 184if (e .is_object ()) { 185return e .value ("message" ,"unknown error" ); 186 } 187if (e .is_string ()) { 188return e .get < std::string > (); 189 } 190 } 191return "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 result 196static json mcp_result_to_response (const json & result ) { 197 std::string text ; 198if (result .contains ("content" )&& result .at ("content" ).is_array ()) { 199for (const auto & part :result .at ("content" )) { 200if (part .is_object ()&& part .value ("type" ,"" )== "text" ) { 201if (!text .empty ()) { 202text += "\n" ; 203 } 204text += part .value ("text" ,"" ); 205 } 206 } 207 } 208if (result .is_object ()&& result .value ("isError" , false)) { 209return {{"error" ,text .empty () ?"MCP tool returned an error" :text }}; 210 } 211return {{"plain_text_response" ,text }}; 212} 213 214json server_mcp_transport::send_rpc (const json & request ,const std::function < bool ()> & should_stop ) { 215if (!to_server .write (request .dump ())) { 216return {{"error" , {{"code" ,-32603 }, {"message" ,"transport closed" }}}}; 217 } 218 219const bool has_id = request .contains ("id" ); 220const auto deadline = std::chrono::steady_clock::now ()+ std::chrono::milliseconds (timeout_ms ); 221auto stop = [& ]() { 222return (should_stop && should_stop ())|| std::chrono::steady_clock::now () >=deadline ; 223 }; 224 225 std::string frame ; 226while (from_server .read (frame ,stop , false)) { 227json reply ; 228try { 229reply = json::parse (frame ); 230 }catch (...) { 231if (std::chrono::steady_clock::now () >=deadline ) { 232break ; 233 } 234continue ;// skip malformed frame 235 } 236// no id: a notification. mismatched id: a stale reply from a timed-out request (ids are monotonic, never a future one) 237if (!has_id || (reply .contains ("id" )&& reply .at ("id" )== request .at ("id" ))) { 238return reply ; 239 } 240if (std::chrono::steady_clock::now () >=deadline ) { 241break ;// a flood of notifications must not outrun the deadline 242 } 243 } 244 245if (should_stop && should_stop ()) { 246return {{"error" , {{"code" ,-32603 }, {"message" ,"cancelled" }}}}; 247 } 248if (std::chrono::steady_clock::now () >=deadline ) { 249return {{"error" , {{"code" ,-32603 }, {"message" ,"request timed out" }}}}; 250 } 251return {{"error" , {{"code" ,-32603 }, {"message" ,"transport closed" }}}}; 252} 253 254bool server_mcp_transport::ensure_init (const std::function < bool ()> & should_stop ) { 255if (initialized ) { 256return true; 257 } 258 259json 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 }; 269json resp = send_rpc (init_req ,should_stop ); 270if (!resp .contains ("result" )) { 271last_error = "initialize failed: " + rpc_error_message (resp ); 272return false; 273 } 274 275// notifications/initialized: no id, no reply expected 276json notif = {{"jsonrpc" ,"2.0" }, {"method" ,"notifications/initialized" }}; 277to_server .write (notif .dump ()); 278 279initialized = true; 280return 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 ); 285if (!ensure_init (should_stop )) { 286return {}; 287 } 288if (!tools .empty ()) { 289return tools ; 290 } 291 292json req = {{"jsonrpc" ,"2.0" }, {"id" ,next_id ++ }, {"method" ,"tools/list" }}; 293json resp = send_rpc (req ,should_stop ); 294if (!resp .contains ("result" )) { 295last_error = "tools/list failed: " + rpc_error_message (resp ); 296return {}; 297 } 298 299const json & result = resp .at ("result" ); 300if (result .contains ("tools" )&& result .at ("tools" ).is_array ()) { 301for (const auto & t :result .at ("tools" )) { 302server_mcp_tool_def def ; 303def .server_name = name ; 304def .name = t .value ("name" ,"" ); 305def .description = t .value ("description" ,"" ); 306if (t .contains ("inputSchema" )) { 307def .input_schema = t .at ("inputSchema" ); 308 } 309tools .push_back (std::move (def )); 310 } 311 } 312return tools ; 313} 314 315json server_mcp_transport::call_tool (const std::string & tool_name , 316const json & arguments , 317const std::function < bool ()> & should_stop ) { 318 std::lock_guard < std::mutex > lock (rpc_mutex ); 319if (!ensure_init (should_stop )) { 320return {{"error" ,last_error }}; 321 } 322 323json req = { 324 {"jsonrpc" ,"2.0" }, 325 {"id" ,next_id ++ }, 326 {"method" ,"tools/call" }, 327 {"params" , {{"name" ,tool_name }, {"arguments" ,arguments }}}, 328 }; 329json resp = send_rpc (req ,should_stop ); 330if (resp .contains ("error" )) { 331return {{"error" ,rpc_error_message (resp )}}; 332 } 333if (resp .contains ("result" )) { 334return mcp_result_to_response (resp .at ("result" )); 335 } 336return {{"error" ,"invalid response from MCP server" }}; 337} 338 339// 340// server_mcp_stdio 341// 342 343struct server_mcp_stdio::process_handle { 344common_subproc sp ; 345FILE * in = nullptr ;// child stdin 346FILE * out = nullptr ;// child stdout 347FILE * err = nullptr ;// child stderr 348}; 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 page 352static std::wstring windows_utf8_to_wide (const std::string & s ) { 353if (s .empty ()) { 354return std::wstring (); 355 } 356int n = MultiByteToWideChar (CP_UTF8 ,0 ,s .data (), (int )s .size (),NULL ,0 ); 357if (n <=0 ) { 358return std::wstring (); 359 } 360 std::wstring w ((size_t )n ,L'\0' ); 361MultiByteToWideChar (CP_UTF8 ,0 ,s .data (), (int )s .size (),& w [0 ],n ); 362return w ; 363} 364 365static std::string windows_wide_to_utf8 (const wchar_t * s ,int len /* -1 for NUL-terminated */ ) { 366int n = WideCharToMultiByte (CP_UTF8 ,0 ,s ,len ,NULL ,0 ,NULL ,NULL ); 367if (n <=0 ) { 368return std::string (); 369 } 370 std::string out ((size_t )n ,'\0' ); 371WideCharToMultiByte (CP_UTF8 ,0 ,s ,len ,& out [0 ],n ,NULL ,NULL ); 372if (len == -1 && !out .empty ()&& out .back ()== '\0' ) { 373out .pop_back ();// drop the terminator WideCharToMultiByte counts for -1 374 } 375return out ; 376} 377#endif 378 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-8 382 std::wstring wcmd = windows_utf8_to_wide (command ); 383wchar_t buf [MAX_PATH * 4 ]; 384const DWORD cap = (DWORD ) (sizeof (buf ) /sizeof (buf [0 ])); 385 386auto search = [& ](const wchar_t * ext )-> std::string { 387DWORD n = SearchPathW (NULL ,wcmd .c_str (),ext ,cap ,buf ,NULL ); 388return (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 PATH 392if (!found .empty ()) { 393return found ; 394 } 395 396 std::wstring pathext ; 397DWORD need = GetEnvironmentVariableW (L"PATHEXT" ,NULL ,0 ); 398if (need > 0 ) { 399pathext .resize (need ); 400DWORD got = GetEnvironmentVariableW (L"PATHEXT" ,& pathext [0 ],need ); 401pathext .resize (got ); 402 } 403if (pathext .empty ()) { 404pathext = L".COM;.EXE;.BAT;.CMD" ; 405 } 406for (size_t start = 0 ;start <=pathext .size ();) { 407size_t sep = pathext .find (L';' ,start ); 408 std::wstring ext = pathext .substr (start ,sep == std::wstring::npos ? std::wstring::npos :sep - start ); 409if (!ext .empty ()) { 410found = search (ext .c_str ()); 411if (!found .empty ()) { 412return found ; 413 } 414 } 415if (sep == std::wstring::npos ) { 416break ; 417 } 418start = sep + 1 ; 419 } 420return command ;// give up and let subprocess.h report the spawn error 421#else 422return command ; 423#endif // _WIN32 424} 425 426static std::vector < std::string > mcp_parent_env () { 427 std::vector < std::string > env ; 428#if defined(_WIN32 ) 429LPWCH block = GetEnvironmentStringsW (); 430if (block ) { 431for (LPWCH e = block ;* e ;e += wcslen (e )+ 1 ) { 432env .emplace_back (windows_wide_to_utf8 (e ,-1 )); 433 } 434FreeEnvironmentStringsW (block ); 435 } 436#else 437if (environ ) { 438for (char ** e = environ ;* e ;++ e ) { 439env .emplace_back (* e ); 440 } 441 } 442#endif 443return env ; 444} 445 446// parent env with the config overrides applied, in "KEY=VALUE" form 447static std::vector < std::string > mcp_build_env (const std::map < std::string , std::string > & overrides ) { 448 std::vector < std::string > env ; 449for (auto & e :mcp_parent_env ()) { 450size_t eq = e .find ('=' ); 451 std::string key = eq == std::string::npos ?e :e .substr (0 ,eq ); 452if (overrides .find (key )== overrides .end ()) { 453env .push_back (e ); 454 } 455 } 456for (auto & [k ,v ] :overrides ) { 457env .push_back (k + "=" + v ); 458 } 459return env ; 460} 461 462server_mcp_stdio::server_mcp_stdio (const server_mcp_server_config & config ) :config (config ) { 463name = config .name ; 464timeout_ms = config .timeout_ms ; 465// bound the reply queue: send_rpc only drains during a call, so unsolicited notifications would otherwise grow it without limit 466from_server .max_size = 65536 ; 467} 468 469server_mcp_stdio::~server_mcp_stdio () { 470join_pumps (); 471} 472 473bool server_mcp_stdio::start () { 474 std::vector < std::string > argv_s ; 475argv_s .push_back (mcp_resolve_command (config .command )); 476argv_s .insert (argv_s .end (),config .args .begin (),config .args .end ()); 477 478int options = subprocess_option_no_window |subprocess_option_search_user_path ; 479 std::vector < std::string > envp_s ; 480if (config .env .empty ()) { 481options |=subprocess_option_inherit_environment ; 482 }else { 483envp_s = mcp_build_env (config .env ); 484 } 485 486auto handle = std::make_unique < process_handle > (); 487bool ok = handle -> sp .create (argv_s ,options ,envp_s ,config .cwd .empty () ?nullptr :config .cwd .c_str ()); 488if (!ok ) { 489SRV_WRN ("MCP '%s': failed to spawn '%s'\n" ,config .name .c_str (),config .command .c_str ()); 490return false; 491 } 492handle -> in = handle -> sp .stdin_file (); 493handle -> out = handle -> sp .stdout_file (); 494handle -> err = handle -> sp .stderr_file (); 495 496proc = std::move (handle ); 497running .store (true); 498reader = std::thread ([this ] {reader_loop (); }); 499writer = std::thread ([this ] {writer_loop (); }); 500errlog = std::thread ([this ] {errlog_loop (); }); 501return true; 502} 503 504void server_mcp_stdio::close () { 505join_pumps (); 506} 507 508bool server_mcp_stdio::is_alive ()const { 509return 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 callers 516out = last_error ; 517 } 518 std::lock_guard < std::mutex > lk (err_mu ); 519if (!err_tail .empty ()) { 520if (!out .empty ()) { 521out += "; " ; 522 } 523out += "last stderr: " + err_tail ; 524 } 525return out ; 526} 527 528void server_mcp_stdio::reader_loop () { 529mcp_pump_ndjson (proc -> out ,running , [this ](std::string && line ) { 530return from_server .write (std::move (line ));// false => consumer gone, stop 531 }); 532running .store (false); 533to_server .close_write ();// stop the writer 534from_server .close_write ();// EOF to any waiting caller 535} 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 ) { 539if (!f ) { 540return false; 541 } 542size_t total = 0 ; 543#if defined(_WIN32 ) 544HANDLE h = (HANDLE )_get_osfhandle (_fileno (f )); 545DWORD nowait = PIPE_NOWAIT ; 546SetNamedPipeHandleState (h ,& nowait ,NULL ,NULL ); 547while (total < data .size ()&& running .load ()) { 548DWORD written = 0 ; 549BOOL ok = WriteFile (h ,data .data ()+ total , (DWORD ) (data .size ()- total ),& written ,NULL ); 550if (ok && written > 0 ) { 551total += written ; 552continue ; 553 } 554if (!ok ) { 555DWORD err = GetLastError (); 556if (err != ERROR_NO_DATA && err != ERROR_PIPE_BUSY ) { 557return 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#else 565int fd = fileno (f ); 566int fl = fcntl (fd ,F_GETFL ,0 ); 567if (fl >=0 ) { 568fcntl (fd ,F_SETFL ,fl |O_NONBLOCK ); 569 } 570while (total < data .size ()&& running .load ()) { 571ssize_t n = write (fd ,data .data ()+ total ,data .size ()- total ); 572if (n > 0 ) { 573total += (size_t )n ; 574continue ; 575 } 576if (n == 0 ) { 577return false; 578 } 579if (errno == EINTR ) { 580continue ; 581 } 582if (errno != EAGAIN && errno != EWOULDBLOCK ) { 583return false; 584 } 585struct pollfd pfd ; 586pfd .fd = fd ; 587pfd .events = POLLOUT ; 588pfd .revents = 0 ; 589int pr = poll (& pfd ,1 ,50 ); 590if (pr < 0 ) { 591if (errno == EINTR ) { 592continue ; 593 } 594return false; 595 } 596if (pfd .revents & (POLLERR |POLLNVAL |POLLHUP )) { 597return false; 598 } 599 } 600#endif 601return total == data .size (); 602} 603 604void server_mcp_stdio::writer_loop () { 605auto should_stop = [this ] {return !running .load (); }; 606 std::string msg ; 607while (to_server .read (msg ,should_stop )) { 608msg .push_back ('\n' ); 609if (!mcp_write_all (proc -> in ,msg ,running )) { 610break ;// child gone or shutting down 611 } 612 } 613running .store (false); 614to_server .close_read ();// fail fast on any further send_rpc write 615from_server .close_write ();// wake any caller waiting for a reply 616} 617 618void server_mcp_stdio::errlog_loop () { 619static 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 dies 622mcp_pump_ndjson (proc -> err ,running , [this ](std::string && line ) { 623SRV_DBG ("MCP '%s' stderr: %s\n" ,name .c_str (),line .c_str ()); 624 std::lock_guard < std::mutex > lk (err_mu ); 625err_tail += line ; 626err_tail += '\n' ; 627if (err_tail .size ()> ERR_TAIL_MAX ) { 628err_tail .erase (0 ,err_tail .size ()- ERR_TAIL_MAX ); 629 } 630return true; 631 }); 632} 633 634void server_mcp_stdio::join_pumps () { 635if (!proc ) { 636return ; 637 } 638running .store (false); 639to_server .close_write ();// wake the writer if it waits for a message 640from_server .close_write ();// wake any caller waiting for a reply 641 642proc -> sp .terminate ();// child death unblocks the blocked fread/fwrite 643 644if (writer .joinable ())writer .join (); 645if (reader .joinable ())reader .join (); 646if (errlog .joinable ())errlog .join (); 647 648proc -> sp .join ();// reap the child: never waiting would leave the pid a zombie for the process lifetime 649proc .reset (); 650} 651 652 653// 654// server_mcp 655// 656 657static constexpr int MCP_COOLDOWN_SECONDS = 5 ; 658static constexpr int MCP_WARMUP_TIMEOUT_SECONDS = 10 ;// cap per-server tool discovery at startup 659 660server_mcp::~server_mcp () { 661shutdown (); 662 663 std::vector < std::shared_ptr < server_mcp_transport >> to_close ; 664 { 665 std::lock_guard < std::mutex > lock (mutex ); 666for (auto & [name ,t ] :transports ) { 667to_close .push_back (std::move (t )); 668 } 669transports .clear (); 670 } 671for (auto & t :to_close ) { 672t -> close (); 673 } 674} 675 676std::shared_ptr < server_mcp_transport > server_mcp::create_transport (const server_mcp_server_config & cfg ) { 677return std::make_shared < server_mcp_stdio > (cfg ); 678} 679 680void server_mcp::shutdown () { 681stopping .store (true); 682} 683 684const server_mcp_server_config * server_mcp::find_config (const std::string & name )const { 685for (const auto & c :configs ) { 686if (c .name == name ) { 687return & c ; 688 } 689 } 690return nullptr ; 691} 692 693void server_mcp::start (const common_params & params ) { 694auto append = [this ](const std::string & json_str ) { 695try { 696auto parsed = server_mcp_server_config::parse_from_json (json_str ); 697if (parsed .empty ()) { 698SRV_WRN ("%s" ,"MCP config: no servers found in JSON\n" ); 699 } 700for (auto & p :parsed ) { 701// names must be unique across both config sources: get_or_create / find_config key on the name 702if (find_config (p .name )) { 703SRV_WRN ("MCP config: duplicate server name '%s', skipping\n" ,p .name .c_str ()); 704continue ; 705 } 706configs .push_back (std::move (p )); 707 } 708 }catch (const std::exception & e ) { 709throw std::runtime_error (std::string ("failed to parse MCP config JSON: " )+ e .what ()); 710 } 711 }; 712if (!params .mcp_servers_config .empty ()) { 713 std::ifstream f = fs_open_ifstream (params .mcp_servers_config , std::ios::in ); 714if (!f ) { 715throw std::runtime_error ("failed to open MCP config file: " + params .mcp_servers_config ); 716 } 717 std::stringstream ss ; 718ss <<f .rdbuf (); 719append (ss .str ()); 720 } 721if (!params .mcp_servers_json .empty ()) { 722append (params .mcp_servers_json ); 723 } 724 725if (configs .empty ()) { 726return ; 727 } 728 729 std::vector < server_mcp_tool_def > discovered ; 730for (const auto & cfg :configs ) { 731auto t = create_transport (cfg ); 732if (!t -> start ()) { 733SRV_WRN ("MCP warmup: failed to spawn '%s': %s\n" ,cfg .name .c_str (),t -> diagnostics ().c_str ()); 734continue ; 735 } 736// bound warmup per server so an unresponsive one can't stall startup for the full per-call timeout 737const auto deadline = std::chrono::steady_clock::now ()+ std::chrono::seconds (MCP_WARMUP_TIMEOUT_SECONDS ); 738auto should_stop = [this ,deadline ]() { 739return stopping .load ()|| std::chrono::steady_clock::now () >=deadline ; 740 }; 741auto tools = t -> list_tools (should_stop ); 742SRV_INF ("MCP warmup: '%s' discovered %zu tools\n" ,cfg .name .c_str (),tools .size ()); 743discovered .insert (discovered .end (),tools .begin (),tools .end ()); 744t -> close (); 745 } 746 747 std::lock_guard < std::mutex > lock (mutex ); 748registry .swap (discovered ); 749} 750 751std::vector < server_mcp_tool_def > server_mcp::list_tools ()const { 752 std::lock_guard < std::mutex > lock (mutex ); 753return registry ; 754} 755 756json server_mcp::call_tool (const std::string & server_name , 757const std::string & tool_name , 758const json & arguments , 759const std::function < bool ()> & should_stop ) { 760auto transport = get_or_create (server_name ); 761if (!transport ) { 762return {{"error" ,"MCP server unavailable: " + server_name }}; 763 } 764 765auto stop = [this ,& should_stop ]() { 766return stopping .load ()|| (should_stop && should_stop ()); 767 }; 768return 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 unlock 773 std::shared_ptr < server_mcp_transport > result ; 774 775 { 776 std::lock_guard < std::mutex > lock (mutex ); 777if (stopping .load ()) { 778return nullptr ; 779 } 780 781auto now = std::chrono::steady_clock::now (); 782auto dead_it = dead_servers .find (name ); 783if (dead_it != dead_servers .end ()) { 784if (now < dead_it -> second ) { 785return nullptr ; 786 } 787dead_servers .erase (dead_it ); 788 } 789 790auto it = transports .find (name ); 791if (it != transports .end ()) { 792if (it -> second -> is_alive ()) { 793return it -> second ; 794 } 795SRV_WRN ("MCP '%s' is no longer alive: %s\n" ,name .c_str (),it -> second -> diagnostics ().c_str ()); 796to_close .push_back (std::move (it -> second )); 797transports .erase (it ); 798 } 799 800const server_mcp_server_config * cfg = find_config (name ); 801if (cfg ) { 802auto fresh = create_transport (* cfg ); 803if (fresh -> start ()&& fresh -> is_alive ()) { 804transports [name ]= fresh ; 805result = fresh ; 806 }else { 807SRV_WRN ("MCP '%s': failed to start: %s\n" ,name .c_str (),fresh -> diagnostics ().c_str ()); 808to_close .push_back (std::move (fresh )); 809dead_servers [name ]= now + std::chrono::seconds (MCP_COOLDOWN_SECONDS ); 810 } 811 } 812 } 813 814for (auto & t :to_close ) { 815t -> close ();// blocking call, no leaks 816 } 817 818return result ; 819} 820