sublime-mcp 1.4.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }
@@ -272,6 +272,26 @@
272
272
  "properties": {}
273
273
  }
274
274
  },
275
+ {
276
+ "name": "discover_tools",
277
+ "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.",
278
+ "inputSchema": {
279
+ "type": "object",
280
+ "properties": {
281
+ "query": {
282
+ "type": "string",
283
+ "description": "Capability to find, such as bookmarks, tabs, syntax, or commands."
284
+ },
285
+ "limit": {
286
+ "type": "integer",
287
+ "default": 10
288
+ }
289
+ },
290
+ "required": [
291
+ "query"
292
+ ]
293
+ }
294
+ },
275
295
  {
276
296
  "name": "duplicate_line",
277
297
  "description": "Duplicate the current line(s) in the active file.",
@@ -588,6 +608,38 @@
588
608
  "properties": {}
589
609
  }
590
610
  },
611
+ {
612
+ "name": "focus_sheet",
613
+ "description": "Move input focus to one sheet by global index or stable sheet ID without changing the selected sheet set.",
614
+ "inputSchema": {
615
+ "type": "object",
616
+ "properties": {
617
+ "index": {
618
+ "type": "integer",
619
+ "minimum": 0
620
+ },
621
+ "id": {
622
+ "type": "integer"
623
+ }
624
+ }
625
+ }
626
+ },
627
+ {
628
+ "name": "focus_to_left",
629
+ "description": "Move input focus to the selected sheet on the left.",
630
+ "inputSchema": {
631
+ "type": "object",
632
+ "properties": {}
633
+ }
634
+ },
635
+ {
636
+ "name": "focus_to_right",
637
+ "description": "Move input focus to the selected sheet on the right.",
638
+ "inputSchema": {
639
+ "type": "object",
640
+ "properties": {}
641
+ }
642
+ },
591
643
  {
592
644
  "name": "fold_lines",
593
645
  "description": "Fold (collapse) lines begin through end (1-based) in the active file.",
@@ -681,9 +733,31 @@
681
733
  }
682
734
  }
683
735
  },
