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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user