webnav-ts-mcp 0.1.0 → 0.2.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/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  An MCP server that gives AI agents JS/TS/HTML/CSS navigation, plus a
4
4
  cross-file index of CSS custom properties and `#id`/`.class` selectors that
5
- single-file language servers can't provide. It is the TypeScript port of the
6
- Python [`webnav-mcp`](https://pypi.org/project/webnav-mcp/), so a web project can
7
- add it with its own package manager and have no Python or `uv` in the chain.
5
+ single-file language servers can't provide, and write tools that type-check an
6
+ edit before it is written. It needs only Node.js and installs with a web
7
+ project's own package manager.
8
8
 
9
9
  ## Quick start
10
10
 
@@ -93,6 +93,53 @@ Then use the position tools once you have a `path:line:col`:
93
93
  Positions are **1-indexed**; `column` is a UTF-16 character offset (a leading
94
94
  tab counts as one character).
95
95
 
96
+ ## Write tools: change code with a type check before every write
97
+
98
+ The navigation tools above are read-only. These change code, and share one rule:
99
+ **the edit is shown to the language servers as an in-memory overlay first, the
100
+ edited files and the script files importing them are re-checked, and the
101
+ response says which diagnostics the edit would add or fix. Nothing is written
102
+ until that is on the table.**
103
+
104
+ | Tool | Does |
105
+ |------|------|
106
+ | `edit` | Type-checked text edit of a JS/TS/HTML/CSS file: `old_string`/`new_string` or a whole `new_text` (also creates a file). For anything that is not a whole definition. Line endings follow the file |
107
+ | `edit_symbol` | `action="replace"` / `"insert"` / `"delete"` a function, class, interface, enum, const or member by name: re-indented to the file's own style (tabs or spaces), doc comment kept, `imports=[...]` merged into the file, blank-line spacing handled. Delete refuses while anything uses the symbol and prunes imports only the deleted code used |
108
+ | `rename_symbol` | JS/TS: rename through the TypeScript language server (imports, aliases, overriding members; `parameter=` for one parameter). CSS/HTML: a name starting with `--`, `#` or `.` renames that custom property / id / class across stylesheets, `<style>` and `style=""`, markup (`class`, `id`, `for`, `aria-*`, `href="#id"`) and scripts (`getElementById`, `classList`, `querySelector`, `className`, `setProperty`, markup in strings). Names built dynamically and script strings that merely equal the name are listed, not renamed; other places still using the old name (docs, `.vue`/`.scss`, strings) are listed too |
109
+ | `move` | With `name`: move a top-level function/class/interface/type/const to another file with the imports it needs, repoint every importer and re-export, drop imports only the moved code used, warn about import cycles. Without it: move or rename the file `file_path` (any file): imports (the language server's own rewrite), `<script src>`/`<link href>`/`<img src>` and CSS `url()`/`@import` follow, and the moved file's own relative references are re-based |
110
+ | `quick_fix` | Apply the language server's own fixes (missing imports, misspellings, ...) one by one, re-checking after each; or a source action: `organize_imports`, `remove_unused_imports`, `sort_imports`, `fix_all`. Ambiguous fixes are listed instead of guessed |
111
+ | `verify_changes` | After editing with any tool (your own included): which diagnostics the working tree gained or lost since a git revision (default `HEAD`) |
112
+ | `apply_edit` / `undo_edit` | Write a previewed edit by id; revert an applied one |
113
+
114
+ One rule for every write tool: it writes when the edit adds no more than
115
+ `max_new_errors` (default 0) errors in the edited files and their importers and
116
+ the result parses. Otherwise nothing is written and you get the diagnostics plus
117
+ an id for `apply_edit`. `apply=false` always previews. Hints and warnings are
118
+ shown but not counted. Syntax errors (TypeScript's 1xxx codes, any CSS error)
119
+ block a write even with `max_new_errors=null`.
120
+
121
+ Safety rules, enforced in code:
122
+
123
+ - Only `.js`/`.mjs`/`.cjs`/`.jsx`/`.ts`/`.mts`/`.cts`/`.tsx`/`.html`/`.css` files inside the
124
+ workspace are edited (`move` also carries any file, such as an image a stylesheet
125
+ uses); `node_modules`, `dist`, `.git` and the other skipped folders, generated output
126
+ (`WEBNAV_MCP_EXCLUDE`) and symlinks are refused.
127
+ - A preview remembers the exact bytes of every file; `apply_edit` refuses if any file
128
+ changed since. Files are replaced atomically (temp file + rename), and a failure
129
+ part-way restores the ones already written.
130
+ - Every applied edit is undoable (`undo_edit`) unless a file was changed after it.
131
+ - Line endings and a leading BOM are preserved.
132
+ - Edits touching more than 40 files need `allow_large=true`.
133
+ - `WEBNAV_MCP_READ_ONLY=1` turns every write into a refusal (previews still work).
134
+
135
+ Scope: the importers that get re-checked are found by relative `import`/`export
136
+ ... from`/`require()`/`import()` specifiers (`./a`, `./a.js` for `a.ts`, `./dir`
137
+ for `dir/index.ts`); a file reached only through a path alias is checked with its
138
+ own edit alone. `move`'s HTML/CSS rewrite understands relative and root-absolute
139
+ (`/src/main.ts`) references; bundler config, path aliases and string paths are
140
+ listed as mentions, not rewritten. HTML/CSS edits are checked by their language
141
+ servers (syntax and CSS lint), not against the cross-file index.
142
+
96
143
  ## Environment
97
144
 
98
145
  | Variable | Default | Purpose |
@@ -100,158 +147,94 @@ tab counts as one character).
100
147
  | `WEBNAV_MCP_WORKSPACE` | unset: follows the client's MCP roots when they name a worktree of the same git repository, else `CLAUDE_PROJECT_DIR`, else the working directory | Pins the project root (never overridden). See the `workspace` tool |
101
148
  | `WEBNAV_MCP_ROOTS` | the whole workspace as one root, labelled `web` | Comma-separated `label=relative/path` pairs to index separately, e.g. `app=src,prototypes=design` |
102
149
  | `WEBNAV_MCP_EXCLUDE` | nothing | Comma-separated workspace-relative paths of generated script output (e.g. the JS a TS build emits). Not opened, hidden from `search_symbol`, rejected by the position tools; the CSS/selector index still reads them |
103
-
104
- Tool names, parameter names and environment variables are identical to the
105
- Python package, so existing prompts, hosts and configs keep working.
106
-
107
- ## Differences from the Python package
108
-
109
- | | Python `webnav-mcp` | webnav-ts-mcp |
110
- |---|---|---|
111
- | Runtime | Python ≥ 3.11 **and** Node ≥ 18 | Node ≥ 20 only (tested on 22) |
112
- | Language servers | found in the project's `node_modules`, then the package dir, then `PATH`, then `npx --yes` (downloads at first use) | ordinary npm dependencies: a TypeScript ≥ 7 in the *project* wins, else the copy installed with webnav. Nothing is downloaded at runtime |
113
- | Launch | `.bin` shims (`.cmd` special case on Windows) | `node <server>.js`, no shims, no `PATH` dependence |
114
- | JSON-RPC framing | hand-written | [`vscode-jsonrpc`](https://www.npmjs.com/package/vscode-jsonrpc) |
115
- | UTF-16 columns | encode/decode helpers | JS strings already are UTF-16 |
116
- | Cold start (init handshake) | ~530 ms | ~100 ms (see [Speed](#speed)) |
150
+ | `WEBNAV_MCP_READ_ONLY` | unset | `1`/`true`: write tools refuse to write (previews and checks still work) |
151
+ | `WEBNAV_MCP_PUBLIC` | nothing | Comma-separated workspace-relative stylesheets (files or directories) that are a public API, e.g. a design-token file consumed by other projects. `diagnostics` stops reporting their custom properties as "declared but never used" and their selectors as "never referenced"; undefined `var()` usages are still reported |
117
152
 
118
153
  ## Layout
119
154
 
120
155
  ```
121
156
  src/
122
157
  cli.ts stdio entry point (exits when the host closes stdin)
123
- server.ts registers the twelve tools on the MCP SDK
158
+ server.ts registers the tools on the MCP SDK
124
159
  webnav.ts the tools as plain async functions returning text; owns all state
125
- webIndex.ts CSS var / selector index (port of web_index.py)
160
+ webIndex.ts CSS var / selector index
126
161
  langCommand.ts how to launch tsc / html / css language servers
127
162
  glob.ts just enough glob for jsconfig `include`
128
- shared/ port of mcp_nav_shared: lspClient, format, resolve, workspace,
129
- notices, errors, params, exclude
163
+ shared/ lspClient, format, resolve, workspace, notices, errors,
164
+ params, exclude
165
+ write/ the write tools: edits (plans, diffs), transaction (guard,
166
+ journal, undo), simulate + diagnosticsDelta (the type check),
167
+ finish (preview/apply), one module per tool, webTokens
168
+ (CSS var/id/class finder), jsImports (import rewriting)
130
169
  tests/ vitest; fixtures/sample-app is the shared fixture
131
- scripts/parity.mjs drives Python and TS servers side by side and diffs the output
132
170
  ```
133
171
 
134
- `shared/` is deliberately a directory, not a second package: see
135
- [Shared library](#the-shared-library).
136
-
137
172
  ## Development
138
173
 
139
174
  ```bash
140
175
  npm install
141
176
  npm run check # biome + tsc + vitest (pretest builds dist/)
142
- npm run bench # speed/memory vs the Python package (same sibling checkout requirement)
143
- npm run parity # needs the Python webnav-mcp checked out at ../webnav-mcp with `uv sync`; see the script header
177
+ npm run upload # check + publish to npm (--build-only / --dry-run / --otp CODE after `--`); needs `npm login`
178
+ npm run test-package # install the published version in a throwaway project and drive it over MCP stdio
144
179
  node bin/launch.mjs # checkout launcher: installs + builds when needed, then starts the server
145
180
  ```
146
181
 
147
182
  Source: [github.com/illescasDaniel/webnav-ts-mcp](https://github.com/illescasDaniel/webnav-ts-mcp).
148
183
 
149
- ## Speed
150
-
151
- Measured with `npm run bench` on Linux (Node 24, Python venv binary started directly so `uv`'s ~20 ms
152
- is excluded), real MCP over stdio against a real web project (SpaceMaker: 3 roots, ~60 JS/TS/CSS/HTML
153
- files, a 3k-line wireframe), median of 7 alternating runs, 15 warm calls per tool:
154
-
155
- | Metric | Python | TypeScript | |
156
- |---|---|---|---|
157
- | Cold start (spawn → `initialize`) | 528 ms | 103 ms | 5.1× |
158
- | First `symbol_info` (spawns `tsc` LSP) | 717 ms | 106 ms | 6.7× |
159
- | First HTML `diagnostics` (spawns HTML server) | 1197 ms | 978 ms | 1.2× |
160
- | Warm `symbol_info` / `references` / `hover` | 8.3 / 7.9 / 7.2 ms | 4.4 / 3.6 / 2.7 ms | ~2× |
161
- | Warm `outline` / `search_symbol` | 7.1 / 8.7 ms | 2.0 / 4.0 ms | 2–3.5× |
162
- | Warm `css_var` / `selector` (own index) | 2.6 / 2.6 ms | 0.5 / 0.7 ms | 4–5× |
163
- | Whole session (connect + every tool once + 15 warm calls each) | 3.5 s | 1.6 s | 2.2× |
164
- | Memory, server + language servers | 477 MiB | 359 MiB | 1.3× |
165
- | Shutdown after stdin closes | 61 ms | 13 ms | 4.6× |
166
-
167
- Warm calls are dominated by the language servers, which are identical in both, so the gap there is
168
- the fixed cost of the MCP layer. Launching through `npx webnav-ts-mcp` adds roughly 0.2 s to the
169
- cold start (npm resolving the `.bin` shim); a locally installed copy started with `node` does not.
170
-
171
- ## Notes from the port
184
+ ## Design notes
172
185
 
173
186
  `webnav` is glue: it spawns three Node language servers and formats their answers,
174
- plus a regex-grade index. Nothing in it depends on Python. The MCP SDK, the LSP
175
- plumbing and the file scanning all have first-class Node equivalents.
176
-
177
- **Behavioural parity**, measured by `scripts/parity.mjs` (real MCP over stdio,
178
- identical calls, diffed text):
179
-
180
- - A real web project ([SpaceMaker](https://github.com/illescasDaniel/SpaceMaker)'s assets, three roots): **37 / 37 identical**
181
- - `tests/fixtures/sample-app`, including edits behind the servers' backs (fix a
182
- type error, create/delete a file, change a stylesheet, change `jsconfig.json`
183
- and get the restart notice): **71 / 71 identical** (counting the one intentional
184
- inherited-member difference below)
185
-
186
- The one intentional difference is the reason string of "Cannot read file as
187
- UTF-8 text: …", which is a Python codec detail; the parity script masks it.
187
+ plus a regex-grade index.
188
188
 
189
- **Tests:** 152 vitest tests (unit ports of the index/format tests, real
190
- language servers against the fixture, MCP protocol over an in-memory transport
191
- incl. worktree selection via client roots with a real `git worktree`, LSP
192
- client failure modes, and the built executable). The Python side has ~260 tests
193
- across the two packages; the port covers the behaviour that matters for the
194
- tools, not every case one-to-one.
189
+ **Tests:** vitest (unit tests for the index/format code and the pure parts of the
190
+ write tools, real language servers against the fixture and temporary projects,
191
+ MCP protocol over an in-memory transport incl. worktree selection via client roots
192
+ with a real `git worktree`, LSP client failure modes, and the built executable).
195
193
 
196
- ### Things the port surfaced
194
+ ### Things worth knowing
197
195
 
198
196
  - **The MCP SDK's stdio server does not treat stdin EOF as a close.** With a
199
197
  language server child alive, the process kept running after the host went
200
198
  away (found by installing the tarball and launching via `npx`). `cli.ts` now
201
199
  handles `stdin` `end`/`close`; `tests/cli.test.ts` fails without it.
202
- - **UTF-16 handling gets simpler:** JS strings are UTF-16, so the column
203
- conversion helpers in the Python `format.py`/`lsp_client.py` disappear.
204
- - **One known regex divergence:** Python's `\w` is Unicode-aware, JS's is ASCII.
205
- The index regexes use `(?<![\w-])` to avoid matching `data-id=`; a non-ASCII
206
- letter directly before `id=`/`class=`/`style=` is treated differently (a
207
- match in JS, none in Python). Vanishingly rare in real markup, not covered by
208
- parity fixtures.
209
- - **Inherited-member lookup needed a different approach.** The Python package
210
- finds `Class.member` for a member declared on a base class through the LSP's
211
- type hierarchy, but TypeScript 7's language server advertises no type
212
- hierarchy (checked against its `initialize` capabilities), so
213
- `symbol_info("Widget.greet")` used to say "not found" in both implementations.
214
- This port reads the `extends`/`implements` clauses from the source and resolves
200
+ - **Columns are UTF-16 offsets,** which is what LSP uses and what JS strings
201
+ already are, so no conversion is needed.
202
+ - **Regex index limit:** `\w` is ASCII in JS. The index regexes use
203
+ `(?<![\w-])` to avoid matching `data-id=`, so a non-ASCII letter directly
204
+ before `id=`/`class=`/`style=` still counts as a match. Vanishingly rare in
205
+ real markup.
206
+ - **Inherited-member lookup needs a different approach.** TypeScript 7's
207
+ language server advertises no type hierarchy (checked against its `initialize`
208
+ capabilities), so a member declared on a base class can't be found that way.
209
+ webnav reads the `extends`/`implements` clauses from the source and resolves
215
210
  each base with `textDocument/definition`, breadth-first across files (generic
216
211
  bases, qualified names and mixin calls handled; `.d.ts`/`node_modules` bases
217
- aren't followed). It is the one intentional difference the parity script
218
- shows.
219
- - **Two tools the Python webnav never had:** the same capability check showed the
220
- server offers `callHierarchyProvider` and `implementationProvider`, so this
221
- port adds `callers` and `implementations` (12 tools now; the Python one has 10).
212
+ aren't followed).
213
+ - **`callers` and `implementations`** are offered because the server advertises
214
+ `callHierarchyProvider` and `implementationProvider`.
215
+ - **Type-checking an edit without writing it** relies on documents the client
216
+ opens with `didOpen`/`didChange` shadowing the disk (`LspClient.withOverlay`).
217
+ tsc's native server resolves imports of a file that exists only as an overlay,
218
+ but needs its *directory* to exist and remembers a failed resolution, so the
219
+ overlay opens new files first and creates (then removes) empty directories.
220
+ - **A file with no `import`/`export` is a global script** to TypeScript. Moving
221
+ the last export out of a module turns it into one, and every top-level name in
222
+ it becomes visible everywhere; the type check can then miss references that
223
+ should have broken.
224
+ - **Symbol ranges from `documentSymbol` are tight:** no doc comment, no
225
+ `export const` keyword, one symbol per declarator. `edit_symbol` and `move`
226
+ widen them (`extentOf`) and refuse declarators that share a statement.
227
+ - **The server's unused-import hints identify what a deletion orphaned:** hints
228
+ after the edit minus hints before it, so imports that were already unused stay.
222
229
 
223
230
  ### Costs
224
231
 
225
232
  - **Install size:** a fresh app installing this next to its own TypeScript 5:
226
233
  177 MB `node_modules` (`vscode-langservers-extracted` 66 MB, TypeScript 7
227
234
  ~50 MB with its native binary). An app already on TypeScript ≥ 7 shares that
228
- copy. The Python package had the same npm payload, plus Python.
229
- - **Code size:** ~4.2k lines of TypeScript (2.3k of it in `shared/`) vs ~3.7k
230
- lines of Python for webnav + the shared library (which also carries
231
- codenav-only code that wasn't ported).
232
- - **Not ported** (codenav-only in the shared library): `typeDefinition`,
233
- scratch documents, source-root helpers. (Call hierarchy was ported after all,
234
- for `callers`.)
235
+ copy.
235
236
  - **Not verified:** Windows, macOS, Node 20/24. Only Linux + Node 22.
236
237
 
237
- ### The shared library
238
-
239
- The user-visible downside of the port is that `mcp-nav-shared` (LSP client,
240
- formatting, symbol resolution, workspace selection) now exists twice. Options,
241
- roughly in order of effort:
242
-
243
- 1. **Keep `src/shared/` internal to the TS package** (what this package does).
244
- Zero coordination cost; the two copies can drift. Mitigated by
245
- `scripts/parity.mjs`, which fails when their output diverges.
246
- 2. **Publish it as its own npm package** (`mcp-nav-shared`) only if a second TS
247
- server appears. `codenav` is tied to `ty` (Python) and has no reason to move.
248
- 3. **Retire the Python `webnav-mcp`** and keep only the TS one, leaving
249
- `mcp-nav-shared` for `codenav-mcp` alone. Removes the duplication, breaks
250
- `uvx webnav-mcp` users.
251
-
252
- The Python shared library's webnav-relevant part is small (LSP client, format,
253
- resolve, workspace, notices), which is why the duplication is tolerable.
254
-
255
238
  ## License
256
239
 
257
240
  MIT. See [LICENSE](https://github.com/illescasDaniel/webnav-ts-mcp/blob/main/LICENSE).
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Resolves how to launch the Node-based language servers webnav multiplexes to.
3
3
  *
4
- * Unlike the Python distribution, the servers are ordinary npm dependencies of
4
+ * The servers are ordinary npm dependencies of
5
5
  * this package, so there is nothing to download at runtime: a TypeScript 7+
6
6
  * install in the navigated project wins (it matches the project's own
7
7
  * compiler), else the copy that came with webnav. Both are launched through
package/dist/server.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * MCP surface of webnav: registers `Webnav` methods as tools.
3
3
  *
4
- * Tool names and parameter names (snake_case) match the Python `webnav-mcp`
5
- * so hosts and prompts written against either keep working. Name-ish
4
+ * Tool and parameter names are snake_case. Name-ish
6
5
  * parameters are all optional in the schema: a missing one is answered with a
7
6
  * short hint by the tool itself, which beats the SDK's schema-validation error
8
7
  * for agents that guess `query` vs `name`.
@@ -12,6 +11,17 @@ import { z } from "zod";
12
11
  import { INSTRUCTIONS, Webnav } from "./webnav.js";
13
12
  const POSITION_DOC = "`line` and `column` are 1-indexed. `column` is a UTF-16 character offset on the line " +
14
13
  "(not a visual/display column): a leading tab counts as one character.";
14
+ const WRITE_RULE = "The edit is shown to the language servers as an in-memory overlay first and the edited files and the " +
15
+ "files importing them are re-checked: it is written only when that adds no more errors than " +
16
+ "`max_new_errors` (default 0; null = no limit) and the result parses. Otherwise nothing is written and " +
17
+ "you get the new diagnostics plus a preview id for `apply_edit`. `apply=false` always previews. " +
18
+ "`undo_edit` reverts an applied edit.";
19
+ const WRITES = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
20
+ const READS = { readOnlyHint: true, openWorldHint: false };
21
+ const writeParams = {
22
+ apply: z.boolean().default(true),
23
+ max_new_errors: z.number().int().min(0).nullable().optional(),
24
+ };
15
25
  const text = (value) => ({
16
26
  content: [{ type: "text", text: value }],
17
27
  });
@@ -161,5 +171,189 @@ export function createServer(version) {
161
171
  "accepted as an alias for `name`.",
162
172
  inputSchema: { name: z.string().optional(), query: z.string().optional() },
163
173
  }, async (args) => text(await webnav.selectorLookup(args)));
174
+ registerWriteTools(server, webnav);
164
175
  return { server, webnav };
165
176
  }
177
+ function registerWriteTools(server, webnav) {
178
+ server.registerTool("edit", {
179
+ description: 'Edit text in a JS/TS/HTML/CSS file, type-checked: `edit(file_path="src/app.ts", old_string="x = 1", new_string="x = 2")`.\n\n' +
180
+ "For changes that are not a whole definition (a few lines, an import, a rule, markup) or for rewriting a " +
181
+ "whole file (`new_text`, which also creates a new file). To replace, add or delete a function/class/method " +
182
+ "by name use `edit_symbol`. `old_string` must match exactly once unless `replace_all=true` " +
183
+ "(line endings follow the file). `include_dependents=false` skips checking the importers. " +
184
+ WRITE_RULE,
185
+ inputSchema: {
186
+ file_path: z.string().optional(),
187
+ old_string: z.string().optional(),
188
+ new_string: z.string().optional(),
189
+ replace_all: z.boolean().default(false),
190
+ new_text: z.string().optional(),
191
+ include_dependents: z.boolean().default(true),
192
+ ...writeParams,
193
+ },
194
+ annotations: WRITES,
195
+ }, async ({ max_new_errors, ...a }) => text(await webnav.edit({
196
+ filePath: a.file_path,
197
+ oldString: a.old_string,
198
+ newString: a.new_string,
199
+ replaceAll: a.replace_all,
200
+ newText: a.new_text,
201
+ includeDependents: a.include_dependents,
202
+ apply: a.apply,
203
+ maxNewErrors: max_new_errors,
204
+ })));
205
+ server.registerTool("edit_symbol", {
206
+ description: "Replace, add or delete a JS/TS function, class, interface, enum, const or member by name. Examples: " +
207
+ '`edit_symbol(action="replace", name="Cart.total", source="total(): number { ... }")`, ' +
208
+ '`edit_symbol(action="insert", file_path="src/utils.ts", name="parse", source="function helper() {}")`, ' +
209
+ '`edit_symbol(action="delete", name="legacyParse")`.\n\n' +
210
+ '- `action="replace"`: `name` (dotted `Class.method` ok) and `source` (the complete new definition, ' +
211
+ "decorators included; re-indented to the old one's place in the file's own indent style). The doc comment " +
212
+ "above is kept unless `source` brings its own. A changed signature shows up as errors at call sites.\n" +
213
+ '- `action="insert"`: `source` and `file_path`. `position` is relative to `name`: "after" (default when ' +
214
+ '`name` is given) / "before" a symbol, "into" a class/interface/enum/namespace (appended to its body), or ' +
215
+ '"end" of the file (default without `name`; creates the file if missing). Refuses a name that already exists there.\n' +
216
+ '- `action="delete"`: `name` only. Refuses while anything still uses it and lists the users (`force=true` ' +
217
+ "deletes anyway and shows what breaks). Imports only the deleted code used are removed (`prune_imports`). " +
218
+ "Strings, comments and docs naming it are listed and the deletion is previewed instead of applied.\n\n" +
219
+ "`file_path` narrows `name` when several symbols share it. `imports` (replace/insert) is a list of " +
220
+ 'import statements the new code needs (`["import { Money } from \\"./money\\""]`), merged into the file if missing. ' +
221
+ WRITE_RULE,
222
+ inputSchema: {
223
+ action: z.enum(["replace", "insert", "delete"]).optional(),
224
+ name: z.string().optional(),
225
+ source: z.string().optional(),
226
+ file_path: z.string().optional(),
227
+ position: z.enum(["after", "before", "into", "end"]).optional(),
228
+ imports: z.array(z.string()).optional(),
229
+ force: z.boolean().default(false),
230
+ prune_imports: z.boolean().default(true),
231
+ ...writeParams,
232
+ },
233
+ annotations: WRITES,
234
+ }, async ({ max_new_errors, ...a }) => text(await webnav.editSymbol({
235
+ action: a.action,
236
+ name: a.name,
237
+ source: a.source,
238
+ filePath: a.file_path,
239
+ position: a.position,
240
+ imports: a.imports,
241
+ force: a.force,
242
+ pruneImports: a.prune_imports,
243
+ apply: a.apply,
244
+ maxNewErrors: max_new_errors,
245
+ })));
246
+ server.registerTool("rename_symbol", {
247
+ description: 'Rename a symbol everywhere it is used. Examples: `rename_symbol(name="Cart.total", new_name="sum")`, ' +
248
+ '`rename_symbol(name="--accent", new_name="--brand")`, `rename_symbol(name=".card-title", new_name="heading")`.\n\n' +
249
+ "JS/TS: give `name` (dotted `Class.method` ok; `file_path` narrows it) or `file_path` + `line` + `column` " +
250
+ "(1-indexed). Built on the TypeScript language server's rename (imports, aliases, overriding members); " +
251
+ '`parameter="x"` with a function\'s name renames one parameter. Places that still say the old name that ' +
252
+ "the type checker cannot link (strings, comments, docs, other languages) are listed.\n\n" +
253
+ "CSS/HTML: a `name` starting with `--`, `#` or `.` renames that custom property / id / class across " +
254
+ "stylesheets (declarations, `var()`, selectors), HTML (`class`, `id`, and the attributes that refer to ids: " +
255
+ '`for`, `aria-*`, `href="#id"`) and scripts (`getElementById`, `classList`, `querySelector`, `className`, ' +
256
+ '`setProperty`/`getPropertyValue`, markup inside strings). Names built dynamically (`"row-" + id`) are listed, ' +
257
+ "not renamed; script string literals that merely equal the name are listed and renamed only with " +
258
+ "`include_string_literals=true`. Refuses a new name that already exists (`force=true` merges). With several " +
259
+ "`WEBNAV_MCP_ROOTS` pass `file_path` to say which root. " +
260
+ WRITE_RULE,
261
+ inputSchema: {
262
+ new_name: z.string().optional(),
263
+ name: z.string().optional(),
264
+ file_path: z.string().optional(),
265
+ line: z.number().int().optional(),
266
+ column: z.number().int().optional(),
267
+ parameter: z.string().optional(),
268
+ force: z.boolean().default(false),
269
+ include_string_literals: z.boolean().default(false),
270
+ allow_large: z.boolean().default(false),
271
+ ...writeParams,
272
+ },
273
+ annotations: WRITES,
274
+ }, async ({ max_new_errors, ...a }) => text(await webnav.renameSymbol({
275
+ newName: a.new_name,
276
+ name: a.name,
277
+ filePath: a.file_path,
278
+ line: a.line,
279
+ column: a.column,
280
+ parameter: a.parameter,
281
+ force: a.force,
282
+ includeStringLiterals: a.include_string_literals,
283
+ allowLarge: a.allow_large,
284
+ apply: a.apply,
285
+ maxNewErrors: max_new_errors,
286
+ })));
287
+ server.registerTool("move", {
288
+ description: "Move a declaration to another file, or a whole file, and fix every reference. Examples: " +
289
+ '`move(name="parseDate", to_file="src/app/dates.ts")`, `move(file_path="src/old/util.ts", to_file="src/lib/util.ts")`.\n\n' +
290
+ "With `name`: moves that top-level function/class/interface/enum/type/const (`file_path` narrows which). " +
291
+ "The destination is created if missing; the code gets the imports it needs (found by the language server), " +
292
+ '`export` is added if it was private, every `import { name } from "old"` elsewhere is repointed, the old ' +
293
+ "file imports it from the new place if it still uses it (`keep_reexport=true` also leaves `export { name } " +
294
+ 'from "new"`), and imports only the moved code used are dropped. Import cycles are warned about.\n\n' +
295
+ "Without `name`: `file_path` is the file to move or rename (any file; `to_file` may be a directory). Every " +
296
+ "import/re-export in JS/TS (the language server's own rewrite), `<script src>`/`<link href>`/`<img src>` in " +
297
+ "HTML (relative and root-absolute), and `url()`/`@import` in CSS follow, and the moved file's own relative " +
298
+ "references are re-based. Configs, path aliases and string paths are listed, not rewritten. " +
299
+ WRITE_RULE,
300
+ inputSchema: {
301
+ to_file: z.string().optional(),
302
+ name: z.string().optional(),
303
+ file_path: z.string().optional(),
304
+ keep_reexport: z.boolean().default(false),
305
+ allow_large: z.boolean().default(false),
306
+ ...writeParams,
307
+ },
308
+ annotations: WRITES,
309
+ }, async ({ max_new_errors, ...a }) => text(await webnav.move({
310
+ toFile: a.to_file,
311
+ name: a.name,
312
+ filePath: a.file_path,
313
+ keepReexport: a.keep_reexport,
314
+ allowLarge: a.allow_large,
315
+ apply: a.apply,
316
+ maxNewErrors: max_new_errors,
317
+ })));
318
+ server.registerTool("quick_fix", {
319
+ description: 'Apply the TypeScript language server\'s own fixes to a JS/TS file: `quick_fix(file_path="src/app.ts")`.\n\n' +
320
+ "`action=\"fix\"` (default) fixes the file's errors one by one with the server's code actions (missing " +
321
+ "imports, misspelled names, ...), re-checking after each; `line` limits it to errors starting on that line. " +
322
+ "A fix with several candidates is listed, not guessed. The other actions are source actions on the whole " +
323
+ "file: `organize_imports`, `remove_unused_imports`, `sort_imports`, `fix_all`. " +
324
+ WRITE_RULE,
325
+ inputSchema: {
326
+ file_path: z.string().optional(),
327
+ line: z.number().int().optional(),
328
+ action: z.enum(["fix", "organize_imports", "remove_unused_imports", "sort_imports", "fix_all"]).optional(),
329
+ ...writeParams,
330
+ },
331
+ annotations: WRITES,
332
+ }, async ({ max_new_errors, ...a }) => text(await webnav.quickFix({
333
+ filePath: a.file_path,
334
+ line: a.line,
335
+ action: a.action,
336
+ apply: a.apply,
337
+ maxNewErrors: max_new_errors,
338
+ })));
339
+ server.registerTool("verify_changes", {
340
+ description: "What did the edits since a git revision break? Compares the language servers' diagnostics now against " +
341
+ "`since` (default `HEAD`).\n\nRun it after editing with any tool (your own Edit/Write included): the " +
342
+ "changed JS/TS/HTML/CSS files and the script files importing them are checked, with the committed versions " +
343
+ 'substituted in for the "before" side. Read-only.',
344
+ inputSchema: { since: z.string().default("HEAD"), include_dependents: z.boolean().default(true) },
345
+ annotations: READS,
346
+ }, async ({ since, include_dependents }) => text(await webnav.verifyChanges({ since, includeDependents: include_dependents })));
347
+ server.registerTool("apply_edit", {
348
+ description: "Write a previewed edit (the id comes from a write tool's preview). Fails, writing nothing, if any file " +
349
+ "changed since the preview.",
350
+ inputSchema: { id: z.string().optional(), allow_large: z.boolean().default(false) },
351
+ annotations: WRITES,
352
+ }, async ({ id, allow_large }) => text(await webnav.applyEdit(id ?? "", allow_large)));
353
+ server.registerTool("undo_edit", {
354
+ description: "Revert an edit applied by the write tools (the latest one by default). Refuses when any of its files " +
355
+ "changed after the edit, so later work is never overwritten.",
356
+ inputSchema: { id: z.string().optional() },
357
+ annotations: WRITES,
358
+ }, async ({ id }) => text(await webnav.undoEdit(id)));
359
+ }