736
+ {
737
+ "name": "get_console",
738
+ "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.",
739
+ "inputSchema": {
740
+ "type": "object",
741
+ "properties": {
742
+ "mode": {
743
+ "type": "string",
744
+ "enum": [
745
+ "auto",
746
+ "visible",
747
+ "captured"
748
+ ],
749
+ "default": "auto"
750
+ },
751
+ "tail": {
752
+ "type": "integer",
753
+ "default": 200
754
+ }
755
+ }
756
+ }
757
+ },
684
758
  {
685
759
  "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.",
760
+ "description": "Compatibility alias for a complete visible-console capture.\nCurrently supported on Windows; prefer get_console(mode='visible').",
687
761
  "inputSchema": {
688
762
  "type": "object",
689
763
  "properties": {}
@@ -691,7 +765,7 @@
691
765
  },
692
766
  {
693
767
  "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.",
768
+ "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
769
  "inputSchema": {
696
770
  "type": "object",
697
771
  "properties": {
@@ -704,7 +778,7 @@
704
778
  },
705
779
  {
706
780
  "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.",
781
+ "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
782
  "inputSchema": {
709
783
  "type": "object",
710
784
  "properties": {}
@@ -801,7 +875,7 @@
801
875
  },
802
876
  {
803
877
  "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.",
878
+ "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
879
  "inputSchema": {
806
880
  "type": "object",
807
881
  "properties": {
@@ -851,6 +925,19 @@
851
925
  "properties": {}
852
926
  }
853
927
  },
928
+ {
929
+ "name": "get_selected_sheets",
930
+ "description": "Return the currently multi-selected sheets, optionally limited to one group, with stable IDs and group positions.",
931
+ "inputSchema": {
932
+ "type": "object",
933
+ "properties": {
934
+ "group": {
935
+ "type": "integer",
936
+ "minimum": 0
937
+ }
938
+ }
939
+ }
940
+ },
854
941
  {
855
942
  "name": "get_selection",
856
943
  "description": "Return the current selection(s): text and begin/end line+col for each.",
@@ -893,9 +980,25 @@
893
980
  ]
894
981
  }
895
982
  },
983
+ {
984
+ "name": "get_sheet_index",
985
+ "description": "Return a sheet's current group and index within that group. Identify it by global index or stable sheet ID.",
986
+ "inputSchema": {
987
+ "type": "object",
988
+ "properties": {
989
+ "index": {
990
+ "type": "integer",
991
+ "minimum": 0
992
+ },
993
+ "id": {
994
+ "type": "integer"
995
+ }
996
+ }
997
+ }
998
+ },
896
999
  {
897
1000
  "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.",
1001
+ "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
1002
  "inputSchema": {
900
1003
  "type": "object",
901
1004
  "properties": {}
@@ -1179,6 +1282,46 @@
1179
1282
  "properties": {}
1180
1283
  }
1181
1284
  },
1285
+ {
1286
+ "name": "move_sheets_to_group",
1287
+ "description": "Move one or more sheets together to a group and optional insertion position, preserving native multi-selection when requested.",
1288
+ "inputSchema": {
1289
+ "type": "object",
1290
+ "properties": {
1291
+ "indices": {
1292
+ "type": "array",
1293
+ "items": {
1294
+ "type": "integer",
1295
+ "minimum": 0
1296
+ },
1297
+ "minItems": 1
1298
+ },
1299
+ "ids": {
1300
+ "type": "array",
1301
+ "items": {
1302
+ "type": "integer"
1303
+ },
1304
+ "minItems": 1
1305
+ },
1306
+ "group": {
1307
+ "type": "integer",
1308
+ "minimum": 0
1309
+ },
1310
+ "insertion_index": {
1311
+ "type": "integer",
1312
+ "minimum": -1,
1313
+ "default": -1
1314
+ },
1315
+ "select": {
1316
+ "type": "boolean",
1317
+ "default": true
1318
+ }
1319
+ },
1320
+ "required": [
1321
+ "group"
1322
+ ]
1323
+ }
1324
+ },
1182
1325
  {
1183
1326
  "name": "move_to_neighboring_group",
1184
1327
  "description": "Move the active view to the neighboring pane group.",
@@ -1541,6 +1684,49 @@
1541
1684
  "properties": {}
1542
1685
  }
1543
1686
  },
1687
+ {
1688
+ "name": "project_search",
1689
+ "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.",
1690
+ "inputSchema": {
1691
+ "type": "object",
1692
+ "properties": {
1693
+ "pattern": {
1694
+ "type": "string"
1695
+ },
1696
+ "where": {
1697
+ "type": "string",
1698
+ "description": "ST Where expression; empty uses current project folders."
1699
+ },
1700
+ "case_sensitive": {
1701
+ "type": "boolean",
1702
+ "default": false
1703
+ },
1704
+ "regex": {
1705
+ "type": "boolean",
1706
+ "default": false
1707
+ },
1708
+ "whole_word": {
1709
+ "type": "boolean",
1710
+ "default": false
1711
+ },
1712
+ "limit": {
1713
+ "type": "integer",
1714
+ "default": 200
1715
+ },
1716
+ "timeout": {
1717
+ "type": "number",
1718
+ "default": 30
1719
+ },
1720
+ "show_panel": {
1721
+ "type": "boolean",
1722
+ "default": false
1723
+ }
1724
+ },
1725
+ "required": [
1726
+ "pattern"
1727
+ ]
1728
+ }
1729
+ },
1544
1730
  {
1545
1731
  "name": "prompt_goto_line",
1546
1732
  "description": "Open the Goto Line prompt (WindowCommand).",
@@ -1881,6 +2067,37 @@
1881
2067
  ]
1882
2068
  }
1883
2069
  },
