webnav-ts-mcp
An MCP server that provides AI agents JS/TS/HTML/CSS code navigation and analysis, plus safe type-checked editing across files.
Find type/documentation info at any position (
hover) and jump to definitions (definition).Find all usages of a symbol across the workspace (
references, with optional declaration inclusion).Locate the correct project checkout/worktree (
workspace).Search JS/TS symbols by name with kind/path filters and fuzzy matching (
search_symbol).Get a one-call summary of a JS/TS symbol: header, hover, definition, grouped references (
symbol_info).See who calls a function/method via call hierarchy (
callers).See who implements or extends an interface, class, or method (
implementations).Get an indented source-order outline of a JS/TS file (
outline).Retrieve language-server diagnostics for a file, including unreferenced CSS selectors and undefined CSS variables (
diagnostics).Cross-reference CSS custom properties (
css_var) and#id/.classselectors (selector) across CSS, HTML, and JS.CSS/HTML token lookups fall back to a cross-file index that single-file language servers cannot provide.
Indexes and navigates CSS across the workspace: locates where custom properties (--name) and #id/.class selectors are defined and used across stylesheets, HTML and JS, and surfaces CSS diagnostics including unreferenced-selector and undefined-variable warnings (with an option to mark public stylesheets as an external API).
Provides name-based and position-based navigation of JS/TS source via the TypeScript 7 native language server: symbol info (header, hover, definition, references), file outlines, call hierarchy (callers), interface/class/method implementations, ranked workspace symbol search, hover/definition/references and diagnostics.
webnav-ts-mcp
An MCP server that gives AI agents JS/TS/HTML/CSS navigation, plus a
cross-file index of CSS custom properties and #id/.class selectors that
single-file language servers can't provide, and write tools that type-check an
edit before it is written. It needs only Node.js and installs with a web
project's own package manager.
Quick start
You need Node.js ≥ 20:
npx webnav-ts-mcpRegister it with your MCP host. For Claude Code, from the project root:
claude mcp add webnav -- npx webnav-ts-mcpOr add it to a project .mcp.json (Claude Code) or .cursor/mcp.json (Cursor):
{
"mcpServers": {
"webnav": {
"command": "npx",
"args": ["webnav-ts-mcp"]
}
}
}In Cursor, also set "env": {"WEBNAV_MCP_WORKSPACE": "${workspaceFolder}"},
because Cursor may start MCP servers with your home directory as the working
directory.
To pin it per project instead of resolving through npx each time:
npm install --save-dev webnav-ts-mcpRelated MCP server: webnav-mcp
Language servers
Requests are routed to three Node language servers by file extension:
Extension | Backend |
| TypeScript 7 native LSP: |
|
|
|
|
They are ordinary npm dependencies, so nothing is downloaded at runtime. A TypeScript ≥ 7 in the navigated project wins; otherwise the copy installed with webnav-ts-mcp is used. (An app on an older TypeScript still works, at the cost of the extra install size: see Costs.)
Tools
For JS/TS, start with the name-based tools:
Tool | Answers |
| What is this? Header, hover, definition and references in one call |
| What's in this file? (source order; locals and imports left out unless |
| Who calls this function/method? (call hierarchy) |
| Who implements/extends this interface, class or method? |
| JS/TS workspace symbol search (ranked, capped; optional |
| Which directory is being navigated, and why |
Then use the position tools once you have a path:line:col:
Tool | Answers |
| Type and docs at a position |
| Go to definition (CSS/HTML tokens answer from the index below) |
| All usages (CSS/HTML tokens answer from the index below) |
| Language-server diagnostics; CSS/HTML also get unreferenced-selector and undefined-variable warnings |
Cross-file CSS/HTML index:
Tool | Answers |
| Where is |
| Where is |
search_symbol, symbol_info, outline, callers and implementations are
JS/TS only. Use css_var and selector for markup and stylesheets.
name and query are accepted as aliases of each other on the name-based tools.
Positions are 1-indexed; column is a UTF-16 character offset (a leading
tab counts as one character).
Write tools: change code with a type check before every write
The navigation tools above are read-only. These change code, and share one rule: the edit is shown to the language servers as an in-memory overlay first, the edited files and the script files importing them are re-checked, and the response says which diagnostics the edit would add or fix. Nothing is written until that is on the table.
Tool | Does |
| Type-checked text edit of a JS/TS/HTML/CSS file: |
|
|
| JS/TS: rename through the TypeScript language server (imports, aliases, overriding members; |
| With |
| Apply the language server's own fixes (missing imports, misspellings, ...) one by one, re-checking after each; or a source action: |
| After editing with any tool (your own included): which diagnostics the working tree gained or lost since a git revision (default |
| Write a previewed edit by id; revert an applied one |
One rule for every write tool: it writes when the edit adds no more than
max_new_errors (default 0) errors in the edited files and their importers and
the result parses. Otherwise nothing is written and you get the diagnostics plus
an id for apply_edit. apply=false always previews. Hints and warnings are
shown but not counted. Syntax errors (TypeScript's 1xxx codes, any CSS error)
block a write even with max_new_errors=null.
Safety rules, enforced in code:
Only
.js/.mjs/.cjs/.jsx/.ts/.mts/.cts/.tsx/.html/.cssfiles inside the workspace are edited (movealso carries any file, such as an image a stylesheet uses);node_modules,dist,.gitand the other skipped folders, generated output (WEBNAV_MCP_EXCLUDE) and symlinks are refused.A preview remembers the exact bytes of every file;
apply_editrefuses if any file changed since. Files are replaced atomically (temp file + rename), and a failure part-way restores the ones already written.Every applied edit is undoable (
undo_edit) unless a file was changed after it.Line endings and a leading BOM are preserved.
Edits touching more than 40 files need
allow_large=true.WEBNAV_MCP_READ_ONLY=1turns every write into a refusal (previews still work).
Scope: the importers that get re-checked are found by relative import/export ... from/require()/import() specifiers (./a, ./a.js for a.ts, ./dir
for dir/index.ts); a file reached only through a path alias is checked with its
own edit alone. move's HTML/CSS rewrite understands relative and root-absolute
(/src/main.ts) references; bundler config, path aliases and string paths are
listed as mentions, not rewritten. HTML/CSS edits are checked by their language
servers (syntax and CSS lint), not against the cross-file index.
Environment
Variable | Default | Purpose |
| unset: follows the client's MCP roots when they name a worktree of the same git repository, else | Pins the project root (never overridden). See the |
| the whole workspace as one root, labelled | Comma-separated |
| nothing | Comma-separated workspace-relative paths of generated script output (e.g. the JS a TS build emits). Not opened, hidden from |
| unset |
|
| nothing | Comma-separated workspace-relative stylesheets (files or directories) that are a public API, e.g. a design-token file consumed by other projects. |
Layout
src/
cli.ts stdio entry point (exits when the host closes stdin)
server.ts registers the tools on the MCP SDK
webnav.ts the tools as plain async functions returning text; owns all state
webIndex.ts CSS var / selector index
langCommand.ts how to launch tsc / html / css language servers
glob.ts just enough glob for jsconfig `include`
shared/ lspClient, format, resolve, workspace, notices, errors,
params, exclude
write/ the write tools: edits (plans, diffs), transaction (guard,
journal, undo), simulate + diagnosticsDelta (the type check),
finish (preview/apply), one module per tool, webTokens
(CSS var/id/class finder), jsImports (import rewriting)
tests/ vitest; fixtures/sample-app is the shared fixtureDevelopment
npm install
npm run check # biome + tsc + vitest (pretest builds dist/)
npm run upload # check + publish to npm (--build-only / --dry-run / --otp CODE after `--`); needs `npm login`
npm run test-package # install the published version in a throwaway project and drive it over MCP stdio
node bin/launch.mjs # checkout launcher: installs + builds when needed, then starts the serverSource: github.com/illescasDaniel/webnav-ts-mcp.
Design notes
webnav is glue: it spawns three Node language servers and formats their answers,
plus a regex-grade index.
Tests: vitest (unit tests for the index/format code and the pure parts of the
write tools, real language servers against the fixture and temporary projects,
MCP protocol over an in-memory transport incl. worktree selection via client roots
with a real git worktree, LSP client failure modes, and the built executable).
Things worth knowing
The MCP SDK's stdio server does not treat stdin EOF as a close. With a language server child alive, the process kept running after the host went away (found by installing the tarball and launching via
npx).cli.tsnow handlesstdinend/close;tests/cli.test.tsfails without it.Columns are UTF-16 offsets, which is what LSP uses and what JS strings already are, so no conversion is needed.
Regex index limit:
\wis ASCII in JS. The index regexes use(?<![\w-])to avoid matchingdata-id=, so a non-ASCII letter directly beforeid=/class=/style=still counts as a match. Vanishingly rare in real markup.Inherited-member lookup needs a different approach. TypeScript 7's language server advertises no type hierarchy (checked against its
initializecapabilities), so a member declared on a base class can't be found that way. webnav reads theextends/implementsclauses from the source and resolves each base withtextDocument/definition, breadth-first across files (generic bases, qualified names and mixin calls handled;.d.ts/node_modulesbases aren't followed).callersandimplementationsare offered because the server advertisescallHierarchyProviderandimplementationProvider.Type-checking an edit without writing it relies on documents the client opens with
didOpen/didChangeshadowing the disk (LspClient.withOverlay). tsc's native server resolves imports of a file that exists only as an overlay, but needs its directory to exist and remembers a failed resolution, so the overlay opens new files first and creates (then removes) empty directories.A file with no
import/exportis a global script to TypeScript. Moving the last export out of a module turns it into one, and every top-level name in it becomes visible everywhere; the type check can then miss references that should have broken.Symbol ranges from
documentSymbolare tight: no doc comment, noexport constkeyword, one symbol per declarator.edit_symbolandmovewiden them (extentOf) and refuse declarators that share a statement.The server's unused-import hints identify what a deletion orphaned: hints after the edit minus hints before it, so imports that were already unused stay.
Costs
Install size: a fresh app installing this next to its own TypeScript 5: 177 MB
node_modules(vscode-langservers-extracted66 MB, TypeScript 7 ~50 MB with its native binary). An app already on TypeScript ≥ 7 shares that copy.Not verified: Windows, macOS, Node 20/24. Only Linux + Node 22.
License
MIT. See LICENSE.
Available Tools
12 toolscallersA
Who calls this function? Example: callers(name="renderSidebar").
Narrower than references, since it leaves out imports and type-only usages and only lists actual call sites, each with the calling function and its call-site lines. name resolves the same way as symbol_info (dotted Class.method accepted, inherited members included; pass file_path to disambiguate a common name). query is accepted as an alias for name. JS/TS only.
Inputs: name, query, file_path.
css_varA
Where is this --custom-property defined and used? Example: css_var(name="--bg").
The CSS/HTML language servers only see one file at a time, so var(--x) usages can't be cross-referenced across files that way. This scans .css files and HTML <style>/style="…" blocks/attributes instead. name may be given with or without the leading --. Definitions (value + enclosing context, e.g. @media (prefers-color-scheme: dark) › :root) and usages (grouped by file with line numbers) are reported separately per configured root (see WEBNAV_MCP_ROOTS; a single unnamed root by default), since each may define its own values. query is accepted as an alias for name.
Inputs: name, query.
definitionA
Go to the definition of the symbol at a position.
line and column are 1-indexed. column is a UTF-16 character offset on the line (not a visual/display column): a leading tab counts as one character. On a --custom-property/#id/.class token in a .css/.html file (or a name inside an HTML id="..."/class="..." value), answers from the cross-file index (see css_var/selector) instead of the single-file language server: for definition, just the definition(s) in the file's own root (each WEBNAV_MCP_ROOTS root is separate); for references, that root's definitions and usages.
Inputs: line (required), column (required), file_path (required).
diagnosticsA
Get the relevant language server's diagnostics (errors/warnings) for a single file.
For .css/.html files, this also includes index-derived warnings the single-file language server can't see: var(--x) used with no matching declaration anywhere in the same indexed root, and CSS selectors (#id/.class) with no HTML/JS reference in that root.
Inputs: file_path (required).
hoverB
Get type/documentation info for the symbol at a position.
line and column are 1-indexed. column is a UTF-16 character offset on the line (not a visual/display column): a leading tab counts as one character.
Inputs: line (required), column (required), file_path (required).
implementationsA
Who implements or extends this? Example: implementations(name="Shape").
For an interface or class, lists the classes that implement or extend it (transitively); for an interface or abstract method (Shape.area), lists the methods that implement it. Backed by the language server's textDocument/implementation, so it follows the type checker, not naming. name resolves like symbol_info (pass file_path to disambiguate); query is accepted as an alias. JS/TS only.
Inputs: name, query, file_path.
outlineA
What's in this file? Example: outline(file_path="src/app.ts").
Indented outline (functions, classes, interfaces, with :start-end line spans) of a JS/TS file in source order, so you can navigate without reading it in full. Locals, callbacks and object-literal keys inside functions and variables are left out; pass detailed=true to include them. Follow up with hover/definition/references at a listed line, or symbol_info by name.
Inputs: file_path (required), detailed.
referencesB
Find all usages of the symbol at a position across the workspace.
line and column are 1-indexed. column is a UTF-16 character offset on the line (not a visual/display column): a leading tab counts as one character. On a --custom-property/#id/.class token in a .css/.html file (or a name inside an HTML id="..."/class="..." value), answers from the cross-file index (see css_var/selector) instead of the single-file language server, limited to the file's own root (each WEBNAV_MCP_ROOTS root is separate) unless it has no hits there.
Inputs: line (required), column (required), file_path (required), include_declaration.
search_symbolA
Search JS/TS files for a symbol by name (function, class, const, etc.).
JS/TS-only: the HTML/CSS language servers don't implement useful workspace-wide symbol search (webnav does not reimplement it). Prefer symbol_info for a one-call summary. Returned positions point at the identifier name and use the same character-offset column convention as the other tools. Results include a SymbolKind label and are capped. name is accepted as an alias for query. Narrow broad queries with kind (SymbolKind labels, comma-separated: class, function,method, interface, ...) and path (workspace-relative prefix such as src/, or a glob such as src/**/*.ts). Production code ranks before tests. Loose fuzzy hits whose names don't contain the query are summarised as a count when real matches exist; pass fuzzy=true to list them too.
Inputs: kind, name, path, fuzzy, +1 more.
selectorA
Who uses this #id or .class? Example: selector(name=".card-title").
Looks up the selector across the whole workspace.
Cross-references CSS rule definitions, HTML id=/class= attributes, and JS usages (getElementById, classList.add/remove/toggle/contains, querySelector/querySelectorAll, className assignment, and any JS string literal exactly equal to the bare name, e.g. an id passed to a project's own helper like onClick("btn-save", …), labeled "string literal"). Hits in generated output (WEBNAV_MCP_EXCLUDE) are labeled [generated]; edit their source instead. This is something the single-file CSS/HTML language servers can't do. name must include the leading # or .. Grouped by file with line numbers, separately per configured root (see WEBNAV_MCP_ROOTS). A JS hit built from string concatenation (e.g. getElementById("view-" + x)) is reported against only its static prefix and labeled "dynamic partial match"; a query whose name starts with such a prefix (e.g. #view-components against a stored #view-) also surfaces that hit, labeled "dynamic partial match via ''", instead of being silently dropped or guessed. query is accepted as an alias for name.
Inputs: name, query.
symbol_infoA
What is X and where is it used? Example: symbol_info(name="renderSidebar").
One-call summary for a JS/TS name: header, hover text, definition, and references grouped by file, the usual first lookup instead of chaining search_symbol → hover → definition → references by hand. Pass file_path to disambiguate; query is accepted as an alias for name. For CSS/HTML cross-file lookups use css_var / selector instead.
Inputs: name, query, file_path, include_references.
workspaceA
Which directory is webnav navigating, and why? Use when results look like they come from the wrong checkout/worktree.
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 12 tools
Most tools map to distinct navigation operations, and descriptions explicitly delineate boundaries (e.g. callers is 'narrower than references', symbol_info is a one-call bundle). The main risk is overlap between symbol_info and the chained search_symbol/hover/definition/references set, plus references vs callers vs implementations, but the descriptions steer selection clearly.
Names are uniformly lowercase, mostly bare LSP-style nouns (hover, definition, references, diagnostics, outline) with a few snake_case compound names (search_symbol, symbol_info, css_var). The style is predictable and readable despite not being a strict single verb_noun pattern.
Twelve tools is well within the ideal 3-15 range and each tool earns its place (a distinct LSP capability or a deliberate cross-file index feature). No obvious redundancy or padding.
Covers the core navigation lifecycle: diagnostics, hover, definition, references, outline, symbol search, callers, implementations, plus CSS custom-property and selector cross-referencing. Missing rename/refactor, code actions, and workspace symbol search for non-JS/TS languages (a limitation the descriptions acknowledge), which are minor workaroundable gaps.
Related MCP Connectors
Code intelligence for coding agents: semantic, AST, graph, and full-text search. 279+ languages.
Codebase intelligence for AI agents — dead code, blast radius, ownership.
Code intelligence platform for AI agents. 20 tools for architecture, security & impact analysis.
Give your AI agent a persistent map of your project's structure, dependencies, and bugs.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes type-aware code navigation and fast file search to AI agents via language servers, enabling definitions, references, symbols, and file lookup without reading entire codebases.1,620 npmMIT
- AlicenseAqualityCmaintenanceEnables AI agents to navigate JS/TS/HTML/CSS code through TypeScript, HTML, and CSS language servers, answering hover, definition, references, symbol search, outline, and diagnostics queries. Adds a cross-file index of CSS custom properties and #id/.class selectors that single-file language servers cannot provide.10MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to navigate and refactor a codebase through an AST index, offering project overviews, symbol and usage search, module dependency exploration, and LSP-backed operations such as rename, go-to-definition, diagnostics, and extract-method. Supports TypeScript/JavaScript and C# with automatic reindexing and watch mode.MIT
- FlicenseNot gradedqualityAmaintenanceEnables AI coding agents to retrieve real-time LSP diagnostics, symbol and type information, and code references, and to perform workspace-aware file operations such as symbol renaming. This avoids waiting for slow tsc/eslint checks while improving code navigation and editing accuracy.-