Skip to content

Latest commit

 

History

History
753 lines (591 loc) · 35.1 KB

File metadata and controls

753 lines (591 loc) · 35.1 KB

MCP Server and CLI Tools

Section Description
Overview Purpose, audience, and design rationale
Architecture Directory structure, data flow, and design decisions
CLI Tools hoist-docs and hoist-ts shell commands
MCP Server Setup Prerequisites, startup methods, and debug logging
MCP Tools Reference Documentation and TypeScript tool APIs
MCP Resources Direct URI-based access to documentation files
Maintaining the Developer Tools Registry sync, maintenance checklist, and update points
Extending the Developer Tools Adding new tools, resources, and doc registry entries
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. gridcmp/grid/README.md, corecore/README.md, authenticationdocs/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.

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

# 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

# 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:

{
  "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

yarn hoist-mcp

Method 3: npx (from installed package)

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:

{
  "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 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:

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:

/**
 * 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):

// 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:

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:

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:

{
    "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().

// 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 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.