sublime-mcp 1.3.1 → 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/agents.md +32 -0
- package/index.js +423 -393
- package/package.json +32 -32
package/AGENT_GUIDE.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Agent Guide for sublime-mcp (MCP Commander)
|
|
2
|
+
|
|
3
|
+
This document teaches AI agents how to use sublime-mcp tools correctly.
|
|
4
|
+
Read this before editing files, closing tabs, or running ST commands.
|
|
5
|
+
|
|
6
|
+
## Critical Rules
|
|
7
|
+
|
|
8
|
+
### Editing Files
|
|
9
|
+
**The `str_replace_based_edit_tool` edits the ST buffer in memory but does NOT save to disk.**
|
|
10
|
+
After every edit, you MUST call `save_file` with the file path:
|
|
11
|
+
|
|
12
|
+
1. `str_replace_based_edit_tool` (command="str_replace") → edits buffer
|
|
13
|
+
2. `save_file` (path="C:\\path\\to\\file.py") → writes to disk
|
|
14
|
+
|
|
15
|
+
If you skip step 2, the file will be dirty in ST but unchanged on disk.
|
|
16
|
+
This causes desync — ST shows your edits but `git diff` shows nothing.
|
|
17
|
+
|
|
18
|
+
### Disk vs Buffer
|
|
19
|
+
- `edit` tool (OpenClaw): writes directly to disk, ST buffer may be stale
|
|
20
|
+
- `str_replace_based_edit_tool` (sublime-mcp): writes to ST buffer, disk may be stale
|
|
21
|
+
- If you use `edit` on a file open in ST, call `revert_file` after so ST reloads from disk
|
|
22
|
+
- If you use `str_replace_based_edit_tool`, call `save_file` after so disk matches buffer
|
|
23
|
+
|
|
24
|
+
### Closing Tabs
|
|
25
|
+
`close_file` takes a `path` parameter. If the file is dirty (unsaved changes),
|
|
26
|
+
ST will prompt the user to save — the tool will hang.
|
|
27
|
+
|
|
28
|
+
To close a dirty/scratch tab safely:
|
|
29
|
+
```python
|
|
30
|
+
# eval_python: mark as scratch and close
|
|
31
|
+
v = window.find_open_by_name("untitled") # or find by id
|
|
32
|
+
v.set_scratch(True)
|
|
33
|
+
v.close()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`eval_python` works — use `print()` to get output. The environment has:
|
|
37
|
+
- `sublime` — the sublime module
|
|
38
|
+
- `window` — the active window
|
|
39
|
+
- `view` — the active view
|
|
40
|
+
- `print` — writes to captured output
|
|
41
|
+
|
|
42
|
+
### eval_python Usage
|
|
43
|
+
- Use `print()` to return values — the output is captured and returned
|
|
44
|
+
- Do NOT use `return` at top level (syntax error)
|
|
45
|
+
- Do NOT use bare expressions expecting output (use `print(expr)`)
|
|
46
|
+
- The environment has `sublime`, `window`, `view` available
|
|
47
|
+
|
|
48
|
+
## Key ST API Methods for Agents
|
|
49
|
+
|
|
50
|
+
### Window
|
|
51
|
+
- `window.views()` — list all views in the window
|
|
52
|
+
- `window.active_view()` — get the focused view
|
|
53
|
+
- `window.active_group()` — get active group index
|
|
54
|
+
- `window.find_open_file(path)` — find a view by file path
|
|
55
|
+
- `window.open_file(path)` — open a file
|
|
56
|
+
- `window.run_command(cmd, args)` — run a window command
|
|
57
|
+
- `window.new_file()` — create untitled buffer
|
|
58
|
+
- `window.sheets()` — list all sheets (tabs)
|
|
59
|
+
|
|
60
|
+
### View
|
|
61
|
+
- `view.file_name()` — file path (None for untitled)
|
|
62
|
+
- `view.name()` — display name
|
|
63
|
+
- `view.is_dirty()` — has unsaved changes
|
|
64
|
+
- `view.set_scratch(True)` — mark as scratch (no save prompt on close)
|
|
65
|
+
- `view.close()` — close the view (may prompt if dirty)
|
|
66
|
+
- `view.run_command(cmd, args)` — run a text command on this view
|
|
67
|
+
- `view.substr(region)` — get text in region
|
|
68
|
+
- `view.size()` — total character count
|
|
69
|
+
- `view.find_all(pattern, flags)` — find all matches
|
|
70
|
+
|
|
71
|
+
### Sheet
|
|
72
|
+
- `sheet.close()` — close a sheet (may not work on dirty sheets)
|
|
73
|
+
- `sheet.view()` — get the view for a text sheet
|
|
74
|
+
|
|
75
|
+
## Common Operations
|
|
76
|
+
|
|
77
|
+
### Save a file
|
|
78
|
+
```
|
|
79
|
+
save_file(path="C:\\path\\to\\file.py")
|
|
80
|
+
```
|
|
81
|
+
Omit path to save the active file.
|
|
82
|
+
|
|
83
|
+
### Save all open files
|
|
84
|
+
```
|
|
85
|
+
save_all()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Revert file (discard unsaved changes, reload from disk)
|
|
89
|
+
```
|
|
90
|
+
revert_file()
|
|
91
|
+
```
|
|
92
|
+
Works on the active file only.
|
|
93
|
+
|
|
94
|
+
### Close a file by path
|
|
95
|
+
```
|
|
96
|
+
close_file(path="C:\\path\\to\\file.py")
|
|
97
|
+
```
|
|
98
|
+
Only works if the file is NOT dirty. If dirty, save first or use eval_python with set_scratch.
|
|
99
|
+
|
|
100
|
+
### Close a dirty/untitled/scratch tab
|
|
101
|
+
```python
|
|
102
|
+
# eval_python
|
|
103
|
+
for v in window.views():
|
|
104
|
+
if v.name() == "Config warnings:" or (v.is_dirty() and not v.file_name()):
|
|
105
|
+
v.set_scratch(True)
|
|
106
|
+
v.close()
|
|
107
|
+
print("closed")
|
|
108
|
+
break
|
|
109
|
+
print("done")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Open a file at a specific line
|
|
113
|
+
```
|
|
114
|
+
open_file(path="C:\\path\\to\\file.py", line=42, col=1)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Run a Sublime command
|
|
118
|
+
```
|
|
119
|
+
run_command(command="close_file", scope="window")
|
|
120
|
+
```
|
|
121
|
+
Scopes: "window" (default), "view", "application".
|
|
122
|
+
Check available commands with `get_commands`.
|
|
123
|
+
|
|
124
|
+
### Get all tabs info
|
|
125
|
+
```
|
|
126
|
+
get_sheets()
|
|
127
|
+
```
|
|
128
|
+
Returns index, type, path, name, is_dirty for each tab.
|
|
129
|
+
|
|
130
|
+
### Read a tab by index
|
|
131
|
+
```
|
|
132
|
+
get_sheet_content(index=2)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Tool Reliability Notes
|
|
136
|
+
|
|
137
|
+
- `str_replace_based_edit_tool` reports success but does NOT persist to disk
|
|
138
|
+
- `close_file` by path works only for non-dirty files
|
|
139
|
+
- `eval_python` works reliably — use `print()` for output
|
|
140
|
+
- `save_file` by path works reliably when the file is open in ST
|
|
141
|
+
- `get_sheets` / `get_sheet_content` work reliably for reading tab state
|
|
142
|
+
- `run_command` works but you must know the correct command name and scope
|
|
143
|
+
- `find_in_files` works for searching project files
|
|
144
|
+
- `get_commands` lists available commands but without descriptions or arg schemas
|
|
145
|
+
|
|
146
|
+
## Gaps to Address in sublime-mcp
|
|
147
|
+
|
|
148
|
+
1. **str_replace should auto-save** (or offer a save option)
|
|
149
|
+
2. **close_file should handle dirty files** (set_scratch + close, or return error with guidance)
|
|
150
|
+
3. **get_commands should return descriptions and arg schemas** (not just IDs)
|
|
151
|
+
4. **No close_sheet by index/ID** — must use eval_python workaround
|
|
152
|
+
5. **eval_python output can be empty if code uses return instead of print**
|
|
153
|
+
6. **No tool to check if buffer matches disk** (is_dirty is available but not "is stale")
|
|
154
|
+
|
|
155
|
+
## Critical Lessons Learned
|
|
156
|
+
|
|
157
|
+
### NEVER use `insert` command to paste large content
|
|
158
|
+
ST's auto-indent will mangle every line by accumulating indentation. Using `v.run_command("insert", {"characters": content})` on a full file will destroy the indentation — each line gets indented on top of the previous line's indentation.
|
|
159
|
+
|
|
160
|
+
**Instead use:**
|
|
161
|
+
- `str_replace_based_edit_tool` for targeted text replacements
|
|
162
|
+
- `replace_lines` to replace specific line ranges by line number
|
|
163
|
+
- For copying content between views: read source with `substr`, then use `replace_lines` on the target
|
|
164
|
+
|
|
165
|
+
### To revert a file to disk state
|
|
166
|
+
`revert` command sometimes doesn't fully reload the buffer. Reliable approach:
|
|
167
|
+
```python
|
|
168
|
+
# eval_python
|
|
169
|
+
path = view.file_name()
|
|
170
|
+
view.set_scratch(True)
|
|
171
|
+
view.close()
|
|
172
|
+
window.open_file(path)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### To apply edits from a preview tab to the real file
|
|
176
|
+
Do NOT copy entire file content via `insert`. Instead:
|
|
177
|
+
1. Make targeted edits directly on the real file using `str_replace_based_edit_tool`
|
|
178
|
+
2. Save with `save_file`
|
|
179
|
+
3. Verify with `is_dirty` check (should be False after save)
|
|
180
|
+
|
|
181
|
+
### File editing workflow with diff preview
|
|
182
|
+
1. Open the real file in ST
|
|
183
|
+
2. Create a new untitled tab with a copy of the file content (use `replace_lines` not `insert`)
|
|
184
|
+
3. Make edits in the preview tab
|
|
185
|
+
4. Use DiffTabs (`diff_tabs_palette`) or side-by-side layout to review
|
|
186
|
+
5. After approval, make the same targeted edits on the real file with `str_replace_based_edit_tool`
|
|
187
|
+
6. Save with `save_file`
|
|
188
|
+
7. Close the preview tab
|
package/agents.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# sublime-mcp — Agent Context
|
|
2
|
+
|
|
3
|
+
**Root law:** If you need the org map, go to ../router.md
|
|
4
|
+
**If this isn't the right place for your task, go back to:** ../agents.md
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What This Project Is
|
|
9
|
+
MCP server that exposes Sublime Text commands and state to Claude and other AI agents.
|
|
10
|
+
5638 Sublime Text packages exist — potential to auto-generate MCPs from them.
|
|
11
|
+
|
|
12
|
+
## Critical Assumption
|
|
13
|
+
Plugin edits in this repo are NOT live in Sublime Text until deployed to the installed Sublime Text Packages directory. Never assume test results are valid until the file has been copied there. If something isn't working, check deployment before investigating the code.
|
|
14
|
+
|
|
15
|
+
## Key Files
|
|
16
|
+
- sublime_mcp.py (111KB) — Main MCP server implementation
|
|
17
|
+
- mcp_server.py — MCP server entry point
|
|
18
|
+
- MCP Commander.sublime-commands — Command palette entries (may be incomplete)
|
|
19
|
+
- index.js — JavaScript component
|
|
20
|
+
|
|
21
|
+
## Known Issues
|
|
22
|
+
- MCP Commander.sublime-commands may be incomplete
|
|
23
|
+
- Some commands in C:\Users\donal\projects\SText\Default.sublime-commands may belong here
|
|
24
|
+
|
|
25
|
+
## Active Ideas
|
|
26
|
+
- Auto-generate MCPs from installed ST packages
|
|
27
|
+
- See https://modelcontextprotocol.io/extensions/apps/overview for the target vision
|
|
28
|
+
|
|
29
|
+
## Related Projects
|
|
30
|
+
- SText (uses this MCP server)
|
|
31
|
+
- joelekstrom's sublime-context-MCP (external, Donal left a comment there)
|
|
32
|
+
- OmkarGowda990's sublime-text-mcp (external)
|
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);
|
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "sublime-mcp",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "MCP server for Sublime Text 4 — exposes editor state and editing tools to AI assistants via the Model Context Protocol.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"sublime-mcp": "index.js"
|
|
8
|
-
},
|
|
9
|
-
"engines": {
|
|
10
|
-
"node": ">=18"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/dpc00/sublime-mcp.git"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"sublime-text",
|
|
18
|
-
"mcp",
|
|
19
|
-
"ai",
|
|
20
|
-
"claude"
|
|
21
|
-
],
|
|
22
|
-
"author": "Donald Chitester",
|
|
23
|
-
"license": "MIT",
|
|
24
|
-
"bugs": {
|
|
25
|
-
"url": "https://github.com/dpc00/sublime-mcp/issues"
|
|
26
|
-
},
|
|
27
|
-
"homepage": "https://github.com/dpc00/sublime-mcp#readme",
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
-
"zod": "^4.4.3"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "sublime-mcp",
|
|
3
|
+
"version": "1.3.3",
|
|
4
|
+
"description": "MCP server for Sublime Text 4 — exposes editor state and editing tools to AI assistants via the Model Context Protocol.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sublime-mcp": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/dpc00/sublime-mcp.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sublime-text",
|
|
18
|
+
"mcp",
|
|
19
|
+
"ai",
|
|
20
|
+
"claude"
|
|
21
|
+
],
|
|
22
|
+
"author": "Donald Chitester",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/dpc00/sublime-mcp/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/dpc00/sublime-mcp#readme",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"zod": "^4.4.3"
|
|
31
|
+
}
|
|
32
|
+
}
|