@pired/lightroom-mcp 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/LightroomMCP.lrplugin/HandlerAI.lua +466 -0
  4. package/dist/LightroomMCP.lrplugin/HandlerAIMasks.lua +549 -0
  5. package/dist/LightroomMCP.lrplugin/HandlerCatalog.lua +256 -0
  6. package/dist/LightroomMCP.lrplugin/HandlerCollections.lua +337 -0
  7. package/dist/LightroomMCP.lrplugin/HandlerDevelop.lua +1668 -0
  8. package/dist/LightroomMCP.lrplugin/HandlerExport.lua +149 -0
  9. package/dist/LightroomMCP.lrplugin/HandlerImport.lua +108 -0
  10. package/dist/LightroomMCP.lrplugin/HandlerLocalAdjustments.lua +330 -0
  11. package/dist/LightroomMCP.lrplugin/HandlerMetadata.lua +169 -0
  12. package/dist/LightroomMCP.lrplugin/HandlerOrganization.lua +783 -0
  13. package/dist/LightroomMCP.lrplugin/HandlerPreview.lua +189 -0
  14. package/dist/LightroomMCP.lrplugin/HandlerSearch.lua +185 -0
  15. package/dist/LightroomMCP.lrplugin/HandlerSelection.lua +302 -0
  16. package/dist/LightroomMCP.lrplugin/HandlerSpots.lua +313 -0
  17. package/dist/LightroomMCP.lrplugin/HandlerWatermark.lua +59 -0
  18. package/dist/LightroomMCP.lrplugin/Info.lua +25 -0
  19. package/dist/LightroomMCP.lrplugin/JSON.lua +270 -0
  20. package/dist/LightroomMCP.lrplugin/Log.lua +92 -0
  21. package/dist/LightroomMCP.lrplugin/MenuShowStatus.lua +22 -0
  22. package/dist/LightroomMCP.lrplugin/PhotoLookup.lua +39 -0
  23. package/dist/LightroomMCP.lrplugin/PluginInfoProvider.lua +865 -0
  24. package/dist/LightroomMCP.lrplugin/PluginInit.lua +48 -0
  25. package/dist/cli.d.ts +6 -0
  26. package/dist/cli.d.ts.map +1 -0
  27. package/dist/cli.js +31 -0
  28. package/dist/cli.js.map +1 -0
  29. package/dist/configs/claude-code.mcp.json +9 -0
  30. package/dist/configs/claude-desktop.json +12 -0
  31. package/dist/configs/generic-stdio-client.json +10 -0
  32. package/dist/configs/mcp-proxy-http.json +9 -0
  33. package/dist/create-server.d.ts +12 -0
  34. package/dist/create-server.d.ts.map +1 -0
  35. package/dist/create-server.js +17 -0
  36. package/dist/create-server.js.map +1 -0
  37. package/dist/deepseek-harness/README.md +112 -0
  38. package/dist/deepseek-harness/deepseek-mcp-harness.mjs +292 -0
  39. package/dist/dispatcher.d.ts +34 -0
  40. package/dist/dispatcher.d.ts.map +1 -0
  41. package/dist/dispatcher.js +72 -0
  42. package/dist/dispatcher.js.map +1 -0
  43. package/dist/heartbeat.d.ts +17 -0
  44. package/dist/heartbeat.d.ts.map +1 -0
  45. package/dist/heartbeat.js +36 -0
  46. package/dist/heartbeat.js.map +1 -0
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +255 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/install-plugin.d.ts +16 -0
  52. package/dist/install-plugin.d.ts.map +1 -0
  53. package/dist/install-plugin.js +125 -0
  54. package/dist/install-plugin.js.map +1 -0
  55. package/dist/instance-lock.d.ts +5 -0
  56. package/dist/instance-lock.d.ts.map +1 -0
  57. package/dist/instance-lock.js +77 -0
  58. package/dist/instance-lock.js.map +1 -0
  59. package/dist/list-tools-handler.d.ts +16 -0
  60. package/dist/list-tools-handler.d.ts.map +1 -0
  61. package/dist/list-tools-handler.js +50 -0
  62. package/dist/list-tools-handler.js.map +1 -0
  63. package/dist/plugin-liveness.d.ts +48 -0
  64. package/dist/plugin-liveness.d.ts.map +1 -0
  65. package/dist/plugin-liveness.js +75 -0
  66. package/dist/plugin-liveness.js.map +1 -0
  67. package/dist/plugin-socket.d.ts +32 -0
  68. package/dist/plugin-socket.d.ts.map +1 -0
  69. package/dist/plugin-socket.js +97 -0
  70. package/dist/plugin-socket.js.map +1 -0
  71. package/dist/ports.d.ts +3 -0
  72. package/dist/ports.d.ts.map +1 -0
  73. package/dist/ports.js +22 -0
  74. package/dist/ports.js.map +1 -0
  75. package/dist/token.d.ts +3 -0
  76. package/dist/token.d.ts.map +1 -0
  77. package/dist/token.js +23 -0
  78. package/dist/token.js.map +1 -0
  79. package/dist/tool-contracts.d.ts +13 -0
  80. package/dist/tool-contracts.d.ts.map +1 -0
  81. package/dist/tool-contracts.js +1422 -0
  82. package/dist/tool-contracts.js.map +1 -0
  83. package/dist/tool-handler.d.ts +23 -0
  84. package/dist/tool-handler.d.ts.map +1 -0
  85. package/dist/tool-handler.js +96 -0
  86. package/dist/tool-handler.js.map +1 -0
  87. package/dist/validate-args.d.ts +7 -0
  88. package/dist/validate-args.d.ts.map +1 -0
  89. package/dist/validate-args.js +37 -0
  90. package/dist/validate-args.js.map +1 -0
  91. package/dist/version.d.ts +2 -0
  92. package/dist/version.d.ts.map +1 -0
  93. package/dist/version.js +4 -0
  94. package/dist/version.js.map +1 -0
  95. package/dist/wait-until.d.ts +7 -0
  96. package/dist/wait-until.d.ts.map +1 -0
  97. package/dist/wait-until.js +15 -0
  98. package/dist/wait-until.js.map +1 -0
  99. package/package.json +73 -0
