# MCP Server and CLI Tools

| Section | Description |
|---------|-------------|
| [Overview](#overview) | Purpose, audience, and design rationale |
| [Architecture](#architecture) | Directory structure, data flow, and design decisions |
| [CLI Tools](#cli-tools) | `hoist-docs` and `hoist-ts` shell commands |
| [MCP Server Setup](#mcp-server-setup) | Prerequisites, startup methods, and debug logging |
| [MCP Tools Reference](#mcp-tools-reference) | Documentation and TypeScript tool APIs |
| [MCP Resources](#mcp-resources) | Direct URI-based access to documentation files |
| [Maintaining the Developer Tools](#maintaining-the-developer-tools) | Registry sync, maintenance checklist, and update points |
| [Extending the Developer Tools](#extending-the-developer-tools) | Adding new tools, resources, and doc registry entries |
| [Common Pitfalls](#common-pitfalls) | Stdout corruption, path traversal, and naming conventions |

## Overview

The Hoist developer tools give AI coding assistants structured access to hoist-react's documentation
and TypeScript type information. Two interfaces are available:

- **MCP Server** -- a stdio-based server for MCP-compatible clients (e.g. Claude Code)
- **CLI Tools** -- `hoist-docs` and `hoist-ts` commands for shell-capable agents and developers

Both interfaces share the same underlying registries and produce identical output.

**What they provide:**

- **Documentation tools** -- search and browse all hoist-react READMEs, concept docs, and upgrade notes
- **TypeScript tools** -- search symbols, inspect types, and list class/interface members
- **Documentation resources** (MCP only) -- direct access to any doc by URI

**Audience:** AI assistants working with hoist-react codebases, and developers configuring those
assistants. These tools are not used at runtime by applications.

**Why embed them:** Unlike external documentation services, these tools read directly from the
framework source -- no indexing service, no API key, no sync lag. Documentation, type information,
and source code are always consistent with the developer's checked-out version of hoist-react.

**Authorship note:** This sub-project was entirely generated by Claude Code, making it the first
AI-authored component in hoist-react.

## Architecture

### Directory Structure

```
mcp/
├── server.ts                  # MCP entry point -- creates McpServer, registers all capabilities
├── cli/
│   ├── docs.ts                # CLI entry point for hoist-docs
│   └── ts.ts                  # CLI entry point for hoist-ts
├── data/
│   ├── doc-registry.ts        # Documentation inventory loader (reads docs/doc-registry.json)
│   ├── doc-id-resolver.ts     # Tolerant doc-id resolver (canonical + shortenings + aliases)
│   └── ts-registry.ts         # Lazy ts-morph symbol index with on-demand type extraction
├── formatters/
│   ├── docs.ts                # Shared doc formatting (used by MCP tools and CLI)
│   └── typescript.ts          # Shared TypeScript formatting (used by MCP tools and CLI)
├── resources/
│   └── docs.ts                # MCP resource registrations (static + template)
├── tools/
│   ├── docs.ts                # MCP documentation tools (search, list, ping)
│   └── typescript.ts          # MCP TypeScript tools (search-symbols, get-symbol, get-members)
├── package.json               # ES module config (type: "module")
├── tsconfig.json              # TypeScript config (target: ES2022, module: Node16)
└── util/
    ├── logger.ts              # Stderr-only logging (protects stdio JSON-RPC)
    └── paths.ts               # Repo root resolution and path traversal safety
```

### Data Flow

```
MCP Client (e.g. Claude Code)          Shell / AI Agent
    │                                       │
    │  JSON-RPC over stdio                  │  bash commands
    │                                       │
    ▼                                       ▼
server.ts (McpServer)                  cli/docs.ts, cli/ts.ts
    │                                       │
    ├── resources/docs.ts ──┐               │
    ├── tools/docs.ts ──────┤               │
    └── tools/typescript.ts ┤               │
                            │               │
                            ▼               ▼
                     formatters/docs.ts, formatters/typescript.ts
                            │               │
                            ▼               ▼
                     data/doc-registry.ts ──► README files on disk
                     data/ts-registry.ts ──► ts-morph AST parsing
```

### Design Decisions

**Bundle isolation via import chains.** hoist-react ships as raw TypeScript source -- applications
compile it via webpack during their own build. Webpack only processes files reachable via import
chains from app entry points. As long as no browser-targeted hoist code imports from `mcp/`, the
MCP server's Node-only dependencies (`@modelcontextprotocol/sdk`, `ts-morph`, etc.) will never
enter application bundles. The separate `tsconfig.json` provides an additional safety net at the
type-checking level, and all MCP dependencies are `devDependencies` in the root `package.json`.

**Tolerant doc-id resolution.** `data/doc-id-resolver.ts` accepts shortened or
slightly-off doc IDs that agents naturally try (e.g. `grid` → `cmp/grid/README.md`,
`core` → `core/README.md`, `authentication` → `docs/authentication.md`, `v85` → the
v85 upgrade notes) and resolves them to the canonical id when unambiguous. The
canonical id always wins (Tier 0); shortenings are tried in declared tiers; when
multiple docs could match, the resolver fails closed with "did you mean?"
candidates rather than guessing. Each registry entry may declare optional
`aliases` in `doc-registry.json` for semantic synonyms that auto-rules wouldn't
produce. See the module header for the full tier ordering.

**Hardcoded doc registry over filesystem scanning.** The doc registry (`data/doc-registry.ts`)
defines each documentation entry in code rather than discovering files on disk. This was chosen
because the documentation corpus is bounded and well-known (~40 files), and each entry needs
curated metadata (title, description, category, search keywords) that cannot be reliably derived
from filenames alone. The metadata is aligned with the `docs/README.md` index tables. The
tradeoff is manual maintenance -- see [Maintaining the Developer Tools](#maintaining-the-developer-tools).

**Destructured export expansion.** Hoist components are exported via array destructuring --
`export const [Button, button] = hoistCmp.withFactory(...)` -- where the first element is the
React component and the second is its element factory. ts-morph's `decl.getName()` returns the
entire binding pattern as a single string (e.g. `"[Button, button]"`). The indexer detects
`ArrayBindingPattern` and `ObjectBindingPattern` name nodes and expands them into individual symbol
entries, so both `Button` and `button` are separately searchable and resolvable via exact-name
lookup.

**Eager async TypeScript initialization.** Parsing hoist-react's ~700 TypeScript files with ts-morph
is expensive (~2-3s). The MCP server calls `beginInitialization()` after connect so the index builds
in the background while the client sets up. In both cases, `ensureInitialized()` awaits in-flight
work if a query arrives before init completes.

**Disk-persisted index cache.** A serialized snapshot of the symbol and member indexes is written
to `node_modules/.cache/hoist-mcp/index-v1.json` after each fresh build. Subsequent invocations
load it in ~10-20ms when the file set hasn't changed, making CLI search calls sub-second on
remote/slow workstations where ts-morph builds otherwise dominate. Invalidation is automatic: a
fingerprint over every indexable file's path/mtime/size (plus `package.json`) is recomputed at
startup and any drift triggers a rebuild. The live ts-morph `Project` is constructed lazily via
`ensureProject()` only when detail/member extraction needs AST access -- pure search calls served
from the cache skip it entirely. Set `HOIST_MCP_NO_CACHE=1` to bypass for debugging.

**Resources for nouns, tools for verbs.** Following MCP protocol design guidance: resources serve
passive, addressable content (individual docs by URI), while tools handle dynamic computation
(keyword search across the corpus, symbol lookup).

**Inheritance walking for classes and interfaces.** `hoist-get-members` walks the full inheritance
chain for both classes and interfaces rather than showing only directly declared members. This is
critical for hoist-react where key framework patterns use deep hierarchies -- `FieldModel` delegates
everything to `BaseFieldModel`, and `DashContainerModel` inherits essential members from `DashModel`
-- and where Props interfaces compose multiple parents (e.g. `PlaceholderProps` extends `HoistProps`
and `BoxProps`). Classes use a linear walk (single inheritance); interfaces use BFS to handle
multiple `extends` parents. Both walkers resolve parents through the symbol index (not the type
system), so they stop at types outside hoist-react's index. Members are deduplicated by name, with
child declarations winning. Inherited members are tagged with their declaring type in the formatted
output. The same `_`-prefix and `private` filtering applied by the member search index is also
applied here, so `getMembers()` and `searchMembers()` show a consistent public API view.

For class members with no own JSDoc, the walker also looks up the class's `implements` clause and
inherits the JSDoc -- including `@param` and `@returns` content -- from the first interface (in
declaration order) that declares a matching member with documentation. The structured output
records this with `jsDocInheritedFrom`, distinct from `inheritedFrom` (which remains reserved for
the `extends` chain). Both fields can be set together when a subclass inherits a member from a
parent class whose docs the parent itself inherited from an interface. A second pass at index
build-time mirrors this fallback into the member-name search index so JSDoc-content searches
(e.g. `"refresh background"`) reach impl members that single-source their docs on an interface.

**Promise extension indexing via AST navigation.** Hoist's Promise prototype extensions are declared
in a `declare global { interface Promise<T> { ... } }` block, which standard ts-morph APIs like
`sourceFile.getFunction()` and `sourceFile.getInterface()` cannot reach. The indexer explicitly
navigates the AST: `ModuleDeclaration("global") → ModuleBlock → InterfaceDeclaration("Promise") →
MethodSignature`. Because these methods can't be extracted on-demand by the standard
`extractSymbolDetail` path, their `SymbolDetail` objects are pre-computed at index time and stored
in a separate lookup map.

**Shared formatters.** The `formatters/` directory contains pure formatting functions used by both
MCP tools and CLI commands. This ensures identical output regardless of interface, and keeps the
MCP tool handlers and CLI subcommands thin.

**Stdio transport with stderr logging discipline.** Stdout corruption from stray `console.log()`
calls is the most common bug in MCP server implementations. The custom logger in `util/logger.ts`
writes exclusively to stderr. The CLI uses `process.stdout.write()` for output and `console.error()`
for errors.

## CLI Tools

The CLI tools provide the same documentation and TypeScript capabilities as the MCP server, but
via shell commands. They are the recommended interface for AI agents without MCP support.

### `hoist-docs` -- Documentation Search and Reading

```bash
# Search documentation by keyword
npx hoist-docs search "grid sorting"
npx hoist-docs search "authentication" --category concept

# List all available documents
npx hoist-docs list
npx hoist-docs list --category package

# Read a specific document by ID
npx hoist-docs read cmp/grid
npx hoist-docs read lifecycle-app

# Shortcuts for common documents
npx hoist-docs conventions     # AGENTS.md coding conventions
npx hoist-docs index           # docs/README.md documentation catalog
```

Run `npx hoist-docs --help` for full usage.

### `hoist-ts` -- TypeScript Symbol Exploration

```bash
# Search for symbols by name, keyword, or multi-word query
npx hoist-ts search GridModel
npx hoist-ts search lastLoadCompleted
npx hoist-ts search Store --kind class
npx hoist-ts search "panel modal"           # Matches name + JSDoc content

# Get detailed type information for a symbol
npx hoist-ts symbol GridModel
npx hoist-ts symbol View --file data/cube/View.ts   # Disambiguate by file path

# List all members of a class or interface
npx hoist-ts members GridModel
npx hoist-ts members Store
```

Run `npx hoist-ts --help` for full usage.

**Note:** The first `hoist-ts` invocation builds the TypeScript index (~2-3s). Subsequent commands
in the same process are fast, but each CLI invocation pays the cold start cost.

## MCP Server Setup

### Prerequisites

- Node.js 18+
- `tsx` available (included in hoist-react's devDependencies)
- A checked-out hoist-react repository

### Starting the Server

**Method 1: `.mcp.json` (recommended for Claude Code)**

The repository includes a `.mcp.json` file that Claude Code reads automatically:

```json
{
  "mcpServers": {
    "hoist-react": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "mcp/server.ts"],
      "env": {}
    }
  }
}
```

No manual setup is needed -- Claude Code discovers and starts the server when you open a session
in the hoist-react directory.

**Method 2: yarn script**

```bash
yarn hoist-mcp
```

**Method 3: npx (from installed package)**

```bash
npx hoist-mcp
```

### Verification

After starting, call the `hoist-ping` tool to verify connectivity. In Claude Code, the MCP tools
appear automatically in the tool list (e.g. `mcp__hoist-react__hoist-ping`).

### Debug Logging

Set the `HOIST_MCP_DEBUG` environment variable to enable verbose debug output on stderr:

```json
{
  "mcpServers": {
    "hoist-react": {
      "type": "stdio",
      "command": "npx",
      "args": ["tsx", "mcp/server.ts"],
      "env": {"HOIST_MCP_DEBUG": "1"}
    }
  }
}
```

## MCP Tools Reference

### Documentation Tools

#### `hoist-search-docs`

Search across all hoist-react documentation by keyword. Returns matching documents with context
snippets showing where terms appear.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Search keywords (e.g. `"grid column sorting"`) |
| `category` | enum | No | Filter: `package`, `concept`, `devops`, `conventions`, `all` (default) |
| `limit` | number | No | Max results, 1-20. Default: 10 |

**Example output:**
```
Found 3 results for "grid sorting":

1. [Grid Component] (id: cmp/grid, category: package)
   Primary data grid built on ag-Grid.
   Matches: 6 | Snippets:
   - L45: GridModel manages sorting, grouping, selection...
```

#### `hoist-list-docs`

List all available documentation with descriptions, grouped by category.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `category` | enum | No | Filter: `package`, `concept`, `devops`, `conventions`, `all` (default) |

#### `hoist-read-doc`

Read the full text of a single document. Accepts the canonical ID (repo-relative path) and also
tolerates common shortenings via `data/doc-id-resolver.ts` — a bare subsystem (`core`), a path
without README (`cmp/grid`), a docs-doc without the `docs/` prefix (`authentication`), a
last-segment shortcut (`grid`), or a version code for upgrade notes (`v85`). The tool-based
equivalent of the `hoist://docs/{id}` resource — useful when resource fetching is unavailable or
inconvenient. Returns the markdown body as text plus structured `{id, title, category, content,
matchedAs?}` (`matchedAs` is set only when the input differed from the canonical id).

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | Yes | Canonical document ID (repo-relative path), e.g. `cmp/grid/README.md` — or a tolerated shortening (see above). |

#### `hoist-ping`

Verify the MCP server is running and responsive. Takes no parameters. Reports the indexed
`@xh/hoist` library version.

### TypeScript Tools

#### `hoist-search-symbols`

Search for TypeScript classes, interfaces, types, and functions by name, JSDoc content, and own
member names. Multi-word queries split into tokens matched with AND logic against the combined
searchable text, so queries like `"panel modal"` find `ModalSupportModel` (which mentions Panel
in its JSDoc) and `"StoreRecord raw"` finds the `StoreRecord` class (which has a `raw` property).
Results are ranked with name matches above JSDoc/member-only matches. Also searches public members
(properties, methods, accessors) of every exported class and every exported `*Config` interface,
matching against the combined owner name, member name, and member JSDoc.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | Search query - a symbol name (e.g. `"GridModel"`), keyword (e.g. `"tooltip"`), a class + member name (e.g. `"StoreRecord raw"`), or multiple terms (e.g. `"panel modal"`, `"cube view store"`) |
| `kind` | enum | No | Filter symbols by kind: `class`, `interface`, `type`, `function`, `const`, `enum`. Does not affect member results. |
| `exported` | boolean | No | Exported symbols only. Default: `true` |
| `limit` | number | No | Max symbol results, 1-50. Default: 20. Member results have a separate cap of 15. |

**Member-indexed owners:** Public members of every *exported class* and every exported interface
whose name ends in `Config` (e.g. `GridConfig`, `StoreConfig`, `CubeConfig`, `QueryConfig`) are
indexed for search. The `*Config` rule captures the configuration-object shapes consumed by Hoist
class constructors, so queries like `"groupSortFn"` or `"omitFn"` reach both the class property
and the corresponding config-interface field. Only public members are indexed (members with
`private` scope or names starting with `_` are excluded).

Key framework classes and interfaces carry a short hint shown alongside their name in member
search results, sourced from an optional `@mcpHint` JSDoc tag on the declaration (e.g.
`@mcpHint model backing all grid components` on `GridModel`). Owners without the tag are still
searchable; their results just display without the extra hint. See
[Member-Indexed Owners](#member-indexed-owners) for how to add or revise a hint.

**Promise prototype extensions:** Hoist augments `Promise.prototype` with methods like
`catchDefault`, `track`, `linkTo`, `timeout`, `tap`, `wait`, `thenAction`, `catchWhen`, and
`catchDefaultWhen` (declared in `promise/Promise.ts`). These are indexed both as standalone symbol
entries (searchable by name) and as member entries on `Promise` (shown in member search results with
context). `hoist-get-symbol` returns their full signature and JSDoc. The internal helper
`throwIfFailsSelector` is excluded.

**Note:** The TypeScript index is built asynchronously after server startup (~2-3s). It is typically
ready before the first tool call. Subsequent calls are fast in-memory lookups.

#### `hoist-get-symbol`

Get detailed type information for a specific symbol: full signature, JSDoc, inheritance, decorators,
and source location. Use `hoist-search-symbols` first to find the exact name.

For classes that use the config-object constructor pattern (e.g. `GridModel`, `FormModel`, `Store`),
the output includes a `Constructor:` line showing the config type name. This gives agents a natural
follow-up: call `hoist-get-members` on the config interface to see available options.

When multiple exported symbols share the same name (e.g. `View` in both `cmp/viewmanager` and
`data/cube`), the output appends a disambiguation note listing alternates with their file paths.
Pass `filePath` to select a specific one. Dynamics stubs are excluded from the alternates list.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Exact symbol name (e.g. `"GridModel"`) |
| `filePath` | string | No | Source file path to disambiguate duplicate names |

**Example output:**
```
# GridModel (class)
Package: cmp/grid
File: cmp/grid/GridModel.ts
Exported: yes
Extends: HoistModel
Constructor: new GridModel(config: GridConfig)

## Signature
export class GridModel extends HoistModel

## Documentation
Core Model for a Grid, specifying the grid's data store, column definitions...
```

**Constructor detection logic:** The tool checks if the class has a constructor with exactly one
parameter that has a named type annotation. Classes using destructured parameters (e.g.
`TabContainerModel`) or multiple parameters (e.g. `Column(spec, gridModel)`) do not show a
constructor line.

#### `hoist-get-members`

List all properties and methods of a class or interface with types, decorators, and JSDoc.

For classes, walks the full inheritance chain and includes inherited members tagged with their
declaring class. For interfaces, walks the `extends` chain (with BFS fan-out for multiple parents)
and includes inherited members tagged with their declaring interface. This is essential for
framework classes with deep hierarchies -- e.g. `DashContainerModel` inherits key members like
`viewSpecs` and `viewModels` from `DashModel`, and `FieldModel` inherits all of its members from
`BaseFieldModel` -- and for Props interfaces that compose multiple parent interfaces (e.g.
`PlaceholderProps` extends `HoistProps` and `BoxProps`).

For methods, the structured output exposes `@param` descriptions via `parameters[].description`
(rendered as a `Parameters:` block in text output) and `@returns` via a top-level
`returns: {type, description}` field (rendered as a `Returns:` line). Class methods that declare
no own JSDoc inherit it -- including `@param` and `@returns` content -- from the first matching
member on an implemented interface, with the source interface recorded in `jsDocInheritedFrom`.
This lets impl classes single-source their docs on the interface they implement without losing
fidelity in MCP/CLI output.

Members prefixed with `_` and those with the `private` keyword are excluded from the output,
matching the member-index search behavior.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Class or interface name (e.g. `"GridModel"`) |
| `filePath` | string | No | Source file path to disambiguate duplicate names |

**Example output (abbreviated):**
```
# DashContainerModel Members

### Properties (10)
- @bindable showMenuButton: boolean
- renderMode: RenderMode

### Methods (11)
- @action restoreDefaultsAsync(): Promise<void>
- addView(specId: string, container: any, index: number): void

## Inherited from DashModel (12)

### Properties (8)
- viewSpecs: VSPEC[]  (inherited from DashModel)
- @managed @ref viewModels: VMODEL[]  (inherited from DashModel)
- @bindable layoutLocked: boolean  (inherited from DashModel)
```

**Inheritance walking logic:** For classes, the tool resolves the `extends` clause at each level,
looks up the base class in the symbol index, and extracts its members. For interfaces, it performs a
BFS traversal of the `extends` chain, handling multiple parents. In both cases, deduplication ensures
that if a child overrides a parent member, only the child version appears. The walk stops when it
reaches a type not in the index (e.g. a third-party class or React's `HTMLAttributes`) or a type
with no `extends` clause. For class members at any level of the chain that declare no own JSDoc,
the tool additionally consults that class's `implements` clause (declaration order) and inherits
the JSDoc from the first interface that declares a matching member with non-empty docs. Static
members are exempt from this fallback (interfaces declare instance members only).

## MCP Resources

Resources provide direct read access to documentation files via URI.

| Name | URI | Content |
|------|-----|---------|
| `doc-index` | `hoist://docs/index` | `docs/README.md` -- the primary documentation catalog |
| `conventions` | `hoist://docs/conventions` | `AGENTS.md` -- coding conventions and patterns |
| `hoist-doc` | `hoist://docs/{+docId}` | Any document by ID (e.g. `hoist://docs/cmp/grid`) |

The `hoist-doc` template uses RFC 6570 reserved expansion (`{+docId}`) so slashes in doc IDs
(e.g. `cmp/grid`) are preserved rather than percent-encoded.

The resource resolves `docId` through the same tolerant resolver used by `hoist-read-doc` (see
`data/doc-id-resolver.ts`), so the URI accepts canonical ids (`hoist://docs/cmp/grid/README.md`)
and the same set of shortenings tolerated by the tool. Notably, because concept-doc IDs are
themselves `docs/`-prefixed (e.g. `docs/routing.md`) and the scheme prefix is also `hoist://docs/`,
`hoist://docs/routing.md` and `hoist://docs/docs/routing.md` both resolve to the same entry. For
a friction-free read by bare ID, prefer the `hoist-read-doc` tool.

**Discovering available documents:** The `hoist-doc` resource supports `list` and `complete`
operations. MCP clients can enumerate all available doc IDs or get tab-completion suggestions.

## Maintaining the Developer Tools

The developer tools contain several hardcoded data points that must be kept in sync with the
hoist-react codebase. This section catalogs each maintenance point, its location, and when updates
are needed.

### Doc Registry Entries

**File:** `docs/doc-registry.json`

The doc registry is the single source of truth for all documentation that both the MCP server
and CLI tools can search and serve. It is a JSON file loaded at startup by
`mcp/data/doc-registry.ts`. Each entry in the `entries` array specifies an `id` (which doubles
as the file path relative to repo root), `title`, `mcpCategory`, `viewerCategory`,
`description`, and `keywords` array. Entries may optionally declare an `aliases` array of
curated short names consumed by the tolerant doc-id resolver
(`mcp/data/doc-id-resolver.ts`) — only needed for synonyms that the resolver's auto-rules
don't already produce (e.g. `components` for `cmp/README.md`, `services` for `svc/README.md`).

**When to update:**
- A new README or concept doc is added to hoist-react
- A new major version's upgrade notes are created in `docs/upgrade-notes/`
- A documentation file is renamed or moved
- A documentation file is removed
- The description or key topics for a doc change significantly

**How to update:** Add, modify, or remove the corresponding entry object in the `entries` array
of `docs/doc-registry.json`. The `id` field is the file path relative to the repo root (e.g.
`cmp/grid/README.md`). Keywords are a JSON array of strings.

**Automated support:** The `xh-update-doc-links` Claude Code skill
(`.claude/skills/xh-update-doc-links/`) includes a dedicated step that reconciles the doc registry
against documentation files on disk. Running this skill after editing or adding docs will detect
missing, stale, or moved entries and update the registry accordingly.

### Top-Level Packages Array

**File:** `mcp/data/ts-registry.ts` (constant `TOP_LEVEL_PACKAGES`)

This array lists all top-level directories that contain TypeScript source files. It is used to
derive the `sourcePackage` for each symbol in the index (e.g. a file at `cmp/grid/GridModel.ts`
maps to package `cmp/grid`).

**When to update:**
- A new top-level package directory is added to hoist-react
- A top-level package directory is renamed or removed

**Current value:**
```typescript
const TOP_LEVEL_PACKAGES = [
    'core', 'data', 'svc', 'cmp', 'desktop', 'mobile',
    'format', 'appcontainer', 'utils', 'promise', 'mobx',
    'public', 'static', 'admin', 'inspector', 'icon'
];
```

### Member-Indexed Owners

**File:** `mcp/data/ts-registry.ts` (functions `shouldIndexClassMembers`,
`shouldIndexInterfaceMembers`, and `extractMcpHint`)

Which owners have their public members indexed is determined by rule, not a hand-maintained list:

- **Every exported class** (`shouldIndexClassMembers` returns `cls.isExported()`)
- **Every exported interface whose name ends in `Config`** (`shouldIndexInterfaceMembers`)

The `Config` suffix rule captures configuration-object shapes consumed by Hoist class constructors
(e.g. `GridConfig`, `StoreConfig`, `CubeConfig`, `QueryConfig`), so member search surfaces both a
class property and its corresponding config-interface field for the same query. Other interface
kinds (`*Props`, `*Spec`) are intentionally excluded -- indexing them floods generic queries like
`"label"`, `"title"`, `"disabled"` with component-prop hits that dilute more specific results.

**Owner hints via the `@mcpHint` JSDoc tag.** Framework authors attach an optional `@mcpHint`
tag to the class or interface JSDoc block to give a short hint shown alongside the owner name in
member search results. Example:

```ts
/**
 * Core Model for a Grid, specifying the grid's data store and column definitions.
 *
 * @mcpHint model backing all grid components
 */
export class GridModel extends HoistModel { ... }
```

Collocating the hint with the declaration avoids the name-collision and maintenance-drift problems
of a separate hand-curated registry, and lets framework authors add or revise the hint right where
they're writing the class. Owners without an `@mcpHint` tag still appear in search results -- they
just display without the extra hint. The `@mcpHint` tag is declared in the project-root
`tsdoc.json` so the tsdoc ESLint plugin treats it as a known tag.

**When to update:**
- Add or revise `@mcpHint` on the class/interface JSDoc block directly in its source file. No
  edit to `ts-registry.ts` is required.
- Edit `shouldIndexClassMembers` / `shouldIndexInterfaceMembers` only if the indexing rule itself
  needs to change (e.g. adding `*Spec` interfaces, or scoping out a noisy subtree).

### Summary: Maintenance Checklist

| Change | Files to Update |
|--------|----------------|
| Add/rename/remove a documentation file | `docs/doc-registry.json`, `docs/README.md` |
| Add upgrade notes for a new major version | `docs/doc-registry.json`, `docs/README.md` |
| Add/rename/remove a top-level package | `mcp/data/ts-registry.ts` |
| Add or revise the search-result hint for a key framework class | `@mcpHint` tag on the class/interface JSDoc (in its source file) |
| Change which owners have members indexed | `mcp/data/ts-registry.ts` (`shouldIndexClassMembers` / `shouldIndexInterfaceMembers`) |

## Extending the Developer Tools

### Adding a New MCP Tool

Register tools in `tools/docs.ts` or `tools/typescript.ts` (or create a new file and wire it
into `server.ts`):

```typescript
// In tools/my-tools.ts
import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {z} from 'zod';

export function registerMyTools(server: McpServer): void {
    server.registerTool(
        'hoist-my-tool',                      // Name (prefix with hoist-)
        {
            title: 'My Tool Title',
            description: 'What the tool does',
            inputSchema: z.object({
                param: z.string().describe('Parameter description')
            }),
            annotations: {
                readOnlyHint: true,
                destructiveHint: false,
                idempotentHint: true,
                openWorldHint: false
            }
        },
        async ({param}) => ({
            content: [{type: 'text' as const, text: `Result for ${param}`}]
        })
    );
}
```

Then in `server.ts`:

```typescript
import {registerMyTools} from './tools/my-tools.js';
// ...
registerMyTools(server);
```

### Adding a New Resource

Add resource registrations in `resources/docs.ts` or create a new resource file:

```typescript
server.registerResource(
    'my-resource',                             // Registration name
    'hoist://my-resource/path',                // URI
    {
        title: 'My Resource',
        description: 'What it provides',
        mimeType: 'text/markdown'
    },
    async uri => ({
        contents: [{uri: uri.href, text: 'Content here', mimeType: 'text/markdown'}]
    })
);
```

### Adding a Doc Registry Entry

Add a new entry to the `entries` array in `docs/doc-registry.json`:

```json
{
    "id": "my-package/README.md",
    "title": "My Package",
    "mcpCategory": "package",
    "viewerCategory": "components",
    "description": "What this package does.",
    "keywords": ["keyword1", "keyword2", "keyword3"]
}
```

**MCP categories:** `package`, `concept`, `devops`, `conventions`, `index`.
**Viewer categories:** `overview`, `concepts`, `core`, `components`, `desktop`, `mobile`,
`utilities`, `supporting`, `devops`, `upgrade`.

**Optional `aliases`:** add `"aliases": ["short-name", "synonym"]` to make those strings
resolve to this entry via `hoist-read-doc` and the `hoist://docs/{id}` resource. The
resolver already auto-generates safe shortenings (the bare path without `/README.md`, the
last path segment, the docs/-stripped form, version codes for upgrade notes) -- explicit
aliases are only needed for semantic synonyms the auto-rules wouldn't produce. Auto-aliases
that would map to more than one entry are dropped at build time as ambiguous; explicit
aliases that collide with another entry's canonical id are logged as unreachable and skipped.

## Common Pitfalls

### Stdout Corruption (MCP Server)

All logging **must** go to stderr, never stdout. The MCP protocol uses stdout exclusively for
JSON-RPC messages. Use the `log` object from `util/logger.ts` -- never `console.log()`.

```typescript
// Do: Use the logger
import {log} from '../util/logger.js';
log.info('Server started');

// Don't: Use console.log (corrupts JSON-RPC on stdout)
console.log('Server started');
```

### Path Traversal

The `resolveDocPath()` utility in `util/paths.ts` validates that resolved paths stay within the
repository root. It rejects paths containing `..` segments. Always use this function when resolving
file paths from external input.

### Path Separators (Cross-Platform)

ts-morph's `SourceFile.getFilePath()` always returns **forward-slash** paths on every platform
(e.g. `D:/hoist-react/cmp/grid/GridModel.ts` on Windows), whereas `resolveRepoRoot()` returns a
native path from Node's `path` module -- **backslash-separated** on Windows (`D:\hoist-react`).
Comparing or slicing one against the other (e.g. `filePath.startsWith(repoRoot + '/')`) silently
fails on Windows, filtering out every source file and yielding an empty symbol index. When
comparing against or slicing a ts-morph path, use `resolveRepoRootPosix()` (and `toPosixPath()` for
any incoming file-path argument) from `util/paths.ts` rather than `resolveRepoRoot()`. Filesystem
access that stays within Node's `path`/`fs` APIs (e.g. the doc registry, the index cache) can keep
using `resolveRepoRoot()`, since those are separator-consistent on both sides.

### Registry Sync

The doc registry is hardcoded, not filesystem-scanned. When documentation files are added or
removed, the registry must be updated manually. If a file referenced by a registry entry is missing
on disk, the entry is logged as a warning and skipped at startup -- it does not cause a crash.

See [Maintaining the Developer Tools](#maintaining-the-developer-tools) for the full maintenance
checklist.

### Tool Naming Conventions

Documentation and TypeScript tools are prefixed with `hoist-` (e.g. `hoist-search-docs`,
`hoist-get-symbol`). CLI commands use `hoist-docs` and `hoist-ts` as the top-level command names
with subcommands for each operation.
