sublime-mcp 1.4.2 → 1.7.1

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/bin/cli.js CHANGED
@@ -1,69 +1,71 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
- const fs = require('fs/promises');
4
- const path = require('path');
5
- const prompts = require('prompts');
3
+ const command = process.argv[2] || 'serve';
6
4
 
7
- async function main() {
8
- console.log('Sublime-MCP Agent Configurator');
5
+ async function probe(name, httpBase, mcpUrl) {
6
+ const report = { name, ok: false, httpBridge: null, mcp: null,
7
+ recommendedCodexConfig: { type: 'http', url: mcpUrl } };
9
8
 
10
- const response = await prompts({
11
- type: 'text',
12
- name: 'configFile',
13
- message: 'Please enter the path to your AI agent\'s configuration file (e.g., ~/.claude/settings.json):',
14
- validate: async (value) => {
15
- try {
16
- // Resolve ~ to the home directory
17
- const resolvedPath = value.startsWith('~') ? path.join(process.env.HOME || process.env.USERPROFILE, value.slice(1)) : value;
18
- await fs.access(resolvedPath);
19
- return true;
20
- } catch (e) {
21
- return 'File not found. Please enter a valid path.';
22
- }
23
- }
24
- });
25
-
26
- if (!response.configFile) {
27
- console.log('Configuration cancelled.');
28
- return;
9
+ try {
10
+ const response = await fetch(new URL('/health', httpBase), { signal: AbortSignal.timeout(5000) });
11
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
12
+ report.httpBridge = await response.json();
13
+ } catch (error) {
14
+ report.httpBridge = { ok: false, error: error.message };
29
15
  }
30
16
 
31
- const configPath = response.configFile.startsWith('~') ? path.join(process.env.HOME || process.env.USERPROFILE, response.configFile.slice(1)) : response.configFile;
32
- console.log(Reading configuration file: );
33
-
34
17
  try {
35
- const configContent = await fs.readFile(configPath, 'utf-8');
36
- const config = JSON.parse(configContent);
37
-
38
- console.log('Successfully parsed configuration file.');
39
-
40
- // Determine the correct port based on the platform
41
- const port = process.platform === 'win32' ? 9502 : 9503;
42
- const mcpEntry = {
43
- "sublime-mcp": { "type": "sse", "url": http://127.0.0.1:/sse }
18
+ const rpc = async (id, method, params = {}) => {
19
+ const response = await fetch(mcpUrl, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
22
+ body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
23
+ signal: AbortSignal.timeout(5000),
24
+ });
25
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
26
+ return response.json();
27
+ };
28
+ const initialized = await rpc(1, 'initialize', {
29
+ protocolVersion: '2025-03-26', capabilities: {},
30
+ clientInfo: { name: 'sublime-mcp-doctor', version: '1' },
31
+ });
32
+ const listed = await rpc(2, 'tools/list');
33
+ const tools = listed?.result?.tools ?? [];
34
+ report.mcp = {
35
+ ok: Boolean(initialized?.result) && tools.length > 0,
36
+ serverInfo: initialized?.result?.serverInfo ?? null,
37
+ toolCount: tools.length,
38
+ tools: tools.map(tool => tool.name),
44
39
  };
45
-
46
- // Add or update the mcpServers entry
47
- if (!config.mcpServers) {
48
- config.mcpServers = {};
49
- }
50
- config.mcpServers = { ...config.mcpServers, ...mcpEntry };
51
-
52
- // Write the updated configuration back to the file
53
- const newConfigContent = JSON.stringify(config, null, 2);
54
- await fs.writeFile(configPath, newConfigContent, 'utf-8');
55
-
56
- console.log('Successfully updated the configuration file with the sublime-mcp server.');
57
- console.log('Please restart your AI agent for the changes to take effect.');
58
-
59
40
  } catch (error) {
60
- console.error(Error processing configuration file: );
61
- console.error('Please ensure the file is a valid JSON file.');
62
- process.exit(1);
41
+ report.mcp = { ok: false, error: error.message, toolCount: 0 };
63
42
  }
43
+
44
+ report.ok = Boolean(report.httpBridge?.ok && report.mcp?.ok);
45
+ return report;
64
46
  }
65
47
 
66
- main().catch(err => {
67
- console.error('An error occurred:', err);
68
- process.exit(1);
69
- });
48
+ async function doctor() {
49
+ const isWindows = process.platform === 'win32';
50
+ const httpBase = process.env.SUBLIME_MCP_BASE ?? `http://127.0.0.1:${isWindows ? 9500 : 9501}`;
51
+ const mcpUrl = process.env.SUBLIME_MCP_URL ?? `http://127.0.0.1:${isWindows ? 9502 : 9503}/mcp`;
52
+ const main = await probe('sublime-mcp', httpBase, mcpUrl);
53
+ const includeAll = process.argv.includes('--all');
54
+ const companions = includeAll ? await Promise.all([
55
+ probe('debugger-mcp', 'http://127.0.0.1:9515', 'http://127.0.0.1:9505/mcp'),
56
+ probe('lsp-mcp', 'http://127.0.0.1:9516', 'http://127.0.0.1:9506/mcp'),
57
+ ]) : [];
58
+ const report = { ...main, companions };
59
+ report.ok = main.ok && companions.every(item => item.ok);
60
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
61
+ process.exitCode = report.ok ? 0 : 1;
62
+ }
63
+
64
+ if (command === 'doctor') {
65
+ await doctor();
66
+ } else if (command === 'serve') {
67
+ await import('../index.js');
68
+ } else {
69
+ process.stderr.write('Usage: sublime-mcp [serve|doctor [--all]]\n');
70
+ process.exitCode = 2;
71
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "GENERATED FILE - do not edit. Produced by tools/generate_fallback_catalog.py from packages/st-plugin/sublime_mcp.py::_MCP_TOOLS. Regenerate after changing the backend tool catalog.",
2
+ "_comment": "GENERATED FILE - do not edit. Produced by tools/generate_fallback_catalog.py from sublime_mcp.py::_MCP_TOOLS. Regenerate after changing the backend tool catalog.",
3
3
  "tools": [
4
4
  {
5
5
  "name": "add_folder",
@@ -272,6 +272,34 @@
272
272
  "properties": {}
273
273
  }
274
274
  },
275
+ {
276
+ "name": "diagnostics",
277
+ "description": "One-call plugin health snapshot: main-thread heartbeat freshness (and a likely_wedged flag), the currently in-flight _on_main dispatch (if any) with its label and running time, open IDE-companion diff reviews, on_activated/on_load/on_post_save/on_selection_modified/on_close event counts (total vs. suppressed as agent-caused), console-capture buffer size, and a live main-thread stack trace. Answers 'wedged vs slow vs fine' without needing the main thread's cooperation -- use this before assuming a timeout means the server is dead.",
278
+ "inputSchema": {
279
+ "type": "object",
280
+ "properties": {}
281
+ }
282
+ },
283
+ {
284
+ "name": "discover_tools",
285
+ "description": "Search advanced Sublime capabilities hidden from the default tool surface. Returns matching names, descriptions, and schemas. Invoke a result through batch(calls=[{tool: <name>, args: {...}}]), including for a single call.",
286
+ "inputSchema": {
287
+ "type": "object",
288
+ "properties": {
289
+ "query": {
290
+ "type": "string",
291
+ "description": "Capability to find, such as bookmarks, tabs, syntax, or commands."
292
+ },
293
+ "limit": {
294
+ "type": "integer",
295
+ "default": 10
296
+ }
297
+ },
298
+ "required": [
299
+ "query"
300
+ ]
301
+ }
302
+ },
275
303
  {
276
304
  "name": "duplicate_line",
277
305
  "description": "Duplicate the current line(s) in the active file.",
@@ -588,6 +616,38 @@
588
616
  "properties": {}
589
617
  }
590
618
  },
619
+ {
620
+ "name": "focus_sheet",
621
+ "description": "Move input focus to one sheet by global index or stable sheet ID without changing the selected sheet set.",
622
+ "inputSchema": {
623
+ "type": "object",
624
+ "properties": {
625
+ "index": {
626
+ "type": "integer",
627
+ "minimum": 0
628
+ },
629
+ "id": {
630
+ "type": "integer"
631
+ }
632
+ }
633
+ }
634
+ },
635
+ {
636
+ "name": "focus_to_left",
637
+ "description": "Move input focus to the selected sheet on the left.",
638
+ "inputSchema": {
639
+ "type": "object",
640
+ "properties": {}
641
+ }
642
+ },
643
+ {
644
+ "name": "focus_to_right",
645
+ "description": "Move input focus to the selected sheet on the right.",
646
+ "inputSchema": {
647
+ "type": "object",
648
+ "properties": {}
649
+ }
650
+ },
591
651
  {
592
652
  "name": "fold_lines",
593
653
  "description": "Fold (collapse) lines begin through end (1-based) in the active file.",
@@ -681,9 +741,31 @@
681
741
  }
682
742
  }
683
743
  },
744
+ {
745
+ "name": "get_console",
746
+ "description": "Read Sublime Text's built-in console. mode='auto' prefers a complete visible-console capture and falls back to the reload-safe prospective capture; mode='visible' requires a complete capture; mode='captured' is non-invasive but contains only messages observed since capture began. Results include source and complete metadata.",
747
+ "inputSchema": {
748
+ "type": "object",
749
+ "properties": {
750
+ "mode": {
751
+ "type": "string",
752
+ "enum": [
753
+ "auto",
754
+ "visible",
755
+ "captured"
756
+ ],
757
+ "default": "auto"
758
+ },
759
+ "tail": {
760
+ "type": "integer",
761
+ "default": 200
762
+ }
763
+ }
764
+ }
765
+ },
684
766
  {
685
767
  "name": "get_console_full",
686
- "description": "Return the entire captured ST console buffer with no tail limit.\nIncludes startup messages, plugin load events, and all errors since ST started.",
768
+ "description": "Compatibility alias for a complete visible-console capture.\nCurrently supported on Windows; prefer get_console(mode='visible').",
687
769
  "inputSchema": {
688
770
  "type": "object",
689
771
  "properties": {}
@@ -691,7 +773,7 @@
691
773
  },
692
774
  {
693
775
  "name": "get_console_log",
694
- "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.",
776
+ "description": "Compatibility tool for reload-safe prospective console capture.\ntail=N limits to the last N entries; tail=0 returns all retained entries.\nPrefer get_console(mode='captured') for explicit completeness metadata.",
695
777
  "inputSchema": {
696
778
  "type": "object",
697
779
  "properties": {
@@ -704,7 +786,7 @@
704
786
  },
705
787
  {
706
788
  "name": "get_console_win",
707
- "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. Returns error on non-Windows.",
789
+ "description": "Windows complete-console backend using reversible UI automation. Restores the previous panel, editor focus, pointer position, and text clipboard. Prefer get_console(mode='auto').",
708
790
  "inputSchema": {
709
791
  "type": "object",
710
792
  "properties": {}
@@ -801,7 +883,7 @@
801
883
  },
802
884
  {
803
885
  "name": "get_output_panel",
804
- "description": "Return the text content of an output panel.\nIf name is omitted, read the active output panel. Use name='exec' for build output.",
886
+ "description": "Return the text content of an output panel.\nIf name is omitted, read the active output panel. Use name='exec' for build output.\nUse name='Console' to read Sublime's built-in console through the best available backend.",
805
887
  "inputSchema": {
806
888
  "type": "object",
807
889
  "properties": {
@@ -814,7 +896,7 @@
814
896
  },
815
897
  {
816
898
  "name": "get_package_mcp_info",
817
- "description": "Return everything needed to write an MCP extension for an installed Package Control package.\nReturns: path, output_file, commands (with captions and args), settings_keys, python_files, extension_template.\nWrite the extension to output_file following extension_template; ST loads it automatically.",
899
+ "description": "Discover what an installed Sublime package can do, then just control it directly --\nno separate MCP server needed. Primary workflow: call this for the package name, Read\nthe files it lists under python_files to find the package's real command-dispatch table\nand settings (this tool's own commands/settings_keys are a starting point, not the full\npicture -- a package that routes many actions through one command with an 'action' arg,\nfor example, only shows up here as that one wrapper command), then call the real thing\nwith run_command or eval_python. That loop -- discover, read source, call directly -- is\nalmost always enough on its own.\nReturns: path, commands (with captions and args), settings_keys, python_files, plus\noutput_file/extension_template for the rarer case where a standing, independently-\nreachable MCP server is actually needed (e.g. a client with no eval_python-equivalent of\nits own) -- write the extension to output_file following extension_template and ST loads\nit automatically; see the package-mcp-generator skill for that heavier path.",
818
900
  "inputSchema": {
819
901
  "type": "object",
820
902
  "properties": {
@@ -851,6 +933,19 @@
851
933
  "properties": {}
852
934
  }
853
935
  },
936
+ {
937
+ "name": "get_selected_sheets",
938
+ "description": "Return the currently multi-selected sheets, optionally limited to one group, with stable IDs and group positions.",
939
+ "inputSchema": {
940
+ "type": "object",
941
+ "properties": {
942
+ "group": {
943
+ "type": "integer",
944
+ "minimum": 0
945
+ }
946
+ }
947
+ }
948
+ },
854
949
  {
855
950
  "name": "get_selection",
856
951
  "description": "Return the current selection(s): text and begin/end line+col for each.",
@@ -893,9 +988,25 @@
893
988
  ]
894
989
  }
895
990
  },
991
+ {
992
+ "name": "get_sheet_index",
993
+ "description": "Return a sheet's current group and index within that group. Identify it by global index or stable sheet ID.",
994
+ "inputSchema": {
995
+ "type": "object",
996
+ "properties": {
997
+ "index": {
998
+ "type": "integer",
999
+ "minimum": 0
1000
+ },
1001
+ "id": {
1002
+ "type": "integer"
1003
+ }
1004
+ }
1005
+ }
1006
+ },
896
1007
  {
897
1008
  "name": "get_sheets",
898
- "description": "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.",
1009
+ "description": "List ALL sheets (tabs) in the current window by index, including images and untitled buffers.\nReturns identity, group position, selection/focus state, type, path, name, and dirty state.\nUse index with get_sheet_content to read a specific tab.",
899
1010
  "inputSchema": {
900
1011
  "type": "object",
901
1012
  "properties": {}
@@ -1163,6 +1274,14 @@
1163
1274
  "properties": {}
1164
1275
  }
1165
1276
  },
1277
+ {
1278
+ "name": "main_thread_stack",
1279
+ "description": "Return ST's main thread's current Python stack trace right now, read directly via sys._current_frames() from whichever thread handles this request. Works even while the main thread is wedged, since it needs no cooperation from it -- use this to see exactly what a stuck call is doing instead of guessing from symptoms.",
1280
+ "inputSchema": {
1281
+ "type": "object",
1282
+ "properties": {}
1283
+ }
1284
+ },
1166
1285
  {
1167
1286
  "name": "move_line_down",
1168
1287
  "description": "Move the current line(s) down by one line.",
@@ -1179,6 +1298,46 @@
1179
1298
  "properties": {}
1180
1299
  }
1181
1300
  },
1301
+ {
1302
+ "name": "move_sheets_to_group",
1303
+ "description": "Move one or more sheets together to a group and optional insertion position, preserving native multi-selection when requested.",
1304
+ "inputSchema": {
1305
+ "type": "object",
1306
+ "properties": {
1307
+ "indices": {
1308
+ "type": "array",
1309
+ "items": {
1310
+ "type": "integer",
1311
+ "minimum": 0
1312
+ },
1313
+ "minItems": 1
1314
+ },
1315
+ "ids": {
1316
+ "type": "array",
1317
+ "items": {
1318
+ "type": "integer"
1319
+ },
1320
+ "minItems": 1
1321
+ },
1322
+ "group": {
1323
+ "type": "integer",
1324
+ "minimum": 0
1325
+ },
1326
+ "insertion_index": {
1327
+ "type": "integer",
1328
+ "minimum": -1,
1329
+ "default": -1
1330
+ },
1331
+ "select": {
1332
+ "type": "boolean",
1333
+ "default": true
1334
+ }
1335
+ },
1336
+ "required": [
1337
+ "group"
1338
+ ]
1339
+ }
1340
+ },
1182
1341
  {
1183
1342
  "name": "move_to_neighboring_group",
1184
1343
  "description": "Move the active view to the neighboring pane group.",
@@ -1541,6 +1700,49 @@
1541
1700
  "properties": {}
1542
1701
  }
1543
1702
  },
1703
+ {
1704
+ "name": "project_search",
1705
+ "description": "Search project files with Sublime Text's native Find in Files engine and return structured {path,line,col,text} matches. This is the preferred project search tool.",
1706
+ "inputSchema": {
1707
+ "type": "object",
1708
+ "properties": {
1709
+ "pattern": {
1710
+ "type": "string"
1711
+ },
1712
+ "where": {
1713
+ "type": "string",
1714
+ "description": "ST Where expression; empty uses current project folders."
1715
+ },
1716
+ "case_sensitive": {
1717
+ "type": "boolean",
1718
+ "default": false
1719
+ },
1720
+ "regex": {
1721
+ "type": "boolean",
1722
+ "default": false
1723
+ },
1724
+ "whole_word": {
1725
+ "type": "boolean",
1726
+ "default": false
1727
+ },
1728
+ "limit": {
1729
+ "type": "integer",
1730
+ "default": 200
1731
+ },
1732
+ "timeout": {
1733
+ "type": "number",
1734
+ "default": 30
1735
+ },
1736
+ "show_panel": {
1737
+ "type": "boolean",
1738
+ "default": false
1739
+ }
1740
+ },
1741
+ "required": [
1742
+ "pattern"
1743
+ ]
1744
+ }
1745
+ },
1544
1746
  {
1545
1747
  "name": "prompt_goto_line",
1546
1748
  "description": "Open the Goto Line prompt (WindowCommand).",
@@ -1747,7 +1949,7 @@
1747
1949
  },
1748
1950
  {
1749
1951
  "name": "run_command",
1750
- "description": "Run any Sublime Text command. scope='window' (default) or 'view'.",
1952
+ "description": "Run any Sublime Text command. scope='window' (default) or 'view'.\nFor scope='view'/'text', optionally target a specific tab with name (partial, case-insensitive) or index (0-based, from get_open_files) instead of whatever tab happens to be globally focused -- important when another agent/session may be sharing this Sublime instance.",
1751
1953
  "inputSchema": {
1752
1954
  "type": "object",
1753
1955
  "properties": {
@@ -1760,6 +1962,14 @@
1760
1962
  "scope": {
1761
1963
  "type": "string",
1762
1964
  "default": "window"
1965
+ },
1966
+ "name": {
1967
+ "type": "string",
1968
+ "default": ""
1969
+ },
1970
+ "index": {
1971
+ "type": "integer",
1972
+ "default": -1
1763
1973
  }
1764
1974
  },
1765
1975
  "required": [
@@ -1881,6 +2091,37 @@
1881
2091
  ]
1882
2092
  }
1883
2093
  },
2094
+ {
2095
+ "name": "select_sheets",
2096
+ "description": "Change native tab multi-selection across the window. Identify sheets by global indices or stable sheet IDs; optionally choose the focused sheet.",
2097
+ "inputSchema": {
2098
+ "type": "object",
2099
+ "properties": {
2100
+ "indices": {
2101
+ "type": "array",
2102
+ "items": {
2103
+ "type": "integer",
2104
+ "minimum": 0
2105
+ },
2106
+ "minItems": 1
2107
+ },
2108
+ "ids": {
2109
+ "type": "array",
2110
+ "items": {
2111
+ "type": "integer"
2112
+ },
2113
+ "minItems": 1
2114
+ },
2115
+ "focus_index": {
2116
+ "type": "integer",
2117
+ "minimum": 0
2118
+ },
2119
+ "focus_id": {
2120
+ "type": "integer"
2121
+ }
2122
+ }
2123
+ }
2124
+ },
1884
2125
  {
1885
2126
  "name": "select_theme",
1886
2127
  "description": "Open the theme picker (WindowCommand).",
@@ -1889,6 +2130,14 @@
1889
2130
  "properties": {}
1890
2131
  }
1891
2132
  },
2133
+ {
2134
+ "name": "select_to_left",
2135
+ "description": "Add the sheet left of the focused sheet to the tab multi-selection.",
2136
+ "inputSchema": {
2137
+ "type": "object",
2138
+ "properties": {}
2139
+ }
2140
+ },
1892
2141
  {
1893
2142
  "name": "select_to_mark",
1894
2143
  "description": "Select the text between the cursor and the previously set mark (TextCommand).",
@@ -1897,6 +2146,14 @@
1897
2146
  "properties": {}
1898
2147
  }
1899
2148
  },
2149
+ {
2150
+ "name": "select_to_right",
2151
+ "description": "Add the sheet right of the focused sheet to the tab multi-selection.",
2152
+ "inputSchema": {
2153
+ "type": "object",
2154
+ "properties": {}
2155
+ }
2156
+ },
1900
2157
  {
1901
2158
  "name": "send_to_view",
1902
2159
  "description": "Send a string to any open tab by name (partial match, case-insensitive).\nInserts the text at the cursor of the resolved view using the standard insert command; returns an error if the view is read-only.\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.",
@@ -2010,6 +2267,34 @@
2010
2267
  ]
2011
2268
  }
2012
2269
  },