2070
+ {
2071
+ "name": "select_sheets",
2072
+ "description": "Change native tab multi-selection across the window. Identify sheets by global indices or stable sheet IDs; optionally choose the focused sheet.",
2073
+ "inputSchema": {
2074
+ "type": "object",
2075
+ "properties": {
2076
+ "indices": {
2077
+ "type": "array",
2078
+ "items": {
2079
+ "type": "integer",
2080
+ "minimum": 0
2081
+ },
2082
+ "minItems": 1
2083
+ },
2084
+ "ids": {
2085
+ "type": "array",
2086
+ "items": {
2087
+ "type": "integer"
2088
+ },
2089
+ "minItems": 1
2090
+ },
2091
+ "focus_index": {
2092
+ "type": "integer",
2093
+ "minimum": 0
2094
+ },
2095
+ "focus_id": {
2096
+ "type": "integer"
2097
+ }
2098
+ }
2099
+ }
2100
+ },
1884
2101
  {
1885
2102
  "name": "select_theme",
1886
2103
  "description": "Open the theme picker (WindowCommand).",
@@ -1889,6 +2106,14 @@
1889
2106
  "properties": {}
1890
2107
  }
1891
2108
  },
2109
+ {
2110
+ "name": "select_to_left",
2111
+ "description": "Add the sheet left of the focused sheet to the tab multi-selection.",
2112
+ "inputSchema": {
2113
+ "type": "object",
2114
+ "properties": {}
2115
+ }
2116
+ },
1892
2117
  {
1893
2118
  "name": "select_to_mark",
1894
2119
  "description": "Select the text between the cursor and the previously set mark (TextCommand).",
@@ -1897,6 +2122,14 @@
1897
2122
  "properties": {}
1898
2123
  }
1899
2124
  },
2125
+ {
2126
+ "name": "select_to_right",
2127
+ "description": "Add the sheet right of the focused sheet to the tab multi-selection.",
2128
+ "inputSchema": {
2129
+ "type": "object",
2130
+ "properties": {}
2131
+ }
2132
+ },
1900
2133
  {
1901
2134
  "name": "send_to_view",
1902
2135
  "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 +2243,34 @@
2010
2243
  ]
2011
2244
  }
2012
2245
  },
2246
+ {
2247
+ "name": "set_sheet_index",
2248
+ "description": "Move one sheet to a group and position using Sublime's set_sheet_index API.",
2249
+ "inputSchema": {
2250
+ "type": "object",
2251
+ "properties": {
2252
+ "index": {
2253
+ "type": "integer",
2254
+ "minimum": 0
2255
+ },
2256
+ "id": {
2257
+ "type": "integer"
2258
+ },
2259
+ "group": {
2260
+ "type": "integer",
2261
+ "minimum": 0
2262
+ },
2263
+ "sheet_index": {
2264
+ "type": "integer",
2265
+ "minimum": 0
2266
+ }
2267
+ },
2268
+ "required": [
2269
+ "group",
2270
+ "sheet_index"
2271
+ ]
2272
+ }
2273
+ },
2013
2274
  {
2014
2275
  "name": "set_status",
2015
2276
  "description": "Write a message to Sublime Text's status bar.",
@@ -2405,6 +2666,30 @@
2405
2666
  "properties": {}
2406
2667
  }
2407
2668
  },
2669
+ {
2670
+ "name": "unselect_others",
2671
+ "description": "Collapse tab multi-selection to the focused sheet.",
2672
+ "inputSchema": {
2673
+ "type": "object",
2674
+ "properties": {}
2675
+ }
2676
+ },
2677
+ {
2678
+ "name": "unselect_to_left",
2679
+ "description": "Remove sheets left of the focused sheet from the tab multi-selection.",
2680
+ "inputSchema": {
2681
+ "type": "object",
2682
+ "properties": {}
2683
+ }
2684
+ },
2685
+ {
2686
+ "name": "unselect_to_right",
2687
+ "description": "Remove sheets right of the focused sheet from the tab multi-selection.",
2688
+ "inputSchema": {
2689
+ "type": "object",
2690
+ "properties": {}
2691
+ }
2692
+ },
2408
2693
  {
2409
2694
  "name": "upper_case",
2410
2695
  "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.6.0",
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
- });