| 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 |
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-docsandhoist-tscommands 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.
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
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
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.
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.
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.
# 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 catalogRun npx hoist-docs --help for full usage.
# 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 StoreRun 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.
- Node.js 18+
tsxavailable (included in hoist-react's devDependencies)- A checked-out hoist-react repository
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-mcpMethod 3: npx (from installed package)
npx hoist-mcpAfter 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).
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"}
}
}
}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...
List all available documentation with descriptions, grouped by category.
| Parameter | Type | Required | Description |
|---|---|---|---|
category |
enum | No | Filter: package, concept, devops, conventions, all (default) |
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). |
Verify the MCP server is running and responsive. Takes no parameters. Reports the indexed
@xh/hoist library version.
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.
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.
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).
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.
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.
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.
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'
];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 (
shouldIndexClassMembersreturnscls.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
@mcpHinton the class/interface JSDoc block directly in its source file. No edit tots-registry.tsis required. - Edit
shouldIndexClassMembers/shouldIndexInterfaceMembersonly if the indexing rule itself needs to change (e.g. adding*Specinterfaces, or scoping out a noisy subtree).
| 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) |
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);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'}]
})
);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.
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');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.
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.
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.
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.