@expcat/tigercat-mcp 2.0.0-rc.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yizhe Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+
3
+ type TigercatFramework = 'react' | 'vue';
4
+ interface TigercatMcpOptions {
5
+ root?: string;
6
+ }
7
+ interface ReferenceSource {
8
+ path: string;
9
+ reason: string;
10
+ truncated: boolean;
11
+ text?: string;
12
+ }
13
+ interface ComponentReferences {
14
+ componentIndex: string;
15
+ props: string;
16
+ examples: string;
17
+ react: string;
18
+ vue: string;
19
+ }
20
+ interface ComponentMetadata {
21
+ name: string;
22
+ aliases: string[];
23
+ category: string;
24
+ slug: string;
25
+ testGroup: string;
26
+ packageSubpath: string;
27
+ packageTarget: string;
28
+ typeSource: string;
29
+ sourceFiles: string[];
30
+ propsInterfaces: string[];
31
+ frameworks: TigercatFramework[];
32
+ references: ComponentReferences;
33
+ }
34
+ interface TopicMetadata {
35
+ title: string;
36
+ references: string[];
37
+ keywords: string[];
38
+ }
39
+ interface CommandApiMetadata {
40
+ name: string;
41
+ title: string;
42
+ topic: string;
43
+ references: string[];
44
+ }
45
+ interface TigercatContext7 {
46
+ url?: string;
47
+ public_key?: string;
48
+ generated_by?: string;
49
+ component_count?: number;
50
+ reference_paths?: Record<string, unknown>;
51
+ aliases?: Record<string, string | string[]>;
52
+ command_apis?: Record<string, CommandApiMetadata>;
53
+ topics?: Record<string, TopicMetadata>;
54
+ components?: Record<string, ComponentMetadata>;
55
+ component_index?: Record<string, {
56
+ props: string;
57
+ vue: string;
58
+ react: string;
59
+ components: string[];
60
+ examples: string;
61
+ }>;
62
+ }
63
+ interface SkillIndex {
64
+ root: string;
65
+ context7: TigercatContext7;
66
+ components: Map<string, ComponentMetadata>;
67
+ componentsByNormalizedName: Map<string, ComponentMetadata>;
68
+ aliasTargetsByNormalizedName: Map<string, string[]>;
69
+ topics: Map<string, TopicMetadata>;
70
+ allowedReferencePaths: Set<string>;
71
+ }
72
+ interface ComponentRoute {
73
+ component: ComponentMetadata;
74
+ sources: ReferenceSource[];
75
+ }
76
+ interface ComponentLookupResult {
77
+ query: string;
78
+ found: boolean;
79
+ matches: ComponentRoute[];
80
+ candidates: SearchResult[];
81
+ }
82
+ interface SearchResult {
83
+ kind: 'component' | 'alias' | 'category' | 'topic' | 'command';
84
+ name: string;
85
+ score: number;
86
+ reason: string;
87
+ component?: ComponentMetadata;
88
+ components?: ComponentMetadata[];
89
+ topic?: string;
90
+ }
91
+ interface SearchResponse {
92
+ query: string;
93
+ framework?: TigercatFramework;
94
+ results: SearchResult[];
95
+ }
96
+ interface TopicRoute {
97
+ slug: string;
98
+ title: string;
99
+ sources: ReferenceSource[];
100
+ }
101
+ interface TaskRouteResult {
102
+ task: string;
103
+ framework?: TigercatFramework;
104
+ intent: 'component' | 'topic' | 'mixed' | 'unknown';
105
+ matches: ComponentRoute[];
106
+ topics: TopicRoute[];
107
+ candidates: SearchResult[];
108
+ sources: ReferenceSource[];
109
+ }
110
+ interface InventorySummary {
111
+ componentCount: number;
112
+ categories: Array<{
113
+ slug: string;
114
+ name: string;
115
+ count: number;
116
+ }>;
117
+ aliases: Record<string, string[]>;
118
+ topics: Record<string, {
119
+ title: string;
120
+ keywords: string[];
121
+ }>;
122
+ components: ComponentMetadata[];
123
+ }
124
+ interface DoctorResult {
125
+ ok: boolean;
126
+ root: string;
127
+ componentCount: number;
128
+ aliasCount: number;
129
+ topicCount: number;
130
+ readableReferenceCount: number;
131
+ issues: string[];
132
+ }
133
+
134
+ declare function createTigercatMcpServer(options?: TigercatMcpOptions): Server;
135
+
136
+ declare function loadSkillIndex(root?: string): Promise<SkillIndex>;
137
+ declare function diagnoseTigercatMcp(root?: string): Promise<DoctorResult>;
138
+
139
+ interface RouteTaskInput {
140
+ task: string;
141
+ framework?: TigercatFramework;
142
+ maxBytes?: number;
143
+ limit?: number;
144
+ }
145
+ interface ComponentInput {
146
+ component: string;
147
+ framework?: TigercatFramework;
148
+ maxBytes?: number;
149
+ }
150
+ interface SearchInput {
151
+ query: string;
152
+ framework?: TigercatFramework;
153
+ limit?: number;
154
+ }
155
+ declare function searchTigercat(index: SkillIndex, input: SearchInput): Promise<SearchResponse>;
156
+ declare function getTigercatComponent(index: SkillIndex, input: ComponentInput): Promise<ComponentLookupResult>;
157
+ declare function routeTigercatTask(index: SkillIndex, input: RouteTaskInput): Promise<TaskRouteResult>;
158
+ declare function getInventory(index: SkillIndex): InventorySummary;
159
+ declare function getCategoryComponents(index: SkillIndex, slugOrCategory: string): ComponentMetadata[];
160
+
161
+ export { type ComponentLookupResult, type ComponentMetadata, type ComponentRoute, type DoctorResult, type InventorySummary, type ReferenceSource, type SearchResponse, type SearchResult, type SkillIndex, type TaskRouteResult, type TigercatFramework, type TigercatMcpOptions, createTigercatMcpServer, diagnoseTigercatMcp, getCategoryComponents, getInventory, getTigercatComponent, loadSkillIndex, routeTigercatTask, searchTigercat };
package/dist/index.js ADDED
@@ -0,0 +1,896 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import { resolve, join, relative, isAbsolute, dirname } from 'path';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
7
+ import { existsSync } from 'fs';
8
+ import { access, readFile, realpath, readdir } from 'fs/promises';
9
+
10
+ var DEFAULT_MAX_BYTES = 12e3;
11
+ var SKILL_ROOT = "skills/tigercat";
12
+ var REFERENCES_ROOT = "skills/tigercat/references";
13
+ var DEFAULT_COMPONENT_INDEX = "skills/tigercat/references/component-index.md";
14
+ var DEFAULT_REACT_REFERENCE = "skills/tigercat/references/react/index.md";
15
+ var DEFAULT_VUE_REFERENCE = "skills/tigercat/references/vue/index.md";
16
+ async function loadSkillIndex(root) {
17
+ const resolvedRoot = root ? resolve(root) : await findTigercatRoot(process.cwd());
18
+ const contextPath = join(resolvedRoot, "context7.json");
19
+ if (!existsSync(contextPath)) {
20
+ throw new Error(`Missing context7.json under ${resolvedRoot}`);
21
+ }
22
+ const context7 = JSON.parse(await readFile(contextPath, "utf8"));
23
+ const components = buildComponentMap(context7);
24
+ const componentsByNormalizedName = /* @__PURE__ */ new Map();
25
+ const aliasTargetsByNormalizedName = /* @__PURE__ */ new Map();
26
+ for (const [alias, targets] of Object.entries(context7.aliases ?? {})) {
27
+ addAliasTarget(
28
+ aliasTargetsByNormalizedName,
29
+ alias,
30
+ Array.isArray(targets) ? targets : [targets]
31
+ );
32
+ }
33
+ for (const entry of components.values()) {
34
+ componentsByNormalizedName.set(normalizeName(entry.name), entry);
35
+ for (const alias of entry.aliases) {
36
+ addAliasTarget(aliasTargetsByNormalizedName, alias, [entry.name]);
37
+ }
38
+ }
39
+ const allowedReferencePaths = /* @__PURE__ */ new Set();
40
+ for (const path of collectReferencePaths(context7)) {
41
+ allowedReferencePaths.add(normalizeRelativePath(path));
42
+ }
43
+ if (existsSync(join(resolvedRoot, REFERENCES_ROOT))) {
44
+ for (const path of await collectMarkdownReferences(join(resolvedRoot, REFERENCES_ROOT))) {
45
+ allowedReferencePaths.add(normalizeRelativePath(relative(resolvedRoot, path)));
46
+ }
47
+ }
48
+ allowedReferencePaths.add("skills/tigercat/SKILL.md");
49
+ for (const path of allowedReferencePaths) {
50
+ const absolutePath = join(resolvedRoot, path);
51
+ if (!existsSync(absolutePath)) {
52
+ throw new Error(`Missing Tigercat skill reference: ${path}`);
53
+ }
54
+ }
55
+ return {
56
+ root: resolvedRoot,
57
+ context7,
58
+ components,
59
+ componentsByNormalizedName,
60
+ aliasTargetsByNormalizedName,
61
+ topics: new Map(Object.entries(context7.topics ?? {})),
62
+ allowedReferencePaths
63
+ };
64
+ }
65
+ async function diagnoseTigercatMcp(root) {
66
+ const index = await loadSkillIndex(root);
67
+ const issues = [];
68
+ let readableReferenceCount = 0;
69
+ if (index.context7.component_count !== void 0 && index.context7.component_count !== index.components.size) {
70
+ issues.push(
71
+ `context7 component_count ${index.context7.component_count} does not match ${index.components.size}`
72
+ );
73
+ }
74
+ for (const [alias, targets] of index.aliasTargetsByNormalizedName) {
75
+ const missing = targets.filter((target) => !index.components.has(target));
76
+ if (missing.length > 0) {
77
+ issues.push(`alias ${alias} targets missing component(s): ${missing.join(", ")}`);
78
+ }
79
+ }
80
+ for (const path of index.allowedReferencePaths) {
81
+ try {
82
+ await access(join(index.root, path));
83
+ readableReferenceCount += 1;
84
+ } catch {
85
+ issues.push(`reference is not readable: ${path}`);
86
+ }
87
+ }
88
+ return {
89
+ ok: issues.length === 0,
90
+ root: index.root,
91
+ componentCount: index.components.size,
92
+ aliasCount: index.aliasTargetsByNormalizedName.size,
93
+ topicCount: index.topics.size,
94
+ readableReferenceCount,
95
+ issues
96
+ };
97
+ }
98
+ async function readReferenceSource(index, path, reason, maxBytes = DEFAULT_MAX_BYTES) {
99
+ const normalizedPath = normalizeRelativePath(path);
100
+ if (!index.allowedReferencePaths.has(normalizedPath)) {
101
+ throw new Error(`Reference path is not allowed: ${path}`);
102
+ }
103
+ const absolutePath = join(index.root, normalizedPath);
104
+ const rootPath = await realpath(index.root);
105
+ const filePath = await realpath(absolutePath);
106
+ const relativePath = relative(rootPath, filePath);
107
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
108
+ throw new Error(`Reference path escapes the Tigercat repo: ${path}`);
109
+ }
110
+ const text = await readFile(filePath, "utf8");
111
+ const limit = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.floor(maxBytes) : DEFAULT_MAX_BYTES;
112
+ const truncated = Buffer.byteLength(text, "utf8") > limit;
113
+ return {
114
+ path: normalizedPath,
115
+ reason,
116
+ truncated,
117
+ text: truncateUtf8(text, limit)
118
+ };
119
+ }
120
+ async function findTigercatRoot(startDirectory) {
121
+ let current = resolve(startDirectory);
122
+ while (true) {
123
+ if (existsSync(join(current, "context7.json")) && existsSync(join(current, SKILL_ROOT))) {
124
+ return current;
125
+ }
126
+ const next = dirname(current);
127
+ if (next === current) {
128
+ throw new Error(`Could not find Tigercat repo root from ${startDirectory}`);
129
+ }
130
+ current = next;
131
+ }
132
+ }
133
+ function normalizeName(value) {
134
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
135
+ }
136
+ function normalizeRelativePath(path) {
137
+ const normalized = path.replaceAll("\\", "/").replace(/^\.?\//, "");
138
+ const parts = normalized.split("/").filter(Boolean);
139
+ if (parts.some((part) => part === "..")) {
140
+ throw new Error(`Reference path may not contain parent segments: ${path}`);
141
+ }
142
+ return parts.join("/");
143
+ }
144
+ function buildComponentMap(context7) {
145
+ const components = /* @__PURE__ */ new Map();
146
+ for (const entry of Object.values(context7.components ?? {})) {
147
+ components.set(entry.name, {
148
+ ...entry,
149
+ aliases: entry.aliases ?? [],
150
+ sourceFiles: entry.sourceFiles ?? [],
151
+ propsInterfaces: entry.propsInterfaces ?? [],
152
+ frameworks: entry.frameworks ?? ["react", "vue"]
153
+ });
154
+ }
155
+ if (components.size > 0) return components;
156
+ for (const [slug, entry] of Object.entries(context7.component_index ?? {})) {
157
+ for (const component of entry.components ?? []) {
158
+ components.set(component, {
159
+ name: component,
160
+ aliases: [],
161
+ category: toCategoryName(slug),
162
+ slug,
163
+ testGroup: slug,
164
+ packageSubpath: `./${component}`,
165
+ packageTarget: component,
166
+ typeSource: "unknown",
167
+ sourceFiles: [],
168
+ propsInterfaces: [],
169
+ frameworks: ["react", "vue"],
170
+ references: {
171
+ componentIndex: DEFAULT_COMPONENT_INDEX,
172
+ props: entry.props,
173
+ examples: entry.examples,
174
+ react: entry.react || DEFAULT_REACT_REFERENCE,
175
+ vue: entry.vue || DEFAULT_VUE_REFERENCE
176
+ }
177
+ });
178
+ }
179
+ }
180
+ return components;
181
+ }
182
+ function collectReferencePaths(value) {
183
+ if (typeof value === "string") {
184
+ return value.startsWith("skills/tigercat/") ? [value] : [];
185
+ }
186
+ if (!value || typeof value !== "object") return [];
187
+ if (Array.isArray(value)) return value.flatMap((item) => collectReferencePaths(item));
188
+ return Object.values(value).flatMap((item) => collectReferencePaths(item));
189
+ }
190
+ function addAliasTarget(targetsByAlias, alias, targets) {
191
+ const normalizedAlias = normalizeName(alias);
192
+ if (!normalizedAlias) return;
193
+ const existing = targetsByAlias.get(normalizedAlias) ?? [];
194
+ targetsByAlias.set(normalizedAlias, [.../* @__PURE__ */ new Set([...existing, ...targets])]);
195
+ }
196
+ async function collectMarkdownReferences(directory) {
197
+ const entries = await readdir(directory, { withFileTypes: true });
198
+ const paths = [];
199
+ for (const entry of entries) {
200
+ const path = join(directory, entry.name);
201
+ if (entry.isDirectory()) {
202
+ paths.push(...await collectMarkdownReferences(path));
203
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
204
+ paths.push(path);
205
+ }
206
+ }
207
+ return paths;
208
+ }
209
+ function truncateUtf8(text, maxBytes) {
210
+ const bytes = Buffer.from(text, "utf8");
211
+ if (bytes.byteLength <= maxBytes) return text;
212
+ return bytes.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "");
213
+ }
214
+ function toCategoryName(slug) {
215
+ return slug.charAt(0).toUpperCase() + slug.slice(1);
216
+ }
217
+
218
+ // src/router.ts
219
+ var SKILL_INDEX = "skills/tigercat/SKILL.md";
220
+ var SHARED_PATTERNS = "skills/tigercat/references/shared/patterns/common.md";
221
+ var SHARED_GLOSSARY = "skills/tigercat/references/shared/glossary.md";
222
+ var DEFAULT_LIMIT = 8;
223
+ async function searchTigercat(index, input) {
224
+ const query = input.query?.trim();
225
+ if (!query) throw new Error("tigercat_search requires a non-empty query");
226
+ return {
227
+ query,
228
+ framework: input.framework,
229
+ results: findSearchResults(index, query, input.limit)
230
+ };
231
+ }
232
+ async function getTigercatComponent(index, input) {
233
+ const query = input.component?.trim();
234
+ if (!query) throw new Error("tigercat_component requires a non-empty component");
235
+ const components = resolveComponentQuery(index, query);
236
+ if (components.length === 0) {
237
+ return {
238
+ query,
239
+ found: false,
240
+ matches: [],
241
+ candidates: findSearchResults(index, query, DEFAULT_LIMIT).filter(
242
+ (result) => result.kind === "component" || result.kind === "alias"
243
+ )
244
+ };
245
+ }
246
+ return {
247
+ query,
248
+ found: true,
249
+ matches: await Promise.all(
250
+ components.map((entry) => createComponentRoute(index, entry, input.framework, input.maxBytes))
251
+ ),
252
+ candidates: []
253
+ };
254
+ }
255
+ async function routeTigercatTask(index, input) {
256
+ const task = input.task?.trim();
257
+ if (!task) throw new Error("tigercat_route requires a non-empty task");
258
+ const mentionedComponents = findMentionedComponents(index, task);
259
+ const topicMatches = findTopicMatches(index, task);
260
+ const matches = await Promise.all(
261
+ mentionedComponents.map(
262
+ (entry) => createComponentRoute(index, entry, input.framework, input.maxBytes)
263
+ )
264
+ );
265
+ const topics = await Promise.all(
266
+ topicMatches.map((topic) => createTopicRoute(index, topic.slug, input.maxBytes))
267
+ );
268
+ const sources = mergeSources([
269
+ ...matches.flatMap((match) => match.sources),
270
+ ...topics.flatMap((topic) => topic.sources)
271
+ ]);
272
+ if (matches.length > 0 || topics.length > 0) {
273
+ return {
274
+ task,
275
+ framework: input.framework,
276
+ intent: matches.length > 0 && topics.length > 0 ? "mixed" : matches.length > 0 ? "component" : "topic",
277
+ matches,
278
+ topics,
279
+ candidates: [],
280
+ sources
281
+ };
282
+ }
283
+ return {
284
+ task,
285
+ framework: input.framework,
286
+ intent: "unknown",
287
+ matches: [],
288
+ topics: [],
289
+ candidates: findSearchResults(index, task, input.limit),
290
+ sources: [
291
+ await readReferenceSource(
292
+ index,
293
+ SKILL_INDEX,
294
+ "Top-level skill route index for choosing the next reference.",
295
+ input.maxBytes
296
+ )
297
+ ]
298
+ };
299
+ }
300
+ async function createComponentRoute(index, entry, framework, maxBytes) {
301
+ const sourceSpecs = [
302
+ {
303
+ path: entry.references.componentIndex,
304
+ reason: "Canonical generated component inventory and package subpath map."
305
+ },
306
+ {
307
+ path: entry.references.props,
308
+ reason: `${entry.name} props, events, methods, and type source.`
309
+ },
310
+ { path: entry.references.examples, reason: `${entry.name} compact Vue/React example routes.` },
311
+ ...framework ? [
312
+ {
313
+ path: entry.references[framework],
314
+ reason: `${framework} binding and import notes.`
315
+ }
316
+ ] : [
317
+ { path: entry.references.react, reason: "React binding and import notes." },
318
+ { path: entry.references.vue, reason: "Vue binding and import notes." }
319
+ ],
320
+ { path: SHARED_PATTERNS, reason: "Cross-framework binding differences and common patterns." },
321
+ { path: SHARED_GLOSSARY, reason: "Shared Tigercat terminology." }
322
+ ];
323
+ const sources = await Promise.all(
324
+ dedupeByPath(sourceSpecs).map(
325
+ (spec) => readReferenceSource(index, spec.path, spec.reason, maxBytes)
326
+ )
327
+ );
328
+ return {
329
+ component: entry,
330
+ sources
331
+ };
332
+ }
333
+ async function createTopicRoute(index, slug, maxBytes) {
334
+ const topic = index.topics.get(slug);
335
+ if (!topic) throw new Error(`Unknown Tigercat topic: ${slug}`);
336
+ const sources = await Promise.all(
337
+ dedupeByPath([
338
+ { path: SKILL_INDEX, reason: "Top-level skill route index." },
339
+ ...topic.references.map((path) => ({
340
+ path,
341
+ reason: `${topic.title} reference.`
342
+ }))
343
+ ]).map((spec) => readReferenceSource(index, spec.path, spec.reason, maxBytes))
344
+ );
345
+ return {
346
+ slug,
347
+ title: topic.title,
348
+ sources
349
+ };
350
+ }
351
+ function getInventory(index) {
352
+ const categories = /* @__PURE__ */ new Map();
353
+ for (const component of index.components.values()) {
354
+ const current = categories.get(component.slug) ?? {
355
+ slug: component.slug,
356
+ name: component.category,
357
+ count: 0
358
+ };
359
+ current.count += 1;
360
+ categories.set(component.slug, current);
361
+ }
362
+ return {
363
+ componentCount: index.components.size,
364
+ categories: [...categories.values()].sort((a, b) => a.slug.localeCompare(b.slug)),
365
+ aliases: Object.fromEntries(
366
+ [...index.aliasTargetsByNormalizedName.entries()].map(([alias, targets]) => [alias, targets])
367
+ ),
368
+ topics: Object.fromEntries(
369
+ [...index.topics.entries()].map(([slug, topic]) => [
370
+ slug,
371
+ { title: topic.title, keywords: topic.keywords }
372
+ ])
373
+ ),
374
+ components: [...index.components.values()]
375
+ };
376
+ }
377
+ function getCategoryComponents(index, slugOrCategory) {
378
+ const normalized = normalizeName(slugOrCategory);
379
+ return [...index.components.values()].filter(
380
+ (component) => normalizeName(component.slug) === normalized || normalizeName(component.category) === normalized
381
+ ).sort((a, b) => a.name.localeCompare(b.name));
382
+ }
383
+ function resolveComponentQuery(index, query) {
384
+ const normalizedQuery = normalizeName(query);
385
+ const exact = index.componentsByNormalizedName.get(normalizedQuery);
386
+ if (exact) return [exact];
387
+ const aliasTargets = index.aliasTargetsByNormalizedName.get(normalizedQuery);
388
+ if (aliasTargets) return resolveTargetNames(index, aliasTargets);
389
+ const tokens = query.split(/[^A-Za-z0-9]+/).map((token) => normalizeName(token)).filter(Boolean);
390
+ const matches = tokens.flatMap((token) => {
391
+ const tokenExact = index.componentsByNormalizedName.get(token);
392
+ if (tokenExact) return [tokenExact];
393
+ const tokenAlias = index.aliasTargetsByNormalizedName.get(token);
394
+ return tokenAlias ? resolveTargetNames(index, tokenAlias) : [];
395
+ });
396
+ return uniqueComponents(matches);
397
+ }
398
+ function findMentionedComponents(index, task) {
399
+ const normalizedTask = normalizeName(task);
400
+ const matches = [];
401
+ for (const [alias, targets] of index.aliasTargetsByNormalizedName) {
402
+ if (!normalizedTask.includes(alias)) continue;
403
+ matches.push(...resolveTargetNames(index, targets));
404
+ }
405
+ for (const [normalizedName, component] of index.componentsByNormalizedName) {
406
+ if (normalizedTask.includes(normalizedName)) matches.push(component);
407
+ }
408
+ return uniqueComponents(matches).sort((a, b) => b.name.length - a.name.length);
409
+ }
410
+ function findTopicMatches(index, task) {
411
+ const normalizedTask = normalizeName(task);
412
+ const matches = [];
413
+ for (const [slug, topic] of index.topics) {
414
+ let score = 0;
415
+ if (normalizedTask.includes(normalizeName(slug))) score += 6;
416
+ for (const keyword of topic.keywords) {
417
+ if (keywordMatches(task, normalizedTask, keyword)) score += 4;
418
+ }
419
+ if (score > 0) matches.push({ slug, score });
420
+ }
421
+ return matches.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
422
+ }
423
+ function findSearchResults(index, query, limit = DEFAULT_LIMIT) {
424
+ const normalizedQuery = normalizeName(query);
425
+ if (!normalizedQuery) return [];
426
+ const results = [];
427
+ for (const component of index.components.values()) {
428
+ const normalizedName = normalizeName(component.name);
429
+ let score = 0;
430
+ if (normalizedName === normalizedQuery) score += 100;
431
+ if (normalizedName.startsWith(normalizedQuery)) score += 70;
432
+ if (normalizedName.includes(normalizedQuery) || normalizedQuery.includes(normalizedName)) {
433
+ score += 45;
434
+ }
435
+ if (normalizeName(component.category) === normalizedQuery || component.slug === normalizedQuery) {
436
+ score += 35;
437
+ }
438
+ if (component.aliases.some((alias) => normalizeName(alias).includes(normalizedQuery))) {
439
+ score += 35;
440
+ }
441
+ if (score > 0) {
442
+ results.push({
443
+ kind: "component",
444
+ name: component.name,
445
+ score,
446
+ reason: `${component.category} component`,
447
+ component
448
+ });
449
+ }
450
+ }
451
+ for (const [alias, targets] of index.aliasTargetsByNormalizedName) {
452
+ if (!alias.includes(normalizedQuery) && !normalizedQuery.includes(alias)) continue;
453
+ const components = resolveTargetNames(index, targets);
454
+ results.push({
455
+ kind: "alias",
456
+ name: alias,
457
+ score: alias === normalizedQuery ? 95 : 55,
458
+ reason: `Alias for ${targets.join(", ")}`,
459
+ components
460
+ });
461
+ }
462
+ for (const category of getInventory(index).categories) {
463
+ const normalizedCategory = normalizeName(category.name);
464
+ if (!normalizedCategory.includes(normalizedQuery) && !category.slug.includes(normalizedQuery)) {
465
+ continue;
466
+ }
467
+ results.push({
468
+ kind: "category",
469
+ name: category.slug,
470
+ score: category.slug === normalizedQuery ? 80 : 40,
471
+ reason: `${category.count} ${category.name} components`,
472
+ components: getCategoryComponents(index, category.slug)
473
+ });
474
+ }
475
+ for (const topic of findTopicMatches(index, query)) {
476
+ const metadata = index.topics.get(topic.slug);
477
+ if (!metadata) continue;
478
+ results.push({
479
+ kind: topic.slug === "commandApis" ? "command" : "topic",
480
+ name: topic.slug,
481
+ score: topic.score,
482
+ reason: metadata.title,
483
+ topic: topic.slug
484
+ });
485
+ }
486
+ return results.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, Math.max(1, Math.floor(limit)));
487
+ }
488
+ function resolveTargetNames(index, names) {
489
+ const components = names.flatMap((name) => {
490
+ const component = index.components.get(name);
491
+ return component ? [component] : [];
492
+ });
493
+ return uniqueComponents(components);
494
+ }
495
+ function uniqueComponents(components) {
496
+ const seen = /* @__PURE__ */ new Set();
497
+ const result = [];
498
+ for (const component of components) {
499
+ if (seen.has(component.name)) continue;
500
+ seen.add(component.name);
501
+ result.push(component);
502
+ }
503
+ return result;
504
+ }
505
+ function dedupeByPath(items) {
506
+ const seen = /* @__PURE__ */ new Set();
507
+ const result = [];
508
+ for (const item of items) {
509
+ if (seen.has(item.path)) continue;
510
+ seen.add(item.path);
511
+ result.push(item);
512
+ }
513
+ return result;
514
+ }
515
+ function mergeSources(sources) {
516
+ return dedupeByPath(sources);
517
+ }
518
+ function keywordMatches(task, normalizedTask, keyword) {
519
+ const normalizedKeyword = normalizeName(keyword);
520
+ if (normalizedKeyword) return normalizedTask.includes(normalizedKeyword);
521
+ return task.toLowerCase().includes(keyword.toLowerCase());
522
+ }
523
+
524
+ // src/server.ts
525
+ var JSON_MIME = "application/json";
526
+ var MARKDOWN_MIME = "text/markdown";
527
+ function createTigercatMcpServer(options = {}) {
528
+ const server = new Server(
529
+ {
530
+ name: "@expcat/tigercat-mcp",
531
+ version: "2.0.0-rc.1"
532
+ },
533
+ {
534
+ capabilities: {
535
+ tools: {},
536
+ resources: {},
537
+ prompts: {}
538
+ }
539
+ }
540
+ );
541
+ let indexPromise;
542
+ const getIndex = () => {
543
+ indexPromise ??= loadSkillIndex(options.root);
544
+ return indexPromise;
545
+ };
546
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
547
+ tools: [
548
+ {
549
+ name: "tigercat_search",
550
+ description: "Search Tigercat components, aliases, categories, topics, and command APIs.",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {
554
+ query: {
555
+ type: "string",
556
+ description: "Component, alias, category, topic, or use case."
557
+ },
558
+ framework: { type: "string", enum: ["react", "vue"] },
559
+ limit: { type: "number", minimum: 1, maximum: 30 }
560
+ },
561
+ required: ["query"],
562
+ additionalProperties: false
563
+ }
564
+ },
565
+ {
566
+ name: "tigercat_component",
567
+ description: "Return exact Tigercat component metadata, import paths, docs, examples, and framework notes.",
568
+ inputSchema: {
569
+ type: "object",
570
+ properties: {
571
+ component: {
572
+ type: "string",
573
+ description: "Component name or alias, such as Button or Grid."
574
+ },
575
+ framework: { type: "string", enum: ["react", "vue"] },
576
+ maxBytes: { type: "number", minimum: 200, maximum: 5e4 }
577
+ },
578
+ required: ["component"],
579
+ additionalProperties: false
580
+ }
581
+ },
582
+ {
583
+ name: "tigercat_route",
584
+ description: "Route a natural-language Tigercat task to the smallest useful component/topic references.",
585
+ inputSchema: {
586
+ type: "object",
587
+ properties: {
588
+ task: { type: "string", description: "Natural-language Tigercat task." },
589
+ framework: { type: "string", enum: ["react", "vue"] },
590
+ maxBytes: { type: "number", minimum: 200, maximum: 5e4 },
591
+ limit: { type: "number", minimum: 1, maximum: 30 }
592
+ },
593
+ required: ["task"],
594
+ additionalProperties: false
595
+ }
596
+ },
597
+ {
598
+ name: "tigercat_reference",
599
+ description: "Read an allow-listed Tigercat skill reference with optional byte truncation.",
600
+ inputSchema: {
601
+ type: "object",
602
+ properties: {
603
+ path: {
604
+ type: "string",
605
+ description: "Repo-relative skill reference path under skills/tigercat."
606
+ },
607
+ maxBytes: { type: "number", minimum: 200, maximum: 5e4 }
608
+ },
609
+ required: ["path"],
610
+ additionalProperties: false
611
+ }
612
+ }
613
+ ]
614
+ }));
615
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
616
+ const index = await getIndex();
617
+ const args = request.params.arguments ?? {};
618
+ try {
619
+ if (request.params.name === "tigercat_search") {
620
+ return jsonContent(
621
+ await searchTigercat(index, {
622
+ query: stringArg(args.query),
623
+ framework: frameworkArg(args.framework),
624
+ limit: numberArg(args.limit)
625
+ })
626
+ );
627
+ }
628
+ if (request.params.name === "tigercat_component") {
629
+ return jsonContent(
630
+ await getTigercatComponent(index, {
631
+ component: stringArg(args.component),
632
+ framework: frameworkArg(args.framework),
633
+ maxBytes: numberArg(args.maxBytes)
634
+ })
635
+ );
636
+ }
637
+ if (request.params.name === "tigercat_route") {
638
+ return jsonContent(
639
+ await routeTigercatTask(index, {
640
+ task: stringArg(args.task),
641
+ framework: frameworkArg(args.framework),
642
+ maxBytes: numberArg(args.maxBytes),
643
+ limit: numberArg(args.limit)
644
+ })
645
+ );
646
+ }
647
+ if (request.params.name === "tigercat_reference") {
648
+ return jsonContent(
649
+ await readReferenceSource(
650
+ index,
651
+ stringArg(args.path),
652
+ "Direct allow-listed skill reference read.",
653
+ numberArg(args.maxBytes)
654
+ )
655
+ );
656
+ }
657
+ throw new Error(`Unknown Tigercat MCP tool: ${request.params.name}`);
658
+ } catch (error) {
659
+ return jsonContent({ error: formatError(error) }, true);
660
+ }
661
+ });
662
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
663
+ resources: [
664
+ {
665
+ uri: "tigercat://inventory",
666
+ name: "Tigercat inventory",
667
+ description: "Generated component, alias, category, and topic inventory.",
668
+ mimeType: JSON_MIME
669
+ }
670
+ ]
671
+ }));
672
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
673
+ resourceTemplates: [
674
+ {
675
+ uriTemplate: "tigercat://component/{name}",
676
+ name: "Tigercat component bundle",
677
+ description: "Component metadata plus props/examples/framework reference bundle.",
678
+ mimeType: JSON_MIME
679
+ },
680
+ {
681
+ uriTemplate: "tigercat://category/{slug}",
682
+ name: "Tigercat category inventory",
683
+ description: "Components in a generated component category.",
684
+ mimeType: JSON_MIME
685
+ },
686
+ {
687
+ uriTemplate: "tigercat://topic/{topic}",
688
+ name: "Tigercat topic bundle",
689
+ description: "Hand-written topic route plus reference snippets.",
690
+ mimeType: JSON_MIME
691
+ },
692
+ {
693
+ uriTemplate: "tigercat://reference/{path}",
694
+ name: "Tigercat skill reference",
695
+ description: "Allow-listed skill reference content.",
696
+ mimeType: MARKDOWN_MIME
697
+ }
698
+ ]
699
+ }));
700
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
701
+ const index = await getIndex();
702
+ const uri = request.params.uri;
703
+ if (uri === "tigercat://inventory") {
704
+ return resourceText(uri, JSON.stringify(getInventory(index), null, 2), JSON_MIME);
705
+ }
706
+ if (uri.startsWith("tigercat://component/")) {
707
+ const component = decodeURIComponent(uri.slice("tigercat://component/".length));
708
+ const lookup = await getTigercatComponent(index, { component });
709
+ return resourceText(uri, JSON.stringify(lookup, null, 2), JSON_MIME);
710
+ }
711
+ if (uri.startsWith("tigercat://category/")) {
712
+ const category = decodeURIComponent(uri.slice("tigercat://category/".length));
713
+ const components = getCategoryComponents(index, category);
714
+ return resourceText(uri, JSON.stringify({ category, components }, null, 2), JSON_MIME);
715
+ }
716
+ if (uri.startsWith("tigercat://topic/")) {
717
+ const topic = decodeURIComponent(uri.slice("tigercat://topic/".length));
718
+ const route = await createTopicRoute(index, topic);
719
+ return resourceText(uri, JSON.stringify(route, null, 2), JSON_MIME);
720
+ }
721
+ if (uri.startsWith("tigercat://reference/")) {
722
+ const path = decodeURIComponent(uri.slice("tigercat://reference/".length));
723
+ const source = await readReferenceSource(index, path, "Direct resource reference read.");
724
+ return resourceText(uri, source.text ?? "", MARKDOWN_MIME);
725
+ }
726
+ throw new Error(`Unknown Tigercat MCP resource: ${uri}`);
727
+ });
728
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
729
+ prompts: [
730
+ {
731
+ name: "tigercat-usage",
732
+ description: "Guide an LLM to route, read, and answer a Tigercat usage task.",
733
+ arguments: [
734
+ {
735
+ name: "task",
736
+ description: "Usage, migration, implementation, or debugging task.",
737
+ required: true
738
+ },
739
+ {
740
+ name: "framework",
741
+ description: "Target framework: react or vue.",
742
+ required: false
743
+ }
744
+ ]
745
+ }
746
+ ]
747
+ }));
748
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
749
+ if (request.params.name !== "tigercat-usage") {
750
+ throw new Error(`Unknown Tigercat MCP prompt: ${request.params.name}`);
751
+ }
752
+ const args = request.params.arguments ?? {};
753
+ const task = stringArg(args.task);
754
+ const framework = optionalStringArg(args.framework) ?? "react or vue";
755
+ return {
756
+ description: "Route Tigercat usage references before answering.",
757
+ messages: [
758
+ {
759
+ role: "user",
760
+ content: {
761
+ type: "text",
762
+ text: [
763
+ `Use tigercat_route with framework "${framework}" before answering.`,
764
+ "Use tigercat_component for exact component imports and props when route results mention components.",
765
+ "Read only the returned sources needed for the task.",
766
+ `Task: ${task}`,
767
+ "Answer with exact import paths, key props/events, and React/Vue binding differences."
768
+ ].join("\n")
769
+ }
770
+ }
771
+ ]
772
+ };
773
+ });
774
+ return server;
775
+ }
776
+ function jsonContent(value, isError = false) {
777
+ return {
778
+ isError,
779
+ content: [
780
+ {
781
+ type: "text",
782
+ text: JSON.stringify(value, null, 2)
783
+ }
784
+ ]
785
+ };
786
+ }
787
+ function resourceText(uri, text, mimeType) {
788
+ return {
789
+ contents: [
790
+ {
791
+ uri,
792
+ mimeType,
793
+ text
794
+ }
795
+ ]
796
+ };
797
+ }
798
+ function stringArg(value) {
799
+ if (typeof value !== "string") {
800
+ throw new Error("Expected string argument");
801
+ }
802
+ return value;
803
+ }
804
+ function optionalStringArg(value) {
805
+ if (value === void 0) return void 0;
806
+ return stringArg(value);
807
+ }
808
+ function frameworkArg(value) {
809
+ if (value === void 0) return void 0;
810
+ if (value === "react" || value === "vue") return value;
811
+ throw new Error("framework must be react or vue");
812
+ }
813
+ function numberArg(value) {
814
+ if (value === void 0) return void 0;
815
+ if (typeof value === "number") return value;
816
+ throw new Error("Expected number argument");
817
+ }
818
+ function formatError(error) {
819
+ return error instanceof Error ? error.message : String(error);
820
+ }
821
+
822
+ // src/index.ts
823
+ async function main() {
824
+ const options = parseArgs(process.argv.slice(2));
825
+ if (options.help) {
826
+ console.log(helpText());
827
+ return;
828
+ }
829
+ if (options.doctor) {
830
+ const result = await diagnoseTigercatMcp(options.root);
831
+ console.log(
832
+ [
833
+ `Tigercat MCP doctor: ${result.ok ? "ok" : "failed"}`,
834
+ `root: ${result.root}`,
835
+ `components: ${result.componentCount}`,
836
+ `aliases: ${result.aliasCount}`,
837
+ `topics: ${result.topicCount}`,
838
+ `readable references: ${result.readableReferenceCount}`,
839
+ ...result.issues.map((issue) => `- ${issue}`)
840
+ ].join("\n")
841
+ );
842
+ if (!result.ok) process.exitCode = 1;
843
+ return;
844
+ }
845
+ const server = createTigercatMcpServer({ root: options.root });
846
+ const transport = new StdioServerTransport();
847
+ await server.connect(transport);
848
+ }
849
+ function parseArgs(args) {
850
+ let root;
851
+ let help = false;
852
+ let doctor = false;
853
+ for (let index = 0; index < args.length; index++) {
854
+ const arg = args[index];
855
+ if (arg === "--help" || arg === "-h") {
856
+ help = true;
857
+ continue;
858
+ }
859
+ if (arg === "--doctor") {
860
+ doctor = true;
861
+ continue;
862
+ }
863
+ if (arg === "--root") {
864
+ const value = args[index + 1];
865
+ if (!value) {
866
+ throw new Error("Usage: tigercat-mcp --root <repo-root>");
867
+ }
868
+ root = resolve(value);
869
+ index++;
870
+ continue;
871
+ }
872
+ throw new Error(`Unknown argument: ${arg}`);
873
+ }
874
+ return { root, help, doctor };
875
+ }
876
+ function helpText() {
877
+ return [
878
+ "Usage: tigercat-mcp [--root <repo-root>] [--doctor]",
879
+ "",
880
+ "Runs the Tigercat skill MCP server over stdio.",
881
+ "--doctor validates the generated inventory and exits without starting stdio.",
882
+ "If --root is omitted, the server searches upward from the current directory for context7.json.",
883
+ "",
884
+ "Example MCP client command:",
885
+ " tigercat-mcp --root /path/to/Tigercat"
886
+ ].join("\n");
887
+ }
888
+ var isDirectRun = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
889
+ if (isDirectRun) {
890
+ main().catch((error) => {
891
+ console.error(error instanceof Error ? error.message : String(error));
892
+ process.exitCode = 1;
893
+ });
894
+ }
895
+
896
+ export { createTigercatMcpServer, diagnoseTigercatMcp, getCategoryComponents, getInventory, getTigercatComponent, loadSkillIndex, routeTigercatTask, searchTigercat };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@expcat/tigercat-mcp",
3
+ "version": "2.0.0-rc.2",
4
+ "type": "module",
5
+ "description": "Local MCP server for routing LLMs to Tigercat skill references",
6
+ "license": "MIT",
7
+ "author": "Yizhe Wang",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/expcat/Tigercat",
11
+ "directory": "packages/mcp"
12
+ },
13
+ "homepage": "https://github.com/expcat/Tigercat#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/expcat/Tigercat/issues"
16
+ },
17
+ "keywords": [
18
+ "tigercat",
19
+ "mcp",
20
+ "llm",
21
+ "skill",
22
+ "docs"
23
+ ],
24
+ "bin": {
25
+ "tigercat-mcp": "./dist/index.js"
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.13.0"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^26.1.1",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^6.0.3"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup --config tsup.config.ts",
49
+ "dev": "tsup --config tsup.config.ts --watch",
50
+ "clean": "node ../../scripts/rimraf.mjs dist"
51
+ }
52
+ }