Skip to main content
Glama

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-mcp

Register it with your MCP host. For Claude Code, from the project root:

claude mcp add webnav -- npx webnav-ts-mcp

Or 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-mcp

Related MCP server: webnav-mcp

Language servers

Requests are routed to three Node language servers by file extension:

Extension

Backend

.js / .mjs / .cjs / .jsx / .ts / .mts / .cts / .tsx

TypeScript 7 native LSP: tsc --lsp --stdio (JS via allowJs / jsconfig.json)

.html

vscode-html-language-server

.css

vscode-css-language-server

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

symbol_info

What is this? Header, hover, definition and references in one call

outline

What's in this file? (source order; locals and imports left out unless detailed=true)

callers

Who calls this function/method? (call hierarchy)

implementations

Who implements/extends this interface, class or method?

search_symbol

JS/TS workspace symbol search (ranked, capped; optional kind= / path= filters)

workspace

Which directory is being navigated, and why

Then use the position tools once you have a path:line:col:

Tool

Answers

hover

Type and docs at a position

definition

Go to definition (CSS/HTML tokens answer from the index below)

references

All usages (CSS/HTML tokens answer from the index below)

diagnostics

Language-server diagnostics; CSS/HTML also get unreferenced-selector and undefined-variable warnings

Cross-file CSS/HTML index:

Tool

Answers

css_var

Where is --name defined and used?

selector

Where is #id or .class defined and used (CSS, HTML, JS)?

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

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

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

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

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

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

verify_changes

After editing with any tool (your own included): which diagnostics the working tree gained or lost since a git revision (default HEAD)

apply_edit / undo_edit

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/.css files inside the workspace are edited (move also carries any file, such as an image a stylesheet uses); node_modules, dist, .git and the other skipped folders, generated output (WEBNAV_MCP_EXCLUDE) and symlinks are refused.

  • A preview remembers the exact bytes of every file; apply_edit refuses 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=1 turns 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

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

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

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

WEBNAV_MCP_READ_ONLY

unset

1/true: write tools refuse to write (previews and checks still work)

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

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 fixture

Development

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 server

Source: 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.ts now handles stdin end/close; tests/cli.test.ts fails 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: \w is ASCII in JS. The index regexes use (?<![\w-]) to avoid matching data-id=, so a non-ASCII letter directly before id=/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 initialize capabilities), so a member declared on a base class can't be found that way. webnav reads the extends/implements clauses from the source and resolves each base with textDocument/definition, breadth-first across files (generic bases, qualified names and mixin calls handled; .d.ts/node_modules bases aren't followed).

  • callers and implementations are offered because the server advertises callHierarchyProvider and implementationProvider.

  • Type-checking an edit without writing it relies on documents the client opens with didOpen/didChange shadowing 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/export is 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 documentSymbol are tight: no doc comment, no export const keyword, one symbol per declarator. edit_symbol and move widen 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-extracted 66 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 tools
callersA

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

    A3.9/5.0

    Scored across 12 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Exposes 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 npm
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables 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.
      10
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables 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.
      -