204 lines · 5.2 KB · C
RawDownload
// SPDX-License-Identifier: ISC#include <stdio.h>#include <stdlib.h>#include <string.h> #include <cjson/cJSON.h> #include "clm/clm.h"#include "clm/cleanup.h"#include "clm/mcp.h"#include "clm/lua_plugin.h"#include "mcp_setup.h" /* * config.lua shape: *   mcp_servers = { *     { name = "fs", transport = "stdio", command = {"mcp-server-fs", "/tmp"} }, *     { name = "search", transport = "http", url = "https://.../mcp", api_key = "..." }, *   } * "transport" defaults to "stdio" if omitted. "timeout_ms" is optional on * either kind (per tool-call deadline). */ struct mcp_ready_ctx {	char *name;	clm_cli_mcp_status_cb status_cb;	void *status_user;}; static voidemit_status(clm_cli_mcp_status_cb status_cb, void *user, const char *msg){	if (status_cb != NULL)		status_cb(msg, user);	else		fprintf(stderr, "%s\n", msg);} static voidon_mcp_ready(int status, size_t tool_count, void *user){	struct mcp_ready_ctx *ctx = user;	char msg[256]; 	if (status == 0)		(void)snprintf(msg, sizeof(msg), "mcp: %s: %zu tool%s registered",		    ctx->name, tool_count, tool_count == 1 ? "" : "s");	else		(void)snprintf(msg, sizeof(msg), "mcp: %s: connect failed (%d)",		    ctx->name, status);	emit_status(ctx->status_cb, ctx->status_user, msg);} static voidfree_mcp_ready_ctx(void *user){	struct mcp_ready_ctx *ctx = user; 	free(ctx->name);	free(ctx);} static const char *json_str(cJSON *obj, const char *key){	cJSON *v = cJSON_GetObjectItemCaseSensitive(obj, key);	if (v == NULL || !cJSON_IsString(v))		return NULL;	return cJSON_GetStringValue(v);} /* returns the connected client handle, or NULL if this entry was skipped or * failed to start (already reported through status_cb either way). */static struct clm_mcp_client *connect_one(struct clm_agent *agent, uv_loop_t *loop, cJSON *srv,    clm_cli_mcp_status_cb status_cb, void *status_user){	const char *name = json_str(srv, "name");	const char *transport = json_str(srv, "transport");	struct clm_mcp_server_cfg server_cfg = {0};	struct mcp_ready_ctx *ready_ctx;	cJSON *jtimeout, *jcmd;	autofree char **argv = NULL;	struct clm_mcp_client *client = NULL; 	if (name == NULL) {		emit_status(status_cb, status_user,		    "mcp: skipping server with no 'name'");		return NULL;	} 	server_cfg.name = name;	jtimeout = cJSON_GetObjectItemCaseSensitive(srv, "timeout_ms");	if (jtimeout != NULL && cJSON_IsNumber(jtimeout))		server_cfg.timeout_ms = (uint64_t)cJSON_GetNumberValue(jtimeout); 	if (transport != NULL && strcmp(transport, "http") == 0) {		server_cfg.transport = CLM_MCP_HTTP;		server_cfg.url = json_str(srv, "url");		server_cfg.api_key = json_str(srv, "api_key");		if (server_cfg.url == NULL) {			char msg[256];			(void)snprintf(msg, sizeof(msg),			    "mcp: %s: http transport needs 'url'", name);			emit_status(status_cb, status_user, msg);			return NULL;		}	} else {		size_t n, i; 		server_cfg.transport = CLM_MCP_STDIO;		jcmd = cJSON_GetObjectItemCaseSensitive(srv, "command");		if (jcmd == NULL || !cJSON_IsArray(jcmd) ||		    cJSON_GetArraySize(jcmd) == 0) {			char msg[256];			(void)snprintf(msg, sizeof(msg),			    "mcp: %s: stdio transport needs a non-empty 'command' array",			    name);			emit_status(status_cb, status_user, msg);			return NULL;		}		n = (size_t)cJSON_GetArraySize(jcmd);		argv = calloc(n + 1, sizeof(*argv));		if (argv == NULL)			return NULL;		for (i = 0; i < n; i++) {			cJSON *e = cJSON_GetArrayItem(jcmd, i);			argv[i] = cJSON_GetStringValue(e);		}		server_cfg.argv = argv;	} 	ready_ctx = malloc(sizeof(*ready_ctx));	if (ready_ctx == NULL)		return NULL;	ready_ctx->name = strdup(name);	ready_ctx->status_cb = status_cb;	ready_ctx->status_user = status_user; 	if (clm_mcp_connect(agent, loop, &server_cfg, on_mcp_ready, ready_ctx,	    free_mcp_ready_ctx, &client) != 0) {		char msg[256];		(void)snprintf(msg, sizeof(msg), "mcp: %s: failed to start", name);		emit_status(status_cb, status_user, msg);		free(ready_ctx->name);		free(ready_ctx);		return NULL;	}	return client;} struct clm_mcp_client **clm_cli_connect_mcp_servers(struct clm_agent *agent, uv_loop_t *loop,    struct clm_lua_cfg *lcfg, clm_cli_mcp_status_cb status_cb,    void *status_user, size_t *out_count){	autofree char *json = NULL;	json_cleanup cJSON *arr = NULL;	struct clm_mcp_client **clients;	size_t n, i; 	if (out_count != NULL)		*out_count = 0;	if (lcfg == NULL)		return NULL; 	json = clm_lua_cfg_mcp_servers_json(lcfg);	if (json == NULL)		return NULL; 	arr = cJSON_Parse(json);	if (arr == NULL || !cJSON_IsArray(arr))		return NULL; 	n = (size_t)cJSON_GetArraySize(arr);	if (n == 0)		return NULL; 	clients = calloc(n, sizeof(*clients));	if (clients == NULL) {		emit_status(status_cb, status_user,		    "mcp: out of memory starting configured servers");		return NULL;	} 	for (i = 0; i < n; i++)		clients[i] = connect_one(agent, loop, cJSON_GetArrayItem(arr, i),		    status_cb, status_user); 	if (out_count != NULL)		*out_count = n;	return clients;} voidclm_cli_free_mcp_servers(struct clm_mcp_client **clients, size_t count){	size_t i;	if (clients == NULL)		return;	for (i = 0; i < count; i++)		clm_mcp_client_free(clients[i]); /* NULL entries: no-op */	free(clients);}