2270
+ {
2271
+ "name": "set_sheet_index",
2272
+ "description": "Move one sheet to a group and position using Sublime's set_sheet_index API.",
2273
+ "inputSchema": {
2274
+ "type": "object",
2275
+ "properties": {
2276
+ "index": {
2277
+ "type": "integer",
2278
+ "minimum": 0
2279
+ },
2280
+ "id": {
2281
+ "type": "integer"
2282
+ },
2283
+ "group": {
2284
+ "type": "integer",
2285
+ "minimum": 0
2286
+ },
2287
+ "sheet_index": {
2288
+ "type": "integer",
2289
+ "minimum": 0
2290
+ }
2291
+ },
2292
+ "required": [
2293
+ "group",
2294
+ "sheet_index"
2295
+ ]
2296
+ }
2297
+ },
2013
2298
  {
2014
2299
  "name": "set_status",
2015
2300
  "description": "Write a message to Sublime Text's status bar.",
@@ -2405,6 +2690,30 @@
2405
2690
  "properties": {}
2406
2691
  }
2407
2692
  },
2693
+ {
2694
+ "name": "unselect_others",
2695
+ "description": "Collapse tab multi-selection to the focused sheet.",
2696
+ "inputSchema": {
2697
+ "type": "object",
2698
+ "properties": {}
2699
+ }
2700
+ },
2701
+ {
2702
+ "name": "unselect_to_left",
2703
+ "description": "Remove sheets left of the focused sheet from the tab multi-selection.",
2704
+ "inputSchema": {
2705
+ "type": "object",
2706
+ "properties": {}
2707
+ }
2708
+ },
2709
+ {
2710
+ "name": "unselect_to_right",
2711
+ "description": "Remove sheets right of the focused sheet from the tab multi-selection.",
2712
+ "inputSchema": {
2713
+ "type": "object",
2714
+ "properties": {}
2715
+ }
2716
+ },
2408
2717
  {
2409
2718
  "name": "upper_case",
2410
2719
  "description": "Convert the current selection(s) to UPPER CASE.",
package/http.js CHANGED
@@ -13,6 +13,7 @@ export const SLOW_ENDPOINTS = new Set([
13
13
  '/install_package',
14
14
  '/search_packages',
15
15
  '/find_in_files',
16
+ '/project_search',
16
17
  '/eval_python_latest',
17
18
  '/run_build',
18
19
  ]);
package/index.js CHANGED
@@ -14,6 +14,35 @@ const FALLBACK_TOOLS = JSON.parse(
14
14
  readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'fallback-tools.json'), 'utf8'),
