sublime-mcp 1.3.0 → 1.3.3
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/AGENT_GUIDE.md +188 -0
- package/MCP Commander.sublime-commands +6 -0
- package/README.md +86 -220
- package/agents.md +32 -0
- package/index.js +423 -393
- package/package.json +32 -32
package/index.js
CHANGED
|
@@ -1,393 +1,423 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
const port = process.platform === 'win32' ? 9500 : 9501;
|
|
7
|
-
const BASE = process.env.SUBLIME_MCP_BASE ?? `http://127.0.0.1:${port}`;
|
|
8
|
-
const TIMEOUT = 10_000;
|
|
9
|
-
|
|
10
|
-
process.stderr.write(`mcp-commander: BASE=${BASE} platform=${process.platform}\n`);
|
|
11
|
-
|
|
12
|
-
async function get(endpoint, params = {}) {
|
|
13
|
-
const url = new URL(endpoint, BASE);
|
|
14
|
-
for (const [k, v] of Object.entries(params)) {
|
|
15
|
-
url.searchParams.set(k, String(v));
|
|
16
|
-
}
|
|
17
|
-
const r = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT) });
|
|
18
|
-
if (!r.ok) throw new Error(`HTTP ${r.status} from ${endpoint}`);
|
|
19
|
-
return r.json();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function post(endpoint, body = {}) {
|
|
23
|
-
const r = await fetch(new URL(endpoint, BASE), {
|
|
24
|
-
method: 'POST',
|
|
25
|
-
headers: { 'Content-Type': 'application/json' },
|
|
26
|
-
body: JSON.stringify(body),
|
|
27
|
-
signal: AbortSignal.timeout(TIMEOUT),
|
|
28
|
-
});
|
|
29
|
-
if (!r.ok) throw new Error(`HTTP ${r.status} from ${endpoint}`);
|
|
30
|
-
return r.json();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function ok(data) {
|
|
34
|
-
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const server = new McpServer({ name: 'sublime-mcp', version: '1.
|
|
38
|
-
|
|
39
|
-
// ── no-parameter passthrough tools ────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
const PASSTHROUGH = [
|
|
42
|
-
['get_active_file', 'GET', '/active_file', "Return the active file's path, full content, cursor line/col, dirty flag, and syntax name."],
|
|
43
|
-
['get_selection', 'GET', '/selection', 'Return the current selection(s): text and begin/end line+col for each.'],
|
|
44
|
-
['get_open_files', 'GET', '/open_files', 'List all files open in the current window (path, name, is_dirty).'],
|
|
45
|
-
['get_sheets', 'GET', '/sheets', 'List ALL sheets (tabs) in the current window by index, including images and untitled buffers.\nReturns index, type (TextSheet/ImageSheet), path, name, is_dirty for each.\nUse index with get_sheet_content to read a specific tab.'],
|
|
46
|
-
['get_project_folders', 'GET', '/project_folders', "Return the project's root folder paths."],
|
|
47
|
-
['get_symbols', 'GET', '/symbols', 'Return all symbols (functions, classes, etc.) in the active file with line numbers.'],
|
|
48
|
-
['get_project_data', 'GET', '/project_data', 'Return the raw .sublime-project JSON data for the current project.'],
|
|
49
|
-
['get_variables', 'GET', '/variables', "Return Sublime Text's build variables: $file, $project_path, $platform, etc."],
|
|
50
|
-
['get_active_panel', 'GET', '/active_panel', 'Return the active panel id and, if it is an output panel, its content.'],
|
|
51
|
-
['get_syntaxes', 'GET', '/syntaxes', 'List all syntax definitions available in Sublime Text (name + path).'],
|
|
52
|
-
['get_encoding', 'GET', '/encoding', 'Return the character encoding of the active file.'],
|
|
53
|
-
['get_scope_at_cursor', 'GET', '/scope_at_cursor', 'Return the full syntax scope string at the cursor position.'],
|
|
54
|
-
['get_word_at_cursor', 'GET', '/word_at_cursor', 'Return the word under the cursor and its line/col.'],
|
|
55
|
-
['get_bookmarks', 'GET', '/bookmarks', 'Return all bookmarked positions in the active file.'],
|
|
56
|
-
['get_line_count', 'GET', '/line_count', 'Return the total number of lines in the active file.'],
|
|
57
|
-
['get_layout', 'GET', '/layout', 'Return the current window layout (groups, cells) and which files are in each group.'],
|
|
58
|
-
['save_all', 'POST', '/save_all', 'Save all open files.'],
|
|
59
|
-
['revert_file', 'POST', '/revert_file', 'Revert the active file to its last saved state, discarding unsaved changes.'],
|
|
60
|
-
['undo', 'POST', '/undo', 'Undo the last edit in the active file.'],
|
|
61
|
-
['redo', 'POST', '/redo', 'Redo the last undone edit in the active file.'],
|
|
62
|
-
['duplicate_line', 'POST', '/duplicate_line', 'Duplicate the current line(s) in the active file.'],
|
|
63
|
-
['toggle_sidebar', 'POST', '/toggle_sidebar', 'Show or hide the Sublime Text sidebar.'],
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
for (const [name, method, endpoint, description] of PASSTHROUGH) {
|
|
67
|
-
server.registerTool(name, { description }, async () =>
|
|
68
|
-
ok(await (method === 'GET' ? get(endpoint) : post(endpoint)))
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── parameterised tools ───────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
server.registerTool('get_cursor_context', {
|
|
75
|
-
description: 'Return `lines` lines above and below the cursor with 1-based line numbers prepended.',
|
|
76
|
-
inputSchema: { lines: z.number().int().default(10) },
|
|
77
|
-
}, async ({ lines }) => ok(await get('/cursor_context', { lines })));
|
|
78
|
-
|
|
79
|
-
server.registerTool('get_sheet_content', {
|
|
80
|
-
description: 'Return the content of any tab by its sheet index (from get_sheets).\nWorks for text tabs including untitled buffers and Terminus tabs.\nFor image tabs returns the file path only.',
|
|
81
|
-
inputSchema: { index: z.number().int() },
|
|
82
|
-
}, async ({ index }) => ok(await get('/sheet_content', { index })));
|
|
83
|
-
|
|
84
|
-
server.registerTool('get_file_content', {
|
|
85
|
-
description: 'Return the full content of an already-open file by its path.',
|
|
86
|
-
inputSchema: { path: z.string() },
|
|
87
|
-
}, async ({ path }) => ok(await get('/file_content', { path })));
|
|
88
|
-
|
|
89
|
-
server.registerTool('get_view_content', {
|
|
90
|
-
description: 'Return the full content of any open tab by name (partial match, case-insensitive).\nWorks for Terminus tabs and other nameless views that have no file path.\nUse index (0-based, from get_open_files) to target a tab by position instead of name.\nOmit both to read the active view.',
|
|
91
|
-
inputSchema: { name: z.string().default(''), index: z.number().int().default(-1) },
|
|
92
|
-
}, async ({ name, index }) => ok(await get('/view_content', { name, index })));
|
|
93
|
-
|
|
94
|
-
server.registerTool('get_view_size', {
|
|
95
|
-
description: 'Return the total character count of any open tab by name (partial match, case-insensitive).\nUse before get_view_chars to compute offsets — e.g. begin=size-5000, end=size for the tail.\nOmit name for the active view.',
|
|
96
|
-
inputSchema: { name: z.string().default('') },
|
|
97
|
-
}, async ({ name }) => ok(await get('/view_size', { name })));
|
|
98
|
-
|
|
99
|
-
server.registerTool('get_view_chars', {
|
|
100
|
-
description: 'Return text at character offsets begin..end (0-based, end exclusive) from any open tab.\nWorks for Terminus tabs and any other view. Clamps to buffer bounds automatically.\nUse get_view_size first, then e.g. begin=size-5000, end=size to read the last 5000 chars.\nOmit name for the active view.',
|
|
101
|
-
inputSchema: { begin: z.number().int(), end: z.number().int(), name: z.string().default('') },
|
|
102
|
-
}, async ({ begin, end, name }) => ok(await get('/view_chars', { name, begin, end })));
|
|
103
|
-
|
|
104
|
-
server.registerTool('get_view_phantoms', {
|
|
105
|
-
description: "Return phantom HTML and extracted text from a view by name.\nIf key is omitted, defaults to the common 'pybackup' phantom key.",
|
|
106
|
-
inputSchema: { name: z.string().default(''), key: z.string().default('') },
|
|
107
|
-
}, async ({ name, key }) => ok(await get('/view_phantoms', { name, key })));
|
|
108
|
-
|
|
109
|
-
server.registerTool('get_output_panel', {
|
|
110
|
-
description: "Return the text content of an output panel.\nIf name is omitted, read the active output panel. Use name='exec' for build output.",
|
|
111
|
-
inputSchema: { name: z.string().default('') },
|
|
112
|
-
}, async ({ name }) => ok(await get('/output_panel', { name })));
|
|
113
|
-
|
|
114
|
-
server.registerTool('lookup_symbol', {
|
|
115
|
-
description: 'Find where a symbol is defined across all open files.',
|
|
116
|
-
inputSchema: { symbol: z.string() },
|
|
117
|
-
}, async ({ symbol }) => ok(await get('/lookup_symbol', { symbol })));
|
|
118
|
-
|
|
119
|
-
server.registerTool('add_folder', {
|
|
120
|
-
description: 'Add a folder to the current project.',
|
|
121
|
-
inputSchema: { path: z.string() },
|
|
122
|
-
}, async ({ path }) => {
|
|
123
|
-
const data = (await get('/project_data')).project_data ?? {};
|
|
124
|
-
const folders = data.folders ?? [];
|
|
125
|
-
if (folders.some(f => f.path === path)) return ok({ ok: true, note: 'already present' });
|
|
126
|
-
folders.push({ path });
|
|
127
|
-
data.folders = folders;
|
|
128
|
-
return ok(await post('/set_project_data', { data }));
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
server.registerTool('remove_folder', {
|
|
132
|
-
description: 'Remove a folder from the current project by path.',
|
|
133
|
-
inputSchema: { path: z.string() },
|
|
134
|
-
}, async ({ path }) => {
|
|
135
|
-
const data = (await get('/project_data')).project_data ?? {};
|
|
136
|
-
const folders = data.folders ?? [];
|
|
137
|
-
const newFolders = folders.filter(f => f.path !== path);
|
|
138
|
-
if (newFolders.length === folders.length) return ok({ ok: false, note: 'folder not found' });
|
|
139
|
-
data.folders = newFolders;
|
|
140
|
-
return ok(await post('/set_project_data', { data }));
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
server.registerTool('send_to_view', {
|
|
144
|
-
description: 'Send a string to any open tab by name (partial match, case-insensitive).\nFor Terminus tabs this types the text into the terminal as if the user typed it.\nInclude a trailing newline (\\n) to execute a command.\nUse index (0-based, from get_open_files) to target a tab by position instead of name.\nOmit both name and index to target the active view.',
|
|
145
|
-
inputSchema: { text: z.string(), name: z.string().default(''), index: z.number().int().default(-1) },
|
|
146
|
-
}, async ({ text, name, index }) => ok(await post('/send_to_view', { text, name, index })));
|
|
147
|
-
|
|
148
|
-
server.registerTool('open_file', {
|
|
149
|
-
description: 'Open a file in Sublime Text, optionally jumping to a specific line and column.',
|
|
150
|
-
inputSchema: { path: z.string(), line: z.number().int().default(0), col: z.number().int().default(0) },
|
|
151
|
-
}, async ({ path, line, col }) => ok(await post('/open_file', { path, line, col })));
|
|
152
|
-
|
|
153
|
-
server.registerTool('goto_line', {
|
|
154
|
-
description: 'Move the cursor to a line (and optional column) in the active file.',
|
|
155
|
-
inputSchema: { line: z.number().int(), col: z.number().int().default(1) },
|
|
156
|
-
}, async ({ line, col }) => ok(await post('/goto_line', { line, col })));
|
|
157
|
-
|
|
158
|
-
server.registerTool('show_panel', {
|
|
159
|
-
description: "Bring an output panel to the front. Use name='exec' for the build panel.",
|
|
160
|
-
inputSchema: { name: z.string().default('exec') },
|
|
161
|
-
}, async ({ name }) => ok(await post('/show_panel', { name })));
|
|
162
|
-
|
|
163
|
-
server.registerTool('replace_selection', {
|
|
164
|
-
description: 'Replace the current selection(s) with text.',
|
|
165
|
-
inputSchema: { text: z.string() },
|
|
166
|
-
}, async ({ text }) => ok(await post('/replace_selection', { text })));
|
|
167
|
-
|
|
168
|
-
server.registerTool('replace_lines', {
|
|
169
|
-
description: 'Replace lines begin through end (inclusive, 1-based) in the active file with text.\nPass path to target a specific open file regardless of which tab is focused.\nUse index (0-based, from get_open_files) to target a nameless tab by position.',
|
|
170
|
-
inputSchema: {
|
|
171
|
-
begin: z.number().int(),
|
|
172
|
-
end: z.number().int(),
|
|
173
|
-
text: z.string(),
|
|
174
|
-
path: z.string().default(''),
|
|
175
|
-
index: z.number().int().default(-1),
|
|
176
|
-
},
|
|
177
|
-
}, async ({ begin, end, text, path, index }) =>
|
|
178
|
-
ok(await post('/replace_lines', { begin, end, text, path, index })));
|
|
179
|
-
|
|
180
|
-
server.registerTool('run_command', {
|
|
181
|
-
description: "Run any Sublime Text command. scope='window' (default) or 'view'.",
|
|
182
|
-
inputSchema: {
|
|
183
|
-
command: z.string(),
|
|
184
|
-
args: z.record(z.unknown()).optional(),
|
|
185
|
-
scope: z.string().default('window'),
|
|
186
|
-
},
|
|
187
|
-
}, async ({ command, args, scope }) =>
|
|
188
|
-
ok(await post('/run_command', { command, args: args ?? {}, scope })));
|
|
189
|
-
|
|
190
|
-
server.registerTool('run_build', {
|
|
191
|
-
description: 'Trigger the current build system, or pass cmd/shell_cmd to run a specific command.',
|
|
192
|
-
inputSchema: {
|
|
193
|
-
cmd: z.array(z.string()).optional(),
|
|
194
|
-
shell_cmd: z.string().optional(),
|
|
195
|
-
working_dir: z.string().default(''),
|
|
196
|
-
},
|
|
197
|
-
}, async ({ cmd, shell_cmd, working_dir }) => {
|
|
198
|
-
const body = {};
|
|
199
|
-
if (cmd) body.cmd = cmd;
|
|
200
|
-
if (shell_cmd) body.shell_cmd = shell_cmd;
|
|
201
|
-
if (working_dir) body.working_dir = working_dir;
|
|
202
|
-
return ok(await post('/run_build', body));
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
server.registerTool('set_status', {
|
|
206
|
-
description: "Write a message to Sublime Text's status bar.",
|
|
207
|
-
inputSchema: { value: z.string(), key: z.string().default('sublime_mcp') },
|
|
208
|
-
}, async ({ value, key }) => ok(await post('/set_status', { key, value })));
|
|
209
|
-
|
|
210
|
-
server.registerTool('save_file', {
|
|
211
|
-
description: 'Save a file. Pass path to save a specific open file; omit path to save the active file.',
|
|
212
|
-
inputSchema: { path: z.string().default('') },
|
|
213
|
-
}, async ({ path }) => ok(await post('/save_file', path ? { path } : {})));
|
|
214
|
-
|
|
215
|
-
server.registerTool('close_file', {
|
|
216
|
-
description: 'Close a file by path, or close the active file if path is omitted.',
|
|
217
|
-
inputSchema: { path: z.string().default('') },
|
|
218
|
-
}, async ({ path }) => ok(await post('/close_file', { path })));
|
|
219
|
-
|
|
220
|
-
server.registerTool('toggle_comment', {
|
|
221
|
-
description: 'Toggle line comment (or block comment if block=true) on the current selection.',
|
|
222
|
-
inputSchema: { block: z.boolean().default(false) },
|
|
223
|
-
}, async ({ block }) => ok(await post('/toggle_comment', { block })));
|
|
224
|
-
|
|
225
|
-
server.registerTool('sort_lines', {
|
|
226
|
-
description: 'Sort the selected lines (or all lines if nothing is selected).',
|
|
227
|
-
inputSchema: { case_sensitive: z.boolean().default(false) },
|
|
228
|
-
}, async ({ case_sensitive }) => ok(await post('/sort_lines', { case_sensitive })));
|
|
229
|
-
|
|
230
|
-
server.registerTool('select_lines', {
|
|
231
|
-
description: 'Select lines begin through end (1-based, inclusive). end defaults to begin.',
|
|
232
|
-
inputSchema: { begin: z.number().int(), end: z.number().int().default(0) },
|
|
233
|
-
}, async ({ begin, end }) => ok(await post('/select_lines', { begin, end: end || begin })));
|
|
234
|
-
|
|
235
|
-
server.registerTool('fold_lines', {
|
|
236
|
-
description: 'Fold (collapse) lines begin through end (1-based) in the active file.',
|
|
237
|
-
inputSchema: { begin: z.number().int(), end: z.number().int() },
|
|
238
|
-
}, async ({ begin, end }) => ok(await post('/fold_lines', { begin, end })));
|
|
239
|
-
|
|
240
|
-
server.registerTool('insert_snippet', {
|
|
241
|
-
description: "Insert a snippet at the cursor using Sublime Text's snippet syntax (e.g. $1 for tab stops).",
|
|
242
|
-
inputSchema: { contents: z.string() },
|
|
243
|
-
}, async ({ contents }) => ok(await post('/insert_snippet', { contents })));
|
|
244
|
-
|
|
245
|
-
server.registerTool('find_in_file', {
|
|
246
|
-
description: 'Find all occurrences of pattern in the active file. Returns list of {line, col, text}.',
|
|
247
|
-
inputSchema: {
|
|
248
|
-
pattern: z.string(),
|
|
249
|
-
case_sensitive: z.boolean().default(false),
|
|
250
|
-
regex: z.boolean().default(false),
|
|
251
|
-
},
|
|
252
|
-
}, async ({ pattern, case_sensitive, regex }) =>
|
|
253
|
-
ok(await post('/find_in_file', { pattern, case_sensitive, regex })));
|
|
254
|
-
|
|
255
|
-
server.registerTool('find_in_files', {
|
|
256
|
-
description: 'Search for pattern across project folders (or the supplied folder list).\nSkips .git, __pycache__, node_modules, .venv. Returns list of {path, line, match}.',
|
|
257
|
-
inputSchema: {
|
|
258
|
-
pattern: z.string(),
|
|
259
|
-
folders: z.array(z.string()).optional(),
|
|
260
|
-
case_sensitive: z.boolean().default(false),
|
|
261
|
-
regex: z.boolean().default(false),
|
|
262
|
-
max_results: z.number().int().default(200),
|
|
263
|
-
},
|
|
264
|
-
}, async ({ pattern, folders, case_sensitive, regex, max_results }) => {
|
|
265
|
-
const body = { pattern, case_sensitive, regex, max_results };
|
|
266
|
-
if (folders) body.folders = folders;
|
|
267
|
-
return ok(await post('/find_in_files', body));
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
server.registerTool('get_command_palette', {
|
|
271
|
-
description: 'List Command Palette entries from installed *.sublime-commands resources.\nOptional filters: package, command id, or caption substring.',
|
|
272
|
-
inputSchema: {
|
|
273
|
-
package: z.string().default(''),
|
|
274
|
-
command: z.string().default(''),
|
|
275
|
-
caption: z.string().default(''),
|
|
276
|
-
},
|
|
277
|
-
}, async ({ package: pkg, command, caption }) =>
|
|
278
|
-
ok(await get('/command_palette', { package: pkg, command, caption })));
|
|
279
|
-
|
|
280
|
-
server.registerTool('get_commands', {
|
|
281
|
-
description: 'List runnable Sublime command ids from loaded command classes, optionally enriched\nwith matching Command Palette entries from installed packages.',
|
|
282
|
-
inputSchema: {
|
|
283
|
-
package: z.string().default(''),
|
|
284
|
-
command: z.string().default(''),
|
|
285
|
-
include_palette: z.boolean().default(true),
|
|
286
|
-
},
|
|
287
|
-
}, async ({ package: pkg, command, include_palette }) =>
|
|
288
|
-
ok(await get('/commands', { package: pkg, command, include_palette: String(include_palette) })));
|
|
289
|
-
|
|
290
|
-
server.registerTool('get_menu_items', {
|
|
291
|
-
description: 'List installed menu items from *.sublime-menu resources.\nOptional filters: menu filename, caption substring, or command id substring.',
|
|
292
|
-
inputSchema: {
|
|
293
|
-
menu: z.string().default(''),
|
|
294
|
-
caption: z.string().default(''),
|
|
295
|
-
command: z.string().default(''),
|
|
296
|
-
},
|
|
297
|
-
}, async ({ menu, caption, command }) =>
|
|
298
|
-
ok(await get('/menu_items', { menu, caption, command })));
|
|
299
|
-
|
|
300
|
-
server.registerTool('set_syntax', {
|
|
301
|
-
description: 'Set the syntax of the active file by name (case-insensitive partial match is fine).',
|
|
302
|
-
inputSchema: { name: z.string() },
|
|
303
|
-
}, async ({ name }) => ok(await post('/set_syntax', { name })));
|
|
304
|
-
|
|
305
|
-
server.registerTool('set_encoding', {
|
|
306
|
-
description: "Set the character encoding of the active file (e.g. 'UTF-8', 'Western (Windows 1252)').",
|
|
307
|
-
inputSchema: { encoding: z.string() },
|
|
308
|
-
}, async ({ encoding }) => ok(await post('/set_encoding', { encoding })));
|
|
309
|
-
|
|
310
|
-
server.registerTool('get_setting', {
|
|
311
|
-
description: "Get a Sublime Text setting by key. scope='view' (default) or 'window'.",
|
|
312
|
-
inputSchema: { key: z.string(), scope: z.string().default('view') },
|
|
313
|
-
}, async ({ key, scope }) => ok(await post('/get_setting', { key, scope })));
|
|
314
|
-
|
|
315
|
-
server.registerTool('set_setting', {
|
|
316
|
-
description: "Set a Sublime Text setting by key. scope='view' (default) or 'window'.",
|
|
317
|
-
inputSchema: { key: z.string(), value: z.unknown(), scope: z.string().default('view') },
|
|
318
|
-
}, async ({ key, value, scope }) => ok(await post('/set_setting', { key, value, scope })));
|
|
319
|
-
|
|
320
|
-
server.registerTool('focus_group', {
|
|
321
|
-
description: 'Move focus to a pane group by 0-based index.',
|
|
322
|
-
inputSchema: { group: z.number().int() },
|
|
323
|
-
}, async ({ group }) => ok(await post('/focus_group', { group })));
|
|
324
|
-
|
|
325
|
-
server.registerTool('set_layout', {
|
|
326
|
-
description: 'Set the window pane layout. layout must be a ST layout dict with cols, rows, cells keys.',
|
|
327
|
-
inputSchema: { layout: z.record(z.unknown()) },
|
|
328
|
-
}, async ({ layout }) => ok(await post('/set_layout', { layout })));
|
|
329
|
-
|
|
330
|
-
server.registerTool('str_replace_based_edit_tool', {
|
|
331
|
-
description: `ST-native file editor implementing the standard str_replace_based_edit_tool interface.
|
|
332
|
-
Edits appear live in Sublime Text with full undo (Ctrl+Z), gutter diff markers,
|
|
333
|
-
and 30-second highlight annotations showing what changed.
|
|
334
|
-
|
|
335
|
-
command='str_replace': replace old_str with new_str in path.
|
|
336
|
-
old_str must match exactly once (whitespace-sensitive).
|
|
337
|
-
Returns error if 0 or 2+ matches, listing ambiguous line numbers.
|
|
338
|
-
|
|
339
|
-
command='insert': insert insert_text after line insert_line (1-based).
|
|
340
|
-
insert_line=0 inserts at the very start of the file.
|
|
341
|
-
|
|
342
|
-
command='create': create a new file at path with file_text content.
|
|
343
|
-
Syntax is auto-detected from the file extension. Errors if path exists.
|
|
344
|
-
|
|
345
|
-
command='view': return file content with 1-based line numbers prepended.
|
|
346
|
-
Optional view_range=[start, end] to read a slice (end=-1 for EOF).
|
|
347
|
-
|
|
348
|
-
All commands auto-open the file in ST if not already open.`,
|
|
349
|
-
inputSchema: {
|
|
350
|
-
command: z.string(),
|
|
351
|
-
path: z.string().default(''),
|
|
352
|
-
old_str: z.string().optional(),
|
|
353
|
-
new_str: z.string().optional(),
|
|
354
|
-
insert_line: z.number().int().optional(),
|
|
355
|
-
insert_text: z.string().optional(),
|
|
356
|
-
file_text: z.string().optional(),
|
|
357
|
-
view_range: z.array(z.number().int()).length(2).optional(),
|
|
358
|
-
},
|
|
359
|
-
}, async ({ command, path, old_str, new_str, insert_line, insert_text, file_text, view_range }) => {
|
|
360
|
-
const body = { command, path };
|
|
361
|
-
if (old_str !== undefined) body.old_str = old_str;
|
|
362
|
-
if (new_str !== undefined) body.new_str = new_str;
|
|
363
|
-
if (insert_line !== undefined) body.insert_line = insert_line;
|
|
364
|
-
if (insert_text !== undefined) body.insert_text = insert_text;
|
|
365
|
-
if (file_text !== undefined) body.file_text = file_text;
|
|
366
|
-
if (view_range !== undefined) body.view_range = view_range;
|
|
367
|
-
return ok(await post('/edit_file', body));
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
server.registerTool('eval_python', {
|
|
371
|
-
description: "Execute arbitrary Python in Sublime Text's main thread.\nLocals: sublime, window, view, print. Returns captured stdout in 'output'.",
|
|
372
|
-
inputSchema: { code: z.string() },
|
|
373
|
-
}, async ({ code }) => ok(await post('/eval_python', { code })));
|
|
374
|
-
|
|
375
|
-
server.registerTool('get_console_log', {
|
|
376
|
-
description: 'Return recent Sublime Text console output (plugin log messages and stdout).\ntail=N limits to the last N entries. tail=0 returns all captured entries.',
|
|
377
|
-
inputSchema: { tail: z.number().int().default(100) },
|
|
378
|
-
}, async ({ tail }) => ok(await get('/console_log', { tail })));
|
|
379
|
-
|
|
380
|
-
server.registerTool('get_console_full', {
|
|
381
|
-
description: 'Return the entire captured ST console buffer with no tail limit.\nIncludes startup messages, plugin load events, and all errors since ST started.\nUse this when get_console_log (tail=N) does not show enough history.',
|
|
382
|
-
inputSchema: {},
|
|
383
|
-
}, async () => ok(await get('/console_full')));
|
|
384
|
-
|
|
385
|
-
server.registerTool('eval_python_latest', {
|
|
386
|
-
description: "Execute Python code using the system Python interpreter outside Sublime Text's embedded sandbox.\nUseful for newer stdlib features or third-party packages not available in ST's embedded Python.\nReturns stdout, stderr, and returncode.",
|
|
387
|
-
inputSchema: { code: z.string() },
|
|
388
|
-
}, async ({ code }) => ok(await post('/eval_python_latest', { code })));
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
await
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const port = process.platform === 'win32' ? 9500 : 9501;
|
|
7
|
+
const BASE = process.env.SUBLIME_MCP_BASE ?? `http://127.0.0.1:${port}`;
|
|
8
|
+
const TIMEOUT = 10_000;
|
|
9
|
+
|
|
10
|
+
process.stderr.write(`mcp-commander: BASE=${BASE} platform=${process.platform}\n`);
|
|
11
|
+
|
|
12
|
+
async function get(endpoint, params = {}) {
|
|
13
|
+
const url = new URL(endpoint, BASE);
|
|
14
|
+
for (const [k, v] of Object.entries(params)) {
|
|
15
|
+
url.searchParams.set(k, String(v));
|
|
16
|
+
}
|
|
17
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(TIMEOUT) });
|
|
18
|
+
if (!r.ok) throw new Error(`HTTP ${r.status} from ${endpoint}`);
|
|
19
|
+
return r.json();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function post(endpoint, body = {}) {
|
|
23
|
+
const r = await fetch(new URL(endpoint, BASE), {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
signal: AbortSignal.timeout(TIMEOUT),
|
|
28
|
+
});
|
|
29
|
+
if (!r.ok) throw new Error(`HTTP ${r.status} from ${endpoint}`);
|
|
30
|
+
return r.json();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ok(data) {
|
|
34
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const server = new McpServer({ name: 'sublime-mcp', version: '1.3.3' });
|
|
38
|
+
|
|
39
|
+
// ── no-parameter passthrough tools ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const PASSTHROUGH = [
|
|
42
|
+
['get_active_file', 'GET', '/active_file', "Return the active file's path, full content, cursor line/col, dirty flag, and syntax name."],
|
|
43
|
+
['get_selection', 'GET', '/selection', 'Return the current selection(s): text and begin/end line+col for each.'],
|
|
44
|
+
['get_open_files', 'GET', '/open_files', 'List all files open in the current window (path, name, is_dirty).'],
|
|
45
|
+
['get_sheets', 'GET', '/sheets', 'List ALL sheets (tabs) in the current window by index, including images and untitled buffers.\nReturns index, type (TextSheet/ImageSheet), path, name, is_dirty for each.\nUse index with get_sheet_content to read a specific tab.'],
|
|
46
|
+
['get_project_folders', 'GET', '/project_folders', "Return the project's root folder paths."],
|
|
47
|
+
['get_symbols', 'GET', '/symbols', 'Return all symbols (functions, classes, etc.) in the active file with line numbers.'],
|
|
48
|
+
['get_project_data', 'GET', '/project_data', 'Return the raw .sublime-project JSON data for the current project.'],
|
|
49
|
+
['get_variables', 'GET', '/variables', "Return Sublime Text's build variables: $file, $project_path, $platform, etc."],
|
|
50
|
+
['get_active_panel', 'GET', '/active_panel', 'Return the active panel id and, if it is an output panel, its content.'],
|
|
51
|
+
['get_syntaxes', 'GET', '/syntaxes', 'List all syntax definitions available in Sublime Text (name + path).'],
|
|
52
|
+
['get_encoding', 'GET', '/encoding', 'Return the character encoding of the active file.'],
|
|
53
|
+
['get_scope_at_cursor', 'GET', '/scope_at_cursor', 'Return the full syntax scope string at the cursor position.'],
|
|
54
|
+
['get_word_at_cursor', 'GET', '/word_at_cursor', 'Return the word under the cursor and its line/col.'],
|
|
55
|
+
['get_bookmarks', 'GET', '/bookmarks', 'Return all bookmarked positions in the active file.'],
|
|
56
|
+
['get_line_count', 'GET', '/line_count', 'Return the total number of lines in the active file.'],
|
|
57
|
+
['get_layout', 'GET', '/layout', 'Return the current window layout (groups, cells) and which files are in each group.'],
|
|
58
|
+
['save_all', 'POST', '/save_all', 'Save all open files.'],
|
|
59
|
+
['revert_file', 'POST', '/revert_file', 'Revert the active file to its last saved state, discarding unsaved changes.'],
|
|
60
|
+
['undo', 'POST', '/undo', 'Undo the last edit in the active file.'],
|
|
61
|
+
['redo', 'POST', '/redo', 'Redo the last undone edit in the active file.'],
|
|
62
|
+
['duplicate_line', 'POST', '/duplicate_line', 'Duplicate the current line(s) in the active file.'],
|
|
63
|
+
['toggle_sidebar', 'POST', '/toggle_sidebar', 'Show or hide the Sublime Text sidebar.'],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const [name, method, endpoint, description] of PASSTHROUGH) {
|
|
67
|
+
server.registerTool(name, { description }, async () =>
|
|
68
|
+
ok(await (method === 'GET' ? get(endpoint) : post(endpoint)))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── parameterised tools ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
server.registerTool('get_cursor_context', {
|
|
75
|
+
description: 'Return `lines` lines above and below the cursor with 1-based line numbers prepended.',
|
|
76
|
+
inputSchema: { lines: z.number().int().default(10) },
|
|
77
|
+
}, async ({ lines }) => ok(await get('/cursor_context', { lines })));
|
|
78
|
+
|
|
79
|
+
server.registerTool('get_sheet_content', {
|
|
80
|
+
description: 'Return the content of any tab by its sheet index (from get_sheets).\nWorks for text tabs including untitled buffers and Terminus tabs.\nFor image tabs returns the file path only.',
|
|
81
|
+
inputSchema: { index: z.number().int() },
|
|
82
|
+
}, async ({ index }) => ok(await get('/sheet_content', { index })));
|
|
83
|
+
|
|
84
|
+
server.registerTool('get_file_content', {
|
|
85
|
+
description: 'Return the full content of an already-open file by its path.',
|
|
86
|
+
inputSchema: { path: z.string() },
|
|
87
|
+
}, async ({ path }) => ok(await get('/file_content', { path })));
|
|
88
|
+
|
|
89
|
+
server.registerTool('get_view_content', {
|
|
90
|
+
description: 'Return the full content of any open tab by name (partial match, case-insensitive).\nWorks for Terminus tabs and other nameless views that have no file path.\nUse index (0-based, from get_open_files) to target a tab by position instead of name.\nOmit both to read the active view.',
|
|
91
|
+
inputSchema: { name: z.string().default(''), index: z.number().int().default(-1) },
|
|
92
|
+
}, async ({ name, index }) => ok(await get('/view_content', { name, index })));
|
|
93
|
+
|
|
94
|
+
server.registerTool('get_view_size', {
|
|
95
|
+
description: 'Return the total character count of any open tab by name (partial match, case-insensitive).\nUse before get_view_chars to compute offsets — e.g. begin=size-5000, end=size for the tail.\nOmit name for the active view.',
|
|
96
|
+
inputSchema: { name: z.string().default('') },
|
|
97
|
+
}, async ({ name }) => ok(await get('/view_size', { name })));
|
|
98
|
+
|
|
99
|
+
server.registerTool('get_view_chars', {
|
|
100
|
+
description: 'Return text at character offsets begin..end (0-based, end exclusive) from any open tab.\nWorks for Terminus tabs and any other view. Clamps to buffer bounds automatically.\nUse get_view_size first, then e.g. begin=size-5000, end=size to read the last 5000 chars.\nOmit name for the active view.',
|
|
101
|
+
inputSchema: { begin: z.number().int(), end: z.number().int(), name: z.string().default('') },
|
|
102
|
+
}, async ({ begin, end, name }) => ok(await get('/view_chars', { name, begin, end })));
|
|
103
|
+
|
|
104
|
+
server.registerTool('get_view_phantoms', {
|
|
105
|
+
description: "Return phantom HTML and extracted text from a view by name.\nIf key is omitted, defaults to the common 'pybackup' phantom key.",
|
|
106
|
+
inputSchema: { name: z.string().default(''), key: z.string().default('') },
|
|
107
|
+
}, async ({ name, key }) => ok(await get('/view_phantoms', { name, key })));
|
|
108
|
+
|
|
109
|
+
server.registerTool('get_output_panel', {
|
|
110
|
+
description: "Return the text content of an output panel.\nIf name is omitted, read the active output panel. Use name='exec' for build output.",
|
|
111
|
+
inputSchema: { name: z.string().default('') },
|
|
112
|
+
}, async ({ name }) => ok(await get('/output_panel', { name })));
|
|
113
|
+
|
|
114
|
+
server.registerTool('lookup_symbol', {
|
|
115
|
+
description: 'Find where a symbol is defined across all open files.',
|
|
116
|
+
inputSchema: { symbol: z.string() },
|
|
117
|
+
}, async ({ symbol }) => ok(await get('/lookup_symbol', { symbol })));
|
|
118
|
+
|
|
119
|
+
server.registerTool('add_folder', {
|
|
120
|
+
description: 'Add a folder to the current project.',
|
|
121
|
+
inputSchema: { path: z.string() },
|
|
122
|
+
}, async ({ path }) => {
|
|
123
|
+
const data = (await get('/project_data')).project_data ?? {};
|
|
124
|
+
const folders = data.folders ?? [];
|
|
125
|
+
if (folders.some(f => f.path === path)) return ok({ ok: true, note: 'already present' });
|
|
126
|
+
folders.push({ path });
|
|
127
|
+
data.folders = folders;
|
|
128
|
+
return ok(await post('/set_project_data', { data }));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
server.registerTool('remove_folder', {
|
|
132
|
+
description: 'Remove a folder from the current project by path.',
|
|
133
|
+
inputSchema: { path: z.string() },
|
|
134
|
+
}, async ({ path }) => {
|
|
135
|
+
const data = (await get('/project_data')).project_data ?? {};
|
|
136
|
+
const folders = data.folders ?? [];
|
|
137
|
+
const newFolders = folders.filter(f => f.path !== path);
|
|
138
|
+
if (newFolders.length === folders.length) return ok({ ok: false, note: 'folder not found' });
|
|
139
|
+
data.folders = newFolders;
|
|
140
|
+
return ok(await post('/set_project_data', { data }));
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
server.registerTool('send_to_view', {
|
|
144
|
+
description: 'Send a string to any open tab by name (partial match, case-insensitive).\nFor Terminus tabs this types the text into the terminal as if the user typed it.\nInclude a trailing newline (\\n) to execute a command.\nUse index (0-based, from get_open_files) to target a tab by position instead of name.\nOmit both name and index to target the active view.',
|
|
145
|
+
inputSchema: { text: z.string(), name: z.string().default(''), index: z.number().int().default(-1) },
|
|
146
|
+
}, async ({ text, name, index }) => ok(await post('/send_to_view', { text, name, index })));
|
|
147
|
+
|
|
148
|
+
server.registerTool('open_file', {
|
|
149
|
+
description: 'Open a file in Sublime Text, optionally jumping to a specific line and column.',
|
|
150
|
+
inputSchema: { path: z.string(), line: z.number().int().default(0), col: z.number().int().default(0) },
|
|
151
|
+
}, async ({ path, line, col }) => ok(await post('/open_file', { path, line, col })));
|
|
152
|
+
|
|
153
|
+
server.registerTool('goto_line', {
|
|
154
|
+
description: 'Move the cursor to a line (and optional column) in the active file.',
|
|
155
|
+
inputSchema: { line: z.number().int(), col: z.number().int().default(1) },
|
|
156
|
+
}, async ({ line, col }) => ok(await post('/goto_line', { line, col })));
|
|
157
|
+
|
|
158
|
+
server.registerTool('show_panel', {
|
|
159
|
+
description: "Bring an output panel to the front. Use name='exec' for the build panel.",
|
|
160
|
+
inputSchema: { name: z.string().default('exec') },
|
|
161
|
+
}, async ({ name }) => ok(await post('/show_panel', { name })));
|
|
162
|
+
|
|
163
|
+
server.registerTool('replace_selection', {
|
|
164
|
+
description: 'Replace the current selection(s) with text.',
|
|
165
|
+
inputSchema: { text: z.string() },
|
|
166
|
+
}, async ({ text }) => ok(await post('/replace_selection', { text })));
|
|
167
|
+
|
|
168
|
+
server.registerTool('replace_lines', {
|
|
169
|
+
description: 'Replace lines begin through end (inclusive, 1-based) in the active file with text.\nPass path to target a specific open file regardless of which tab is focused.\nUse index (0-based, from get_open_files) to target a nameless tab by position.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
begin: z.number().int(),
|
|
172
|
+
end: z.number().int(),
|
|
173
|
+
text: z.string(),
|
|
174
|
+
path: z.string().default(''),
|
|
175
|
+
index: z.number().int().default(-1),
|
|
176
|
+
},
|
|
177
|
+
}, async ({ begin, end, text, path, index }) =>
|
|
178
|
+
ok(await post('/replace_lines', { begin, end, text, path, index })));
|
|
179
|
+
|
|
180
|
+
server.registerTool('run_command', {
|
|
181
|
+
description: "Run any Sublime Text command. scope='window' (default) or 'view'.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
command: z.string(),
|
|
184
|
+
args: z.record(z.unknown()).optional(),
|
|
185
|
+
scope: z.string().default('window'),
|
|
186
|
+
},
|
|
187
|
+
}, async ({ command, args, scope }) =>
|
|
188
|
+
ok(await post('/run_command', { command, args: args ?? {}, scope })));
|
|
189
|
+
|
|
190
|
+
server.registerTool('run_build', {
|
|
191
|
+
description: 'Trigger the current build system, or pass cmd/shell_cmd to run a specific command.',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
cmd: z.array(z.string()).optional(),
|
|
194
|
+
shell_cmd: z.string().optional(),
|
|
195
|
+
working_dir: z.string().default(''),
|
|
196
|
+
},
|
|
197
|
+
}, async ({ cmd, shell_cmd, working_dir }) => {
|
|
198
|
+
const body = {};
|
|
199
|
+
if (cmd) body.cmd = cmd;
|
|
200
|
+
if (shell_cmd) body.shell_cmd = shell_cmd;
|
|
201
|
+
if (working_dir) body.working_dir = working_dir;
|
|
202
|
+
return ok(await post('/run_build', body));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
server.registerTool('set_status', {
|
|
206
|
+
description: "Write a message to Sublime Text's status bar.",
|
|
207
|
+
inputSchema: { value: z.string(), key: z.string().default('sublime_mcp') },
|
|
208
|
+
}, async ({ value, key }) => ok(await post('/set_status', { key, value })));
|
|
209
|
+
|
|
210
|
+
server.registerTool('save_file', {
|
|
211
|
+
description: 'Save a file. Pass path to save a specific open file; omit path to save the active file.',
|
|
212
|
+
inputSchema: { path: z.string().default('') },
|
|
213
|
+
}, async ({ path }) => ok(await post('/save_file', path ? { path } : {})));
|
|
214
|
+
|
|
215
|
+
server.registerTool('close_file', {
|
|
216
|
+
description: 'Close a file by path, or close the active file if path is omitted.',
|
|
217
|
+
inputSchema: { path: z.string().default('') },
|
|
218
|
+
}, async ({ path }) => ok(await post('/close_file', { path })));
|
|
219
|
+
|
|
220
|
+
server.registerTool('toggle_comment', {
|
|
221
|
+
description: 'Toggle line comment (or block comment if block=true) on the current selection.',
|
|
222
|
+
inputSchema: { block: z.boolean().default(false) },
|
|
223
|
+
}, async ({ block }) => ok(await post('/toggle_comment', { block })));
|
|
224
|
+
|
|
225
|
+
server.registerTool('sort_lines', {
|
|
226
|
+
description: 'Sort the selected lines (or all lines if nothing is selected).',
|
|
227
|
+
inputSchema: { case_sensitive: z.boolean().default(false) },
|
|
228
|
+
}, async ({ case_sensitive }) => ok(await post('/sort_lines', { case_sensitive })));
|
|
229
|
+
|
|
230
|
+
server.registerTool('select_lines', {
|
|
231
|
+
description: 'Select lines begin through end (1-based, inclusive). end defaults to begin.',
|
|
232
|
+
inputSchema: { begin: z.number().int(), end: z.number().int().default(0) },
|
|
233
|
+
}, async ({ begin, end }) => ok(await post('/select_lines', { begin, end: end || begin })));
|
|
234
|
+
|
|
235
|
+
server.registerTool('fold_lines', {
|
|
236
|
+
description: 'Fold (collapse) lines begin through end (1-based) in the active file.',
|
|
237
|
+
inputSchema: { begin: z.number().int(), end: z.number().int() },
|
|
238
|
+
}, async ({ begin, end }) => ok(await post('/fold_lines', { begin, end })));
|
|
239
|
+
|
|
240
|
+
server.registerTool('insert_snippet', {
|
|
241
|
+
description: "Insert a snippet at the cursor using Sublime Text's snippet syntax (e.g. $1 for tab stops).",
|
|
242
|
+
inputSchema: { contents: z.string() },
|
|
243
|
+
}, async ({ contents }) => ok(await post('/insert_snippet', { contents })));
|
|
244
|
+
|
|
245
|
+
server.registerTool('find_in_file', {
|
|
246
|
+
description: 'Find all occurrences of pattern in the active file. Returns list of {line, col, text}.',
|
|
247
|
+
inputSchema: {
|
|
248
|
+
pattern: z.string(),
|
|
249
|
+
case_sensitive: z.boolean().default(false),
|
|
250
|
+
regex: z.boolean().default(false),
|
|
251
|
+
},
|
|
252
|
+
}, async ({ pattern, case_sensitive, regex }) =>
|
|
253
|
+
ok(await post('/find_in_file', { pattern, case_sensitive, regex })));
|
|
254
|
+
|
|
255
|
+
server.registerTool('find_in_files', {
|
|
256
|
+
description: 'Search for pattern across project folders (or the supplied folder list).\nSkips .git, __pycache__, node_modules, .venv. Returns list of {path, line, match}.',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
pattern: z.string(),
|
|
259
|
+
folders: z.array(z.string()).optional(),
|
|
260
|
+
case_sensitive: z.boolean().default(false),
|
|
261
|
+
regex: z.boolean().default(false),
|
|
262
|
+
max_results: z.number().int().default(200),
|
|
263
|
+
},
|
|
264
|
+
}, async ({ pattern, folders, case_sensitive, regex, max_results }) => {
|
|
265
|
+
const body = { pattern, case_sensitive, regex, max_results };
|
|
266
|
+
if (folders) body.folders = folders;
|
|
267
|
+
return ok(await post('/find_in_files', body));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
server.registerTool('get_command_palette', {
|
|
271
|
+
description: 'List Command Palette entries from installed *.sublime-commands resources.\nOptional filters: package, command id, or caption substring.',
|
|
272
|
+
inputSchema: {
|
|
273
|
+
package: z.string().default(''),
|
|
274
|
+
command: z.string().default(''),
|
|
275
|
+
caption: z.string().default(''),
|
|
276
|
+
},
|
|
277
|
+
}, async ({ package: pkg, command, caption }) =>
|
|
278
|
+
ok(await get('/command_palette', { package: pkg, command, caption })));
|
|
279
|
+
|
|
280
|
+
server.registerTool('get_commands', {
|
|
281
|
+
description: 'List runnable Sublime command ids from loaded command classes, optionally enriched\nwith matching Command Palette entries from installed packages.',
|
|
282
|
+
inputSchema: {
|
|
283
|
+
package: z.string().default(''),
|
|
284
|
+
command: z.string().default(''),
|
|
285
|
+
include_palette: z.boolean().default(true),
|
|
286
|
+
},
|
|
287
|
+
}, async ({ package: pkg, command, include_palette }) =>
|
|
288
|
+
ok(await get('/commands', { package: pkg, command, include_palette: String(include_palette) })));
|
|
289
|
+
|
|
290
|
+
server.registerTool('get_menu_items', {
|
|
291
|
+
description: 'List installed menu items from *.sublime-menu resources.\nOptional filters: menu filename, caption substring, or command id substring.',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
menu: z.string().default(''),
|
|
294
|
+
caption: z.string().default(''),
|
|
295
|
+
command: z.string().default(''),
|
|
296
|
+
},
|
|
297
|
+
}, async ({ menu, caption, command }) =>
|
|
298
|
+
ok(await get('/menu_items', { menu, caption, command })));
|
|
299
|
+
|
|
300
|
+
server.registerTool('set_syntax', {
|
|
301
|
+
description: 'Set the syntax of the active file by name (case-insensitive partial match is fine).',
|
|
302
|
+
inputSchema: { name: z.string() },
|
|
303
|
+
}, async ({ name }) => ok(await post('/set_syntax', { name })));
|
|
304
|
+
|
|
305
|
+
server.registerTool('set_encoding', {
|
|
306
|
+
description: "Set the character encoding of the active file (e.g. 'UTF-8', 'Western (Windows 1252)').",
|
|
307
|
+
inputSchema: { encoding: z.string() },
|
|
308
|
+
}, async ({ encoding }) => ok(await post('/set_encoding', { encoding })));
|
|
309
|
+
|
|
310
|
+
server.registerTool('get_setting', {
|
|
311
|
+
description: "Get a Sublime Text setting by key. scope='view' (default) or 'window'.",
|
|
312
|
+
inputSchema: { key: z.string(), scope: z.string().default('view') },
|
|
313
|
+
}, async ({ key, scope }) => ok(await post('/get_setting', { key, scope })));
|
|
314
|
+
|
|
315
|
+
server.registerTool('set_setting', {
|
|
316
|
+
description: "Set a Sublime Text setting by key. scope='view' (default) or 'window'.",
|
|
317
|
+
inputSchema: { key: z.string(), value: z.unknown(), scope: z.string().default('view') },
|
|
318
|
+
}, async ({ key, value, scope }) => ok(await post('/set_setting', { key, value, scope })));
|
|
319
|
+
|
|
320
|
+
server.registerTool('focus_group', {
|
|
321
|
+
description: 'Move focus to a pane group by 0-based index.',
|
|
322
|
+
inputSchema: { group: z.number().int() },
|
|
323
|
+
}, async ({ group }) => ok(await post('/focus_group', { group })));
|
|
324
|
+
|
|
325
|
+
server.registerTool('set_layout', {
|
|
326
|
+
description: 'Set the window pane layout. layout must be a ST layout dict with cols, rows, cells keys.',
|
|
327
|
+
inputSchema: { layout: z.record(z.unknown()) },
|
|
328
|
+
}, async ({ layout }) => ok(await post('/set_layout', { layout })));
|
|
329
|
+
|
|
330
|
+
server.registerTool('str_replace_based_edit_tool', {
|
|
331
|
+
description: `ST-native file editor implementing the standard str_replace_based_edit_tool interface.
|
|
332
|
+
Edits appear live in Sublime Text with full undo (Ctrl+Z), gutter diff markers,
|
|
333
|
+
and 30-second highlight annotations showing what changed.
|
|
334
|
+
|
|
335
|
+
command='str_replace': replace old_str with new_str in path.
|
|
336
|
+
old_str must match exactly once (whitespace-sensitive).
|
|
337
|
+
Returns error if 0 or 2+ matches, listing ambiguous line numbers.
|
|
338
|
+
|
|
339
|
+
command='insert': insert insert_text after line insert_line (1-based).
|
|
340
|
+
insert_line=0 inserts at the very start of the file.
|
|
341
|
+
|
|
342
|
+
command='create': create a new file at path with file_text content.
|
|
343
|
+
Syntax is auto-detected from the file extension. Errors if path exists.
|
|
344
|
+
|
|
345
|
+
command='view': return file content with 1-based line numbers prepended.
|
|
346
|
+
Optional view_range=[start, end] to read a slice (end=-1 for EOF).
|
|
347
|
+
|
|
348
|
+
All commands auto-open the file in ST if not already open.`,
|
|
349
|
+
inputSchema: {
|
|
350
|
+
command: z.string(),
|
|
351
|
+
path: z.string().default(''),
|
|
352
|
+
old_str: z.string().optional(),
|
|
353
|
+
new_str: z.string().optional(),
|
|
354
|
+
insert_line: z.number().int().optional(),
|
|
355
|
+
insert_text: z.string().optional(),
|
|
356
|
+
file_text: z.string().optional(),
|
|
357
|
+
view_range: z.array(z.number().int()).length(2).optional(),
|
|
358
|
+
},
|
|
359
|
+
}, async ({ command, path, old_str, new_str, insert_line, insert_text, file_text, view_range }) => {
|
|
360
|
+
const body = { command, path };
|
|
361
|
+
if (old_str !== undefined) body.old_str = old_str;
|
|
362
|
+
if (new_str !== undefined) body.new_str = new_str;
|
|
363
|
+
if (insert_line !== undefined) body.insert_line = insert_line;
|
|
364
|
+
if (insert_text !== undefined) body.insert_text = insert_text;
|
|
365
|
+
if (file_text !== undefined) body.file_text = file_text;
|
|
366
|
+
if (view_range !== undefined) body.view_range = view_range;
|
|
367
|
+
return ok(await post('/edit_file', body));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
server.registerTool('eval_python', {
|
|
371
|
+
description: "Execute arbitrary Python in Sublime Text's main thread.\nLocals: sublime, window, view, print. Returns captured stdout in 'output'.",
|
|
372
|
+
inputSchema: { code: z.string() },
|
|
373
|
+
}, async ({ code }) => ok(await post('/eval_python', { code })));
|
|
374
|
+
|
|
375
|
+
server.registerTool('get_console_log', {
|
|
376
|
+
description: 'Return recent Sublime Text console output (plugin log messages and stdout).\ntail=N limits to the last N entries. tail=0 returns all captured entries.',
|
|
377
|
+
inputSchema: { tail: z.number().int().default(100) },
|
|
378
|
+
}, async ({ tail }) => ok(await get('/console_log', { tail })));
|
|
379
|
+
|
|
380
|
+
server.registerTool('get_console_full', {
|
|
381
|
+
description: 'Return the entire captured ST console buffer with no tail limit.\nIncludes startup messages, plugin load events, and all errors since ST started.\nUse this when get_console_log (tail=N) does not show enough history.',
|
|
382
|
+
inputSchema: {},
|
|
383
|
+
}, async () => ok(await get('/console_full')));
|
|
384
|
+
|
|
385
|
+
server.registerTool('eval_python_latest', {
|
|
386
|
+
description: "Execute Python code using the system Python interpreter outside Sublime Text's embedded sandbox.\nUseful for newer stdlib features or third-party packages not available in ST's embedded Python.\nReturns stdout, stderr, and returncode.",
|
|
387
|
+
inputSchema: { code: z.string() },
|
|
388
|
+
}, async ({ code }) => ok(await post('/eval_python_latest', { code })));
|
|
389
|
+
|
|
390
|
+
server.registerTool('get_console_win', {
|
|
391
|
+
description: 'Windows-only fallback: captures ST console by clicking the output area via ctypes then Ctrl+A/Ctrl+C.\nUse when get_console_full fails.',
|
|
392
|
+
inputSchema: {},
|
|
393
|
+
}, async () => ok(await get('/console_win')));
|
|
394
|
+
|
|
395
|
+
server.registerTool('get_help', {
|
|
396
|
+
description: 'Return the Agent Guide (AGENT_GUIDE.md) with detailed instructions on how to use sublime-mcp tools correctly.',
|
|
397
|
+
inputSchema: {},
|
|
398
|
+
}, async () => ok(await get('/get_help')));
|
|
399
|
+
|
|
400
|
+
server.registerTool('open_control_panel', {
|
|
401
|
+
description: 'Open (or focus) the Claude MCP Control Panel: an interactive minihtml dashboard in a dedicated Sublime view.',
|
|
402
|
+
inputSchema: {},
|
|
403
|
+
}, async () => ok(await post('/open_control_panel')));
|
|
404
|
+
|
|
405
|
+
server.registerTool('get_package_mcp_info', {
|
|
406
|
+
description: 'Return everything needed to write an MCP extension for an installed Package Control package.',
|
|
407
|
+
inputSchema: { package: z.string() },
|
|
408
|
+
}, async ({ package }) => ok(await post('/package_mcp_info', { package })));
|
|
409
|
+
|
|
410
|
+
server.registerTool('search_packages', {
|
|
411
|
+
description: 'Search Package Control for installable Sublime Text packages.',
|
|
412
|
+
inputSchema: { query: z.string().default(''), limit: z.number().int().default(20) },
|
|
413
|
+
}, async ({ query, limit }) => ok(await get('/search_packages', { query, limit })));
|
|
414
|
+
|
|
415
|
+
server.registerTool('install_package', {
|
|
416
|
+
description: 'Install a Package Control package by exact name.',
|
|
417
|
+
inputSchema: { package: z.string() },
|
|
418
|
+
}, async ({ package }) => ok(await post('/install_package', { package })));
|
|
419
|
+
|
|
420
|
+
// ── startup ───────────────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
const transport = new StdioServerTransport();
|
|
423
|
+
await server.connect(transport);
|