diff options
Diffstat (limited to 'src/mcp.cpp')
| -rw-r--r-- | src/mcp.cpp | 1010 |
1 files changed, 1010 insertions, 0 deletions
diff --git a/src/mcp.cpp b/src/mcp.cpp new file mode 100644 index 0000000..94615a3 --- /dev/null +++ b/src/mcp.cpp @@ -0,0 +1,1010 @@ +#include "mcp.h" + +#include "config.h" /* LLSH_VERSION */ + +#include <cjson/cJSON.h> +#include <curl/curl.h> + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <poll.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#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; +} |