#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #endif #include "cst_mcp.h" #include #include #include #include #include #include #include #ifdef _WIN32 #include #define close _close #else #include #endif #include "cJSON.h" #include "cst_tools.h" #include "cst_util.h" #include "cst_version.h" #include "os_compat.h" #define MCP_PEND 16 #define MCP_INIT_TIMEOUT_MS 30000 #define MCP_REQ_TIMEOUT_MS 60000 #define MCP_LINE_MAX (16 * 1024 * 1024) #define MCP_OUT_MAX (256 * 1024) #define MCP_DESC_MAX 1024 #define MCP_PROTOCOL "2025-06-18" typedef struct mcp_srv mcp_srv_t; typedef struct mtool { mcp_srv_t *srv; char orig[256]; /* the server's own tool name */ struct mtool *next; } mtool_t; struct mcp_srv { char name[64]; char source[48]; char owner[96]; char cwd[OS_PATH_MAX]; char command[256]; char **argv; /* NULL-terminated */ char **env; /* "K=V", NULL-terminated */ int auto_all; char **auto_tools; /* NULL-terminated */ int disabled; char err_log[OS_PATH_MAX]; pthread_mutex_t mu; /* state, pending */ pthread_mutex_t wmu; /* writes to the server's stdin */ pthread_cond_t cv; int state; char error[200]; int ntools; atomic_int stopping; os_proc_t proc; int spawned; pthread_t reader, starter; int reader_on, starter_on; long next_id; struct { long id; int used, done; char *json; } pend[MCP_PEND]; mtool_t *tools; }; static mcp_srv_t *g_srv[CST_MCP_MAX]; static int g_nsrv; static char g_ws[OS_PATH_MAX]; /* ---------- config ---------- */ static void strv_free(char **v) { for (int i = 0; v && v[i]; i++) free(v[i]); free(v); } /* ${NAME} -> getenv(NAME) (empty when unset). */ static char *expand(const char *s) { cst_buf_t b = {0}; while (*s) { const char *e; if (s[0] == '$' && s[1] == '{' && (e = strchr(s + 2, '}')) != NULL && e - s - 2 < 128) { char var[130]; memcpy(var, s + 2, (size_t)(e - s - 2)); var[e - s - 2] = 0; const char *v = getenv(var); if (v) cst_buf_puts(&b, v); s = e + 1; } else { cst_buf_append(&b, s, 1); s++; } } if (!b.buf) cst_buf_puts(&b, ""); return cst_buf_steal(&b); } static mcp_srv_t *find_srv(const char *name) { for (int i = 0; i < g_nsrv; i++) if (strcmp(g_srv[i]->name, name) == 0) return g_srv[i]; return NULL; } static void srv_free(mcp_srv_t *s) { if (!s) return; strv_free(s->argv); strv_free(s->env); strv_free(s->auto_tools); while (s->tools) { mtool_t *n = s->tools->next; free(s->tools); s->tools = n; } for (int i = 0; i < MCP_PEND; i++) free(s->pend[i].json); pthread_mutex_destroy(&s->mu); pthread_mutex_destroy(&s->wmu); pthread_cond_destroy(&s->cv); free(s); } int cst_mcp_add_config_json(const char *name, const char *server_json, const char *cwd, const char *source) { if (!name || !name[0] || !server_json || g_nsrv >= CST_MCP_MAX) return -1; char sname[64]; cst_tool_sanitize_name(name, sname, sizeof(sname)); if (find_srv(sname)) return -1; cJSON *j = cJSON_Parse(server_json); if (!cJSON_IsObject(j)) { cJSON_Delete(j); return -1; } mcp_srv_t *s = calloc(1, sizeof(*s)); if (!s) { cJSON_Delete(j); return -1; } pthread_mutex_init(&s->mu, NULL); pthread_mutex_init(&s->wmu, NULL); pthread_cond_init(&s->cv, NULL); snprintf(s->name, sizeof(s->name), "%s", sname); snprintf(s->source, sizeof(s->source), "%s", source ? source : "user"); snprintf(s->owner, sizeof(s->owner), "mcp:%s", sname); const cJSON *dis = cJSON_GetObjectItemCaseSensitive(j, "disabled"); const cJSON *cmd = cJSON_GetObjectItemCaseSensitive(j, "command"); const cJSON *url = cJSON_GetObjectItemCaseSensitive(j, "url"); const cJSON *type = cJSON_GetObjectItemCaseSensitive(j, "type"); if (cJSON_IsTrue(dis)) { s->disabled = 1; snprintf(s->error, sizeof(s->error), "disabled in the config"); } if (url || (cJSON_IsString(type) && strcmp(type->valuestring, "stdio"))) { s->state = CST_MCP_FAILED; snprintf(s->error, sizeof(s->error), "only stdio servers (\"command\") are supported"); } else if (!cJSON_IsString(cmd) || !cmd->valuestring[0]) { s->state = CST_MCP_FAILED; snprintf(s->error, sizeof(s->error), "missing \"command\""); } else { const cJSON *args = cJSON_GetObjectItemCaseSensitive(j, "args"); int na = cJSON_IsArray(args) ? cJSON_GetArraySize(args) : 0; s->argv = calloc((size_t)na + 2, sizeof(char *)); if (s->argv) { s->argv[0] = expand(cmd->valuestring); int k = 1; const cJSON *a = NULL; cJSON_ArrayForEach(a, args) { if (cJSON_IsString(a)) s->argv[k++] = expand(a->valuestring); else if (cJSON_IsNumber(a)) { char nb[32]; snprintf(nb, sizeof(nb), "%g", a->valuedouble); s->argv[k++] = strdup(nb); } } cst_buf_t c = {0}; for (int i = 0; s->argv[i]; i++) cst_buf_printf(&c, "%s%s", i ? " " : "", s->argv[i]); snprintf(s->command, sizeof(s->command), "%s", c.buf ? c.buf : ""); cst_buf_free(&c); } const cJSON *env = cJSON_GetObjectItemCaseSensitive(j, "env"); int ne = cJSON_IsObject(env) ? cJSON_GetArraySize(env) : 0; s->env = calloc((size_t)ne + 1, sizeof(char *)); if (s->env) { int k = 0; const cJSON *e = NULL; cJSON_ArrayForEach(e, env) { if (!cJSON_IsString(e) || !e->string) continue; char *v = expand(e->valuestring); cst_buf_t kv = {0}; cst_buf_printf(&kv, "%s=%s", e->string, v ? v : ""); free(v); s->env[k++] = cst_buf_steal(&kv); } } } const cJSON *wd = cJSON_GetObjectItemCaseSensitive(j, "cwd"); if (cJSON_IsString(wd) && wd->valuestring[0]) { char *w = expand(wd->valuestring); if (w && (w[0] == '/' || !cwd || !cwd[0] #ifdef _WIN32 || (w[0] && w[1] == ':') #endif )) snprintf(s->cwd, sizeof(s->cwd), "%s", w); else if (w) snprintf(s->cwd, sizeof(s->cwd), "%.2000s/%.2000s", cwd, w); free(w); } else if (cwd && cwd[0]) { snprintf(s->cwd, sizeof(s->cwd), "%s", cwd); } const cJSON *aa = cJSON_GetObjectItemCaseSensitive(j, "autoApprove"); if (cJSON_IsTrue(aa)) { s->auto_all = 1; } else if (cJSON_IsArray(aa)) { int n = cJSON_GetArraySize(aa), k = 0; s->auto_tools = calloc((size_t)n + 1, sizeof(char *)); const cJSON *t = NULL; cJSON_ArrayForEach(t, aa) { if (s->auto_tools && cJSON_IsString(t)) s->auto_tools[k++] = strdup(t->valuestring); } } cJSON_Delete(j); g_srv[g_nsrv++] = s; return 0; } static char *slurp(const char *path, size_t max) { FILE *fp = fopen(path, "rb"); if (!fp) return NULL; cst_buf_t b = {0}; char tmp[8192]; size_t n; while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0) { if (b.len + n > max) { cst_buf_free(&b); fclose(fp); return NULL; } cst_buf_append(&b, tmp, n); } fclose(fp); if (!b.buf) cst_buf_puts(&b, ""); cst_strip_bom(b.buf, &b.len); return cst_buf_steal(&b); } static int load_file(const char *path, const char *cwd, const char *source, int count_only) { char *text = slurp(path, 1024 * 1024); if (!text) return 0; cJSON *root = cJSON_Parse(text); free(text); const cJSON *servers = cJSON_GetObjectItemCaseSensitive(root, "mcpServers"); if (!servers) servers = cJSON_GetObjectItemCaseSensitive(root, "servers"); int n = 0; const cJSON *sv = NULL; cJSON_ArrayForEach(sv, servers) { if (!sv->string) continue; if (count_only) { n++; continue; } char *sj = cJSON_PrintUnformatted(sv); if (sj && cst_mcp_add_config_json(sv->string, sj, cwd, source) == 0) n++; free(sj); } cJSON_Delete(root); return n; } int cst_mcp_load_configs(const char *ws, int ws_trusted, int *untrusted) { if (untrusted) *untrusted = 0; int n = 0; char p[OS_PATH_MAX]; if (ws && ws[0]) { snprintf(g_ws, sizeof(g_ws), "%s", ws); snprintf(p, sizeof(p), "%.4000s/.agent/mcp.json", ws); if (ws_trusted) n += load_file(p, ws, "workspace", 0); else if (untrusted) *untrusted = load_file(p, ws, "workspace", 1); } char cfg[OS_PATH_MAX]; if (os_config_dir(cfg, sizeof(cfg)) == 0) { snprintf(p, sizeof(p), "%.4000s/mcp.json", cfg); n += load_file(p, ws, "user", 0); } return n; } /* ---------- JSON-RPC ---------- */ static void set_failed(mcp_srv_t *s, int state, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static void set_failed(mcp_srv_t *s, int state, const char *fmt, ...) { pthread_mutex_lock(&s->mu); s->state = state; va_list ap; va_start(ap, fmt); vsnprintf(s->error, sizeof(s->error), fmt, ap); va_end(ap); pthread_cond_broadcast(&s->cv); pthread_mutex_unlock(&s->mu); } /* Last non-empty line of the server's stderr log, for error messages. */ static void stderr_tail(const mcp_srv_t *s, char *out, size_t sz) { out[0] = 0; if (!s->err_log[0]) return; char *t = slurp(s->err_log, 4 * 1024 * 1024); if (!t) return; size_t n = strlen(t); while (n && (t[n - 1] == '\n' || t[n - 1] == '\r' || t[n - 1] == ' ')) t[--n] = 0; char *line = strrchr(t, '\n'); line = line ? line + 1 : t; if (*line) snprintf(out, sz, "%s", line); free(t); } static int send_json(mcp_srv_t *s, cJSON *msg) { char *txt = cJSON_PrintUnformatted(msg); if (!txt) return -1; pthread_mutex_lock(&s->wmu); int rc = os_proc_write(&s->proc, txt, strlen(txt)); if (rc == 0) rc = os_proc_write(&s->proc, "\n", 1); pthread_mutex_unlock(&s->wmu); free(txt); return rc; } static void notify(mcp_srv_t *s, const char *method, cJSON *params) { cJSON *m = cJSON_CreateObject(); cJSON_AddStringToObject(m, "jsonrpc", "2.0"); cJSON_AddStringToObject(m, "method", method); if (params) cJSON_AddItemToObject(m, "params", params); send_json(s, m); cJSON_Delete(m); } static int srv_alive(const mcp_srv_t *s) { return s->state == CST_MCP_STARTING || s->state == CST_MCP_READY; } /* Send a request and wait for its response object. params is consumed. * NULL with err set on timeout, cancel, a dead server or a write error. */ static cJSON *request(mcp_srv_t *s, const char *method, cJSON *params, int timeout_ms, char *err, size_t esz) { pthread_mutex_lock(&s->mu); int slot = -1; for (int i = 0; i < MCP_PEND && slot < 0; i++) if (!s->pend[i].used) slot = i; if (!srv_alive(s) || slot < 0) { snprintf(err, esz, "%s", slot < 0 ? "too many requests in flight" : "server is not running"); pthread_mutex_unlock(&s->mu); cJSON_Delete(params); return NULL; } long id = ++s->next_id; s->pend[slot].used = 1; s->pend[slot].done = 0; s->pend[slot].id = id; s->pend[slot].json = NULL; pthread_mutex_unlock(&s->mu); cJSON *m = cJSON_CreateObject(); cJSON_AddStringToObject(m, "jsonrpc", "2.0"); cJSON_AddNumberToObject(m, "id", (double)id); cJSON_AddStringToObject(m, "method", method); if (params) cJSON_AddItemToObject(m, "params", params); int wrc = send_json(s, m); cJSON_Delete(m); double deadline = os_now_ms() + timeout_ms; char *json = NULL; int why = 0; /* 1 timeout, 2 cancel, 3 dead, 4 write */ pthread_mutex_lock(&s->mu); if (wrc != 0) why = 4; while (!why && !s->pend[slot].done) { if (!srv_alive(s)) { why = 3; break; } if (os_cancel_requested()) { why = 2; break; } if (os_now_ms() > deadline) { why = 1; break; } struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_nsec += 100 * 1000000L; if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } pthread_cond_timedwait(&s->cv, &s->mu, &ts); } if (s->pend[slot].done) { json = s->pend[slot].json; why = 0; } s->pend[slot].json = NULL; s->pend[slot].used = 0; char serr[200]; snprintf(serr, sizeof(serr), "%s", s->error); pthread_mutex_unlock(&s->mu); if (why == 1 || why == 2) { cJSON *p = cJSON_CreateObject(); cJSON_AddNumberToObject(p, "requestId", (double)id); cJSON_AddStringToObject(p, "reason", why == 1 ? "timeout" : "cancelled"); notify(s, "notifications/cancelled", p); } if (why) { if (why == 1) snprintf(err, esz, "no response to %s within %d s", method, timeout_ms / 1000); else if (why == 2) snprintf(err, esz, "cancelled"); else if (why == 3) snprintf(err, esz, "server stopped%s%s", serr[0] ? ": " : "", serr); else snprintf(err, esz, "cannot write to the server"); free(json); return NULL; } cJSON *resp = cJSON_Parse(json); free(json); if (!resp) snprintf(err, esz, "invalid JSON response"); return resp; } /* JSON-RPC error object -> "message (code N)". */ static int rpc_error(const cJSON *resp, char *err, size_t esz) { const cJSON *e = cJSON_GetObjectItemCaseSensitive(resp, "error"); if (!e) return 0; const cJSON *msg = cJSON_GetObjectItemCaseSensitive(e, "message"); const cJSON *code = cJSON_GetObjectItemCaseSensitive(e, "code"); snprintf(err, esz, "%s (code %d)", cJSON_IsString(msg) ? msg->valuestring : "error", cJSON_IsNumber(code) ? code->valueint : 0); return 1; } static void answer_server_request(mcp_srv_t *s, const cJSON *msg) { const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id"); const cJSON *method = cJSON_GetObjectItemCaseSensitive(msg, "method"); cJSON *r = cJSON_CreateObject(); cJSON_AddStringToObject(r, "jsonrpc", "2.0"); cJSON_AddItemToObject(r, "id", cJSON_Duplicate(id, 1)); const char *m = cJSON_IsString(method) ? method->valuestring : ""; if (strcmp(m, "ping") == 0) { cJSON_AddItemToObject(r, "result", cJSON_CreateObject()); } else if (strcmp(m, "roots/list") == 0 && g_ws[0]) { cJSON *res = cJSON_AddObjectToObject(r, "result"); cJSON *roots = cJSON_AddArrayToObject(res, "roots"); cJSON *root = cJSON_CreateObject(); char real[OS_PATH_MAX], uri[OS_PATH_MAX + 16]; if (os_realpath(g_ws, real, sizeof(real)) != 0) snprintf(real, sizeof(real), "%s", g_ws); snprintf(uri, sizeof(uri), "file://%s", real); cJSON_AddStringToObject(root, "uri", uri); cJSON_AddStringToObject(root, "name", "workspace"); cJSON_AddItemToArray(roots, root); } else { cJSON *e = cJSON_AddObjectToObject(r, "error"); cJSON_AddNumberToObject(e, "code", -32601); cJSON_AddStringToObject(e, "message", "method not supported by client"); } send_json(s, r); cJSON_Delete(r); } static void handle_line(mcp_srv_t *s, const char *line) { cJSON *msg = cJSON_Parse(line); if (!cJSON_IsObject(msg)) { cJSON_Delete(msg); /* servers sometimes log to stdout; ignore */ return; } const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id"); const cJSON *method = cJSON_GetObjectItemCaseSensitive(msg, "method"); if (method) { if (id) answer_server_request(s, msg); } else if (cJSON_IsNumber(id)) { long want = (long)id->valuedouble; pthread_mutex_lock(&s->mu); for (int i = 0; i < MCP_PEND; i++) { if (s->pend[i].used && !s->pend[i].done && s->pend[i].id == want) { s->pend[i].json = strdup(line); s->pend[i].done = 1; pthread_cond_broadcast(&s->cv); break; } } pthread_mutex_unlock(&s->mu); } cJSON_Delete(msg); } static void *reader_main(void *arg) { mcp_srv_t *s = arg; cst_buf_t line = {0}; char tmp[16384]; int overflow = 0; for (;;) { long n = os_proc_read(&s->proc, tmp, sizeof(tmp), 200); if (n == -2) { if (atomic_load(&s->stopping)) break; continue; } if (n <= 0) break; for (long i = 0; i < n; i++) { if (tmp[i] != '\n') { if (line.len < MCP_LINE_MAX) cst_buf_append(&line, tmp + i, 1); else overflow = 1; continue; } if (line.len && line.buf[line.len - 1] == '\r') line.buf[--line.len] = 0; if (line.len && !overflow) handle_line(s, line.buf); line.len = 0; if (line.buf) line.buf[0] = 0; overflow = 0; } } cst_buf_free(&line); if (!atomic_load(&s->stopping)) { char tail[160]; stderr_tail(s, tail, sizeof(tail)); set_failed(s, CST_MCP_EXITED, "server exited%s%s", tail[0] ? ": " : "", tail); } else { set_failed(s, CST_MCP_EXITED, "stopped"); } return NULL; } /* ---------- tools ---------- */ static int auto_approved(const mcp_srv_t *s, const char *tool) { if (s->auto_all) return 1; for (int i = 0; s->auto_tools && s->auto_tools[i]; i++) if (strcmp(s->auto_tools[i], tool) == 0) return 1; return 0; } static void put_content(cst_buf_t *b, const cJSON *item) { const cJSON *type = cJSON_GetObjectItemCaseSensitive(item, "type"); const char *t = cJSON_IsString(type) ? type->valuestring : ""; if (b->len && b->buf[b->len - 1] != '\n') cst_buf_puts(b, "\n"); if (strcmp(t, "text") == 0) { const cJSON *x = cJSON_GetObjectItemCaseSensitive(item, "text"); if (cJSON_IsString(x)) cst_buf_puts(b, x->valuestring); } else if (strcmp(t, "image") == 0 || strcmp(t, "audio") == 0) { const cJSON *mt = cJSON_GetObjectItemCaseSensitive(item, "mimeType"); const cJSON *d = cJSON_GetObjectItemCaseSensitive(item, "data"); cst_buf_printf(b, "[%s %s, %zu bytes base64, not shown]", t, cJSON_IsString(mt) ? mt->valuestring : "", cJSON_IsString(d) ? strlen(d->valuestring) : (size_t)0); } else if (strcmp(t, "resource") == 0) { const cJSON *r = cJSON_GetObjectItemCaseSensitive(item, "resource"); const cJSON *x = cJSON_GetObjectItemCaseSensitive(r, "text"); const cJSON *uri = cJSON_GetObjectItemCaseSensitive(r, "uri"); if (cJSON_IsString(x)) cst_buf_puts(b, x->valuestring); else cst_buf_printf(b, "[resource %s]", cJSON_IsString(uri) ? uri->valuestring : ""); } else if (strcmp(t, "resource_link") == 0) { const cJSON *uri = cJSON_GetObjectItemCaseSensitive(item, "uri"); const cJSON *nm = cJSON_GetObjectItemCaseSensitive(item, "name"); cst_buf_printf(b, "[link %s%s%s]", cJSON_IsString(uri) ? uri->valuestring : "", cJSON_IsString(nm) ? " " : "", cJSON_IsString(nm) ? nm->valuestring : ""); } else { char *raw = cJSON_PrintUnformatted(item); if (raw) cst_buf_puts(b, raw); free(raw); } } static int set_out(cst_tool_result_t *r, int code, const char *fmt, ...) __attribute__((format(printf, 3, 4))); static int set_out(cst_tool_result_t *r, int code, const char *fmt, ...) { cst_buf_t b = {0}; char tmp[1024]; va_list ap; va_start(ap, fmt); vsnprintf(tmp, sizeof(tmp), fmt, ap); va_end(ap); cst_buf_puts(&b, tmp); r->exit_code = code; r->out_len = b.len; r->out = cst_buf_steal(&b); return 0; } static int mcp_tool_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r, void *ud) { (void)ctx; mtool_t *mt = ud; mcp_srv_t *s = mt->srv; pthread_mutex_lock(&s->mu); int st = s->state; char serr[200]; snprintf(serr, sizeof(serr), "%s", s->error); pthread_mutex_unlock(&s->mu); if (st != CST_MCP_READY) return set_out(r, 1, "error: MCP server %s is not running%s%s", s->name, serr[0] ? ": " : "", serr); char *aj = cst_tool_args_json(cst_tool_find(c->name), c); cJSON *args = aj ? cJSON_Parse(aj) : NULL; free(aj); cJSON *params = cJSON_CreateObject(); cJSON_AddStringToObject(params, "name", mt->orig); cJSON_AddItemToObject(params, "arguments", args ? args : cJSON_CreateObject()); char err[300]; cJSON *resp = request(s, "tools/call", params, MCP_REQ_TIMEOUT_MS, err, sizeof(err)); if (!resp) return set_out(r, 1, "error: %s: %s", s->name, err); if (rpc_error(resp, err, sizeof(err))) { cJSON_Delete(resp); return set_out(r, 1, "error: %s: %s", s->name, err); } const cJSON *res = cJSON_GetObjectItemCaseSensitive(resp, "result"); const cJSON *content = cJSON_GetObjectItemCaseSensitive(res, "content"); cst_buf_t b = {0}; const cJSON *it = NULL; cJSON_ArrayForEach(it, content) { put_content(&b, it); if (b.len > MCP_OUT_MAX) break; } if (!b.len) { const cJSON *sc = cJSON_GetObjectItemCaseSensitive(res, "structuredContent"); char *raw = sc ? cJSON_Print(sc) : NULL; cst_buf_puts(&b, raw ? raw : "(no output)"); free(raw); } if (b.len > MCP_OUT_MAX) { size_t cut = MCP_OUT_MAX; while (cut > 0 && ((unsigned char)b.buf[cut] & 0xC0) == 0x80) cut--; size_t more = b.len - cut; b.len = cut; b.buf[cut] = 0; cst_buf_printf(&b, "\n… [%zu more bytes cut]", more); } r->exit_code = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(res, "isError")) ? 1 : 0; cJSON_Delete(resp); r->out_len = b.len; r->out = cst_buf_steal(&b); return 0; } static void register_tool(mcp_srv_t *s, const cJSON *t) { const cJSON *nm = cJSON_GetObjectItemCaseSensitive(t, "name"); if (!cJSON_IsString(nm) || !nm->valuestring[0] || strlen(nm->valuestring) >= sizeof(((mtool_t *)0)->orig)) return; char full[340], reg[CTL_MAX_NAME]; snprintf(full, sizeof(full), "mcp__%.63s__%.255s", s->name, nm->valuestring); cst_tool_sanitize_name(full, reg, sizeof(reg)); mtool_t *mt = calloc(1, sizeof(*mt)); if (!mt) return; mt->srv = s; snprintf(mt->orig, sizeof(mt->orig), "%s", nm->valuestring); const cJSON *desc = cJSON_GetObjectItemCaseSensitive(t, "description"); const cJSON *schema = cJSON_GetObjectItemCaseSensitive(t, "inputSchema"); char d[MCP_DESC_MAX + 8]; snprintf(d, sizeof(d), "%s", cJSON_IsString(desc) ? desc->valuestring : ""); if (cJSON_IsString(desc) && strlen(desc->valuestring) > MCP_DESC_MAX) { size_t cut = MCP_DESC_MAX; while (cut > 0 && ((unsigned char)d[cut] & 0xC0) == 0x80) cut--; snprintf(d + cut, sizeof(d) - cut, "…"); } char *sj = cJSON_IsObject(schema) ? cJSON_PrintUnformatted(schema) : strdup("{\"type\":\"object\",\"properties\":{}}"); cst_tool_def_t def; memset(&def, 0, sizeof(def)); def.name = reg; def.desc = d; def.schema_json = sj; def.perm = auto_approved(s, nm->valuestring) ? CST_TP_READ : CST_TP_NET; def.run_ex = mcp_tool_run; def.ud = mt; def.owner = s->owner; if (sj && cst_tool_register(&def) == 0) { mt->next = s->tools; s->tools = mt; pthread_mutex_lock(&s->mu); s->ntools++; pthread_mutex_unlock(&s->mu); } else { free(mt); } free(sj); } static void *starter_main(void *arg) { mcp_srv_t *s = arg; char err[300]; int fd = os_mkstemp(s->err_log, sizeof(s->err_log), "cst-mcp-"); if (fd >= 0) close(fd); else s->err_log[0] = 0; const char *cwd = s->cwd[0] ? s->cwd : g_ws; if (os_proc_spawn(cwd, (const char *const *)s->argv, (const char *const *)s->env, s->err_log[0] ? s->err_log : NULL, &s->proc) != 0) { set_failed(s, CST_MCP_FAILED, "cannot start %s", s->argv[0]); return NULL; } s->spawned = 1; if (pthread_create(&s->reader, NULL, reader_main, s) != 0) { set_failed(s, CST_MCP_FAILED, "cannot create the reader thread"); return NULL; } s->reader_on = 1; cJSON *p = cJSON_CreateObject(); cJSON_AddStringToObject(p, "protocolVersion", MCP_PROTOCOL); cJSON *caps = cJSON_AddObjectToObject(p, "capabilities"); cJSON *roots = cJSON_AddObjectToObject(caps, "roots"); cJSON_AddFalseToObject(roots, "listChanged"); cJSON *ci = cJSON_AddObjectToObject(p, "clientInfo"); cJSON_AddStringToObject(ci, "name", "cst-agent"); cJSON_AddStringToObject(ci, "version", CST_VERSION); cJSON *resp = request(s, "initialize", p, MCP_INIT_TIMEOUT_MS, err, sizeof(err)); if (!resp || rpc_error(resp, err, sizeof(err))) { cJSON_Delete(resp); char tail[160]; stderr_tail(s, tail, sizeof(tail)); if (!atomic_load(&s->stopping)) set_failed(s, CST_MCP_FAILED, "initialize: %.150s%s%.120s", err, tail[0] && !strstr(err, tail) ? " — " : "", tail[0] && !strstr(err, tail) ? tail : ""); return NULL; } cJSON_Delete(resp); notify(s, "notifications/initialized", NULL); char cursor[512] = ""; for (int page = 0; page < 20; page++) { cJSON *lp = cJSON_CreateObject(); if (cursor[0]) cJSON_AddStringToObject(lp, "cursor", cursor); resp = request(s, "tools/list", lp, MCP_INIT_TIMEOUT_MS, err, sizeof(err)); if (!resp || rpc_error(resp, err, sizeof(err))) { cJSON_Delete(resp); if (!atomic_load(&s->stopping)) set_failed(s, CST_MCP_FAILED, "tools/list: %s", err); return NULL; } const cJSON *res = cJSON_GetObjectItemCaseSensitive(resp, "result"); const cJSON *t = NULL; cJSON_ArrayForEach(t, cJSON_GetObjectItemCaseSensitive(res, "tools")) register_tool(s, t); const cJSON *nc = cJSON_GetObjectItemCaseSensitive(res, "nextCursor"); int more = cJSON_IsString(nc) && nc->valuestring[0] && strlen(nc->valuestring) < sizeof(cursor); if (more) snprintf(cursor, sizeof(cursor), "%s", nc->valuestring); cJSON_Delete(resp); if (!more) break; } pthread_mutex_lock(&s->mu); if (s->state == CST_MCP_STARTING) { s->state = CST_MCP_READY; s->error[0] = 0; } pthread_cond_broadcast(&s->cv); pthread_mutex_unlock(&s->mu); return NULL; } /* ---------- lifecycle ---------- */ void cst_mcp_start_all(const char *ws) { if (ws && ws[0]) snprintf(g_ws, sizeof(g_ws), "%s", ws); for (int i = 0; i < g_nsrv; i++) { mcp_srv_t *s = g_srv[i]; if (s->disabled || s->state != CST_MCP_OFF || !s->argv || !s->argv[0]) continue; s->state = CST_MCP_STARTING; if (pthread_create(&s->starter, NULL, starter_main, s) == 0) s->starter_on = 1; else set_failed(s, CST_MCP_FAILED, "cannot create a thread"); } } void cst_mcp_wait_ready(int ms) { double deadline = os_now_ms() + ms; for (;;) { int starting = 0; for (int i = 0; i < g_nsrv; i++) { pthread_mutex_lock(&g_srv[i]->mu); starting += g_srv[i]->state == CST_MCP_STARTING; pthread_mutex_unlock(&g_srv[i]->mu); } if (!starting || os_now_ms() > deadline) return; struct timespec ts = {0, 50 * 1000000L}; nanosleep(&ts, NULL); } } void cst_mcp_stop_all(void) { for (int i = 0; i < g_nsrv; i++) { mcp_srv_t *s = g_srv[i]; atomic_store(&s->stopping, 1); if (s->reader_on) pthread_join(s->reader, NULL); s->reader_on = 0; if (s->spawned) os_proc_kill(&s->proc); s->spawned = 0; if (s->starter_on) pthread_join(s->starter, NULL); s->starter_on = 0; cst_tool_unregister_owner(s->owner); if (s->err_log[0]) remove(s->err_log); srv_free(s); g_srv[i] = NULL; } g_nsrv = 0; } int cst_mcp_info(cst_mcp_info_t *out, int max) { int n = g_nsrv < max ? g_nsrv : max; for (int i = 0; i < n; i++) { mcp_srv_t *s = g_srv[i]; cst_mcp_info_t *o = &out[i]; memset(o, 0, sizeof(*o)); snprintf(o->name, sizeof(o->name), "%s", s->name); snprintf(o->source, sizeof(o->source), "%s", s->source); snprintf(o->command, sizeof(o->command), "%s", s->command); pthread_mutex_lock(&s->mu); o->state = s->disabled ? CST_MCP_OFF : s->state; o->ntools = s->ntools; snprintf(o->error, sizeof(o->error), "%s", s->error); pthread_mutex_unlock(&s->mu); } return n; } void cst_mcp_counts(int *ready, int *total) { int r = 0, t = 0; for (int i = 0; i < g_nsrv; i++) { if (g_srv[i]->disabled) continue; t++; pthread_mutex_lock(&g_srv[i]->mu); r += g_srv[i]->state == CST_MCP_READY; pthread_mutex_unlock(&g_srv[i]->mu); } if (ready) *ready = r; if (total) *total = t; }