15
15
  ).tools;
16
16
 
17
+ const DEFAULT_TOOL_NAMES = new Set([
18
+ 'get_help',
19
+ 'batch',
20
+ 'get_active_file',
21
+ 'project_search',
22
+ 'str_replace_based_edit_tool',
23
+ 'save_file',
24
+ 'discover_tools',
25
+ ]);
26
+ const GATEWAY_FALLBACKS = [
27
+ {
28
+ name: 'discover_tools',
29
+ description: 'Search advanced Sublime capabilities. Invoke a result through batch.',
30
+ inputSchema: { type: 'object', properties: {
31
+ query: { type: 'string' }, limit: { type: 'integer', default: 10 },
32
+ }, required: ['query'] },
33
+ },
34
+ {
35
+ name: 'project_search',
36
+ description: "Search project files with Sublime Text's native Find in Files engine and return structured matches.",
37
+ inputSchema: { type: 'object', properties: {
38
+ pattern: { type: 'string' }, where: { type: 'string' },
39
+ case_sensitive: { type: 'boolean', default: false }, regex: { type: 'boolean', default: false },
40
+ whole_word: { type: 'boolean', default: false }, limit: { type: 'integer', default: 200 },
41
+ timeout: { type: 'number', default: 30 }, show_panel: { type: 'boolean', default: false },
42
+ }, required: ['pattern'] },
43
+ },
44
+ ];
45
+
17
46
  process.stderr.write(`mcp-commander: BASE=${BASE} platform=${process.platform}\n`);
