/* 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; }