/*
This file is part of darktable,
Copyright (C) 2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
darktable is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with darktable. If not, see .
*/
#include "mcp/mcp_jsonrpc.h"
#include "mcp/mcp_tools.h"
#include
#include
#include
#if defined(_WIN32)
#include "win/getdelim.h" // getline() is not declared by MinGW
#endif
#define SERVER_NAME "darktable-mcp"
#define SERVER_VERSION "0.1.0"
#define PROTOCOL_VERSION "2025-06-18"
// JSON-RPC error codes
#define ERR_PARSE -32700
#define ERR_INVALID_REQUEST -32600
#define ERR_METHOD_NOT_FOUND -32601
#define ERR_INVALID_PARAMS -32602
// whether the current peer uses LSP-style Content-Length framing (else the MCP
// stdio default of one JSON object per line)
static gboolean _use_framing = FALSE;
// ---------------------------------------------------------------------------
// output
// ---------------------------------------------------------------------------
static void _send_node(FILE *out, JsonNode *root)
{
JsonGenerator *gen = json_generator_new();
json_generator_set_root(gen, root);
gsize len = 0;
gchar *s = json_generator_to_data(gen, &len);
if(_use_framing) fprintf(out, "Content-Length: %zu\r\n\r\n", (size_t)len);
fwrite(s, 1, len, out);
if(!_use_framing) fputc('\n', out);
fflush(out);
g_free(s);
g_object_unref(gen);
}
static JsonNode *_empty_object(void)
{
JsonNode *n = json_node_new(JSON_NODE_OBJECT);
json_node_take_object(n, json_object_new());
return n;
}
// builds { jsonrpc, id, result }, takes ownership of `result`
static void _send_result(FILE *out, JsonNode *id, JsonNode *result)
{
JsonObject *o = json_object_new();
json_object_set_string_member(o, "jsonrpc", "2.0");
if(id) json_object_set_member(o, "id", json_node_copy(id));
else json_object_set_null_member(o, "id");
json_object_set_member(o, "result", result);
JsonNode *root = json_node_new(JSON_NODE_OBJECT);
json_node_take_object(root, o);
_send_node(out, root);
json_node_unref(root);
}
static void _send_error(FILE *out, JsonNode *id, int code, const char *msg)
{
JsonObject *err = json_object_new();
json_object_set_int_member(err, "code", code);
json_object_set_string_member(err, "message", msg ? msg : "");
JsonObject *o = json_object_new();
json_object_set_string_member(o, "jsonrpc", "2.0");
if(id) json_object_set_member(o, "id", json_node_copy(id));
else json_object_set_null_member(o, "id");
json_object_set_object_member(o, "error", err);
JsonNode *root = json_node_new(JSON_NODE_OBJECT);
json_node_take_object(root, o);
_send_node(out, root);
json_node_unref(root);
}
// ---------------------------------------------------------------------------
// method results
// ---------------------------------------------------------------------------
static JsonNode *_initialize_result(void)
{
JsonObject *tools = json_object_new();
json_object_set_boolean_member(tools, "listChanged", FALSE);
JsonObject *caps = json_object_new();
json_object_set_object_member(caps, "tools", tools);
JsonObject *info = json_object_new();
json_object_set_string_member(info, "name", SERVER_NAME);
json_object_set_string_member(info, "version", SERVER_VERSION);
JsonObject *o = json_object_new();
json_object_set_string_member(o, "protocolVersion", PROTOCOL_VERSION);
json_object_set_object_member(o, "capabilities", caps);
json_object_set_object_member(o, "serverInfo", info);
JsonNode *n = json_node_new(JSON_NODE_OBJECT);
json_node_take_object(n, o);
return n;
}
static JsonNode *_tools_list_result(void)
{
JsonObject *o = json_object_new();
json_object_set_member(o, "tools", mcp_tools_list_node());
JsonNode *n = json_node_new(JSON_NODE_OBJECT);
json_node_take_object(n, o);
return n;
}
static void _handle_tools_call(FILE *out, JsonNode *id, JsonObject *req)
{
JsonObject *params = NULL;
if(json_object_has_member(req, "params"))
{
JsonNode *pn = json_object_get_member(req, "params");
if(JSON_NODE_HOLDS_OBJECT(pn)) params = json_node_get_object(pn);
}
const char *name = params && json_object_has_member(params, "name")
? json_object_get_string_member(params, "name") : NULL;
if(!name) { _send_error(out, id, ERR_INVALID_PARAMS, "missing tool name"); return; }
JsonObject *args = NULL;
if(params && json_object_has_member(params, "arguments"))
{
JsonNode *an = json_object_get_member(params, "arguments");
if(JSON_NODE_HOLDS_OBJECT(an)) args = json_node_get_object(an);
}
gboolean found = FALSE;
JsonNode *result = mcp_tools_call_node(name, args, &found);
if(!found)
{
if(result) json_node_unref(result);
_send_error(out, id, ERR_METHOD_NOT_FOUND, "unknown tool");
return;
}
_send_result(out, id, result);
}
// ---------------------------------------------------------------------------
// read loop
// ---------------------------------------------------------------------------
static void _rstrip(char *s, ssize_t *n)
{
while(*n > 0 && (s[*n - 1] == '\n' || s[*n - 1] == '\r')) s[--(*n)] = '\0';
}
void mcp_jsonrpc_loop(FILE *in, FILE *out)
{
char *line = NULL;
size_t cap = 0;
ssize_t n;
gboolean done = FALSE;
while(!done && (n = getline(&line, &cap, in)) != -1)
{
_rstrip(line, &n);
if(n == 0) continue; // blank line between messages
char *msg = line;
char *framed_msg = NULL;
if(g_ascii_strncasecmp(line, "Content-Length:", 15) == 0)
{
_use_framing = TRUE;
const long clen = strtol(line + 15, NULL, 10);
// consume any remaining header lines up to the blank separator
while((n = getline(&line, &cap, in)) != -1)
{
_rstrip(line, &n);
if(n == 0) break;
}
if(clen <= 0) continue;
framed_msg = g_malloc(clen + 1);
const size_t got = fread(framed_msg, 1, clen, in);
framed_msg[got] = '\0';
msg = framed_msg;
}
JsonParser *parser = json_parser_new();
GError *e = NULL;
if(!json_parser_load_from_data(parser, msg, -1, &e))
{
_send_error(out, NULL, ERR_PARSE, e ? e->message : "parse error");
if(e) g_error_free(e);
g_object_unref(parser);
g_free(framed_msg);
continue;
}
JsonNode *root = json_parser_get_root(parser);
if(!root || !JSON_NODE_HOLDS_OBJECT(root))
{
_send_error(out, NULL, ERR_INVALID_REQUEST, "not a JSON-RPC object");
g_object_unref(parser);
g_free(framed_msg);
continue;
}
JsonObject *req = json_node_get_object(root);
const char *method = json_object_has_member(req, "method")
? json_object_get_string_member(req, "method") : NULL;
JsonNode *id = json_object_has_member(req, "id")
? json_object_get_member(req, "id") : NULL;
if(!method)
{
if(id) _send_error(out, id, ERR_INVALID_REQUEST, "missing method");
}
else if(!strcmp(method, "initialize"))
_send_result(out, id, _initialize_result());
else if(!strcmp(method, "tools/list"))
_send_result(out, id, _tools_list_result());
else if(!strcmp(method, "tools/call"))
_handle_tools_call(out, id, req);
else if(!strcmp(method, "ping"))
_send_result(out, id, _empty_object());
else if(g_str_has_prefix(method, "notifications/"))
{ /* notifications get no response */ }
else if(!strcmp(method, "shutdown"))
{
_send_result(out, id, _empty_object());
done = TRUE;
}
else if(id)
_send_error(out, id, ERR_METHOD_NOT_FOUND, "method not found");
g_object_unref(parser);
g_free(framed_msg);
}
g_free(line);
}