/*
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 "common/darktable.h"
#include "common/file_location.h"
#include "mcp/dt_bridge.h"
#include "mcp/mcp_jsonrpc.h"
#include "mcp/mcp_tools.h"
#include
#include
#include
#include
#include
#include
#include
#ifdef _WIN32
#include "win/main_wrapper.h" // provides wmain() for the -municode CRT entry
#endif
static gboolean _has_flag(char **argv, int argc, const char *flag)
{
for(int i = 0; i < argc; i++)
if(!g_strcmp0(argv[i], flag)) return TRUE;
return FALSE;
}
static gboolean _has_conf(char **argv, int argc, const char *key_prefix)
{
for(int i = 0; i + 1 < argc; i++)
if(!g_strcmp0(argv[i], "--conf")
&& g_str_has_prefix(argv[i + 1], key_prefix))
return TRUE;
return FALSE;
}
int main(int argc, char **argv)
{
dt_loc_init(NULL, NULL, NULL, NULL, NULL, NULL);
char localedir[PATH_MAX] = { 0 };
dt_loc_get_localedir(localedir, sizeof(localedir));
bindtextdomain(GETTEXT_PACKAGE, localedir);
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
textdomain(GETTEXT_PACKAGE);
// everything after "--core" is forwarded verbatim to dt_init, so all darktable
// core options (--configdir, --cachedir, --library, --conf, -d, ...) work as
// they do for darktable itself
int core_start = argc;
for(int i = 1; i < argc; i++)
if(!g_strcmp0(argv[i], "--core")) { core_start = i + 1; break; }
const int n_core = argc - core_start;
char **core = (n_core > 0) ? &argv[core_start] : NULL;
// --read-only is ours, and everything past --core goes to dt_init, which
// answers an option it does not know with its whole usage text
if(_has_flag(core, n_core, "--read-only"))
{
fprintf(stderr, "darktable-mcp: --read-only belongs before --core;"
" usage: darktable-mcp [--read-only]"
" [--core ]\n");
return 1;
}
const gboolean read_only = _has_flag(argv, core_start, "--read-only");
// inject defaults only when the user named neither: both mean "touch
// nothing". naming a real library opts into persistence, so their own
// preference applies there
GPtrArray *m = g_ptr_array_new();
g_ptr_array_add(m, (gpointer)"darktable-mcp");
const gboolean adhoc = !_has_flag(core, n_core, "--library")
&& !_has_flag(core, n_core, "--configdir");
if(adhoc)
{
g_ptr_array_add(m, (gpointer)"--library");
g_ptr_array_add(m, (gpointer)":memory:");
}
if(adhoc && !_has_conf(core, n_core, "write_sidecar_files="))
{
g_ptr_array_add(m, (gpointer)"--conf");
g_ptr_array_add(m, (gpointer)"write_sidecar_files=never");
}
for(int i = 0; i < n_core; i++) g_ptr_array_add(m, core[i]);
const int m_argc = m->len;
char **m_arg = (char **)m->pdata;
// keep the protocol channel clean: libdarktable prints to stdout in places, so
// reserve the real stdout for JSON-RPC responses and give darktable stderr
fflush(stdout);
const int saved_fd = dup(STDOUT_FILENO);
FILE *proto_out = fdopen(saved_fd, "w");
dup2(STDERR_FILENO, STDOUT_FILENO);
if(dt_init(m_argc, m_arg, FALSE, TRUE, NULL))
{
fprintf(stderr, "darktable-mcp: dt_init failed\n");
g_ptr_array_free(m, TRUE);
return 1;
}
// tool descriptions and schemas live in the data folder, editable without a rebuild
char datadir[PATH_MAX] = { 0 };
dt_loc_get_datadir(datadir, sizeof(datadir));
// refuse anything that would change the library; the other valve, an
// in-memory catalog, also costs access to the user's images
dt_bridge_set_read_only(read_only);
gchar *tools_file = g_build_filename(datadir, "mcp_tools.json", NULL);
if(mcp_tools_load(tools_file) < 0)
fprintf(stderr, "darktable-mcp: no tools available (could not load '%s')\n",
tools_file);
g_free(tools_file);
mcp_jsonrpc_loop(stdin, proto_out ? proto_out : stdout);
dt_cleanup();
if(proto_out) fflush(proto_out);
g_ptr_array_free(m, TRUE);
return 0;
}