Extensions: skills, plugins, MCP, sub-agents; configurable keys

- Skills (SKILL.md discovery, skill tool, /skills /skill)
- plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins)
- MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status
- task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents
- Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel
- Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools
- Spawn: close-on-exec pipes and a process-wide fork lock
- Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
WHC
2026-10-05 14:55:31 +08:00
parent c81df42f0b
commit 7d3ec9fcb9
38 changed files with 8356 additions and 399 deletions
+936
View File
@@ -0,0 +1,936 @@
#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE 700
#endif
#include "cst_mcp.h"
#include <pthread.h>
#include <stdatomic.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <io.h>
#define close _close
#else
#include <unistd.h>
#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, "");
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;
}