#include "mcp.h" #include "config.h" /* LLSH_VERSION */ #include #include #include #include #include #include #include #include #include #include #include #define MCP_MAX_MESSAGE_BYTES (4u * 1024u * 1024u) typedef struct { xbuf *buf; size_t cap; } mcp_write_ctx; static size_t mcp_curl_write(char *data, size_t size, size_t nmemb, void *ud) { mcp_write_ctx *ctx = (mcp_write_ctx *)ud; if (size && nmemb > SIZE_MAX / size) return 0; size_t n = size * nmemb; if (n > ctx->cap - ctx->buf->len) return 0; xbuf_putn(ctx->buf, data, n); return n; } /* ------------------------------------------------------------------ */ /* config parsing */ /* ------------------------------------------------------------------ */ static char *jstr(const cJSON *v, const char *key) { const cJSON *x = cJSON_GetObjectItemCaseSensitive(v, key); if (cJSON_IsString(x) && x->valuestring) return xstrdup(x->valuestring); return NULL; } static void jargs(const cJSON *v, mcp_server_config *out) { const cJSON *a = cJSON_GetObjectItemCaseSensitive(v, "args"); if (!cJSON_IsArray(a)) return; size_t n = (size_t)cJSON_GetArraySize(a); char **arr = (char* * ) xmalloc((n ? n : 1) * sizeof(*arr)); size_t used = 0; const cJSON *x = NULL; cJSON_ArrayForEach(x, a) if (cJSON_IsString(x) && x->valuestring) arr[used++] = xstrdup(x->valuestring); out->args = arr; out->arg_count = used; } static void jkv(const cJSON *v, const char *key, mcp_kv **out, size_t *n) { const cJSON *o = cJSON_GetObjectItemCaseSensitive(v, key); if (!cJSON_IsObject(o)) return; size_t nitems = (size_t)cJSON_GetArraySize(o); mcp_kv *arr = (mcp_kv *) xmalloc((nitems ? nitems : 1) * sizeof(*arr)); size_t used = 0; const cJSON *child = o->child; while (child) { if (child->string && cJSON_IsString(child) && child->valuestring) { arr[used].key = xstrdup(child->string); arr[used].val = xstrdup(child->valuestring); used++; } child = child->next; } *out = arr; *n = used; } int mcp_server_config_from_json(const char *json, mcp_server_config *out) { memset(out, 0, sizeof(*out)); cJSON *v = json ? cJSON_Parse(json) : NULL; if (!v || !cJSON_IsObject(v)) { if (v) cJSON_Delete(v); return 0; } out->command = jstr(v, "command"); out->url = jstr(v, "url"); if (!out->command && !out->url) { free(out->command); free(out->url); cJSON_Delete(v); return 0; } jargs(v, out); jkv(v, "env", &out->env, &out->env_count); jkv(v, "headers", &out->headers, &out->header_count); cJSON_Delete(v); return 1; } void mcp_server_config_free(mcp_server_config *c) { if (!c) return; free(c->command); free(c->url); for (size_t i = 0; i < c->arg_count; i++) free(c->args[i]); free(c->args); for (size_t i = 0; i < c->env_count; i++) { free(c->env[i].key); free(c->env[i].val); } free(c->env); for (size_t i = 0; i < c->header_count; i++) { free(c->headers[i].key); free(c->headers[i].val); } free(c->headers); memset(c, 0, sizeof(*c)); } /* ------------------------------------------------------------------ */ /* client */ /* ------------------------------------------------------------------ */ enum { T_STDIO, T_HTTP, T_MEMORY }; struct mcp_client { int kind; unsigned long long next_id; /* stdio */ pid_t pid; int in_fd; int out_fd; char *rb; /* carry-over bytes between line reads (partial line) */ size_t rb_len; /* http */ char *url; mcp_kv *headers; size_t header_count; /* memory */ mcp_memory *mem; }; long mcp_timeout_secs(void) { const char *e = getenv("LLSH_MCP_TIMEOUT"); if (e && e[0]) { errno = 0; char *end = NULL; long v = strtol(e, &end, 10); if (errno == 0 && end != e && *end == '\0' && v > 0) { if (v > 2147483L) return 2147483L; /* poll timeout is milliseconds in an int */ return v; } } return MCP_TIMEOUT_SECS; } void mcp_memory_init(mcp_memory *m) { sl_init(&m->responses); sl_init(&m->sent); } mcp_client *mcp_connect_stdio(const mcp_server_config *cfg, char **err) { if (err) *err = NULL; if (!cfg->command) { if (err) *err = xstrdup("stdio server has no `command`"); return NULL; } int in_pipe[2] = {-1, -1}, out_pipe[2] = {-1, -1}; if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) { if (err) *err = xstrdup("i/o: pipe failed"); if (in_pipe[0] >= 0) close(in_pipe[0]); if (in_pipe[1] >= 0) close(in_pipe[1]); if (out_pipe[0] >= 0) close(out_pipe[0]); if (out_pipe[1] >= 0) close(out_pipe[1]); return NULL; } /* Heap-allocate argv (a VLA sized by config data can overflow the stack * for a large `args` array). Allocated pre-fork so the child inherits it. */ char **argv = (char* * ) xmalloc((cfg->arg_count + 2) * sizeof *argv); argv[0] = cfg->command; for (size_t i = 0; i < cfg->arg_count; i++) argv[i + 1] = cfg->args[i]; argv[cfg->arg_count + 1] = NULL; pid_t pid = fork(); if (pid < 0) { free(argv); if (err) *err = xstrdup("i/o: fork failed"); close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); close(out_pipe[1]); return NULL; } if (pid == 0) { dup2(in_pipe[0], STDIN_FILENO); dup2(out_pipe[1], STDOUT_FILENO); close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); close(out_pipe[1]); int dn = open("/dev/null", O_WRONLY); if (dn >= 0) { dup2(dn, STDERR_FILENO); close(dn); } for (size_t i = 0; i < cfg->env_count; i++) setenv(cfg->env[i].key, cfg->env[i].val, 1); /* Own process group so freeing the client can kill the whole tree. */ setpgid(0, 0); execvp(cfg->command, argv); _exit(127); } free(argv); close(in_pipe[0]); close(out_pipe[1]); mcp_client *c = (mcp_client *) xmalloc(sizeof(*c)); memset(c, 0, sizeof(*c)); c->kind = T_STDIO; c->pid = pid; c->in_fd = in_pipe[1]; c->out_fd = out_pipe[0]; return c; } mcp_client *mcp_connect_http(const mcp_server_config *cfg, char **err) { if (err) *err = NULL; if (!cfg->url) { if (err) *err = xstrdup("HTTP server has no `url`"); return NULL; } mcp_client *c = (mcp_client *) xmalloc(sizeof(*c)); memset(c, 0, sizeof(*c)); c->kind = T_HTTP; c->url = xstrdup(cfg->url); if (cfg->header_count) { c->headers = (mcp_kv *) xmalloc(cfg->header_count * sizeof(*c->headers)); memcpy(c->headers, cfg->headers, cfg->header_count * sizeof(*c->headers)); for (size_t i = 0; i < cfg->header_count; i++) { c->headers[i].key = xstrdup(cfg->headers[i].key); c->headers[i].val = xstrdup(cfg->headers[i].val); } c->header_count = cfg->header_count; } return c; } /* Test-only: wrap already-open stdio fds (the client closes the two it is * given on free; no child process is reaped). */ mcp_client *mcp_connect_fds(int in_fd, int out_fd, char **err) { if (err) *err = NULL; mcp_client *c = (mcp_client *) xmalloc(sizeof(*c)); memset(c, 0, sizeof(*c)); c->kind = T_STDIO; c->in_fd = in_fd; c->out_fd = out_fd; return c; } mcp_client *mcp_connect_memory(mcp_memory *mem, char **err) { if (err) *err = NULL; mcp_client *c = (mcp_client *) xmalloc(sizeof(*c)); memset(c, 0, sizeof(*c)); c->kind = T_MEMORY; c->mem = mem; return c; } void mcp_client_free(mcp_client *c) { if (!c) return; if (c->kind == T_STDIO) { if (c->in_fd >= 0) close(c->in_fd); if (c->out_fd >= 0) close(c->out_fd); free(c->rb); if (c->pid > 0) { int st = 0; pid_t r = waitpid(c->pid, &st, WNOHANG); if (r == 0) { /* Kill the whole process group so grandchildren don't * survive. */ kill(-c->pid, SIGTERM); for (int i = 0; i < 50 && (r = waitpid(c->pid, &st, WNOHANG)) == 0; i++) usleep(10000); if (r == 0) { kill(-c->pid, SIGKILL); waitpid(c->pid, &st, 0); } } } } else if (c->kind == T_HTTP) { free(c->url); for (size_t i = 0; i < c->header_count; i++) { free(c->headers[i].key); free(c->headers[i].val); } free(c->headers); } /* T_MEMORY: the mcp_memory belongs to the caller; we only borrowed it. */ free(c); } /* ------------------------------------------------------------------ */ /* transports */ /* ------------------------------------------------------------------ */ static int write_all_fd(int fd, const char *data, size_t len) { size_t off = 0; while (off < len) { ssize_t w = write(fd, data + off, len - off); if (w < 0) { if (errno == EINTR) continue; return -1; } off += (size_t)w; } return 0; } /* One line (no trailing newline) or NULL on EOF/timeout; *err on failure. * Reads in chunks and carries any partial line in c->rb across calls, so a * burst of lines in one read() is handled without per-byte syscalls. */ static char *read_line_fd(mcp_client *c, const char *method, char **err) { long tmo = mcp_timeout_secs(); char *out = NULL; for (;;) { for (size_t i = 0; i < c->rb_len; i++) { if (c->rb[i] == '\n') { out = xstrndup(c->rb, i); size_t rest = c->rb_len - i - 1; memmove(c->rb, c->rb + i + 1, rest); c->rb_len = rest; goto done; } } struct pollfd p = { .fd = c->out_fd, .events = POLLIN }; int pr = poll(&p, 1, (int)(tmo * 1000)); if (pr < 0) { if (errno == EINTR) continue; if (err) *err = xstrdup("i/o: read failed"); return NULL; } if (pr == 0) { if (c->rb_len == 0) { if (err) *err = xstrfmt("request `%s` timed out after %lds", method, tmo); } else { out = xstrndup(c->rb, c->rb_len); c->rb_len = 0; } goto done; } char tmp[64 * 1024]; ssize_t r = read(c->out_fd, tmp, sizeof tmp); if (r < 0) { if (errno == EINTR) continue; break; } if (r == 0) break; if ((size_t)r > MCP_MAX_MESSAGE_BYTES - c->rb_len) { free(c->rb); c->rb = NULL; c->rb_len = 0; if (err) *err = xstrdup("MCP stdio response line exceeds 4 MiB limit"); return NULL; } char *nb = (char *) xrealloc(c->rb, c->rb_len + (size_t)r); memcpy(nb + c->rb_len, tmp, (size_t)r); c->rb = nb; c->rb_len += (size_t)r; } if (c->rb_len > 0) { out = xstrndup(c->rb, c->rb_len); c->rb_len = 0; } done: if (out) { size_t n = strlen(out); if (n > 0 && out[n - 1] == '\r') out[n - 1] = '\0'; } if (!out || out[0] == '\0') { free(out); /* Keep a more specific error already set (e.g. timeout). */ if (err && !*err) *err = xstrdup("server closed the connection before answering"); return NULL; } return out; } /* Test-only: one buffered line read with carry semantics (read_line_fd). */ char *mcp_read_line(mcp_client *c, const char *method, char **err) { return read_line_fd(c, method, err); } static int rt_stdio(mcp_client *c, const char *line, unsigned long long id, const char *method, cJSON **resp, char **err) { if (write_all_fd(c->in_fd, line, strlen(line)) != 0 || write_all_fd(c->in_fd, "\n", 1) != 0) { if (err) *err = xstrdup("i/o: write failed"); return -1; } for (;;) { char *l = read_line_fd(c, method, err); if (!l) return -1; size_t tlen; const char *t = xtrim_span(l, strlen(l), &tlen); if (tlen == 0) { free(l); continue; } cJSON *v = cJSON_Parse(t); free(l); if (!v) { if (err) *err = xstrdup("json: invalid message from server"); return -1; } cJSON *idv = cJSON_GetObjectItemCaseSensitive(v, "id"); if (cJSON_IsNumber(idv) && (unsigned long long)idv->valuedouble == id && (cJSON_HasObjectItem(v, "result") || cJSON_HasObjectItem(v, "error"))) { *resp = v; return 0; } cJSON_Delete(v); /* notification or unrelated message: skip */ } } static int rt_http(mcp_client *c, const char *json, unsigned long long id, cJSON **resp, char **err) { CURL *e = curl_easy_init(); if (!e) { if (err) *err = xstrdup("http: could not initialize curl"); return -1; } xbuf body; xbuf_init(&body); struct curl_slist *hl = curl_slist_append(NULL, "Content-Type: application/json"); for (size_t i = 0; i < c->header_count; i++) { xbuf hb; xbuf_init(&hb); xbuf_fmt(&hb, "%s: %s", c->headers[i].key, c->headers[i].val); hl = curl_slist_append(hl, hb.data); xbuf_free(&hb); } curl_easy_setopt(e, CURLOPT_URL, c->url); curl_easy_setopt(e, CURLOPT_POST, 1L); curl_easy_setopt(e, CURLOPT_POSTFIELDS, json); curl_easy_setopt(e, CURLOPT_TIMEOUT, mcp_timeout_secs()); curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L); const char *ca = llsh_ca_bundle(); if (ca) curl_easy_setopt(e, CURLOPT_CAINFO, ca); curl_easy_setopt(e, CURLOPT_HTTPHEADER, hl); mcp_write_ctx write_ctx = {&body, MCP_MAX_MESSAGE_BYTES}; curl_easy_setopt(e, CURLOPT_WRITEFUNCTION, mcp_curl_write); curl_easy_setopt(e, CURLOPT_WRITEDATA, &write_ctx); CURLcode rc = curl_easy_perform(e); if (rc != CURLE_OK) { char *e2 = xstrfmt("http: %s", curl_easy_strerror(rc)); free(body.data); curl_slist_free_all(hl); curl_easy_cleanup(e); if (err) *err = e2; return -1; } long code = 0; curl_easy_getinfo(e, CURLINFO_RESPONSE_CODE, &code); if (code >= 400) { char *e2 = xstrfmt("HTTP %ld", code); free(body.data); curl_slist_free_all(hl); curl_easy_cleanup(e); if (err) *err = e2; return -1; } char *ct = NULL; curl_easy_getinfo(e, CURLINFO_CONTENT_TYPE, &ct); char *body_text = xbuf_steal(&body); curl_slist_free_all(hl); curl_easy_cleanup(e); char *local_err = NULL; char *json_text = mcp_extract_json(ct ? ct : "", body_text, &local_err); free(body_text); if (!json_text) { if (err) *err = local_err; else free(local_err); return -1; } cJSON *v = cJSON_Parse(json_text); free(json_text); if (!v) { if (err) *err = xstrdup("json: invalid HTTP body"); return -1; } cJSON *idv = cJSON_GetObjectItemCaseSensitive(v, "id"); if (!cJSON_IsNumber(idv) || (unsigned long long)idv->valuedouble != id) { cJSON_Delete(v); if (err) *err = xstrdup("HTTP response id does not match the request"); return -1; } *resp = v; return 0; } static int rt_memory(mcp_client *c, const char *line, unsigned long long id, cJSON **resp, char **err) { sl_push_copy(&c->mem->sent, line); for (;;) { char *queued = sl_pop_front(&c->mem->responses); if (!queued) { if (err) *err = xstrdup("no queued response (test transport exhausted)"); return -1; } cJSON *v = cJSON_Parse(queued); free(queued); if (!v) continue; cJSON *idv = cJSON_GetObjectItemCaseSensitive(v, "id"); if (cJSON_IsNumber(idv) && (unsigned long long)idv->valuedouble == id && (cJSON_HasObjectItem(v, "result") || cJSON_HasObjectItem(v, "error"))) { *resp = v; return 0; } cJSON_Delete(v); } } static int roundtrip(mcp_client *c, const char *line, unsigned long long id, const char *method, cJSON **resp, char **err) { switch (c->kind) { case T_STDIO: return rt_stdio(c, line, id, method, resp, err); case T_HTTP: return rt_http(c, line, id, resp, err); default: return rt_memory(c, line, id, resp, err); } } static int notify_rt(mcp_client *c, const char *line, char **err) { switch (c->kind) { case T_STDIO: { int rc = write_all_fd(c->in_fd, line, strlen(line)); if (rc == 0) rc = write_all_fd(c->in_fd, "\n", 1); if (rc != 0 && err) *err = xstrdup("i/o: write failed"); return rc; } case T_HTTP: { CURL *e = curl_easy_init(); if (!e) { if (err) *err = xstrdup("http: could not initialize curl"); return -1; } struct curl_slist *hl = curl_slist_append(NULL, "Content-Type: application/json"); for (size_t i = 0; i < c->header_count; i++) { xbuf hb; xbuf_init(&hb); xbuf_fmt(&hb, "%s: %s", c->headers[i].key, c->headers[i].val); hl = curl_slist_append(hl, hb.data); xbuf_free(&hb); } curl_easy_setopt(e, CURLOPT_URL, c->url); curl_easy_setopt(e, CURLOPT_POST, 1L); curl_easy_setopt(e, CURLOPT_POSTFIELDS, line); curl_easy_setopt(e, CURLOPT_TIMEOUT, mcp_timeout_secs()); curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L); const char *ca = llsh_ca_bundle(); if (ca) curl_easy_setopt(e, CURLOPT_CAINFO, ca); curl_easy_setopt(e, CURLOPT_HTTPHEADER, hl); xbuf sink; xbuf_init(&sink); mcp_write_ctx write_ctx = {&sink, MCP_MAX_MESSAGE_BYTES}; curl_easy_setopt(e, CURLOPT_WRITEFUNCTION, mcp_curl_write); curl_easy_setopt(e, CURLOPT_WRITEDATA, &write_ctx); CURLcode rc = curl_easy_perform(e); free(sink.data); curl_slist_free_all(hl); curl_easy_cleanup(e); if (rc != CURLE_OK) { if (err) *err = xstrfmt("http: %s", curl_easy_strerror(rc)); return -1; } return 0; } default: sl_push_copy(&c->mem->sent, line); return 0; } } /* ------------------------------------------------------------------ */ /* JSON-RPC layer */ /* ------------------------------------------------------------------ */ char *mcp_request(mcp_client *c, const char *method, const char *params_json, char **err) { if (err) *err = NULL; unsigned long long id = ++c->next_id; cJSON *req = cJSON_CreateObject(); cJSON_AddStringToObject(req, "jsonrpc", "2.0"); cJSON_AddNumberToObject(req, "id", (double)id); cJSON_AddStringToObject(req, "method", method); cJSON *params = params_json ? cJSON_Parse(params_json) : cJSON_CreateObject(); if (!params) params = cJSON_CreateObject(); cJSON_AddItemToObject(req, "params", params); char *line = cJSON_PrintUnformatted(req); cJSON_Delete(req); cJSON *resp = NULL; int rc = roundtrip(c, line, id, method, &resp, err); free(line); if (rc != 0) return NULL; cJSON *e = cJSON_GetObjectItemCaseSensitive(resp, "error"); if (e && cJSON_IsObject(e)) { cJSON *code = cJSON_GetObjectItemCaseSensitive(e, "code"); char *code_text = code ? cJSON_PrintUnformatted(code) : xstrdup("null"); cJSON *msg = cJSON_GetObjectItemCaseSensitive(e, "message"); char *out = xstrfmt("server error %s: %s", code_text, cJSON_IsString(msg) && msg->valuestring ? msg->valuestring : "unknown error"); free(code_text); cJSON_Delete(resp); if (err) *err = out; else free(out); return NULL; } cJSON *result = cJSON_GetObjectItemCaseSensitive(resp, "result"); char *out = result ? cJSON_PrintUnformatted(result) : xstrdup("null"); cJSON_Delete(resp); return out; } int mcp_notify(mcp_client *c, const char *method, const char *params_json, char **err) { if (err) *err = NULL; cJSON *req = cJSON_CreateObject(); cJSON_AddStringToObject(req, "jsonrpc", "2.0"); cJSON_AddStringToObject(req, "method", method); cJSON *params = params_json ? cJSON_Parse(params_json) : cJSON_CreateObject(); if (!params) params = cJSON_CreateObject(); cJSON_AddItemToObject(req, "params", params); char *line = cJSON_PrintUnformatted(req); cJSON_Delete(req); int rc = notify_rt(c, line, err); free(line); return rc == 0; } int mcp_initialize(mcp_client *c, char **err) { char *params = xstrfmt( "{\"protocolVersion\":\"%s\",\"capabilities\":{},\"clientInfo\":" "{\"name\":\"llsh\",\"version\":\"%s\"}}", MCP_PROTOCOL_VERSION, LLSH_VERSION); char *res = mcp_request(c, "initialize", params, err); free(params); if (!res) return 0; free(res); char *err2 = NULL; int ok = mcp_notify(c, "notifications/initialized", "{}", &err2); if (!ok) { if (err && !*err) *err = err2; else free(err2); return 0; } return 1; } int mcp_list_tools(mcp_client *c, mcp_tool **out, size_t *n, char **err) { char *res = mcp_request(c, "tools/list", "{}", err); if (!res) return 0; cJSON *root = cJSON_Parse(res); free(res); if (!root) { if (err) *err = xstrdup("json: invalid tools/list result"); return 0; } cJSON *tools = cJSON_GetObjectItemCaseSensitive(root, "tools"); size_t count = cJSON_IsArray(tools) ? (size_t)cJSON_GetArraySize(tools) : 0; mcp_tool *arr = (mcp_tool *) xmalloc((count ? count : 1) * sizeof(*arr)); size_t used = 0; if (cJSON_IsArray(tools)) { const cJSON *t; cJSON_ArrayForEach(t, tools) { cJSON *name = cJSON_GetObjectItemCaseSensitive(t, "name"); if (!cJSON_IsString(name) || !name->valuestring) continue; cJSON *desc = cJSON_GetObjectItemCaseSensitive(t, "description"); cJSON *schema = cJSON_GetObjectItemCaseSensitive(t, "inputSchema"); mcp_tool *slot = &arr[used++]; memset(slot, 0, sizeof *slot); slot->name = xstrdup(name->valuestring); slot->description = cJSON_IsString(desc) && desc->valuestring ? xstrdup(desc->valuestring) : xstrdup(""); slot->input_schema = schema ? cJSON_PrintUnformatted(schema) : xstrdup("{\"type\":\"object\"}"); if (!slot->input_schema) slot->input_schema = xstrdup("{\"type\":\"object\"}"); slot->client = c; } } cJSON_Delete(root); *out = arr; *n = used; return 1; } void mcp_tools_free(mcp_tool *tools, size_t n) { if (!tools) return; for (size_t i = 0; i < n; i++) { free(tools[i].name); free(tools[i].qualified_name); free(tools[i].description); free(tools[i].input_schema); } free(tools); } char *mcp_call_tool(mcp_client *c, const char *name, const char *args_json, char **err) { cJSON *name_j = cJSON_CreateString(name ? name : ""); char *name_s = cJSON_PrintUnformatted(name_j); cJSON_Delete(name_j); char *params = xstrfmt("{\"name\":%s,\"arguments\":%s}", name_s ? name_s : "\"\"", args_json ? args_json : "{}"); free(name_s); char *res = mcp_request(c, "tools/call", params, err); free(params); if (!res) return NULL; cJSON *root = cJSON_Parse(res); free(res); if (!root) { if (err) *err = xstrdup("json: invalid tools/call result"); return NULL; } cJSON *iserr = cJSON_GetObjectItemCaseSensitive(root, "isError"); bool err_flag = cJSON_IsTrue(iserr); cJSON *content = cJSON_GetObjectItemCaseSensitive(root, "content"); xbuf text; xbuf_init(&text); if (cJSON_IsArray(content)) { const cJSON *item; cJSON_ArrayForEach(item, content) { cJSON *t = cJSON_GetObjectItemCaseSensitive(item, "text"); if (cJSON_IsString(t) && t->valuestring) { if (text.len) xbuf_push(&text, '\n'); xbuf_put(&text, t->valuestring); } else { cJSON *type = cJSON_GetObjectItemCaseSensitive(item, "type"); cJSON *data = cJSON_GetObjectItemCaseSensitive(item, "data"); if ((cJSON_IsString(type) && type->valuestring && strcmp(type->valuestring, "image") == 0) || data != NULL) { if (text.len) xbuf_push(&text, '\n'); xbuf_put(&text, "[binary content]"); } } } } if (text.len == 0) xbuf_put(&text, "(no content)"); char *out = xbuf_steal(&text); cJSON_Delete(root); if (err_flag) { char *e = xstrfmt("tool `%s` reported an error: %s", name, out); free(out); if (err) *err = e; return NULL; } return out; } char *mcp_extract_json(const char *content_type, const char *body, char **err) { if (err) *err = NULL; if (content_type && strstr(content_type, "event-stream")) { const char *p = body ? body : ""; const char *best = NULL; size_t best_len = 0; while (*p) { const char *nl = strchr(p, '\n'); size_t linelen = nl ? (size_t)(nl - p) : strlen(p); size_t tlen; const char *line = xtrim_span(p, linelen, &tlen); if (tlen > 5 && strncmp(line, "data:", 5) == 0) { size_t dlen; const char *d = xtrim_span(line + 5, tlen - 5, &dlen); if (dlen > 0) { best = d; best_len = dlen; } } if (!nl) break; p = nl + 1; } if (!best) { if (err) *err = xstrdup("SSE body had no `data:` line"); return NULL; } char *txt = xstrndup(best, best_len); cJSON *v = cJSON_Parse(txt); free(txt); if (!v) { if (err) *err = xstrdup("json: invalid SSE data line"); return NULL; } char *out = cJSON_PrintUnformatted(v); cJSON_Delete(v); return out; } cJSON *v = cJSON_Parse(body ? body : ""); if (!v) { if (err) *err = xstrdup("json: invalid HTTP body"); return NULL; } char *out = cJSON_PrintUnformatted(v); cJSON_Delete(v); return out; } /* ------------------------------------------------------------------ */ /* registry */ /* ------------------------------------------------------------------ */ void mcp_connect_all(const char *const *names, const char *const *configs, size_t n, mcp_registry *out, strlist *warnings) { memset(out, 0, sizeof(*out)); mcp_tool *tools = NULL; size_t tool_count = 0; mcp_client **clients = NULL; size_t client_count = 0; for (size_t i = 0; i < n; i++) { const char *name = names[i]; const char *cfg_json = configs[i]; mcp_server_config cfg; if (!mcp_server_config_from_json(cfg_json, &cfg)) { sl_push(warnings, xstrfmt("mcp `%s`: no `command` or `url`; skipped", name)); continue; } mcp_client *c = NULL; char *err = NULL; if (cfg.command) c = mcp_connect_stdio(&cfg, &err); else c = mcp_connect_http(&cfg, &err); if (!c) { sl_push(warnings, xstrfmt("mcp `%s`: failed to start", name)); free(err); mcp_server_config_free(&cfg); continue; } if (!mcp_initialize(c, &err)) { sl_push(warnings, xstrfmt("mcp `%s`: handshake failed: %s", name, err ? err : "unknown")); free(err); mcp_client_free(c); mcp_server_config_free(&cfg); continue; } mcp_tool *t = NULL; size_t tn = 0; char *err2 = NULL; if (!mcp_list_tools(c, &t, &tn, &err2)) { sl_push(warnings, xstrfmt("mcp `%s`: tools/list failed", name)); free(err2); mcp_client_free(c); mcp_server_config_free(&cfg); continue; } for (size_t j = 0; j < tn; j++) { t[j].qualified_name = xstrfmt("mcp__%zu_%s__%s", strlen(name), name, t[j].name); } tools = (mcp_tool *) xrealloc(tools, (tool_count + tn) * sizeof(*tools)); memcpy(tools + tool_count, t, tn * sizeof(*t)); tool_count += tn; free(t); clients = (mcp_client **)xrealloc(clients, (client_count + 1) * sizeof(*clients)); clients[client_count++] = c; mcp_server_config_free(&cfg); } out->tools = tools; out->tool_count = tool_count; out->clients = clients; out->client_count = client_count; } void mcp_registry_free(mcp_registry *r) { if (!r) return; mcp_tools_free(r->tools, r->tool_count); for (size_t i = 0; i < r->client_count; i++) mcp_client_free(r->clients[i]); free(r->clients); memset(r, 0, sizeof(*r)); } char *mcp_tool_invoke(const mcp_tool *t, const char *args_json, char **err) { char *out = mcp_call_tool(t->client, t->name, args_json, err); if (out) return out; char *e = err ? *err : NULL; if (err) *err = xstrfmt("MCP tool `%s` failed: %s", t->name, e ? e : "unknown"); free(e); return NULL; }