18
47
 
19
48
  function ok(data) {
@@ -132,14 +161,16 @@ function registerFallbackTools() {
132
161
  // exposes the same tool surface the backend actually serves instead of a
133
162
  // hand-maintained subset (F10). Every backend tool has a POST /{name}
134
163
  // alias, so one uniform call shape works for all of them.
135
- for (const tool of FALLBACK_TOOLS) {
164
+ const byName = new Map(FALLBACK_TOOLS.map(tool => [tool.name, tool]));
165
+ for (const tool of GATEWAY_FALLBACKS) byName.set(tool.name, tool);
166
+ for (const tool of [...byName.values()].filter(tool => DEFAULT_TOOL_NAMES.has(tool.name))) {
136
167
  server.registerTool(
137
168
  tool.name,
138
169
  { description: tool.description, inputSchema: jsonSchemaToZod(tool.inputSchema) },
139
170
  async (args) => ok(await post('/' + tool.name, args ?? {})),
140
171
  );
141
172
  }
142
- process.stderr.write(`mcp-commander: registered ${FALLBACK_TOOLS.length} generated fallback tools\n`);
173
+ process.stderr.write('mcp-commander: registered focused fallback tool surface\n');
143
174
  }
144
175
 
145
176
  // ── startup ───────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sublime-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.7.1",
4
4
  "description": "MCP server for Sublime Text 4 — exposes editor state and editing tools to AI assistants via the Model Context Protocol.",
5
5
  "type": "module",
6
6
  "bin": {
package/test_batch.mjs DELETED
@@ -1,72 +0,0 @@
1
- // Integration test for packages/node-proxy/index.js.
2
- //
3
- // Unlike hitting port 9500 or 9502 directly, this launches index.js as a real
4
- // subprocess and speaks MCP over stdio — the same code path a real MCP client
5
- // (e.g. Claude Code configured with a stdio server entry) uses. This is what
6
- // caught the missing `_POST["/batch"]` route on the HTTP bridge: node-proxy's
7
- // dynamic tool discovery (loadDynamicTools) picks up `batch` from /mcp_tools
8
- // fine, but the generic passthrough posts to /batch on port 9500, which
9
- // 404'd until that route was added to sublime_mcp.py.
10
- //
11
- // Prerequisites:
12
- // - Sublime Text running with sublime_mcp.py loaded (HTTP bridge on 9500)
13
- // - At least one file open in ST
14
- //
15
- // Run:
16
- // cd packages/node-proxy
17
- // node test_batch.mjs
18
-
19
- import assert from 'node:assert/strict';
20
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
22
-
23
- async function main() {
24
- const transport = new StdioClientTransport({
25
- command: 'node',
26
- args: ['index.js'],
27
- cwd: import.meta.dirname,
28
- stderr: 'pipe',
29
- });
30
-
31
- const client = new Client({ name: 'test-batch', version: '1.0.0' });
32
- await client.connect(transport);
33
-
34
- try {
35
- const tools = await client.listTools();
36
- const names = tools.tools.map(t => t.name);
37
- assert.ok(names.includes('batch'), 'batch tool not discovered from backend');
38
- console.log('PASS: batch discovered dynamically (%d tools total)', names.length);
39
-
40
- const result = await client.callTool({
41
- name: 'batch',
42
- arguments: { calls: [{ tool: 'get_line_count' }, { tool: 'get_selection' }] },
43
- });
44
- const data = JSON.parse(result.content[0].text);
45
- assert.ok(Array.isArray(data.results), 'batch response missing results array');
46
- assert.equal(data.results.length, 2, 'expected 2 results');
47
- assert.ok('line_count' in data.results[0], 'get_line_count result missing line_count');
48
- assert.ok('selections' in data.results[1], 'get_selection result missing selections');
49
- console.log('PASS: batch call returned populated results via the real proxy subprocess');
50
-
51
- const failResult = await client.callTool({
52
- name: 'batch',
53
- arguments: { calls: [{ tool: 'get_line_count' }, { tool: 'no_such_tool_xyz' }] },
54
- });
55
- const failData = JSON.parse(failResult.content[0].text);
56
- assert.equal(failData.results.length, 2);
57
- assert.ok('error' in failData.results[1], 'expected error for unknown tool');
58
- console.log('PASS: batch partial failure does not abort the whole call');
59
- } finally {
60
- await client.close();
61
- }
62
- }
63
-
64
- main()
65
- .then(() => {
66
- console.log('All tests passed.');
67
- process.exit(0);
68
- })
69
- .catch(err => {
70
- console.error('FAIL:', err);
71
- process.exit(1);
72
- });
@@ -1,125 +0,0 @@
1
- // Integration test for the generated fallback catalog (F10).
2
- //
3
- // Launches index.js as a real MCP stdio subprocess against a fake backend
4
- // that deliberately fails `/mcp_tools` discovery, forcing the fallback path.
5
- // Before the fix that path exposed a hand-maintained 71-tool subset; it must
6
- // now expose the full generated catalog and route calls correctly.
7
- //
8
- // No Sublime Text required — the fake backend stands in for the HTTP bridge.
9
- //
10
- // Run:
11
- // cd packages/node-proxy
12
- // node test_fallback_catalog.mjs
13
-
14
- import assert from 'node:assert/strict';
15
- import http from 'node:http';
16
- import { readFileSync } from 'node:fs';
17
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
19
-
20
- const CATALOG = JSON.parse(
21
- readFileSync(new URL('./fallback-tools.json', import.meta.url), 'utf8'),
22
- ).tools;
23
-
24
- // Fake bridge: 404 on /mcp_tools (kills discovery), echo on POST /{tool}.
25
- function startFakeBackend() {
26
- const seen = [];
27
- const server = http.createServer((req, res) => {
28
- if (req.url.startsWith('/mcp_tools')) {
29
- res.writeHead(404).end('no discovery for you');
30
- return;
31
- }
32
- let body = '';
33
- req.on('data', chunk => { body += chunk; });
34
- req.on('end', () => {
35
- seen.push({ url: req.url, method: req.method, body });
36
- res.writeHead(200, { 'Content-Type': 'application/json' });
37
- res.end(JSON.stringify({ echo: req.url, args: body ? JSON.parse(body) : null }));
38
- });
39
- });
40
- return new Promise(resolve => {
41
- server.listen(0, '127.0.0.1', () => resolve({ server, seen, port: server.address().port }));
42
- });
43
- }
44
-
45
- async function main() {
46
- const { server, seen, port } = await startFakeBackend();
47
-
48
- const transport = new StdioClientTransport({
49
- command: 'node',
50
- args: ['index.js'],
51
- cwd: import.meta.dirname,
52
- env: { ...process.env, SUBLIME_MCP_BASE: `http://127.0.0.1:${port}` },
53
- stderr: 'pipe',
54
- });
55
-
56
- const client = new Client({ name: 'test-fallback-catalog', version: '1.0.0' });
57
- await client.connect(transport);
58
-
59
- try {
60
- const listed = await client.listTools();
61
- const names = new Set(listed.tools.map(t => t.name));
62
-
63
- assert.equal(
64
- names.size,
65
- CATALOG.length,
66
- `fallback exposed ${names.size} tools, expected the full generated catalog (${CATALOG.length})`,
67
- );
68
- for (const tool of CATALOG) {
69
- assert.ok(names.has(tool.name), `fallback is missing ${tool.name}`);
70
- }
71
- console.log('PASS: discovery failure still exposes all %d generated tools', names.size);
72
-
73
- // batch was the headline F10 omission: absent from the old hand-written
74
- // fallback, so a discovery miss removed it from the agent entirely.
75
- assert.ok(names.has('batch'), 'batch missing from fallback catalog');
76
- const result = await client.callTool({
77
- name: 'batch',
78
- arguments: { calls: [{ tool: 'get_line_count' }] },
79
- });
80
- const data = JSON.parse(result.content[0].text);
81
- assert.equal(data.echo, '/batch', 'batch did not route to POST /batch');
82
- assert.deepEqual(data.args, { calls: [{ tool: 'get_line_count' }] });
83
- console.log('PASS: batch routes through the fallback path with its arguments intact');
84
-
85
- // A no-parameter tool must still post cleanly.
86
- await client.callTool({ name: 'get_line_count', arguments: {} });
87
- assert.ok(
88
- seen.some(r => r.url === '/get_line_count' && r.method === 'POST'),
89
- 'get_line_count did not POST to its tool-name alias',
90
- );
91
- console.log('PASS: no-parameter tools post to their /{name} alias');
92
-
93
- // Schemas must survive, not degrade to untyped passthrough.
94
- const openFile = listed.tools.find(t => t.name === 'open_file');
95
- assert.ok(openFile.inputSchema?.properties?.path, 'open_file lost its typed schema');
96
- console.log('PASS: generated schemas reach the client');
97
-
98
- // The server must report the published package version. This is also the
99
- // check that catches index.js failing to even start: package.json here is
100
- // BOM-prefixed, and JSON.parse rejects a leading BOM.
101
- const pkg = JSON.parse(
102
- readFileSync(new URL('./package.json', import.meta.url), 'utf8').replace(/^\uFEFF/, ''),
103
- );
104
- const reported = client.getServerVersion();
105
- assert.equal(
106
- reported?.version,
107
- pkg.version,
108
- `server reported version ${reported?.version}, package.json says ${pkg.version}`,
109
- );
110
- console.log('PASS: server reports the package.json version (%s)', pkg.version);
111
- } finally {
112
- await client.close();
113
- server.close();
114
- }
115
- }
116
-
117
- main()
118
- .then(() => {
119
- console.log('All tests passed.');
120
- process.exit(0);
121
- })
122
- .catch(err => {
123
- console.error('FAIL:', err);
124
- process.exit(1);
125
- });