@xh/hoist 82.0.0 → 82.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/kit/blueprint/styles.scss +0 -6
- package/mcp/README.md +42 -12
- package/mcp/data/ts-registry.ts +226 -23
- package/mcp/prompts/form.ts +3 -3
- package/mcp/prompts/grid.ts +3 -3
- package/mcp/prompts/tabs.ts +2 -2
- package/mcp/prompts/util.ts +8 -7
- package/mcp/server.ts +5 -0
- package/mcp/tools/typescript.ts +56 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 82.0.1 - 2026-02-28
|
|
4
|
+
|
|
5
|
+
### 🐞 Bug Fixes
|
|
6
|
+
|
|
7
|
+
* Fixed a CSS issue causing desktop submenus to clip.
|
|
8
|
+
|
|
9
|
+
### ⚙️ Technical
|
|
10
|
+
|
|
11
|
+
* Enhanced the MCP server's `hoist-search-symbols` tool to also search public members (properties,
|
|
12
|
+
methods, accessors) of 18 key framework classes. The TypeScript index is now built asynchronously
|
|
13
|
+
after server startup so the first tool call doesn't pay the initialization cost.
|
|
14
|
+
|
|
3
15
|
## 82.0.0 - 2026-02-27
|
|
4
16
|
|
|
5
17
|
Note that a server-side upgrade to `hoist-core >= 36.3` is recommended to support new Admin Metrics
|
|
@@ -80,12 +80,6 @@
|
|
|
80
80
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
81
81
|
0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
82
82
|
|
|
83
|
-
// Clip content children to the popover's rounded corners so square-edged inner
|
|
84
|
-
// elements (e.g. scrollable lists) don't poke through at the border-radius.
|
|
85
|
-
.bp6-popover-content {
|
|
86
|
-
overflow: hidden;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
83
|
// Arrow border matches the popover border; arrow fill matches the popover background.
|
|
90
84
|
.bp6-popover-arrow-border {
|
|
91
85
|
fill: var(--xh-popup-border-color);
|
package/mcp/README.md
CHANGED
|
@@ -85,12 +85,14 @@ curated metadata (title, description, category, search keywords) that cannot be
|
|
|
85
85
|
from filenames alone. The metadata is aligned with the `docs/README.md` index tables. The
|
|
86
86
|
tradeoff is manual maintenance -- see [Maintaining the MCP Server](#maintaining-the-mcp-server).
|
|
87
87
|
|
|
88
|
-
**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
(
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
**Eager async TypeScript initialization.** Parsing hoist-react's ~700 TypeScript files with ts-morph
|
|
89
|
+
is expensive (~2-3s). After the server connects, `beginInitialization()` kicks off index building
|
|
90
|
+
asynchronously so it runs in the background while the client sets up. If a tool call arrives before
|
|
91
|
+
init completes, `ensureInitialized()` awaits the in-flight work. In practice, the index is
|
|
92
|
+
typically ready before the first tool invocation. The symbol and member indexes are built using
|
|
93
|
+
AST-level methods (`getClasses()`, `getInterfaces()`, etc.) rather than full type resolution.
|
|
94
|
+
Detailed symbol information (signatures, JSDoc, full member lists) is extracted on-demand from
|
|
95
|
+
individual source files.
|
|
94
96
|
|
|
95
97
|
**Resources for nouns, tools for verbs.** Following MCP protocol design guidance: resources serve
|
|
96
98
|
passive, addressable content (individual docs by URI), while tools handle dynamic computation
|
|
@@ -214,17 +216,24 @@ Verify the MCP server is running and responsive. Takes no parameters.
|
|
|
214
216
|
|
|
215
217
|
#### `hoist-search-symbols`
|
|
216
218
|
|
|
217
|
-
Search for TypeScript classes, interfaces, types, and functions by name.
|
|
219
|
+
Search for TypeScript classes, interfaces, types, and functions by name. Also searches public
|
|
220
|
+
members (properties, methods, accessors) of key framework classes, returning results in two sections:
|
|
221
|
+
matching symbols and matching members with their owning class and role description.
|
|
218
222
|
|
|
219
223
|
| Parameter | Type | Required | Description |
|
|
220
224
|
|-----------|------|----------|-------------|
|
|
221
|
-
| `query` | string | Yes | Symbol
|
|
222
|
-
| `kind` | enum | No | Filter: `class`, `interface`, `type`, `function`, `const`, `enum
|
|
225
|
+
| `query` | string | Yes | Symbol or member name to search for (e.g. `"GridModel"`, `"lastLoadCompleted"`, `"setSortBy"`) |
|
|
226
|
+
| `kind` | enum | No | Filter symbols by kind: `class`, `interface`, `type`, `function`, `const`, `enum`. Does not affect member results. |
|
|
223
227
|
| `exported` | boolean | No | Exported symbols only. Default: `true` |
|
|
224
|
-
| `limit` | number | No | Max results, 1-50. Default: 20 |
|
|
228
|
+
| `limit` | number | No | Max symbol results, 1-50. Default: 20. Member results have a separate cap of 15. |
|
|
225
229
|
|
|
226
|
-
**
|
|
227
|
-
|
|
230
|
+
**Member-indexed classes:** HoistBase, HoistModel, HoistService, XHApi, GridModel, Column, Store,
|
|
231
|
+
StoreRecord, StoreSelectionModel, Field, RecordAction, Cube, CubeField, View, FormModel,
|
|
232
|
+
BaseFieldModel, FieldModel, TabContainerModel. Only public members are indexed (private members and
|
|
233
|
+
those prefixed with `_` are excluded).
|
|
234
|
+
|
|
235
|
+
**Note:** The TypeScript index is built asynchronously after server startup (~2-3s). It is typically
|
|
236
|
+
ready before the first tool call. Subsequent calls are fast in-memory lookups.
|
|
228
237
|
|
|
229
238
|
#### `hoist-get-symbol`
|
|
230
239
|
|
|
@@ -373,6 +382,26 @@ const TOP_LEVEL_PACKAGES = [
|
|
|
373
382
|
];
|
|
374
383
|
```
|
|
375
384
|
|
|
385
|
+
### Member-Indexed Classes
|
|
386
|
+
|
|
387
|
+
**File:** `mcp/data/ts-registry.ts` (constant `MEMBER_INDEXED_CLASSES`)
|
|
388
|
+
|
|
389
|
+
This map lists classes whose public members are indexed for search by member name via
|
|
390
|
+
`hoist-search-symbols`. Each entry maps a class name to a brief role description shown alongside
|
|
391
|
+
member search results (e.g. "base class for all application models").
|
|
392
|
+
|
|
393
|
+
**When to update:**
|
|
394
|
+
- A new key base class is added to the framework and should have its members searchable
|
|
395
|
+
- A member-indexed class is renamed or removed
|
|
396
|
+
- The role description of a class should be clarified
|
|
397
|
+
|
|
398
|
+
**Current value:**
|
|
399
|
+
```
|
|
400
|
+
HoistBase, HoistModel, HoistService, XHApi, GridModel, Column, Store,
|
|
401
|
+
StoreRecord, StoreSelectionModel, Field, RecordAction, Cube, CubeField,
|
|
402
|
+
View, FormModel, BaseFieldModel, FieldModel, TabContainerModel
|
|
403
|
+
```
|
|
404
|
+
|
|
376
405
|
### Prompt Documentation Section References
|
|
377
406
|
|
|
378
407
|
**Files:** `mcp/prompts/grid.ts`, `mcp/prompts/form.ts`, `mcp/prompts/tabs.ts`
|
|
@@ -429,6 +458,7 @@ were replaced, or if `makeObservable` were no longer required.
|
|
|
429
458
|
| Add/rename/remove a documentation file | `mcp/data/doc-registry.ts`, `docs/README.md` |
|
|
430
459
|
| Add upgrade notes for a new major version | `mcp/data/doc-registry.ts`, `docs/README.md` |
|
|
431
460
|
| Add/rename/remove a top-level package | `mcp/data/ts-registry.ts` |
|
|
461
|
+
| Add/rename/remove a member-indexed class | `mcp/data/ts-registry.ts` (constant `MEMBER_INDEXED_CLASSES`) |
|
|
432
462
|
| Rename a section header in a component README | Check `mcp/prompts/grid.ts`, `form.ts`, `tabs.ts` |
|
|
433
463
|
| Rename a key class or remove a key member | Check `mcp/prompts/grid.ts`, `form.ts`, `tabs.ts` |
|
|
434
464
|
| Change a fundamental coding convention | `mcp/prompts/util.ts` |
|
package/mcp/data/ts-registry.ts
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* information. This module is consumed by the three TypeScript MCP tools:
|
|
7
7
|
* hoist-search-symbols, hoist-get-symbol, and hoist-get-members.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Initialization is kicked off asynchronously after server startup via
|
|
10
|
+
* `beginInitialization()`, so the index is typically ready before the first
|
|
11
|
+
* tool call arrives. If a tool call arrives before init completes,
|
|
12
|
+
* `ensureInitialized()` awaits the in-flight init. Once created, a
|
|
13
|
+
* lightweight symbol index and parallel member index are built using
|
|
14
|
+
* AST-level methods for fast search without per-query AST traversal.
|
|
15
|
+
* Detailed symbol info is extracted on-demand.
|
|
13
16
|
*/
|
|
14
|
-
import {Project, Node, SyntaxKind} from 'ts-morph';
|
|
17
|
+
import {Project, Node, Scope, SyntaxKind} from 'ts-morph';
|
|
15
18
|
import type {ClassDeclaration, SourceFile} from 'ts-morph';
|
|
16
19
|
import {resolve} from 'node:path';
|
|
17
20
|
|
|
@@ -61,12 +64,27 @@ export interface MemberInfo {
|
|
|
61
64
|
returnType?: string;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** Lightweight index entry for a class member, enabling search by member name. */
|
|
68
|
+
export interface MemberIndexEntry {
|
|
69
|
+
name: string;
|
|
70
|
+
memberKind: 'property' | 'method' | 'accessor';
|
|
71
|
+
ownerName: string;
|
|
72
|
+
ownerDescription: string;
|
|
73
|
+
filePath: string;
|
|
74
|
+
sourcePackage: string;
|
|
75
|
+
isStatic: boolean;
|
|
76
|
+
type: string;
|
|
77
|
+
jsDoc: string;
|
|
78
|
+
decorators: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
//------------------------------------------------------------------
|
|
65
82
|
// Module state (lazy initialization)
|
|
66
83
|
//------------------------------------------------------------------
|
|
67
84
|
|
|
68
85
|
let project: Project | null = null;
|
|
69
86
|
let symbolIndex: Map<string, SymbolEntry[]> | null = null;
|
|
87
|
+
let memberIndex: Map<string, MemberIndexEntry[]> | null = null;
|
|
70
88
|
|
|
71
89
|
//------------------------------------------------------------------
|
|
72
90
|
// Package derivation
|
|
@@ -92,6 +110,43 @@ const TOP_LEVEL_PACKAGES = [
|
|
|
92
110
|
'icon'
|
|
93
111
|
];
|
|
94
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Classes whose public members are indexed for search by member name.
|
|
115
|
+
* Values are brief role descriptions shown in search results to clarify
|
|
116
|
+
* how the class fits into the framework hierarchy.
|
|
117
|
+
*/
|
|
118
|
+
const MEMBER_INDEXED_CLASSES = new Map([
|
|
119
|
+
// Core framework base classes
|
|
120
|
+
['HoistBase', 'base class for all Hoist objects (models, services, stores)'],
|
|
121
|
+
['HoistModel', 'base class for all application models'],
|
|
122
|
+
['HoistService', 'base class for all application services'],
|
|
123
|
+
['XHApi', 'singleton (XH) providing global framework services'],
|
|
124
|
+
|
|
125
|
+
// Grid
|
|
126
|
+
['GridModel', 'model backing all grid components'],
|
|
127
|
+
['Column', 'column configuration for grids'],
|
|
128
|
+
|
|
129
|
+
// Data
|
|
130
|
+
['Store', 'in-memory data store used by grids and other data components'],
|
|
131
|
+
['StoreRecord', 'individual record within a Store'],
|
|
132
|
+
['StoreSelectionModel', 'selection state manager for Store, used by grids'],
|
|
133
|
+
['Field', 'metadata for a data field within a Store or Cube'],
|
|
134
|
+
['RecordAction', 'reusable action for grid context menus and action columns'],
|
|
135
|
+
|
|
136
|
+
// Cube
|
|
137
|
+
['Cube', 'multi-dimensional data store with aggregation and views'],
|
|
138
|
+
['CubeField', 'field with aggregation metadata for use within a Cube'],
|
|
139
|
+
['View', 'live or snapshot view of aggregated Cube data'],
|
|
140
|
+
|
|
141
|
+
// Form
|
|
142
|
+
['FormModel', 'model for form state, field values, and validation'],
|
|
143
|
+
['BaseFieldModel', 'base class for FieldModel — holds value, validation, and dirty tracking'],
|
|
144
|
+
['FieldModel', 'model for a single form field (extends BaseFieldModel)'],
|
|
145
|
+
|
|
146
|
+
// Tabs
|
|
147
|
+
['TabContainerModel', 'model for tabbed container with routing and refresh support']
|
|
148
|
+
]);
|
|
149
|
+
|
|
95
150
|
/**
|
|
96
151
|
* Derive the source package from a file's absolute path.
|
|
97
152
|
* e.g. `/repo/core/HoistBase.ts` maps to `core`,
|
|
@@ -134,18 +189,68 @@ function addToIndex(index: Map<string, SymbolEntry[]>, entry: SymbolEntry): void
|
|
|
134
189
|
}
|
|
135
190
|
}
|
|
136
191
|
|
|
192
|
+
/** Add a member entry to the member index, keyed by lowercase member name. */
|
|
193
|
+
function addToMemberIndex(index: Map<string, MemberIndexEntry[]>, entry: MemberIndexEntry): void {
|
|
194
|
+
const key = entry.name.toLowerCase();
|
|
195
|
+
const existing = index.get(key);
|
|
196
|
+
if (existing) {
|
|
197
|
+
existing.push(entry);
|
|
198
|
+
} else {
|
|
199
|
+
index.set(key, [entry]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a member should be excluded from the index as private/internal.
|
|
205
|
+
* Excludes members with the `private` keyword or names starting with `_`.
|
|
206
|
+
*/
|
|
207
|
+
function isPrivateMember(member: MemberInfo, cls: ClassDeclaration): boolean {
|
|
208
|
+
if (member.name.startsWith('_')) return true;
|
|
209
|
+
|
|
210
|
+
// Check for explicit `private` keyword on the AST node
|
|
211
|
+
try {
|
|
212
|
+
const node =
|
|
213
|
+
cls.getProperty(member.name) ??
|
|
214
|
+
cls.getGetAccessor(member.name) ??
|
|
215
|
+
cls.getMethod(member.name) ??
|
|
216
|
+
cls.getStaticProperty(member.name) ??
|
|
217
|
+
cls.getStaticMethod(member.name);
|
|
218
|
+
if (node) {
|
|
219
|
+
const scope = (node as unknown as {getScope?: () => string}).getScope?.();
|
|
220
|
+
if (scope === Scope.Private) return true;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// If we can't determine scope, keep the member (assume public)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Format a method's parameter list and return type as a compact type string. */
|
|
230
|
+
function formatMethodType(member: MemberInfo): string {
|
|
231
|
+
const params = (member.parameters ?? []).map(p => `${p.name}: ${p.type}`).join(', ');
|
|
232
|
+
const ret = member.returnType ?? 'void';
|
|
233
|
+
return `(${params}) => ${ret}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
137
236
|
/**
|
|
138
237
|
* Build the symbol index by scanning all source files using AST-level methods.
|
|
238
|
+
* Also builds a parallel member index for classes in MEMBER_INDEXED_CLASSES.
|
|
139
239
|
*
|
|
140
240
|
* Uses getClasses(), getInterfaces(), getTypeAliases(), getFunctions(),
|
|
141
241
|
* getEnums(), and getVariableStatements() -- NOT getExportedDeclarations(),
|
|
142
242
|
* which triggers full type binding and is ~1000x slower.
|
|
143
243
|
*/
|
|
144
|
-
function buildSymbolIndex(proj: Project):
|
|
244
|
+
function buildSymbolIndex(proj: Project): {
|
|
245
|
+
symbols: Map<string, SymbolEntry[]>;
|
|
246
|
+
members: Map<string, MemberIndexEntry[]>;
|
|
247
|
+
} {
|
|
145
248
|
const index = new Map<string, SymbolEntry[]>();
|
|
249
|
+
const mIndex = new Map<string, MemberIndexEntry[]>();
|
|
146
250
|
const repoRoot = resolveRepoRoot();
|
|
147
251
|
|
|
148
252
|
const counts = {total: 0, exported: 0, byKind: {} as Record<string, number>};
|
|
253
|
+
let memberCount = 0;
|
|
149
254
|
|
|
150
255
|
for (const sourceFile of proj.getSourceFiles()) {
|
|
151
256
|
const filePath = sourceFile.getFilePath();
|
|
@@ -182,6 +287,33 @@ function buildSymbolIndex(proj: Project): Map<string, SymbolEntry[]> {
|
|
|
182
287
|
counts.total++;
|
|
183
288
|
if (entry.isExported) counts.exported++;
|
|
184
289
|
counts.byKind['class'] = (counts.byKind['class'] || 0) + 1;
|
|
290
|
+
|
|
291
|
+
// Index public members for curated framework classes
|
|
292
|
+
const ownerDescription = MEMBER_INDEXED_CLASSES.get(name);
|
|
293
|
+
if (ownerDescription) {
|
|
294
|
+
try {
|
|
295
|
+
const members = extractClassMembers(sourceFile, name);
|
|
296
|
+
for (const m of members) {
|
|
297
|
+
if (isPrivateMember(m, cls)) continue;
|
|
298
|
+
const mEntry: MemberIndexEntry = {
|
|
299
|
+
name: m.name,
|
|
300
|
+
memberKind: m.kind,
|
|
301
|
+
ownerName: name,
|
|
302
|
+
ownerDescription,
|
|
303
|
+
filePath,
|
|
304
|
+
sourcePackage: pkg,
|
|
305
|
+
isStatic: m.isStatic,
|
|
306
|
+
type: m.kind === 'method' ? formatMethodType(m) : m.type,
|
|
307
|
+
jsDoc: m.jsDoc.split('\n')[0],
|
|
308
|
+
decorators: m.decorators
|
|
309
|
+
};
|
|
310
|
+
addToMemberIndex(mIndex, mEntry);
|
|
311
|
+
memberCount++;
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
log.warn(`Failed to index members for ${name}: ${e}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
185
317
|
}
|
|
186
318
|
|
|
187
319
|
// Interfaces
|
|
@@ -280,21 +412,22 @@ function buildSymbolIndex(proj: Project): Map<string, SymbolEntry[]> {
|
|
|
280
412
|
log.info(
|
|
281
413
|
`Symbol index built: ${counts.total} total symbols (${counts.exported} exported) -- ${kindSummary}`
|
|
282
414
|
);
|
|
415
|
+
log.info(
|
|
416
|
+
`Member index built: ${memberCount} public members across ${MEMBER_INDEXED_CLASSES.size} classes`
|
|
417
|
+
);
|
|
283
418
|
|
|
284
|
-
return index;
|
|
419
|
+
return {symbols: index, members: mIndex};
|
|
285
420
|
}
|
|
286
421
|
|
|
287
422
|
//------------------------------------------------------------------
|
|
288
423
|
// Public API
|
|
289
424
|
//------------------------------------------------------------------
|
|
290
425
|
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
*/
|
|
297
|
-
export function ensureInitialized(): void {
|
|
426
|
+
/** Promise for in-flight initialization, used to coordinate eager and on-demand init. */
|
|
427
|
+
let initPromise: Promise<void> | null = null;
|
|
428
|
+
|
|
429
|
+
/** Synchronous init — heavy lifting, runs on a microtask when kicked off eagerly. */
|
|
430
|
+
function doInitialize(): void {
|
|
298
431
|
if (project) return;
|
|
299
432
|
|
|
300
433
|
const startMs = Date.now();
|
|
@@ -305,7 +438,9 @@ export function ensureInitialized(): void {
|
|
|
305
438
|
});
|
|
306
439
|
project.resolveSourceFileDependencies();
|
|
307
440
|
|
|
308
|
-
|
|
441
|
+
const result = buildSymbolIndex(project);
|
|
442
|
+
symbolIndex = result.symbols;
|
|
443
|
+
memberIndex = result.members;
|
|
309
444
|
|
|
310
445
|
const elapsed = Date.now() - startMs;
|
|
311
446
|
log.info(`TypeScript registry initialized in ${elapsed}ms`);
|
|
@@ -314,17 +449,44 @@ export function ensureInitialized(): void {
|
|
|
314
449
|
}
|
|
315
450
|
}
|
|
316
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Begin TypeScript registry initialization in the background.
|
|
454
|
+
*
|
|
455
|
+
* Call this after server startup to warm the index asynchronously, so the
|
|
456
|
+
* first tool invocation doesn't pay the full init cost. Safe to call
|
|
457
|
+
* multiple times — subsequent calls are no-ops.
|
|
458
|
+
*/
|
|
459
|
+
export function beginInitialization(): void {
|
|
460
|
+
if (project || initPromise) return;
|
|
461
|
+
initPromise = Promise.resolve().then(() => {
|
|
462
|
+
doInitialize();
|
|
463
|
+
initPromise = null;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Ensure the ts-morph Project and symbol index are initialized.
|
|
469
|
+
*
|
|
470
|
+
* If {@link beginInitialization} was called, awaits the in-flight init.
|
|
471
|
+
* Otherwise initializes synchronously. Safe to call multiple times.
|
|
472
|
+
*/
|
|
473
|
+
export async function ensureInitialized(): Promise<void> {
|
|
474
|
+
if (project) return;
|
|
475
|
+
if (initPromise) return initPromise;
|
|
476
|
+
doInitialize();
|
|
477
|
+
}
|
|
478
|
+
|
|
317
479
|
/**
|
|
318
480
|
* Search the symbol index by query string.
|
|
319
481
|
*
|
|
320
482
|
* Supports case-insensitive substring matching against symbol names.
|
|
321
483
|
* Optionally filter by kind and/or export status.
|
|
322
484
|
*/
|
|
323
|
-
export function searchSymbols(
|
|
485
|
+
export async function searchSymbols(
|
|
324
486
|
query: string,
|
|
325
487
|
options?: {kind?: SymbolKind; exported?: boolean; limit?: number}
|
|
326
|
-
): SymbolEntry[] {
|
|
327
|
-
ensureInitialized();
|
|
488
|
+
): Promise<SymbolEntry[]> {
|
|
489
|
+
await ensureInitialized();
|
|
328
490
|
|
|
329
491
|
const queryLower = query.toLowerCase().trim();
|
|
330
492
|
if (!queryLower) return [];
|
|
@@ -358,6 +520,44 @@ export function searchSymbols(
|
|
|
358
520
|
return results.slice(0, limit);
|
|
359
521
|
}
|
|
360
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Search the member index by query string.
|
|
525
|
+
*
|
|
526
|
+
* Supports case-insensitive substring matching against member names.
|
|
527
|
+
* Only searches members of classes in MEMBER_INDEXED_CLASSES.
|
|
528
|
+
*/
|
|
529
|
+
export async function searchMembers(
|
|
530
|
+
query: string,
|
|
531
|
+
options?: {limit?: number}
|
|
532
|
+
): Promise<MemberIndexEntry[]> {
|
|
533
|
+
await ensureInitialized();
|
|
534
|
+
|
|
535
|
+
const queryLower = query.toLowerCase().trim();
|
|
536
|
+
if (!queryLower) return [];
|
|
537
|
+
|
|
538
|
+
const limit = options?.limit ?? 15;
|
|
539
|
+
const results: MemberIndexEntry[] = [];
|
|
540
|
+
|
|
541
|
+
for (const [key, entries] of memberIndex!) {
|
|
542
|
+
if (!key.includes(queryLower)) continue;
|
|
543
|
+
results.push(...entries);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Sort: exact matches first, then alphabetically by member name, then by owner
|
|
547
|
+
results.sort((a, b) => {
|
|
548
|
+
const aExact = a.name.toLowerCase() === queryLower ? 0 : 1;
|
|
549
|
+
const bExact = b.name.toLowerCase() === queryLower ? 0 : 1;
|
|
550
|
+
if (aExact !== bExact) return aExact - bExact;
|
|
551
|
+
|
|
552
|
+
const nameCompare = a.name.localeCompare(b.name);
|
|
553
|
+
if (nameCompare !== 0) return nameCompare;
|
|
554
|
+
|
|
555
|
+
return a.ownerName.localeCompare(b.ownerName);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
return results.slice(0, limit);
|
|
559
|
+
}
|
|
560
|
+
|
|
361
561
|
/**
|
|
362
562
|
* Get detailed information about a specific symbol.
|
|
363
563
|
*
|
|
@@ -365,8 +565,11 @@ export function searchSymbols(
|
|
|
365
565
|
* If filePath is provided, filters to that specific file. If multiple matches
|
|
366
566
|
* and no filePath, returns the first exported match.
|
|
367
567
|
*/
|
|
368
|
-
export function getSymbolDetail(
|
|
369
|
-
|
|
568
|
+
export async function getSymbolDetail(
|
|
569
|
+
name: string,
|
|
570
|
+
filePath?: string
|
|
571
|
+
): Promise<SymbolDetail | null> {
|
|
572
|
+
await ensureInitialized();
|
|
370
573
|
|
|
371
574
|
const entry = findIndexEntry(name, filePath);
|
|
372
575
|
if (!entry) return null;
|
|
@@ -384,11 +587,11 @@ export function getSymbolDetail(name: string, filePath?: string): SymbolDetail |
|
|
|
384
587
|
*
|
|
385
588
|
* Returns null for symbol kinds other than class or interface.
|
|
386
589
|
*/
|
|
387
|
-
export function getMembers(
|
|
590
|
+
export async function getMembers(
|
|
388
591
|
name: string,
|
|
389
592
|
filePath?: string
|
|
390
|
-
): {symbol: SymbolDetail; members: MemberInfo[]} | null {
|
|
391
|
-
ensureInitialized();
|
|
593
|
+
): Promise<{symbol: SymbolDetail; members: MemberInfo[]} | null> {
|
|
594
|
+
await ensureInitialized();
|
|
392
595
|
|
|
393
596
|
const entry = findIndexEntry(name, filePath);
|
|
394
597
|
if (!entry) return null;
|
package/mcp/prompts/form.ts
CHANGED
|
@@ -82,8 +82,8 @@ export async function buildFormPrompt(args: {
|
|
|
82
82
|
//------------------------------------------------------------------
|
|
83
83
|
// Type information
|
|
84
84
|
//------------------------------------------------------------------
|
|
85
|
-
const formModelSummary = formatSymbolSummary('FormModel');
|
|
86
|
-
const formModelMembers = formatKeyMembers('FormModel', [
|
|
85
|
+
const formModelSummary = await formatSymbolSummary('FormModel');
|
|
86
|
+
const formModelMembers = await formatKeyMembers('FormModel', [
|
|
87
87
|
'fields',
|
|
88
88
|
'values',
|
|
89
89
|
'isValid',
|
|
@@ -92,7 +92,7 @@ export async function buildFormPrompt(args: {
|
|
|
92
92
|
'reset',
|
|
93
93
|
'getData'
|
|
94
94
|
]);
|
|
95
|
-
const fieldModelSummary = formatSymbolSummary('FieldModel');
|
|
95
|
+
const fieldModelSummary = await formatSymbolSummary('FieldModel');
|
|
96
96
|
|
|
97
97
|
//------------------------------------------------------------------
|
|
98
98
|
// Build adaptive code template
|
package/mcp/prompts/grid.ts
CHANGED
|
@@ -66,8 +66,8 @@ export async function buildGridPrompt(args: {
|
|
|
66
66
|
//------------------------------------------------------------------
|
|
67
67
|
// Type information
|
|
68
68
|
//------------------------------------------------------------------
|
|
69
|
-
const gridModelSummary = formatSymbolSummary('GridModel');
|
|
70
|
-
const gridModelMembers = formatKeyMembers('GridModel', [
|
|
69
|
+
const gridModelSummary = await formatSymbolSummary('GridModel');
|
|
70
|
+
const gridModelMembers = await formatKeyMembers('GridModel', [
|
|
71
71
|
'store',
|
|
72
72
|
'columns',
|
|
73
73
|
'sortBy',
|
|
@@ -76,7 +76,7 @@ export async function buildGridPrompt(args: {
|
|
|
76
76
|
'emptyText',
|
|
77
77
|
'onRowDoubleClicked'
|
|
78
78
|
]);
|
|
79
|
-
const columnSummary = formatSymbolSummary('Column');
|
|
79
|
+
const columnSummary = await formatSymbolSummary('Column');
|
|
80
80
|
|
|
81
81
|
//------------------------------------------------------------------
|
|
82
82
|
// Build adaptive code template
|
package/mcp/prompts/tabs.ts
CHANGED
|
@@ -78,8 +78,8 @@ export async function buildTabsPrompt(args: {
|
|
|
78
78
|
//------------------------------------------------------------------
|
|
79
79
|
// Type information
|
|
80
80
|
//------------------------------------------------------------------
|
|
81
|
-
const tabContainerModelSummary = formatSymbolSummary('TabContainerModel');
|
|
82
|
-
const tabContainerModelMembers = formatKeyMembers('TabContainerModel', [
|
|
81
|
+
const tabContainerModelSummary = await formatSymbolSummary('TabContainerModel');
|
|
82
|
+
const tabContainerModelMembers = await formatKeyMembers('TabContainerModel', [
|
|
83
83
|
'tabs',
|
|
84
84
|
'activeTabId',
|
|
85
85
|
'route',
|
package/mcp/prompts/util.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* to compose structured, context-rich prompt responses.
|
|
8
8
|
*/
|
|
9
9
|
import {buildRegistry, loadDocContent, type DocEntry} from '../data/doc-registry.js';
|
|
10
|
-
import {
|
|
10
|
+
import {getSymbolDetail, getMembers} from '../data/ts-registry.js';
|
|
11
11
|
import {resolveRepoRoot} from '../util/paths.js';
|
|
12
12
|
|
|
13
13
|
//------------------------------------------------------------------
|
|
@@ -88,9 +88,8 @@ export function extractSection(markdown: string, sectionName: string): string {
|
|
|
88
88
|
*
|
|
89
89
|
* @returns A formatted markdown block, or a "not found" message.
|
|
90
90
|
*/
|
|
91
|
-
export function formatSymbolSummary(name: string): string {
|
|
92
|
-
|
|
93
|
-
const detail = getSymbolDetail(name);
|
|
91
|
+
export async function formatSymbolSummary(name: string): Promise<string> {
|
|
92
|
+
const detail = await getSymbolDetail(name);
|
|
94
93
|
if (!detail) return `(Symbol '${name}' not found)`;
|
|
95
94
|
|
|
96
95
|
const lines: string[] = [
|
|
@@ -120,9 +119,11 @@ export function formatSymbolSummary(name: string): string {
|
|
|
120
119
|
*
|
|
121
120
|
* @returns The formatted member list as a string.
|
|
122
121
|
*/
|
|
123
|
-
export function formatKeyMembers(
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
export async function formatKeyMembers(
|
|
123
|
+
symbolName: string,
|
|
124
|
+
memberNames?: string[]
|
|
125
|
+
): Promise<string> {
|
|
126
|
+
const result = await getMembers(symbolName);
|
|
126
127
|
if (!result) return `(Symbol '${symbolName}' not found or has no members)`;
|
|
127
128
|
|
|
128
129
|
let {members} = result;
|
package/mcp/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {registerDocResources} from './resources/docs.js';
|
|
|
5
5
|
import {registerDocTools} from './tools/docs.js';
|
|
6
6
|
import {registerTsTools} from './tools/typescript.js';
|
|
7
7
|
import {registerPrompts} from './prompts/index.js';
|
|
8
|
+
import {beginInitialization} from './data/ts-registry.js';
|
|
8
9
|
|
|
9
10
|
const server = new McpServer({
|
|
10
11
|
name: 'hoist-react',
|
|
@@ -20,3 +21,7 @@ const transport = new StdioServerTransport();
|
|
|
20
21
|
await server.connect(transport);
|
|
21
22
|
|
|
22
23
|
log.info('Server started, awaiting MCP client connection via stdio');
|
|
24
|
+
|
|
25
|
+
// Warm the TypeScript symbol and member indexes in the background so the
|
|
26
|
+
// first tool invocation doesn't pay the full initialization cost.
|
|
27
|
+
beginInitialization();
|
package/mcp/tools/typescript.ts
CHANGED
|
@@ -9,11 +9,12 @@ import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
9
9
|
import {z} from 'zod';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
ensureInitialized,
|
|
13
12
|
searchSymbols,
|
|
13
|
+
searchMembers,
|
|
14
14
|
getSymbolDetail,
|
|
15
15
|
getMembers,
|
|
16
|
-
type MemberInfo
|
|
16
|
+
type MemberInfo,
|
|
17
|
+
type MemberIndexEntry
|
|
17
18
|
} from '../data/ts-registry.js';
|
|
18
19
|
import {resolveRepoRoot} from '../util/paths.js';
|
|
19
20
|
|
|
@@ -58,6 +59,24 @@ function formatMember(member: MemberInfo): string {
|
|
|
58
59
|
return lines.join('\n');
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Format a MemberIndexEntry as a readable line for search results.
|
|
64
|
+
* e.g. `1. [accessor] lastLoadCompleted: Date (on HoistModel — base class for all application models)`
|
|
65
|
+
* ` Timestamp of most recent successful load completion`
|
|
66
|
+
*/
|
|
67
|
+
function formatMemberIndexEntry(entry: MemberIndexEntry, index: number): string {
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
const staticPrefix = entry.isStatic ? 'static ' : '';
|
|
70
|
+
const typeStr = truncateType(entry.type);
|
|
71
|
+
lines.push(
|
|
72
|
+
`${index}. [${entry.memberKind}] ${staticPrefix}${entry.name}: ${typeStr} (on ${entry.ownerName} \u2014 ${entry.ownerDescription})`
|
|
73
|
+
);
|
|
74
|
+
if (entry.jsDoc) {
|
|
75
|
+
lines.push(` ${entry.jsDoc}`);
|
|
76
|
+
}
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
/**
|
|
62
81
|
* Register all TypeScript symbol exploration tools on the given MCP server.
|
|
63
82
|
*
|
|
@@ -74,12 +93,12 @@ export function registerTsTools(server: McpServer): void {
|
|
|
74
93
|
{
|
|
75
94
|
title: 'Search Hoist TypeScript Symbols',
|
|
76
95
|
description:
|
|
77
|
-
'Search for TypeScript classes, interfaces, types, and functions across the hoist-react framework by name.
|
|
96
|
+
'Search for TypeScript classes, interfaces, types, and functions across the hoist-react framework by name. Also searches public members (properties, methods, accessors) of key framework classes like HoistModel, GridModel, Store, and others. Returns matching symbols and members with their kind, source, and context.',
|
|
78
97
|
inputSchema: z.object({
|
|
79
98
|
query: z
|
|
80
99
|
.string()
|
|
81
100
|
.describe(
|
|
82
|
-
'Symbol
|
|
101
|
+
'Symbol or member name to search for (e.g. "GridModel", "Store", "lastLoadCompleted", "setSortBy")'
|
|
83
102
|
),
|
|
84
103
|
kind: z
|
|
85
104
|
.enum(['class', 'interface', 'type', 'function', 'const', 'enum'])
|
|
@@ -99,26 +118,47 @@ export function registerTsTools(server: McpServer): void {
|
|
|
99
118
|
}
|
|
100
119
|
},
|
|
101
120
|
async ({query, kind, exported, limit}) => {
|
|
102
|
-
|
|
103
|
-
const
|
|
121
|
+
const symbolLimit = limit ?? 20;
|
|
122
|
+
const symbolResults = await searchSymbols(query, {
|
|
104
123
|
kind,
|
|
105
124
|
exported: exported ?? true,
|
|
106
|
-
limit:
|
|
125
|
+
limit: symbolLimit
|
|
107
126
|
});
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
// Search members with a separate cap; if no symbols match, give
|
|
129
|
+
// members more room so the query is still useful.
|
|
130
|
+
const memberLimit = symbolResults.length === 0 ? symbolLimit : 15;
|
|
131
|
+
const memberResults = await searchMembers(query, {limit: memberLimit});
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
|
|
135
|
+
if (symbolResults.length > 0) {
|
|
136
|
+
lines.push(`Symbols (${symbolResults.length} matches):\n`);
|
|
137
|
+
symbolResults.forEach((result, i) => {
|
|
115
138
|
lines.push(
|
|
116
139
|
`${i + 1}. [${result.kind}] ${result.name} (package: ${result.sourcePackage}, file: ${toRelativePath(result.filePath)}, exported: ${result.isExported ? 'yes' : 'no'})`
|
|
117
140
|
);
|
|
118
141
|
});
|
|
119
|
-
text = lines.join('\n');
|
|
120
142
|
}
|
|
121
143
|
|
|
144
|
+
if (memberResults.length > 0) {
|
|
145
|
+
if (lines.length > 0) lines.push('');
|
|
146
|
+
lines.push(`Members of key classes (${memberResults.length} matches):\n`);
|
|
147
|
+
memberResults.forEach((m, i) => {
|
|
148
|
+
lines.push(formatMemberIndexEntry(m, i + 1));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (lines.length > 0) {
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push('Tip: Use hoist-get-members to see all members of a specific class.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const text =
|
|
158
|
+
lines.length > 0
|
|
159
|
+
? lines.join('\n')
|
|
160
|
+
: `No symbols or members found matching '${query}'. Try a broader search term.`;
|
|
161
|
+
|
|
122
162
|
return {content: [{type: 'text' as const, text}]};
|
|
123
163
|
}
|
|
124
164
|
);
|
|
@@ -151,8 +191,7 @@ export function registerTsTools(server: McpServer): void {
|
|
|
151
191
|
}
|
|
152
192
|
},
|
|
153
193
|
async ({name, filePath}) => {
|
|
154
|
-
|
|
155
|
-
const detail = getSymbolDetail(name, filePath);
|
|
194
|
+
const detail = await getSymbolDetail(name, filePath);
|
|
156
195
|
|
|
157
196
|
let text: string;
|
|
158
197
|
if (!detail) {
|
|
@@ -225,8 +264,7 @@ export function registerTsTools(server: McpServer): void {
|
|
|
225
264
|
}
|
|
226
265
|
},
|
|
227
266
|
async ({name, filePath}) => {
|
|
228
|
-
|
|
229
|
-
const result = getMembers(name, filePath);
|
|
267
|
+
const result = await getMembers(name, filePath);
|
|
230
268
|
|
|
231
269
|
let text: string;
|
|
232
270
|
if (!result) {
|