@@ -0,0 +1,48 @@
1
+ local LrPrefs = import 'LrPrefs'
2
+ local LrTasks = import 'LrTasks'
3
+ local LrFunctionContext = import 'LrFunctionContext'
4
+
5
+ local PluginInfoProvider = require 'PluginInfoProvider'
6
+ local Log = require 'Log'
7
+
8
+ -- LrInitPlugin runs on plugin load AND on "Reload Plug-in", but NOT when
9
+ -- Lightroom merely renders the Plug-in Manager panel. On reload a prior
10
+ -- instance's state may still live on _G with `running` stale-true; clear
11
+ -- it so the auto-start below can bind and so a later Plug-in Manager open
12
+ -- reports honest status. Doing this here (not in the InfoProvider module
13
+ -- body) is what keeps opening the manager from killing a live server
14
+ -- (issues #121, #137).
15
+ PluginInfoProvider.resetForReload()
16
+
17
+ local prefs = LrPrefs.prefsForPlugin()
18
+ local autoStart = prefs.autoStartServer
19
+ if autoStart == nil then
20
+ autoStart = true
21
+ prefs.autoStartServer = true
22
+ end
23
+
24
+ if autoStart then
25
+ -- Must use postAsyncTaskWithContext, NOT LrTasks.startAsyncTask. A bare
26
+ -- startAsyncTask here runs in THIS init script's function context; when
27
+ -- LrInitPlugin returns, that context is torn down and the task is
28
+ -- cancelled mid-sleep before startServer() ever runs. On macOS that left
29
+ -- the server stopped after launch despite auto-start being on (issue
30
+ -- #128). A fresh context survives the init script returning.
31
+ LrFunctionContext.postAsyncTaskWithContext("LightroomMCPAutoStart", function()
32
+ -- Brief yield so any prior-instance context cancel (from Reload
33
+ -- Plug-in) can flush and release ports before we try to bind them.
34
+ LrTasks.sleep(0.5)
35
+ -- Guard startServer. This task owns an independent context with no
36
+ -- cleanup handler, so an unhandled throw (token write, socket bind)
37
+ -- would tear the context down and leave auto-start dead with nothing
38
+ -- in the log -- the same invisible "server stopped after launch"
39
+ -- failure #128 was about. Surface it instead.
40
+ local ok, err = LrTasks.pcall(PluginInfoProvider.startServer)
41
+ if not ok then
42
+ -- Route through Log (not raw LrLogger) so this lands in the
43
+ -- OS-resolved file sink the user is told to check; the #128
44
+ -- "server died with nothing in the log" failure mode lived here.
45
+ Log.error("Auto-start failed: " .. tostring(err))
46
+ end
47
+ end)
48
+ end
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export interface ParsedCli {
2
+ command: "stdio" | "install-plugin" | "help" | "version";
3
+ }
4
+ export declare function parseCli(argv: string[]): ParsedCli;
5
+ export declare function helpText(): string;
6
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,GAAG,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAAC;CAC1D;AAeD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAYlD;AAED,wBAAgB,QAAQ,IAAI,MAAM,CAEjC"}
package/dist/cli.js ADDED
@@ -0,0 +1,31 @@
1
+ const HELP = `pired-lightroom-mcp (@pired/lightroom-mcp) — MCP bridge to Adobe Lightroom Classic (AI Denoise, spots, masks)
2
+
3
+ USAGE
4
+ pired-lightroom-mcp [stdio] Run MCP over stdio (default)
5
+ pired-lightroom-mcp install-plugin Install bundled .lrplugin into Lightroom Modules folder
6
+ pired-lightroom-mcp --help | --version
7
+
8
+ ENV
9
+ LIGHTROOM_MCP_REQUEST_PORT plugin request port (default 58763)
10
+ LIGHTROOM_MCP_RESPONSE_PORT plugin response port (default 58764)
11
+ LIGHTROOM_MCP_TOKEN_PATH auth token file (default ~/.config/lightroom-mcp/token)
12
+ `;
13
+ export function parseCli(argv) {
14
+ const args = argv.slice(2);
15
+ if (args.length === 0)
16
+ return { command: "stdio" };
17
+ const first = args[0];
18
+ if (first === "--help" || first === "-h")
19
+ return { command: "help" };
20
+ if (first === "--version" || first === "-v")
21
+ return { command: "version" };
22
+ if (first === "stdio")
23
+ return { command: "stdio" };
24
+ if (first === "install-plugin")
25
+ return { command: "install-plugin" };
26
+ throw new Error(`Unknown command: ${first}\n\n${HELP}`);
27
+ }
28
+ export function helpText() {
29
+ return HELP;
30
+ }
31
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,MAAM,IAAI,GAAG;;;;;;;;;;;CAWZ,CAAC;AAEF,MAAM,UAAU,QAAQ,CAAC,IAAc;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAEnD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACrE,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IAE3E,IAAI,KAAK,KAAK,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IACnD,IAAI,KAAK,KAAK,gBAAgB;QAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAErE,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,OAAO,IAAI,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Claude Code: añade este bloque a .mcp.json del proyecto o usa `claude mcp add lightroom -- npx -y @pired/lightroom-mcp`. Desde fuente: node <ruta>/pired-lightroom-mcp/server/dist/index.js",
3
+ "mcpServers": {
4
+ "lightroom": {
5
+ "command": "npx",
6
+ "args": ["-y", "@pired/lightroom-mcp"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "_comment": "Claude Desktop (Windows): %APPDATA%\\Claude\\claude_desktop_config.json — requiere el paquete publicado en npm (npm i -g @pired/lightroom-mcp). Alternativa desde fuente: command=node y args=[\"<RUTA>\\\\pired-lightroom-mcp\\\\server\\\\dist\\\\index.js\"]",
3
+ "mcpServers": {
4
+ "lightroom": {
5
+ "command": "cmd",
6
+ "args": ["/c", "npx", "-y", "@pired/lightroom-mcp"],
7
+ "env": {
8
+ "LIGHTROOM_MCP_TOOLS": "all"
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "_comment": "Clientes MCP genéricos que aceptan un comando stdio (Cherry Studio, 5ire, Cline, Roo Code, etc.): comando + argumentos. En Cherry Studio: Ajustes > Servidores MCP > Añadir (tipo stdio). Desde fuente: command=node, args=[\"<RUTA>/pired-lightroom-mcp/server/dist/index.js\"]",
3
+ "name": "lightroom",
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": ["-y", "@pired/lightroom-mcp"],
7
+ "env": {
8
+ "LIGHTROOM_MCP_TOOLS": "all"
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Open WebUI / LibreChat y otros frontends que solo hablan MCP-HTTP: monta un puente stdio→HTTP con mcp-proxy (pip install mcp-proxy) y registra la URL que imprime. Ejemplo: mcp-proxy --port 8808 -- npx -y @pired/lightroom-mcp (o, desde fuente: node <RUTA>/pired-lightroom-mcp/server/dist/index.js)",
3
+ "url": "http://127.0.0.1:8808/mcp",
4
+ "_notes": [
5
+ "mcp-proxy expone el servidor stdio como SSE/HTTP en localhost; no abras ese puerto fuera de tu máquina.",
6
+ "LibreChat: archivo librechat.yaml → mcpServers: lightroom: serverUrl: http://127.0.0.1:8808/mcp",
7
+ "Open WebUI: ajustes > Tools > añadir servidor MCP con esa misma URL."
8
+ ]
9
+ }
@@ -0,0 +1,12 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import type { Dispatcher } from "./dispatcher.js";
3
+ export interface ServerDeps {
4
+ dispatcher: Pick<Dispatcher, "call">;
5
+ isReady: () => boolean;
6
+ notReadyMessage?: () => string;
7
+ settleReadiness?: () => Promise<void>;
8
+ /** Override for the LIGHTROOM_MCP_TOOLS env var (tests). */
9
+ toolFilter?: string;
10
+ }
11
+ export declare function createMcpServer(deps: ServerDeps): Server;
12
+ //# sourceMappingURL=create-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAKnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKlD,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,MAAM,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAmBxD"}
@@ -0,0 +1,17 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
3
+ import { createCallToolHandler } from "./tool-handler.js";
4
+ import { listToolsHandler, TOOL_FILTER_ENV_VAR } from "./list-tools-handler.js";
5
+ import { VERSION } from "./version.js";
6
+ export function createMcpServer(deps) {
7
+ const server = new Server({ name: "lightroom-mcp-server", version: VERSION }, { capabilities: { tools: {} } });
8
+ const toolFilter = deps.toolFilter ?? process.env[TOOL_FILTER_ENV_VAR];
9
+ server.setRequestHandler(ListToolsRequestSchema, async () => listToolsHandler(toolFilter, (m) => console.error(m)));
10
+ const callTool = createCallToolHandler(deps);
11
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
12
+ const { name, arguments: args } = request.params;
13
+ return callTool(name, args ?? {});
14
+ });
15
+ return server;
16
+ }
17
+ //# sourceMappingURL=create-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-server.js","sourceRoot":"","sources":["../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAWvC,MAAM,UAAU,eAAe,CAAC,IAAgB;IAC9C,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,EAClD,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IAEF,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAEvE,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAC1D,gBAAgB,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CACtD,CAAC;IAEF,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QACjD,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,112 @@
1
+ # DeepSeek harness para @pired/lightroom-mcp
2
+
3
+ Este harness te permite **usar Lightroom Classic con DeepSeek en lugar de (o además de) Claude**.
4
+ Es un cliente MCP autocontenido: habla el protocolo MCP con el servidor `@pired/lightroom-mcp`
5
+ (a través de stdio, JSON-RPC), convierte las 55 herramientas al formato de *function calling*
6
+ de la API de DeepSeek y ejecuta en bucle las llamadas que el modelo pida.
7
+
8
+ **No tiene dependencias**: solo necesitas Node 18 o superior (usa `fetch` global) y una clave
9
+ de API de DeepSeek.
10
+
11
+ ## Requisitos
12
+
13
+ 1. Lightroom Classic (Windows) abierto, con el plugin instalado y **Start Server** pulsado
14
+ en Plug-in Manager (igual que para Claude Desktop; ver el README principal).
15
+ 2. Node 18+.
16
+ 3. Clave de API de DeepSeek ([platform.deepseek.com](https://platform.deepseek.com)).
17
+
18
+ ## Uso
19
+
20
+ **A — con el paquete instalado desde npm** (recomendado):
21
+
22
+ ```bash
23
+ npm install -g @pired/lightroom-mcp
24
+ node "$(npm root -g)/@pired/lightroom-mcp/dist/deepseek-harness/deepseek-mcp-harness.mjs"
25
+ ```
26
+
27
+ En PowerShell (Windows):
28
+
29
+ ```powershell
30
+ npm install -g @pired/lightroom-mcp
31
+ $env:DEEPSEEK_API_KEY = "sk-..."
32
+ node "$(npm root -g)\@pired\lightroom-mcp\dist\deepseek-harness\deepseek-mcp-harness.mjs"
33
+ ```
34
+
35
+ **B — desde el código fuente** (zip / repo):
36
+
37
+ ```bash
38
+ cd pired-lightroom-mcp/server # el servidor ya compilado (server/dist)
39
+ npm install && npm run build # solo la primera vez
40
+
41
+ DEEPSEEK_API_KEY=sk-... node ../examples/deepseek-harness/deepseek-mcp-harness.mjs
42
+ ```
43
+
44
+ En PowerShell (Windows):
45
+
46
+ ```powershell
47
+ cd pired-lightroom-mcp\server
48
+ $env:DEEPSEEK_API_KEY = "sk-..."
49
+ node ..\examples\deepseek-harness\deepseek-mcp-harness.mjs
50
+ ```
51
+
52
+ Escribes instrucciones en español natural y DeepSeek irá llamando a las herramientas
53
+ (`search_photos`, `set_develop_settings`, `add_ai_mask`…) hasta responder. Escribe `salir`
54
+ para terminar.
55
+
56
+ ## Variables de entorno
57
+
58
+ | Variable | Obligatoria | Default | Descripción |
59
+ |---|---|---|---|
60
+ | `DEEPSEEK_API_KEY` | sí | — | Clave de la API de DeepSeek |
61
+ | `DEEPSEEK_MODEL` | no | `deepseek-chat` | Modelo (`deepseek-reasoner` también funciona) |
62
+ | `DEEPSEEK_BASE_URL` | no | `https://api.deepseek.com` | Endpoint alternativo (p. ej. compatibles) |
63
+ | `LIGHTROOM_MCP_SERVER` | no | auto | Ruta del servidor MCP: `server/dist/index.js` del repo, el `dist` empaquetado en npm o la instalación global |
64
+ | `LIGHTROOM_MCP_TOOLS` | no | `all` | Subconjunto de herramientas expuestas al modelo, separadas por comas |
65
+
66
+ ### Filtrar herramientas (recomendado si el contexto pesa)
67
+
68
+ Con 55 herramientas el listado consume ~8–12 mil tokens de contexto por turno. Si tu
69
+ presupuesto es ajustado, expón solo las que necesites:
70
+
71
+ ```bash
72
+ LIGHTROOM_MCP_TOOLS="search_photos,get_photo_preview,set_develop_settings,copy_develop_settings" \
73
+ DEEPSEEK_API_KEY=sk-... node ../examples/deepseek-harness/deepseek-mcp-harness.mjs
74
+ ```
75
+
76
+ El filtro se aplica dentro del servidor MCP (`tools/list`), así que también sirve para
77
+ cualquier otro cliente que respete esa variable.
78
+
79
+ ## El patrón "preview gate" con DeepSeek
80
+
81
+ El prompt del sistema ya instruye al modelo para que:
82
+
83
+ 1. edite **una sola foto de muestra**,
84
+ 2. genere la vista previa con `get_photo_preview`,
85
+ 3. te pida revisar el JPEG (la ruta se imprime en la respuesta) y
86
+ 4. solo tras tu aprobación replique los ajustes al lote con `copy_develop_settings`.
87
+
88
+ **Diferencia honesta con Claude:** deepseek-chat no acepta imágenes por la API, así que el
89
+ modelo no puede "mirar" la vista previa él mismo — te indica la ruta del archivo para que la
90
+ abras tú. Con Claude Desktop (visón nativa) el asistente sí ve la imagen en línea. Si esa
91
+ verificación automática te importa, usa Claude para los lotes grandes; el mismo servidor
92
+ sirve a ambos.
93
+
94
+ ## Otros clientes (sin script)
95
+
96
+ El servidor es MCP estándar sobre stdio, por lo que funciona con cualquier cliente MCP:
97
+
98
+ - **Claude Desktop**: ver `examples/configs/claude-desktop.json` (y el README principal).
99
+ - **Claude Code**: `examples/configs/claude-code.mcp.json`.
100
+ - **Cherry Studio / 5ire / Cline / Roo Code**: `examples/configs/generic-stdio-client.json`.
101
+ - **Open WebUI / LibreChat** (necesitan MCP sobre HTTP): puente con
102
+ `pip install mcp-proxy && mcp-proxy --port 8808 -- node server/dist/index.js` y registra
103
+ la URL — ver `examples/configs/mcp-proxy-http.json`.
104
+
105
+ ## Solución de problemas
106
+
107
+ | Síntoma | Causa probable |
108
+ |---|---|
109
+ | `No encuentro el servidor MCP` | Ni el paquete instalado (`npm i -g @pired/lightroom-mcp`) ni el repo compilado (`cd server && npm install && npm run build`) |
110
+ | Las herramientas responden "plugin not connected" | Lightroom no está abierto o no pulsaste **Start Server** en el Plug-in Manager |
111
+ | `DeepSeek API 401` | Clave inválida o sin saldo |
112
+ | El modelo "no ve" las fotos | deepseek-chat no tiene visión; revisa tú las rutas de preview que imprime |
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deepseek-mcp-harness.mjs — usa @pired/lightroom-mcp con DeepSeek (function calling).
4
+ *
5
+ * Un "harness" MCP mínimo y sin dependencias: habla el protocolo MCP sobre
6
+ * stdio (JSON-RPC) con el servidor de @pired/lightroom-mcp, convierte las
7
+ * herramientas al formato de function-calling de la API de DeepSeek y ejecuta
8
+ * en bucle las llamadas que el modelo pida. Solo necesita Node 18+ (fetch
9
+ * global) y DEEPSEEK_API_KEY.
10
+ *
11
+ * Uso:
12
+ * cd pired-lightroom-mcp/server # el servidor debe estar compilado (dist/)
13
+ * DEEPSEEK_API_KEY=sk-... node ../examples/deepseek-harness/deepseek-mcp-harness.mjs
14
+ *
15
+ * (instalación npm: npm i -g @pired/lightroom-mcp y luego)
16
+ * node "$(npm root -g)/@pired/lightroom-mcp/dist/deepseek-harness/deepseek-mcp-harness.mjs"
17
+ *
18
+ * Variables de entorno:
19
+ * DEEPSEEK_API_KEY (obligatoria) clave de la API de DeepSeek
20
+ * DEEPSEEK_MODEL (opcional) default "deepseek-chat" (deepseek-reasoner también sirve)
21
+ * DEEPSEEK_BASE_URL (opcional) default https://api.deepseek.com
22
+ * LIGHTROOM_MCP_SERVER (opcional) ruta a dist/index.js del servidor MCP
23
+ * (default: repo server/dist, dist empaquetado o npm -g)
24
+ * LIGHTROOM_MCP_TOOLS (opcional) subconjunto de herramientas expuestas, p. ej.
25
+ * "search_photos,get_photo_preview,set_develop_settings"
26
+ *
27
+ * Requisito previo: Lightroom Classic abierto con el plugin arrancado
28
+ * ("Start Server" en Plug-in Manager), igual que con Claude Desktop.
29
+ */
30
+
31
+ import { spawn, execSync } from "node:child_process";
32
+ import { existsSync } from "node:fs";
33
+ import path from "node:path";
34
+ import readline from "node:readline";
35
+ import { fileURLToPath } from "node:url";
36
+
37
+ const here = path.dirname(fileURLToPath(import.meta.url));
38
+
39
+ // El harness vive en dos sitios: examples/deepseek-harness/ en el repo y
40
+ // dist/deepseek-harness/ dentro del paquete npm. Probamos ambas rutas (y la
41
+ // instalación global de npm) antes de rendirnos.
42
+ const candidates = [
43
+ process.env.LIGHTROOM_MCP_SERVER,
44
+ path.resolve(here, "..", "..", "server", "dist", "index.js"), // repo: examples/deepseek-harness/
45
+ path.resolve(here, "..", "index.js"), // npm: dist/deepseek-harness/
46
+ ].filter(Boolean);
47
+
48
+ let serverPath = candidates.find((p) => existsSync(p));
49
+ if (!serverPath) {
50
+ // Instalación global: npm root -g → @pired/lightroom-mcp/dist/index.js
51
+ try {
52
+ const root = execSync("npm root -g", { encoding: "utf8" }).trim();
53
+ const globalPath = path.join(root, "@pired", "lightroom-mcp", "dist", "index.js");
54
+ if (existsSync(globalPath)) serverPath = globalPath;
55
+ } catch {
56
+ // npm no disponible en PATH — caemos al error de abajo.
57
+ }
58
+ }
59
+
60
+ if (!serverPath) {
61
+ console.error(`No encuentro el servidor MCP (busqué: ${candidates.join(" ; ")})`);
62
+ console.error("Instalá el paquete (npm i -g @pired/lightroom-mcp) o compilá desde fuente (cd server && npm install && npm run build), o apunta LIGHTROOM_MCP_SERVER a dist/index.js.");
63
+ process.exit(1);
64
+ }
65
+
66
+ if (!process.env.DEEPSEEK_API_KEY) {
67
+ console.error("Falta DEEPSEEK_API_KEY (obtén una en https://platform.deepseek.com).");
68
+ process.exit(1);
69
+ }
70
+
71
+ const MODEL = process.env.DEEPSEEK_MODEL || "deepseek-chat";
72
+ const BASE_URL = (process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com").replace(/\/$/, "");
73
+ const MAX_TOOL_ROUNDS = 15;
74
+
75
+ // ---------------------------------------------------------------- MCP client
76
+
77
+ const child = spawn(process.execPath, [serverPath], {
78
+ stdio: ["pipe", "pipe", "pipe"],
79
+ env: { ...process.env },
80
+ });
81
+
82
+ let nextId = 1;
83
+ const pending = new Map();
84
+
85
+ child.stdout.setEncoding("utf8");
86
+ readline.createInterface({ input: child.stdout }).on("line", (line) => {
87
+ if (!line.trim()) return;
88
+ let msg;
89
+ try {
90
+ msg = JSON.parse(line);
91
+ } catch {
92
+ console.error(`[mcp] línea no-JSON ignorada: ${line.slice(0, 120)}`);
93
+ return;
94
+ }
95
+ if (msg.id !== undefined && pending.has(msg.id)) {
96
+ const { resolve, reject } = pending.get(msg.id);
97
+ pending.delete(msg.id);
98
+ if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)));
99
+ else resolve(msg.result);
100
+ }
101
+ // Las notificaciones del servidor no se usan en este harness.
102
+ });
103
+
104
+ child.stderr.setEncoding("utf8");
105
+ child.stderr.on("data", (d) => process.stderr.write(`[mcp] ${d}`));
106
+ child.on("exit", (code) => {
107
+ console.error(`[mcp] servidor terminado (código ${code}).`);
108
+ process.exit(code ?? 0);
109
+ });
110
+
111
+ function request(method, params) {
112
+ const id = nextId++;
113
+ const message = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
114
+ return new Promise((resolve, reject) => {
115
+ pending.set(id, { resolve, reject });
116
+ child.stdin.write(message);
117
+ });
118
+ }
119
+
120
+ async function mcpInitialize() {
121
+ const result = await request("initialize", {
122
+ protocolVersion: "2024-11-05",
123
+ capabilities: {},
124
+ clientInfo: { name: "deepseek-mcp-harness", version: "1.0.0" },
125
+ });
126
+ child.stdin.write(
127
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n",
128
+ );
129
+ return result;
130
+ }
131
+
132
+ async function mcpListTools() {
133
+ return request("tools/list", {});
134
+ }
135
+
136
+ async function mcpCallTool(name, args) {
137
+ return request("tools/call", { name, arguments: args ?? {} });
138
+ }
139
+
140
+ // ------------------------------------------------------ DeepSeek API client
141
+
142
+ async function deepseekChat(messages, tools) {
143
+ const body = { model: MODEL, messages, stream: false };
144
+ if (tools && tools.length > 0) {
145
+ body.tools = tools;
146
+ body.tool_choice = "auto";
147
+ }
148
+ const res = await fetch(`${BASE_URL}/chat/completions`, {
149
+ method: "POST",
150
+ headers: {
151
+ "Content-Type": "application/json",
152
+ Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
153
+ },
154
+ body: JSON.stringify(body),
155
+ });
156
+ if (!res.ok) {
157
+ const text = await res.text().catch(() => "");
158
+ throw new Error(`DeepSeek API ${res.status}: ${text.slice(0, 400)}`);
159
+ }
160
+ return (await res.json()).choices[0].message;
161
+ }
162
+
163
+ // ------------------------------------------------------------------ helpers
164
+
165
+ /** MCP tool → formato de tools de la API de DeepSeek. */
166
+ function toDeepseekTool(tool) {
167
+ return {
168
+ type: "function",
169
+ function: {
170
+ name: tool.name,
171
+ description: tool.description || "",
172
+ parameters: tool.inputSchema || { type: "object", properties: {} },
173
+ },
174
+ };
175
+ }
176
+
177
+ /**
178
+ * El resultado de una herramienta MCP llega como bloques de contenido. Los
179
+ * bloques de texto se concatenan; los de imagen (get_photo_preview) se
180
+ * anotan con su ruta porque deepseek-chat no acepta imágenes — el humano
181
+ * las abre desde el path mientras decide si continuar con el lote.
182
+ */
183
+ function toolResultToText(content) {
184
+ const parts = [];
185
+ for (const block of content ?? []) {
186
+ if (block.type === "text") {
187
+ parts.push(block.text);
188
+ } else if (block.type === "image") {
189
+ parts.push(
190
+ "[El servidor adjuntó una imagen de la vista previa. Ábrela desde la ruta " +
191
+ "indicada arriba para revisarla antes de aplicar el lote.]",
192
+ );
193
+ }
194
+ }
195
+ return parts.join("\n") || "(sin salida)";
196
+ }
197
+
198
+ const SYSTEM_PROMPT = `Eres un asistente de edición fotográfica que controla Adobe Lightroom
199
+ Classic a través de un servidor MCP local (@pired/lightroom-mcp). Trabajo con fotos reales
200
+ del catálogo del usuario: antes de tocar un lote grande, aplica el patrón "preview gate":
201
+
202
+ 1. Busca o selecciona las fotos (search_photos / get_selected_photos).
203
+ 2. Edita UNA foto de muestra (set_develop_settings, add_ai_mask, apply_develop_preset, ...).
204
+ 3. Genera la vista previa (get_photo_preview) y dile al usuario qué archivo abrir
205
+ para revisar el resultado antes de continuar.
206
+ 4. Solo tras la aprobación, aplica los mismos ajustes al resto con copy_develop_settings.
207
+
208
+ Consejos: los ids de foto vienen como números; reutilízalos tal cual. Para máscaras
209
+ IA necesitas Lightroom Classic 12.4+. get_develop_settings (fields=basic) muestra el
210
+ estado actual. create_snapshot guarda un punto de restauración antes de cambios
211
+ arriesgados. Nunca llames a remove_from_catalog sin pedírselo explícitamente al
212
+ usuario. Responde en el idioma del usuario.`;
213
+
214
+ // -------------------------------------------------------------------- main
215
+
216
+ async function main() {
217
+ console.error(`[harness] arrancando servidor MCP: ${serverPath}`);
218
+ const init = await mcpInitialize();
219
+ const toolsResult = await mcpListTools();
220
+ const mcpTools = toolsResult.tools ?? [];
221
+ const deepseekTools = mcpTools.map(toDeepseekTool);
222
+
223
+ console.error(
224
+ `[harness] MCP ${init.serverInfo?.name} v${init.serverInfo?.version} — ` +
225
+ `${mcpTools.length} herramientas expuestas${process.env.LIGHTROOM_MCP_TOOLS ? " (filtradas)" : ""}.`,
226
+ );
227
+ console.error(`[harness] modelo: ${MODEL} — escribe tu instrucción, o "salir" para terminar.\n`);
228
+
229
+ const messages = [{ role: "system", content: SYSTEM_PROMPT }];
230
+
231
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
232
+ const ask = () =>
233
+ new Promise((resolve) => rl.question("tú > ", (answer) => resolve(answer.trim())));
234
+
235
+ while (true) {
236
+ const input = await ask();
237
+ if (input === "") continue;
238
+ if (input === "salir" || input === "exit" || input === "quit") break;
239
+
240
+ messages.push({ role: "user", content: input });
241
+
242
+ try {
243
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
244
+ const message = await deepseekChat(messages, deepseekTools);
245
+ messages.push(message);
246
+
247
+ const calls = message.tool_calls ?? [];
248
+ if (calls.length === 0) {
249
+ console.log(`\nDeepSeek > ${message.content ?? "(sin texto)"}\n`);
250
+ break;
251
+ }
252
+
253
+ for (const call of calls) {
254
+ const name = call.function.name;
255
+ let args = {};
256
+ try {
257
+ args = JSON.parse(call.function.arguments || "{}");
258
+ } catch {
259
+ args = {};
260
+ }
261
+ const argsPreview = JSON.stringify(args);
262
+ process.stdout.write(
263
+ ` -> ${name}(${argsPreview.length > 160 ? argsPreview.slice(0, 160) + "…" : argsPreview}) `,
264
+ );
265
+ const result = await mcpCallTool(name, args);
266
+ const text = toolResultToText(result.content);
267
+ const failed = result.isError === true;
268
+ console.log(failed ? "✗" : "✓");
269
+ messages.push({
270
+ role: "tool",
271
+ tool_call_id: call.id,
272
+ content: failed ? `ERROR: ${text}` : text.slice(0, 12000),
273
+ });
274
+ }
275
+ }
276
+ } catch (err) {
277
+ console.error(`[harness] error: ${err.message}`);
278
+ messages.push({
279
+ role: "user",
280
+ content: `(Nota del sistema: la última llamada falló con "${err.message}". Avisa al usuario y continúa con cuidado.)`,
281
+ });
282
+ }
283
+ }
284
+
285
+ rl.close();
286
+ child.stdin.end();
287
+ }
288
+
289
+ main().catch((err) => {
290
+ console.error(`[harness] fatal: ${err.message}`);
291
+ process.exit(1);
292
+ });
@@ -0,0 +1,34 @@
1
+ export interface PluginResponse {
2
+ id: string;
3
+ result?: unknown;
4
+ error?: string;
5
+ }
6
+ export interface DispatcherOptions {
7
+ send: (line: string) => boolean;
8
+ getToken: () => string;
9
+ timeoutMs?: number;
10
+ /**
11
+ * Per-action timeout overrides (ms), keyed by action name. Long-running
12
+ * actions like batch export/import need far more than the default before a
13
+ * healthy plugin can reply; with too short a timeout the call reports a
14
+ * false failure, and the plugin's eventual (correct) response arrives after
15
+ * the pending entry is gone, so it is dropped as an unknown id. Actions
16
+ * absent here -- or mapped to a non-positive value -- use `timeoutMs`.
17
+ */
18
+ actionTimeoutsMs?: Record<string, number>;
19
+ log?: (msg: string) => void;
20
+ }
21
+ export declare class Dispatcher {
22
+ private pending;
23
+ private idCounter;
24
+ private readonly timeoutMs;
25
+ private readonly actionTimeoutsMs;
26
+ private readonly send;
27
+ private readonly getToken;
28
+ private readonly log;
29
+ constructor(opts: DispatcherOptions);
30
+ handleResponseLine(line: string): void;
31
+ call(action: string, params: unknown, timeoutOverrideMs?: number): Promise<PluginResponse>;
32
+ pendingCount(): number;
33
+ }
34
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../src/dispatcher.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAQD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAChC,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyB;IAC1D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAwB;IAE5C,YAAY,IAAI,EAAE,iBAAiB,EAMlC;IAED,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAiBrC;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAmC/F;IAED,YAAY,IAAI,MAAM,CAErB;CACF"}