npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-morph-readonly-mcp

v0.2.0

Published

Read-only MCP server for cross-package TypeScript symbol traversal in Nx/pnpm monorepos, via ts-morph.

Readme

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