No description
  • TypeScript 97.9%
  • JavaScript 2.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Riccardo Agnoletto 6f380a91d8 chore: relicense to mit and prepare 0.2.0 for npm publish
- LICENSE.md: MIT (matches llm-template and create-node-ddd-app; the
  package becomes a devDependency of every scaffolded project)
- package.json: drop private, add license/author/homepage/bugs/keywords,
  repository -> git.marric.quest/riccardo/llm-ts-morph, publishConfig
  public, prepublishOnly gate (typecheck + test + build), version 0.2.0
- README: install from npm (exact version, prebuilt dist); the git URL
  spec + onlyBuiltDependencies allowlist stays documented as the fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 09:56:55 +02:00
.claude test+docs: close PLAN.md test gaps and add repo conventions 2026-04-20 12:23:08 +02:00
src feat(package): ship as pnpm git dependency 2026-04-20 12:24:45 +02:00
test test+docs: close PLAN.md test gaps and add repo conventions 2026-04-20 12:23:08 +02:00
.gitignore feat: read-only MCP server for cross-package ts-morph symbol traversal 2026-02-11 10:46:29 +01:00
CLAUDE.md test+docs: close PLAN.md test gaps and add repo conventions 2026-04-20 12:23:08 +02:00
LICENSE.md chore: relicense to mit and prepare 0.2.0 for npm publish 2026-09-17 09:56:55 +02:00
package.json chore: relicense to mit and prepare 0.2.0 for npm publish 2026-09-17 09:56:55 +02:00
PLAN.md feat: read-only MCP server for cross-package ts-morph symbol traversal 2026-02-11 10:46:29 +01:00
pnpm-lock.yaml feat: read-only MCP server for cross-package ts-morph symbol traversal 2026-02-11 10:46:29 +01:00
README.md chore: relicense to mit and prepare 0.2.0 for npm publish 2026-09-17 09:56:55 +02:00
tsconfig.json feat: read-only MCP server for cross-package ts-morph symbol traversal 2026-02-11 10:46:29 +01:00

ts-morph-readonly-mcp

A read-only MCP server that provides cross-package TypeScript symbol traversal for Nx/pnpm monorepos using ts-morph.

Problem

In large Nx/pnpm monorepos, tsserver's --useInferredProjectPerProjectRoot creates isolated per-package project scopes. This means "Find All References" and "Go to Definition" stop at package boundaries — you can't trace a symbol from where it's defined in shared-kernel to where it's used in debate-application.

Solution

This MCP server creates a single ts-morph compilation context spanning all packages via a dedicated tsconfig.tsmorph.json. It exposes 4 read-only tools that any MCP client (Claude Code, Cursor, etc.) can call to navigate symbols across the entire monorepo.

Tools

Tool Description
find_references Find all references to a symbol across packages, with code snippets grouped by file
find_definition Go-to-definition that resolves across package boundaries
get_type_info Full type resolution (resolves types tsserver shows as any, e.g. Effect Schema types)
get_exports List exports grouped by kind (classes, interfaces, types, functions, variables), resolves barrel files

All tools are read-only — no rename, move, or delete operations.

Quick Start

Prerequisites

  • Node.js >= 20
  • pnpm

Install & Build

pnpm install
pnpm build

Configure the Target Monorepo

Create a tsconfig.tsmorph.json in your monorepo root that includes all packages in a single compilation:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "@myorg/package-a": ["packages/package-a/src/index.ts"],
      "@myorg/package-b": ["packages/package-b/src/index.ts"]
      // ... all your package aliases
    }
  },
  "include": [
    "packages/*/src/**/*.ts",
    "apps/*/src/**/*.ts"
  ],
  "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/dist/**", "**/node_modules/**"]
}

Optionally create a second tsconfig.tsmorph.with-tests.json that includes test files, for test-aware analysis.

Add to Claude Code

The recommended way is to pin the MCP server as an npm devDependency of the target monorepo. Each consumer repo gets its own exact version pin, the MCP is installed into node_modules on pnpm install (the registry tarball ships a prebuilt dist/, so no build script runs), and the .mcp.json becomes portable (no absolute paths to the server checkout).

In the target monorepo's package.json:

{
  "devDependencies": {
    "ts-morph-readonly-mcp": "0.2.0"
  }
}

Installing from the git repository instead ("ts-morph-readonly-mcp": "git+https://git.marric.quest/riccardo/llm-ts-morph#v0.2.0") also works, but then pnpm runs the package's prepare build on install, and pnpm v10+ gates build scripts by default — allowlist it in pnpm-workspace.yaml:

onlyBuiltDependencies:
  - ts-morph-readonly-mcp

