summaryrefslogtreecommitdiffstats
path: root/src/mcp.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mcp.cpp')
-rw-r--r--src/mcp.cpp1010
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;
+}