autodev-cli 1.4.151 → 1.4.153
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.
- package/media/gimp/gimp_mcp_client.py +204 -0
- package/media/gimp/gimp_mcp_server.py +306 -211
- package/out/commands/mcp.js +36 -30
- package/out/commands/mcp.js.map +1 -1
- package/out/configManager.js +39 -8
- package/out/configManager.js.map +1 -1
- package/out/core/gimpClientSource.d.ts +4 -0
- package/out/core/gimpClientSource.js +221 -0
- package/out/core/gimpClientSource.js.map +1 -0
- package/out/core/gimpPluginInstall.d.ts +36 -13
- package/out/core/gimpPluginInstall.js +150 -78
- package/out/core/gimpPluginInstall.js.map +1 -1
- package/out/core/gimpPluginSource.d.ts +3 -3
- package/out/core/gimpPluginSource.js +310 -215
- package/out/core/gimpPluginSource.js.map +1 -1
- package/out/core/mcpCatalog.d.ts +25 -17
- package/out/core/mcpCatalog.js +69 -36
- package/out/core/mcpCatalog.js.map +1 -1
- package/out/core/mcpCatalogMigrate.d.ts +9 -1
- package/out/core/mcpCatalogMigrate.js +34 -22
- package/out/core/mcpCatalogMigrate.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,280 +5,375 @@
|
|
|
5
5
|
// Source of truth: media/gimp/gimp_mcp_server.py (GPL-3.0; see media/gimp/LICENSE).
|
|
6
6
|
// Regenerate with: node scripts/gen-gimp-plugin-source.mjs (runs in `npm run build`).
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// Embeds the user-verified GIMP 3.x MCP server plug-in as a string so the installer works even when bundled
|
|
9
|
+
// (VS Code extension esbuild) with no media file on disk.
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.GIMP_PLUGIN_SOURCE = exports.GIMP_PLUGIN_SOURCE_HASH = void 0;
|
|
13
13
|
/** sha256 (first 12 hex) of the canonical media/gimp/gimp_mcp_server.py. */
|
|
14
|
-
exports.GIMP_PLUGIN_SOURCE_HASH = "
|
|
15
|
-
/** Full body of the
|
|
14
|
+
exports.GIMP_PLUGIN_SOURCE_HASH = "e9f5d21b82f7";
|
|
15
|
+
/** Full body of the user-verified GIMP 3.x MCP server plug-in (media/gimp/gimp_mcp_server.py). */
|
|
16
16
|
exports.GIMP_PLUGIN_SOURCE = `#!/usr/bin/env python3
|
|
17
|
-
# -*- coding: utf-8 -*-
|
|
18
|
-
#
|
|
19
|
-
# gimp_mcp_server.py — AutoDev auto-start GIMP 3.x MCP bridge
|
|
20
|
-
#
|
|
21
|
-
# Derived from libreearth/gimp-mcp @49c46dd (GPL-3.0); modified by AutoDev to
|
|
22
|
-
# auto-start as a GIMP 3.x PERSISTENT extension. The upstream plugin registered a
|
|
23
|
-
# manual "<Image>/Filters/Development/ → Start MCP Server" ImageProcedure that the
|
|
24
|
-
# user had to click; this variant registers a PERSISTENT extension so GIMP starts
|
|
25
|
-
# the socket server automatically on launch — no menu click, no open image needed.
|
|
26
|
-
#
|
|
27
|
-
# The socket / command-marshalling logic (start_server, handle_client,
|
|
28
|
-
# execute_command, GLib.idle_add onto GIMP's main thread) is kept from upstream.
|
|
29
|
-
# ONLY the registration + run() model changed to PERSISTENT + defensive ordering.
|
|
30
|
-
#
|
|
31
|
-
# ---------------------------------------------------------------------------
|
|
32
|
-
# GIMP 3.x plugin layout (REQUIRED): this file must live at
|
|
33
|
-
# <gimp-config>/<ver>/plug-ins/gimp_mcp_server/gimp_mcp_server.py
|
|
34
|
-
# i.e. inside a sub-folder whose name == this file's basename. A loose .py is
|
|
35
|
-
# IGNORED by GIMP 3.x. On Linux/macOS it must ALSO be executable (chmod 0o755).
|
|
36
17
|
# ---------------------------------------------------------------------------
|
|
18
|
+
# gimp_mcp_server.py — vendored, USER-VERIFIED GIMP 3.2 MCP bridge (plug-in).
|
|
37
19
|
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
# Foundation, either version 3 of the License, or (at your option) any later
|
|
44
|
-
# version.
|
|
20
|
+
# Derived from libreearth/gimp-mcp (GPL-3.0; see media/gimp/LICENSE) and fixed by
|
|
21
|
+
# the user / AutoDev for GIMP 3.2 (Store / MSIX). Verified working on real GIMP
|
|
22
|
+
# 3.2.44: a menu-triggered PLUGIN (Tools -> MCP / Filters -> Development -> Start
|
|
23
|
+
# MCP Server) that listens on 127.0.0.1:9876 and drives GIMP. Keep the "GIMP MCP
|
|
24
|
+
# Server" window open — GLib.MainLoop() holds the out-of-process plug-in alive.
|
|
45
25
|
#
|
|
46
|
-
# This
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
# You should have received a copy of the GNU General Public License along with
|
|
51
|
-
# this program. If not, see <https://www.gnu.org/licenses/>. A copy is vendored
|
|
52
|
-
# next to this file as LICENSE.
|
|
26
|
+
# This is the user's verified file; the body below is kept byte-for-byte. The
|
|
27
|
+
# lines in THIS block are a provenance comment only — do not edit the code below.
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
53
29
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import json
|
|
57
|
-
import socket
|
|
58
|
-
import threading
|
|
59
|
-
import traceback
|
|
60
|
-
import datetime
|
|
30
|
+
# GIMP MCP Server Plugin (GIMP 3.2)
|
|
31
|
+
# Listens on localhost:9876 so the libreearth gimp-mcp client can drive GIMP.
|
|
61
32
|
|
|
62
33
|
import gi
|
|
63
34
|
gi.require_version('Gimp', '3.0')
|
|
64
35
|
from gi.repository import Gimp
|
|
65
36
|
from gi.repository import GLib
|
|
37
|
+
from gi.repository import Gio
|
|
38
|
+
from gi.repository import GObject
|
|
66
39
|
|
|
40
|
+
import io
|
|
41
|
+
import json
|
|
42
|
+
import os
|
|
43
|
+
import socket
|
|
44
|
+
import sys
|
|
45
|
+
import threading
|
|
46
|
+
import traceback
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
HOST = '127.0.0.1'
|
|
49
|
+
PORT = 9876
|
|
50
|
+
# Store GIMP redirects GLib config dir into the MSIX LocalCache. Also write
|
|
51
|
+
# to the real roaming profile so the log is findable.
|
|
52
|
+
_LOG_PATHS = [
|
|
53
|
+
os.path.join(GLib.get_user_config_dir(), 'GIMP', '3.2', 'gimp_mcp_server.log'),
|
|
54
|
+
os.path.join(os.environ.get('APPDATA', ''), 'GIMP', '3.2', 'gimp_mcp_server.log'),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
_server_lock = threading.Lock()
|
|
58
|
+
_server_started = False
|
|
59
|
+
_exec_globals = None
|
|
60
|
+
_main_loop = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _log(msg):
|
|
64
|
+
line = msg.rstrip() + '\\n'
|
|
65
|
+
for path in _LOG_PATHS:
|
|
66
|
+
if not path:
|
|
67
|
+
continue
|
|
79
68
|
try:
|
|
80
|
-
|
|
69
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
70
|
+
with open(path, 'a', encoding='utf-8') as fh:
|
|
71
|
+
fh.write(line)
|
|
81
72
|
except Exception:
|
|
82
|
-
|
|
73
|
+
pass
|
|
74
|
+
print(msg, flush=True)
|
|
83
75
|
|
|
84
76
|
|
|
85
|
-
|
|
77
|
+
def N_(message):
|
|
78
|
+
return message
|
|
86
79
|
|
|
87
80
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
def _(message):
|
|
82
|
+
return GLib.dgettext(None, message)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _serialize(obj):
|
|
86
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
87
|
+
return obj
|
|
88
|
+
if isinstance(obj, (list, tuple)):
|
|
89
|
+
return [_serialize(item) for item in obj]
|
|
90
|
+
if isinstance(obj, dict):
|
|
91
|
+
return {str(k): _serialize(v) for k, v in obj.items()}
|
|
92
|
+
data = {'type': obj.__class__.__name__}
|
|
93
|
+
if hasattr(obj, 'get_id'):
|
|
94
|
+
try:
|
|
95
|
+
data['id'] = int(obj.get_id())
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
elif hasattr(obj, 'ID'):
|
|
99
|
+
data['id'] = obj.ID
|
|
100
|
+
if hasattr(obj, 'get_name'):
|
|
101
|
+
try:
|
|
102
|
+
data['name'] = obj.get_name()
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
if 'id' not in data and len(data) == 1:
|
|
106
|
+
return str(obj)
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_api(api_path):
|
|
111
|
+
parts = [p for p in str(api_path).split('.') if p]
|
|
112
|
+
if parts and parts[0] == 'Gimp':
|
|
113
|
+
parts = parts[1:]
|
|
114
|
+
elif len(parts) >= 3 and parts[0] == 'gi' and parts[1] == 'repository' and parts[2] == 'Gimp':
|
|
115
|
+
parts = parts[3:]
|
|
116
|
+
|
|
117
|
+
aliases = {
|
|
118
|
+
'list_images': 'get_images',
|
|
119
|
+
'list_fonts': 'get_fonts',
|
|
120
|
+
'fonts_get_list': 'get_fonts',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if not parts:
|
|
124
|
+
return Gimp
|
|
125
|
+
|
|
126
|
+
obj = Gimp
|
|
127
|
+
for part in parts:
|
|
128
|
+
part = aliases.get(part, part)
|
|
129
|
+
obj = getattr(obj, part)
|
|
130
|
+
return obj
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _exec_namespace():
|
|
134
|
+
global _exec_globals
|
|
135
|
+
if _exec_globals is None:
|
|
136
|
+
ns = {
|
|
137
|
+
'__builtins__': __builtins__,
|
|
138
|
+
'__name__': '__main__',
|
|
139
|
+
'Gimp': Gimp,
|
|
140
|
+
'GLib': GLib,
|
|
141
|
+
'Gio': Gio,
|
|
142
|
+
'GObject': GObject,
|
|
143
|
+
'sys': sys,
|
|
144
|
+
'os': os,
|
|
145
|
+
'json': json,
|
|
146
|
+
}
|
|
147
|
+
try:
|
|
148
|
+
gi.require_version('Gegl', '0.4')
|
|
149
|
+
from gi.repository import Gegl
|
|
150
|
+
ns['Gegl'] = Gegl
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
_log(f'Gegl import failed: {exc}')
|
|
153
|
+
try:
|
|
154
|
+
gi.require_version('Babl', '0.1')
|
|
155
|
+
from gi.repository import Babl
|
|
156
|
+
ns['Babl'] = Babl
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
_exec_globals = ns
|
|
160
|
+
return _exec_globals
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _extract_code(args, kwargs):
|
|
164
|
+
if kwargs.get('code'):
|
|
165
|
+
return str(kwargs['code'])
|
|
166
|
+
if not args:
|
|
167
|
+
return ''
|
|
168
|
+
if len(args) >= 2 and args[0] in ('exec', 'eval', 'pyGObject-console'):
|
|
169
|
+
payload = args[1]
|
|
170
|
+
if isinstance(payload, (list, tuple)):
|
|
171
|
+
return '\\n'.join(str(item) for item in payload)
|
|
172
|
+
return str(payload)
|
|
173
|
+
if isinstance(args[0], (list, tuple)):
|
|
174
|
+
return '\\n'.join(str(item) for item in args[0])
|
|
175
|
+
return str(args[0])
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _run_python(code):
|
|
179
|
+
ns = _exec_namespace()
|
|
180
|
+
stdout = io.StringIO()
|
|
181
|
+
old_stdout = sys.stdout
|
|
182
|
+
sys.stdout = stdout
|
|
96
183
|
try:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
184
|
+
exec(code, ns)
|
|
185
|
+
finally:
|
|
186
|
+
sys.stdout = old_stdout
|
|
187
|
+
printed = stdout.getvalue()
|
|
188
|
+
result = ns.get('_result')
|
|
189
|
+
if result is not None:
|
|
190
|
+
return {'printed': printed, 'result': _serialize(result)}
|
|
191
|
+
return {'printed': printed, 'result': None}
|
|
100
192
|
|
|
101
193
|
|
|
102
194
|
class MCPPlugin(Gimp.PlugIn):
|
|
103
|
-
|
|
195
|
+
def do_set_i18n(self, name):
|
|
196
|
+
return False
|
|
197
|
+
|
|
104
198
|
def do_query_procedures(self):
|
|
105
|
-
return [
|
|
199
|
+
return ['plug-in-mcp-server']
|
|
106
200
|
|
|
107
201
|
def do_create_procedure(self, name):
|
|
108
|
-
# PERSISTENT + no args + no menu = GIMP auto-starts this extension at
|
|
109
|
-
# launch. Adding ANY argument, a menu label, or a menu path would turn
|
|
110
|
-
# it back into a user-invoked plug-in and disable auto-start.
|
|
111
202
|
procedure = Gimp.Procedure.new(
|
|
112
|
-
self, name, Gimp.PDBProcType.
|
|
203
|
+
self, name, Gimp.PDBProcType.PLUGIN, self.run, None
|
|
113
204
|
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
205
|
+
procedure.set_menu_label(_('Start MCP Server'))
|
|
206
|
+
procedure.set_documentation(
|
|
207
|
+
_('Starts an MCP server to control GIMP externally'),
|
|
208
|
+
_('Listens on 127.0.0.1:9876 for the GIMP MCP client'),
|
|
209
|
+
name,
|
|
210
|
+
)
|
|
211
|
+
procedure.set_attribution('GIMP MCP', 'GIMP MCP', '2026')
|
|
212
|
+
procedure.set_sensitivity_mask(Gimp.ProcedureSensitivityMask.ALWAYS)
|
|
213
|
+
procedure.add_enum_argument(
|
|
214
|
+
'run-mode',
|
|
215
|
+
_('Run mode'),
|
|
216
|
+
_('The run mode'),
|
|
217
|
+
Gimp.RunMode,
|
|
218
|
+
Gimp.RunMode.INTERACTIVE,
|
|
219
|
+
GObject.ParamFlags.READWRITE,
|
|
220
|
+
)
|
|
221
|
+
procedure.add_menu_path('<Image>/Tools/MCP')
|
|
222
|
+
procedure.add_menu_path('<Image>/Filters/Development/')
|
|
127
223
|
return procedure
|
|
128
224
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
225
|
+
def run(self, procedure, config, data):
|
|
226
|
+
global _main_loop
|
|
227
|
+
started, message = self.ensure_server()
|
|
228
|
+
_log(message)
|
|
229
|
+
if not started:
|
|
230
|
+
return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error(message))
|
|
132
231
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# 1) Hand control back to GIMP core IMMEDIATELY so the UI keeps loading.
|
|
139
|
-
try:
|
|
140
|
-
procedure.persistent_ready()
|
|
141
|
-
log("persistent_ready() OK — extension is live")
|
|
142
|
-
except Exception as e:
|
|
143
|
-
# A binding mismatch on this exact call is the known lock-up cause;
|
|
144
|
-
# log it (do NOT re-raise) so GIMP still finishes launching.
|
|
145
|
-
log("persistent_ready() FAILED (non-fatal): %s\\n%s"
|
|
146
|
-
% (e, traceback.format_exc()))
|
|
147
|
-
|
|
148
|
-
# 2) Start the socket server on a daemon thread. A bind failure (port in
|
|
149
|
-
# use, sandbox denial) MUST NOT crash the process or block launch.
|
|
232
|
+
# GIMP 3 launches Python plug-ins out of process. Returning from run()
|
|
233
|
+
# would exit that process and kill the socket thread. Stay alive and
|
|
234
|
+
# dispatch GLib.idle_add work (API calls) on this main loop.
|
|
235
|
+
# A visible window is required: otherwise "Start MCP Server" looks like
|
|
236
|
+
# it did not open anything.
|
|
150
237
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
except Exception as e:
|
|
155
|
-
log("failed to start MCP server thread (non-fatal): %s\\n%s"
|
|
156
|
-
% (e, traceback.format_exc()))
|
|
157
|
-
|
|
158
|
-
# 3) Keep this extension process alive for the whole GIMP session so the
|
|
159
|
-
# socket stays open and idle_add callbacks can marshal onto core.
|
|
160
|
-
try:
|
|
161
|
-
GLib.MainLoop().run()
|
|
162
|
-
except Exception as e:
|
|
163
|
-
log("main loop exited (non-fatal): %s" % e)
|
|
238
|
+
self._show_status_window(message)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
_log(f'Status window failed: {exc}\\n{traceback.format_exc()}')
|
|
164
241
|
|
|
165
|
-
|
|
242
|
+
_main_loop = GLib.MainLoop()
|
|
243
|
+
_log('Entering GLib.MainLoop to keep MCP server process alive')
|
|
244
|
+
try:
|
|
245
|
+
_main_loop.run()
|
|
246
|
+
except Exception as exc:
|
|
247
|
+
_log(f'MainLoop exited: {exc}')
|
|
166
248
|
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
|
|
167
249
|
|
|
168
|
-
|
|
250
|
+
def _show_status_window(self, message):
|
|
251
|
+
gi.require_version('Gtk', '3.0')
|
|
252
|
+
gi.require_version('GimpUi', '3.0')
|
|
253
|
+
from gi.repository import Gtk
|
|
254
|
+
from gi.repository import GimpUi
|
|
255
|
+
|
|
256
|
+
GimpUi.init('gimp_mcp_server.py')
|
|
257
|
+
window = Gtk.Window(title='GIMP MCP Server')
|
|
258
|
+
window.set_default_size(360, 120)
|
|
259
|
+
window.set_keep_above(False)
|
|
260
|
+
window.set_skip_taskbar_hint(False)
|
|
261
|
+
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
262
|
+
box.set_border_width(16)
|
|
263
|
+
label = Gtk.Label(label=message + '\\nLeave this window open.')
|
|
264
|
+
label.set_line_wrap(True)
|
|
265
|
+
box.pack_start(label, True, True, 0)
|
|
266
|
+
button = Gtk.Button(label='Stop server')
|
|
267
|
+
button.connect('clicked', lambda *_: window.destroy())
|
|
268
|
+
box.pack_start(button, False, False, 0)
|
|
269
|
+
window.add(box)
|
|
270
|
+
window.connect('destroy', self._on_status_closed)
|
|
271
|
+
window.show_all()
|
|
272
|
+
_log('Status window opened')
|
|
273
|
+
|
|
274
|
+
def _on_status_closed(self, *_args):
|
|
275
|
+
global _main_loop
|
|
276
|
+
_log('Status window closed; stopping MCP server')
|
|
277
|
+
if _main_loop is not None:
|
|
278
|
+
_main_loop.quit()
|
|
279
|
+
|
|
280
|
+
def ensure_server(self):
|
|
281
|
+
global _server_started
|
|
282
|
+
with _server_lock:
|
|
283
|
+
if _server_started:
|
|
284
|
+
return True, f'MCP Server already listening on {HOST}:{PORT}'
|
|
285
|
+
try:
|
|
286
|
+
thread = threading.Thread(target=self.start_server, daemon=True)
|
|
287
|
+
thread.start()
|
|
288
|
+
_server_started = True
|
|
289
|
+
return True, f'MCP Server started on {HOST}:{PORT}'
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
return False, f'Failed to start MCP Server: {exc}'
|
|
292
|
+
|
|
169
293
|
def start_server(self):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
sock = None
|
|
294
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
295
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
173
296
|
try:
|
|
174
|
-
sock
|
|
175
|
-
sock.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
except Exception as e:
|
|
180
|
-
# Bind/listen failed (e.g. port already taken by Blender's MCP or a
|
|
181
|
-
# second GIMP). Log + bail out of the thread; GIMP keeps running.
|
|
182
|
-
log("bind/listen failed (non-fatal): %s\\n%s" % (e, traceback.format_exc()))
|
|
183
|
-
try:
|
|
184
|
-
if sock is not None:
|
|
185
|
-
sock.close()
|
|
186
|
-
except Exception:
|
|
187
|
-
pass
|
|
297
|
+
sock.bind((HOST, PORT))
|
|
298
|
+
sock.listen(8)
|
|
299
|
+
_log(f'Server listening on {HOST}:{PORT}')
|
|
300
|
+
except Exception as exc:
|
|
301
|
+
_log(f'Bind/listen failed: {exc}')
|
|
188
302
|
return
|
|
189
303
|
|
|
190
304
|
while True:
|
|
191
305
|
try:
|
|
192
306
|
client, addr = sock.accept()
|
|
193
|
-
|
|
194
|
-
threading.Thread(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
except Exception as e:
|
|
198
|
-
log("accept error: %s" % e)
|
|
307
|
+
_log(f'Connected to {addr}')
|
|
308
|
+
threading.Thread(target=self.handle_client, args=(client,), daemon=True).start()
|
|
309
|
+
except Exception as exc:
|
|
310
|
+
_log(f'Server accept error: {exc}')
|
|
199
311
|
break
|
|
200
|
-
|
|
201
|
-
sock.close()
|
|
202
|
-
except Exception:
|
|
203
|
-
pass
|
|
312
|
+
sock.close()
|
|
204
313
|
|
|
205
314
|
def handle_client(self, client):
|
|
206
315
|
buffer = b''
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
316
|
+
try:
|
|
317
|
+
client.settimeout(120)
|
|
318
|
+
while True:
|
|
319
|
+
chunk = client.recv(65536)
|
|
320
|
+
if not chunk:
|
|
211
321
|
break
|
|
212
|
-
buffer +=
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
322
|
+
buffer += chunk
|
|
323
|
+
while buffer:
|
|
324
|
+
try:
|
|
325
|
+
command = json.loads(buffer.decode('utf-8'))
|
|
326
|
+
buffer = b''
|
|
327
|
+
except json.JSONDecodeError:
|
|
328
|
+
break
|
|
217
329
|
GLib.idle_add(self.execute_command, command, client)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
client.close()
|
|
226
|
-
except Exception:
|
|
227
|
-
pass
|
|
330
|
+
except Exception as exc:
|
|
331
|
+
_log(f'Client error: {exc}')
|
|
332
|
+
finally:
|
|
333
|
+
try:
|
|
334
|
+
client.close()
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
228
337
|
|
|
229
338
|
def execute_command(self, command, client):
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
response = {}
|
|
339
|
+
cmd_type = command.get('type')
|
|
340
|
+
params = command.get('params', {}) or {}
|
|
233
341
|
try:
|
|
234
|
-
cmd_type
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if hasattr(result, "ID"):
|
|
249
|
-
result = {"id": result.ID}
|
|
250
|
-
elif isinstance(result, list):
|
|
251
|
-
result = [self.serialize_gimp_object(item) for item in result]
|
|
342
|
+
if cmd_type == 'ping':
|
|
343
|
+
response = {'status': 'success', 'result': 'pong'}
|
|
344
|
+
elif cmd_type == 'exec':
|
|
345
|
+
code = _extract_code(params.get('args', []), params.get('kwargs', {}))
|
|
346
|
+
if not code:
|
|
347
|
+
code = str(params.get('code', ''))
|
|
348
|
+
response = {'status': 'success', 'result': _run_python(code)}
|
|
349
|
+
elif cmd_type == 'call_api':
|
|
350
|
+
api_path = params.get('api_path', '')
|
|
351
|
+
args = params.get('args', []) or []
|
|
352
|
+
kwargs = params.get('kwargs', {}) or {}
|
|
353
|
+
if api_path in ('exec', 'eval', '__exec__'):
|
|
354
|
+
response = {'status': 'success', 'result': _run_python(_extract_code(args, kwargs))}
|
|
252
355
|
else:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
356
|
+
method = _resolve_api(api_path)
|
|
357
|
+
result = method(*args, **kwargs)
|
|
358
|
+
response = {'status': 'success', 'result': _serialize(result)}
|
|
256
359
|
else:
|
|
257
|
-
response = {
|
|
258
|
-
except Exception as
|
|
259
|
-
response = {
|
|
260
|
-
|
|
360
|
+
response = {'status': 'error', 'message': f'Unknown command: {cmd_type}'}
|
|
361
|
+
except Exception as exc:
|
|
362
|
+
response = {
|
|
363
|
+
'status': 'error',
|
|
364
|
+
'message': str(exc),
|
|
365
|
+
'traceback': traceback.format_exc(),
|
|
366
|
+
}
|
|
367
|
+
_log(f'Command error: {response["message"]}\\n{response["traceback"]}')
|
|
261
368
|
|
|
262
369
|
try:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def serialize_gimp_object(self, obj):
|
|
269
|
-
try:
|
|
270
|
-
if hasattr(obj, "ID"):
|
|
271
|
-
return {"id": obj.ID, "type": obj.__class__.__name__}
|
|
272
|
-
except Exception:
|
|
273
|
-
pass
|
|
274
|
-
return str(obj)
|
|
370
|
+
payload = json.dumps(response).encode('utf-8')
|
|
371
|
+
client.sendall(payload)
|
|
372
|
+
except Exception as exc:
|
|
373
|
+
_log(f'Response send failed: {exc}')
|
|
374
|
+
return False
|
|
275
375
|
|
|
276
376
|
|
|
277
|
-
|
|
278
|
-
Gimp.main(MCPPlugin.__gtype__, sys.argv)
|
|
279
|
-
except Exception as _e:
|
|
280
|
-
# If GIMP's main dispatch itself fails, log it — never let the plug-in
|
|
281
|
-
# process take the launch down with an unhandled traceback.
|
|
282
|
-
log("Gimp.main failed: %s\\n%s" % (_e, traceback.format_exc()))
|
|
377
|
+
Gimp.main(MCPPlugin.__gtype__, sys.argv)
|
|
283
378
|
`;
|
|
284
379
|
//# sourceMappingURL=gimpPluginSource.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gimpPluginSource.js","sourceRoot":"","sources":["../../src/core/gimpPluginSource.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,EAAE;AACF
|
|
1
|
+
{"version":3,"file":"gimpPluginSource.js","sourceRoot":"","sources":["../../src/core/gimpPluginSource.ts"],"names":[],"mappings":";AAAA,8EAA8E;AAC9E,wDAAwD;AACxD,EAAE;AACF,oFAAoF;AACpF,wFAAwF;AACxF,EAAE;AACF,4GAA4G;AAC5G,0DAA0D;AAC1D,8EAA8E;;;AAE9E,4EAA4E;AAC/D,QAAA,uBAAuB,GAAG,cAAc,CAAC;AAEtD,kGAAkG;AACrF,QAAA,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0WjC,CAAC"}
|