Then in the target monorepo's .mcp.json (or .claude/settings.local.json):

{
  "mcpServers": {
    "ts-morph-readonly": {
      "command": "node",
      "args": ["./node_modules/ts-morph-readonly-mcp/dist/index.js"],
      "env": {
        "TSMORPH_TSCONFIG": "/abs/path/to/monorepo/tsconfig.tsmorph.json",
        "TSMORPH_TSCONFIG_WITH_TESTS": "/abs/path/to/monorepo/tsconfig.tsmorph.with-tests.json",
        "TSMORPH_ALLOWED_ROOTS": "/abs/path/to/monorepo"
      }
    }
  }
}

Run pnpm install once — the package lands under node_modules/ts-morph-readonly-mcp/ with dist/ already built. Restart Claude Code to pick up the MCP.

A bin entry ts-morph-mcp is also exposed (./node_modules/.bin/ts-morph-mcp), but prefer invoking dist/index.js directly through node: the .bin entry is a pnpm shell wrapper and cannot be used as a node argument.

Env-var paths should be absolute: Claude Code does not guarantee a particular cwd when spawning MCP servers, and the path validator rejects anything outside TSMORPH_ALLOWED_ROOTS.

Alternative: local-development wiring

If you are actively hacking on the MCP, point directly at a local checkout instead of a git dep:

{
  "mcpServers": {
    "ts-morph-readonly": {
      "command": "node",
      "args": ["/abs/path/to/llm-ts-morph/dist/index.js"],
      "env": { "...": "..." }
    }
  }
}

Remember to pnpm build in the checkout after source edits — there's no watch mode.

Environment Variables

Variable Description
TSMORPH_TSCONFIG Default tsconfig path when tools omit tsconfigPath
TSMORPH_TSCONFIG_WITH_TESTS Tsconfig path used when includeTests: true
TSMORPH_ALLOWED_ROOTS Comma-separated allowlisted root directories for path validation

Usage Examples

Find all cross-package references

{
  "tool": "find_references",
  "arguments": {
    "filePath": "/monorepo/packages/shared-kernel/src/domain-event.ts",
    "line": 5,
    "column": 14
  }
}

Returns references from every package that imports and uses DomainEvent, with file paths, line numbers, and code snippets.

Resolve a type that tsserver shows as any

{
  "tool": "get_type_info",
  "arguments": {
    "filePath": "/monorepo/packages/debate-domain/src/aggregate.ts",
    "line": 12,
    "column": 8
  }
}

Returns the fully resolved type string, even for complex types like Effect Schema types.

Response Format

All tools return a structured JSON envelope:

{
  "ok": true,
  "data": { "..." },
  "error": null,
  "meta": {
    "elapsedMs": 42,
    "truncated": false,
    "limits": { "maxReferences": 100, "maxFiles": 50 },
    "tsconfigPath": "/path/to/tsconfig.tsmorph.json",
    "includeTests": false
  }
}

Error codes: INVALID_INPUT, PATH_FORBIDDEN, FILE_NOT_FOUND, SYMBOL_NOT_FOUND, TIMEOUT, INTERNAL_ERROR.

Architecture

src/
├── index.ts               # Entry: creates server, connects StdioServerTransport
├── server.ts              # McpServer factory, registers all 4 tools
├── project.ts             # ts-morph Project singleton cache (keyed by tsconfig path)
├── path-validator.ts      # Security: path containment, extension whitelist, symlink resolution
├── utils.ts               # Shared: position finding, line text extraction, location formatting
└── tools/
    ├── find-references.ts # Cross-package reference finding
    ├── find-definition.ts # Cross-package go-to-definition
    ├── get-type-info.ts   # Full type resolution
    └── get-exports.ts     # List exports grouped by kind

Key design decisions:

  • Lazy initialization — the ts-morph Project is created on the first tool call and cached. Startup takes 5-15s depending on monorepo size, then subsequent calls are near-instant.
  • Concurrency-safe — parallel requests for the same tsconfig share an in-flight Promise (no duplicate init).
  • Path validation — every tool validates file paths through containment checks, extension whitelists, and symlink resolution before processing.
  • Stdout reserved — per MCP convention, stdout carries JSON-RPC messages. All diagnostic logs go to stderr.

Development

pnpm build          # Compile TypeScript to dist/
pnpm start          # Run MCP server via stdio
pnpm test           # Run all tests (node:test)
pnpm typecheck      # Type-check without emitting

Run a single test file:

node --import tsx/esm --test test/tools.test.ts

Test Fixtures

test/fixtures/ contains a minimal 2-package workspace:

  • package-a exports a Greeter class + GreeterConfig type
  • package-b imports from package-a via @test/package-a alias

License

MIT