@expcat/tigercat-mcp 2.0.4 → 2.0.19

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/dist/index.d.ts CHANGED
@@ -2,12 +2,30 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
 
3
3
  type TigercatFramework = 'react' | 'vue';
4
4
  interface TigercatMcpOptions {
5
+ /** 本地 Tigercat 仓库根目录;提供时优先于 baseUrl,走文件系统读取。 */
5
6
  root?: string;
7
+ /** 远程 skills 基地址(GitHub Pages /mcp/ 路由或其镜像);缺省用官方地址。 */
8
+ baseUrl?: string;
9
+ /** 远程 fetch 超时毫秒数。 */
10
+ timeoutMs?: number;
11
+ }
12
+ type LoadSkillIndexOptions = TigercatMcpOptions;
13
+ interface SkillSource {
14
+ readonly kind: 'fs' | 'http';
15
+ /** fs: 绝对仓库根目录;http: 规范化(以 / 结尾)的 base URL。 */
16
+ readonly origin: string;
17
+ /** 读取 origin 下 repo-relative 路径的 UTF-8 文本;path 需已经 normalizeRelativePath。 */
18
+ readText(path: string): Promise<string>;
19
+ /** doctor 可读性探测,不可读时抛错。 */
20
+ probe(path: string): Promise<void>;
6
21
  }
7
22
  interface ReferenceSource {
8
23
  path: string;
9
24
  reason: string;
10
25
  truncated: boolean;
26
+ /** 抽取的 `## <section>` 小节名;整文件读取时缺省。 */
27
+ section?: string;
28
+ /** 内联正文;指针型 source(建议每会话经 tigercat_reference 另读一次)不带 text。 */
11
29
  text?: string;
12
30
  }
13
31
  interface ComponentReferences {
@@ -48,6 +66,8 @@ interface TigercatContext7 {
48
66
  generated_by?: string;
49
67
  component_count?: number;
50
68
  reference_paths?: Record<string, unknown>;
69
+ /** 全部 skill markdown 的仓库相对路径清单(远程模式的 allow-list 契约)。 */
70
+ skill_files?: string[];
51
71
  aliases?: Record<string, string | string[]>;
52
72
  command_apis?: Record<string, CommandApiMetadata>;
53
73
  topics?: Record<string, TopicMetadata>;
@@ -61,7 +81,9 @@ interface TigercatContext7 {
61
81
  }>;
62
82
  }
63
83
  interface SkillIndex {
84
+ /** fs: 绝对仓库根目录;http: base URL。与 source.origin 一致,兼容既有读者。 */
64
85
  root: string;
86
+ source: SkillSource;
65
87
  context7: TigercatContext7;
66
88
  components: Map<string, ComponentMetadata>;
67
89
  componentsByNormalizedName: Map<string, ComponentMetadata>;
@@ -124,17 +146,22 @@ interface InventorySummary {
124
146
  interface DoctorResult {
125
147
  ok: boolean;
126
148
  root: string;
149
+ mode: 'fs' | 'http';
127
150
  componentCount: number;
128
151
  aliasCount: number;
129
152
  topicCount: number;
130
153
  readableReferenceCount: number;
154
+ /** http 模式下 best-effort 读取的远程 version.json 版本。 */
155
+ remoteVersion?: string;
131
156
  issues: string[];
132
157
  }
133
158
 
134
159
  declare function createTigercatMcpServer(options?: TigercatMcpOptions): Server;
135
160
 
136
- declare function loadSkillIndex(root?: string): Promise<SkillIndex>;
137
- declare function diagnoseTigercatMcp(root?: string): Promise<DoctorResult>;
161
+ declare function loadSkillIndex(options?: string | LoadSkillIndexOptions): Promise<SkillIndex>;
162
+ declare function diagnoseTigercatMcp(options?: string | LoadSkillIndexOptions): Promise<DoctorResult>;
163
+
164
+ declare const DEFAULT_REMOTE_BASE_URL = "https://expcat.github.io/Tigercat/mcp/";
138
165
 
139
166
  interface RouteTaskInput {
140
167
  task: string;
@@ -158,4 +185,4 @@ declare function routeTigercatTask(index: SkillIndex, input: RouteTaskInput): Pr
158
185
  declare function getInventory(index: SkillIndex): InventorySummary;
159
186
  declare function getCategoryComponents(index: SkillIndex, slugOrCategory: string): ComponentMetadata[];
160
187
 
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 };
188
+ export { type ComponentLookupResult, type ComponentMetadata, type ComponentRoute, DEFAULT_REMOTE_BASE_URL, type DoctorResult, type InventorySummary, type LoadSkillIndexOptions, type ReferenceSource, type SearchResponse, type SearchResult, type SkillIndex, type SkillSource, type TaskRouteResult, type TigercatFramework, type TigercatMcpOptions, createTigercatMcpServer, diagnoseTigercatMcp, getCategoryComponents, getInventory, getTigercatComponent, loadSkillIndex, routeTigercatTask, searchTigercat };
package/dist/index.js CHANGED
@@ -1,25 +1,178 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from 'url';
3
- import { realpathSync, existsSync } from 'fs';
4
- import { resolve, join, relative, isAbsolute, dirname } from 'path';
3
+ import { readFileSync, realpathSync, existsSync } from 'fs';
4
+ import { resolve, join, relative, isAbsolute } from 'path';
5
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
6
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
7
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8
- import { access, readFile, realpath, readdir } from 'fs/promises';
8
+ import { readFile, readdir, access, realpath } from 'fs/promises';
9
9
 
10
+ var PACKAGE_VERSION = (() => {
11
+ try {
12
+ const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
13
+ const version = JSON.parse(raw).version;
14
+ return typeof version === "string" && version ? version : "0.0.0";
15
+ } catch {
16
+ return "0.0.0";
17
+ }
18
+ })();
19
+
20
+ // src/source.ts
21
+ var DEFAULT_REMOTE_BASE_URL = "https://expcat.github.io/Tigercat/mcp/";
22
+ var DEFAULT_FETCH_TIMEOUT_MS = 15e3;
23
+ function createFsSource(root) {
24
+ return {
25
+ kind: "fs",
26
+ origin: root,
27
+ async readText(path) {
28
+ const rootPath = await realpath(root);
29
+ const filePath = await realpath(join(root, path));
30
+ const relativePath = relative(rootPath, filePath);
31
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
32
+ throw new Error(`Reference path escapes the Tigercat repo: ${path}`);
33
+ }
34
+ return readFile(filePath, "utf8");
35
+ },
36
+ async probe(path) {
37
+ await access(join(root, path));
38
+ }
39
+ };
40
+ }
41
+ function createHttpSource(baseUrl, options = {}) {
42
+ const base = normalizeBaseUrl(baseUrl);
43
+ const timeoutMs = Number.isFinite(options.timeoutMs) && (options.timeoutMs ?? 0) > 0 ? Math.floor(options.timeoutMs) : DEFAULT_FETCH_TIMEOUT_MS;
44
+ const cache = /* @__PURE__ */ new Map();
45
+ const fetchOnce = (url) => fetch(url, {
46
+ signal: AbortSignal.timeout(timeoutMs),
47
+ headers: {
48
+ accept: "text/markdown, application/json;q=0.9, */*;q=0.8",
49
+ "user-agent": `tigercat-mcp/${PACKAGE_VERSION}`
50
+ }
51
+ });
52
+ const fetchText = async (path) => {
53
+ const url = new URL(path, base);
54
+ let response;
55
+ try {
56
+ try {
57
+ response = await fetchOnce(url);
58
+ } catch (error) {
59
+ if (error instanceof Error && error.name === "TimeoutError") throw error;
60
+ response = await fetchOnce(url);
61
+ }
62
+ } catch (error) {
63
+ throw new Error(
64
+ fetchFailureMessage(url, error instanceof Error ? error.message : String(error)),
65
+ { cause: error }
66
+ );
67
+ }
68
+ if (!response.ok) {
69
+ throw new Error(fetchFailureMessage(url, `HTTP ${response.status}`));
70
+ }
71
+ return response.text();
72
+ };
73
+ const readText = (path) => {
74
+ let pending = cache.get(path);
75
+ if (!pending) {
76
+ pending = fetchText(path);
77
+ pending.catch(() => cache.delete(path));
78
+ cache.set(path, pending);
79
+ }
80
+ return pending;
81
+ };
82
+ return {
83
+ kind: "http",
84
+ origin: base,
85
+ readText,
86
+ async probe(path) {
87
+ await readText(path);
88
+ }
89
+ };
90
+ }
91
+ function normalizeBaseUrl(input) {
92
+ let parsed;
93
+ try {
94
+ parsed = new URL(input);
95
+ } catch {
96
+ throw new Error(`Invalid Tigercat skills base URL: ${input}`);
97
+ }
98
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
99
+ throw new Error(`Tigercat skills base URL must use http or https: ${input}`);
100
+ }
101
+ if (!parsed.pathname.endsWith("/")) {
102
+ parsed.pathname = `${parsed.pathname}/`;
103
+ }
104
+ return parsed.toString();
105
+ }
106
+ function fetchFailureMessage(url, reason) {
107
+ return `Failed to fetch Tigercat skill source (${reason}): ${url}. Use --root <repo> for local mode, or --base-url <mirror> if GitHub Pages is unreachable.`;
108
+ }
109
+
110
+ // src/skill-index.ts
10
111
  var DEFAULT_MAX_BYTES = 12e3;
11
- var SKILL_ROOT = "skills/tigercat";
12
112
  var REFERENCES_ROOT = "skills/tigercat/references";
13
113
  var DEFAULT_COMPONENT_INDEX = "skills/tigercat/references/component-index.md";
14
114
  var DEFAULT_REACT_REFERENCE = "skills/tigercat/references/react/index.md";
15
115
  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());
116
+ async function loadSkillIndex(options) {
117
+ const resolved = typeof options === "string" ? { root: options } : options ?? {};
118
+ if (resolved.root) {
119
+ return loadFsSkillIndex(resolve(resolved.root));
120
+ }
121
+ return loadHttpSkillIndex(resolved.baseUrl ?? DEFAULT_REMOTE_BASE_URL, resolved.timeoutMs);
122
+ }
123
+ async function loadFsSkillIndex(resolvedRoot) {
18
124
  const contextPath = join(resolvedRoot, "context7.json");
19
125
  if (!existsSync(contextPath)) {
20
126
  throw new Error(`Missing context7.json under ${resolvedRoot}`);
21
127
  }
22
128
  const context7 = JSON.parse(await readFile(contextPath, "utf8"));
129
+ const allowedReferencePaths = collectAllowedReferencePaths(context7);
130
+ if (existsSync(join(resolvedRoot, REFERENCES_ROOT))) {
131
+ for (const path of await collectMarkdownReferences(join(resolvedRoot, REFERENCES_ROOT))) {
132
+ allowedReferencePaths.add(normalizeRelativePath(relative(resolvedRoot, path)));
133
+ }
134
+ }
135
+ retainMarkdownPaths(allowedReferencePaths);
136
+ for (const path of allowedReferencePaths) {
137
+ const absolutePath = join(resolvedRoot, path);
138
+ if (!existsSync(absolutePath)) {
139
+ throw new Error(`Missing Tigercat skill reference: ${path}`);
140
+ }
141
+ }
142
+ return buildSkillIndex(context7, createFsSource(resolvedRoot), allowedReferencePaths);
143
+ }
144
+ async function loadHttpSkillIndex(baseUrl, timeoutMs) {
145
+ const source = createHttpSource(baseUrl, { timeoutMs });
146
+ const raw = await source.readText("context7.json");
147
+ let context7;
148
+ try {
149
+ context7 = JSON.parse(raw);
150
+ } catch (error) {
151
+ throw new Error(
152
+ `Invalid context7.json from ${source.origin}context7.json: ${error instanceof Error ? error.message : String(error)}`,
153
+ { cause: error }
154
+ );
155
+ }
156
+ const allowedReferencePaths = retainMarkdownPaths(collectAllowedReferencePaths(context7));
157
+ return buildSkillIndex(context7, source, allowedReferencePaths);
158
+ }
159
+ function collectAllowedReferencePaths(context7) {
160
+ const allowedReferencePaths = /* @__PURE__ */ new Set();
161
+ for (const path of collectReferencePaths(context7)) {
162
+ allowedReferencePaths.add(normalizeRelativePath(path));
163
+ }
164
+ allowedReferencePaths.add("skills/tigercat/SKILL.md");
165
+ return allowedReferencePaths;
166
+ }
167
+ function retainMarkdownPaths(paths) {
168
+ for (const path of paths) {
169
+ if (!path.endsWith(".md")) {
170
+ paths.delete(path);
171
+ }
172
+ }
173
+ return paths;
174
+ }
175
+ function buildSkillIndex(context7, source, allowedReferencePaths) {
23
176
  const components = buildComponentMap(context7);
24
177
  const componentsByNormalizedName = /* @__PURE__ */ new Map();
25
178
  const aliasTargetsByNormalizedName = /* @__PURE__ */ new Map();
@@ -36,24 +189,9 @@ async function loadSkillIndex(root) {
36
189
  addAliasTarget(aliasTargetsByNormalizedName, alias, [entry.name]);
37
190
  }
38
191
  }
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
192
  return {
56
- root: resolvedRoot,
193
+ root: source.origin,
194
+ source,
57
195
  context7,
58
196
  components,
59
197
  componentsByNormalizedName,
@@ -62,8 +200,8 @@ async function loadSkillIndex(root) {
62
200
  allowedReferencePaths
63
201
  };
64
202
  }
65
- async function diagnoseTigercatMcp(root) {
66
- const index = await loadSkillIndex(root);
203
+ async function diagnoseTigercatMcp(options) {
204
+ const index = await loadSkillIndex(options);
67
205
  const issues = [];
68
206
  let readableReferenceCount = 0;
69
207
  if (index.context7.component_count !== void 0 && index.context7.component_count !== index.components.size) {
@@ -79,36 +217,53 @@ async function diagnoseTigercatMcp(root) {
79
217
  }
80
218
  for (const path of index.allowedReferencePaths) {
81
219
  try {
82
- await access(join(index.root, path));
220
+ await index.source.probe(path);
83
221
  readableReferenceCount += 1;
84
222
  } catch {
85
223
  issues.push(`reference is not readable: ${path}`);
86
224
  }
87
225
  }
226
+ let remoteVersion;
227
+ if (index.source.kind === "http") {
228
+ try {
229
+ const parsed = JSON.parse(await index.source.readText("version.json"));
230
+ if (typeof parsed.version === "string" && parsed.version) {
231
+ remoteVersion = parsed.version;
232
+ }
233
+ } catch {
234
+ }
235
+ }
88
236
  return {
89
237
  ok: issues.length === 0,
90
238
  root: index.root,
239
+ mode: index.source.kind,
91
240
  componentCount: index.components.size,
92
241
  aliasCount: index.aliasTargetsByNormalizedName.size,
93
242
  topicCount: index.topics.size,
94
243
  readableReferenceCount,
244
+ ...remoteVersion ? { remoteVersion } : {},
95
245
  issues
96
246
  };
97
247
  }
98
- async function readReferenceSource(index, path, reason, maxBytes = DEFAULT_MAX_BYTES) {
248
+ async function readReferenceSource(index, path, reason, maxBytes = DEFAULT_MAX_BYTES, section) {
99
249
  const normalizedPath = normalizeRelativePath(path);
100
250
  if (!index.allowedReferencePaths.has(normalizedPath)) {
101
251
  throw new Error(`Reference path is not allowed: ${path}`);
102
252
  }
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");
253
+ const text = await index.source.readText(normalizedPath);
111
254
  const limit = Number.isFinite(maxBytes) && maxBytes > 0 ? Math.floor(maxBytes) : DEFAULT_MAX_BYTES;
255
+ if (section) {
256
+ const extracted = extractMarkdownSection(text, section);
257
+ if (extracted !== void 0) {
258
+ return {
259
+ path: normalizedPath,
260
+ reason,
261
+ truncated: Buffer.byteLength(extracted, "utf8") > limit,
262
+ section,
263
+ text: truncateUtf8(extracted, limit)
264
+ };
265
+ }
266
+ }
112
267
  const truncated = Buffer.byteLength(text, "utf8") > limit;
113
268
  return {
114
269
  path: normalizedPath,
@@ -117,21 +272,37 @@ async function readReferenceSource(index, path, reason, maxBytes = DEFAULT_MAX_B
117
272
  text: truncateUtf8(text, limit)
118
273
  };
119
274
  }
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;
275
+ function extractMarkdownSection(text, section) {
276
+ const lines = text.split("\n");
277
+ const heading = section.trim().toLowerCase();
278
+ let start = -1;
279
+ for (let index = 0; index < lines.length; index++) {
280
+ const match = /^##\s+(.+?)\s*$/.exec(lines[index]);
281
+ if (match && match[1].toLowerCase() === heading) {
282
+ start = index;
283
+ break;
125
284
  }
126
- const next = dirname(current);
127
- if (next === current) {
128
- throw new Error(`Could not find Tigercat repo root from ${startDirectory}`);
285
+ }
286
+ if (start === -1) return void 0;
287
+ let end = lines.length;
288
+ for (let index = start + 1; index < lines.length; index++) {
289
+ if (/^##\s/.test(lines[index])) {
290
+ end = index;
291
+ break;
129
292
  }
130
- current = next;
131
293
  }
294
+ return `${lines.slice(start, end).join("\n").trimEnd()}
295
+ `;
296
+ }
297
+ function createReferencePointer(index, path, reason) {
298
+ const normalizedPath = normalizeRelativePath(path);
299
+ if (!index.allowedReferencePaths.has(normalizedPath)) {
300
+ throw new Error(`Reference path is not allowed: ${path}`);
301
+ }
302
+ return { path: normalizedPath, reason, truncated: false };
132
303
  }
133
304
  function normalizeName(value) {
134
- return value.toLowerCase().replace(/[^a-z0-9]/g, "");
305
+ return value.toLowerCase().replace(/[^a-z0-9一-鿿]/g, "");
135
306
  }
136
307
  function normalizeRelativePath(path) {
137
308
  const normalized = path.replaceAll("\\", "/").replace(/^\.?\//, "");
@@ -274,8 +445,10 @@ async function routeTigercatTask(index, input) {
274
445
  task,
275
446
  framework: input.framework,
276
447
  intent: matches.length > 0 && topics.length > 0 ? "mixed" : matches.length > 0 ? "component" : "topic",
277
- matches,
278
- topics,
448
+ // 正文只在顶层 sources 内联一次;matches/topics 里保留 path/reason 元数据,
449
+ // 否则同一份 reference 全文会在响应里出现两遍。
450
+ matches: matches.map((match) => ({ ...match, sources: match.sources.map(stripText) })),
451
+ topics: topics.map((topic) => ({ ...topic, sources: topic.sources.map(stripText) })),
279
452
  candidates: [],
280
453
  sources
281
454
  };
@@ -298,14 +471,11 @@ async function routeTigercatTask(index, input) {
298
471
  };
299
472
  }
300
473
  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
- },
474
+ const inlineSpecs = dedupeByPath([
306
475
  {
307
476
  path: entry.references.props,
308
- reason: `${entry.name} props, events, methods, and type source.`
477
+ reason: `${entry.name} props, events, methods, and type source.`,
478
+ section: entry.name
309
479
  },
310
480
  { path: entry.references.examples, reason: `${entry.name} compact Vue/React example routes.` },
311
481
  ...framework ? [
@@ -316,36 +486,60 @@ async function createComponentRoute(index, entry, framework, maxBytes) {
316
486
  ] : [
317
487
  { path: entry.references.react, reason: "React binding and import notes." },
318
488
  { 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
- ];
489
+ ]
490
+ ]);
323
491
  const sources = await Promise.all(
324
- dedupeByPath(sourceSpecs).map(
325
- (spec) => readReferenceSource(index, spec.path, spec.reason, maxBytes)
492
+ inlineSpecs.map(
493
+ (spec) => readReferenceSource(
494
+ index,
495
+ spec.path,
496
+ spec.reason,
497
+ maxBytes,
498
+ "section" in spec ? spec.section : void 0
499
+ )
326
500
  )
327
501
  );
502
+ const pointers = [
503
+ {
504
+ path: entry.references.componentIndex,
505
+ reason: "Full component inventory; read at most once per session via tigercat_reference."
506
+ },
507
+ {
508
+ path: SHARED_PATTERNS,
509
+ reason: "Cross-framework binding differences; read at most once per session via tigercat_reference."
510
+ },
511
+ {
512
+ path: SHARED_GLOSSARY,
513
+ reason: "Shared Tigercat terminology; read at most once per session via tigercat_reference."
514
+ }
515
+ ].map((spec) => createReferencePointer(index, spec.path, spec.reason));
328
516
  return {
329
517
  component: entry,
330
- sources
518
+ sources: dedupeByPath([...sources, ...pointers])
331
519
  };
332
520
  }
333
521
  async function createTopicRoute(index, slug, maxBytes) {
334
522
  const topic = index.topics.get(slug);
335
523
  if (!topic) throw new Error(`Unknown Tigercat topic: ${slug}`);
336
524
  const sources = await Promise.all(
337
- dedupeByPath([
338
- { path: SKILL_INDEX, reason: "Top-level skill route index." },
339
- ...topic.references.map((path) => ({
525
+ dedupeByPath(
526
+ topic.references.map((path) => ({
340
527
  path,
341
528
  reason: `${topic.title} reference.`
342
529
  }))
343
- ]).map((spec) => readReferenceSource(index, spec.path, spec.reason, maxBytes))
530
+ ).map((spec) => readReferenceSource(index, spec.path, spec.reason, maxBytes))
344
531
  );
345
532
  return {
346
533
  slug,
347
534
  title: topic.title,
348
- sources
535
+ sources: dedupeByPath([
536
+ ...sources,
537
+ createReferencePointer(
538
+ index,
539
+ SKILL_INDEX,
540
+ "Top-level skill route index; read via tigercat_reference only if the topic references are insufficient."
541
+ )
542
+ ])
349
543
  };
350
544
  }
351
545
  function getInventory(index) {
@@ -506,8 +700,9 @@ function dedupeByPath(items) {
506
700
  const seen = /* @__PURE__ */ new Set();
507
701
  const result = [];
508
702
  for (const item of items) {
509
- if (seen.has(item.path)) continue;
510
- seen.add(item.path);
703
+ const key = `${item.path}#${item.section ?? ""}`;
704
+ if (seen.has(key)) continue;
705
+ seen.add(key);
511
706
  result.push(item);
512
707
  }
513
708
  return result;
@@ -515,6 +710,10 @@ function dedupeByPath(items) {
515
710
  function mergeSources(sources) {
516
711
  return dedupeByPath(sources);
517
712
  }
713
+ function stripText(source) {
714
+ const { text: _text, ...rest } = source;
715
+ return rest;
716
+ }
518
717
  function keywordMatches(task, normalizedTask, keyword) {
519
718
  const normalizedKeyword = normalizeName(keyword);
520
719
  if (normalizedKeyword) return normalizedTask.includes(normalizedKeyword);
@@ -524,100 +723,147 @@ function keywordMatches(task, normalizedTask, keyword) {
524
723
  // src/server.ts
525
724
  var JSON_MIME = "application/json";
526
725
  var MARKDOWN_MIME = "text/markdown";
726
+ var TIGERCAT_SERVER_INSTRUCTIONS = [
727
+ "Tigercat is a Tailwind CSS React + Vue 3 UI library (149 components, imported from",
728
+ "@expcat/tigercat-react / @expcat/tigercat-vue PascalCase subpaths such as",
729
+ "@expcat/tigercat-react/Button). Use this server BEFORE writing or reviewing any code",
730
+ "that touches Tigercat \u2014 it returns exact import subpaths, props/events, and",
731
+ "per-framework binding notes that differ from generic React/Vue knowledge.",
732
+ "",
733
+ "Tool order: call `tigercat_route` first for any natural-language task (always pass",
734
+ "`framework`; it roughly halves the payload). Call `tigercat_component` when you",
735
+ "already know the component name or alias. Use `tigercat_search` only for fuzzy",
736
+ "discovery, and `tigercat_reference` only to read a path that a previous result",
737
+ "pointed to.",
738
+ "",
739
+ "Responses inline the needed reference text once, as extra content blocks after the",
740
+ 'JSON summary. Sources marked `"inlined": false` are session-level background',
741
+ "(inventory, glossary, patterns): read each at most once per session via",
742
+ "`tigercat_reference`, and only if actually needed. Do not re-read paths you already",
743
+ "received.",
744
+ "",
745
+ "Component matching works with English names and listed Chinese aliases (e.g. \u8868\u5355,",
746
+ "\u65E5\u671F\u9009\u62E9\u5668); prefer English component names for precision. `notification` is a",
747
+ "command API topic, not a component; `Message` is both."
748
+ ].join("\n");
749
+ var FRAMEWORK_SCHEMA = {
750
+ type: "string",
751
+ enum: ["react", "vue"],
752
+ description: "Target framework. Omit to receive notes for both React and Vue (larger payload)."
753
+ };
754
+ var MAX_BYTES_SCHEMA = {
755
+ type: "number",
756
+ minimum: 200,
757
+ maximum: 5e4,
758
+ description: "Byte cap per inlined source (default 12000). Raise only when a source was truncated."
759
+ };
760
+ var TIGERCAT_TOOLS = [
761
+ {
762
+ name: "tigercat_search",
763
+ description: "Fuzzy-search Tigercat components, aliases, categories, topics, and command APIs. Returns ranked name/metadata matches only \u2014 no reference text, cheapest call. Use when unsure of the exact name; otherwise go straight to tigercat_component or tigercat_route.",
764
+ inputSchema: {
765
+ type: "object",
766
+ properties: {
767
+ query: {
768
+ type: "string",
769
+ description: 'Component, alias, category, topic, or use case, e.g. "table" or "\u8868\u683C".'
770
+ },
771
+ framework: FRAMEWORK_SCHEMA,
772
+ limit: {
773
+ type: "number",
774
+ minimum: 1,
775
+ maximum: 30,
776
+ description: "Max results (default 8)."
777
+ }
778
+ },
779
+ required: ["query"],
780
+ additionalProperties: false
781
+ }
782
+ },
783
+ {
784
+ name: "tigercat_component",
785
+ description: "Look up one Tigercat component by exact name or alias (e.g. Button, Grid, \u65E5\u671F\u9009\u62E9\u5668). Returns its import subpath, its own props/events section, compact category examples, and framework binding notes, inlined as content blocks after the JSON summary. Preferred over tigercat_route when you already know the component name; pass framework to shrink the payload.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: {
789
+ component: {
790
+ type: "string",
791
+ description: "Component name or alias, such as Button, Grid, or \u8868\u683C."
792
+ },
793
+ framework: FRAMEWORK_SCHEMA,
794
+ maxBytes: MAX_BYTES_SCHEMA
795
+ },
796
+ required: ["component"],
797
+ additionalProperties: false
798
+ }
799
+ },
800
+ {
801
+ name: "tigercat_route",
802
+ description: 'Route a natural-language Tigercat task to the smallest useful component/topic references. Preferred FIRST call for any Tigercat coding task. Returns matched components with import paths plus each needed reference inlined exactly once; sources marked "inlined": false are session-level background to read at most once via tigercat_reference. Always pass framework. Name components in English or a listed Chinese alias.',
803
+ inputSchema: {
804
+ type: "object",
805
+ properties: {
806
+ task: {
807
+ type: "string",
808
+ description: 'Natural-language Tigercat task, e.g. "add a DatePicker with Form validation" or "\u7ED9\u8868\u5355\u52A0\u65E5\u671F\u9009\u62E9\u5668".'
809
+ },
810
+ framework: FRAMEWORK_SCHEMA,
811
+ maxBytes: MAX_BYTES_SCHEMA,
812
+ limit: {
813
+ type: "number",
814
+ minimum: 1,
815
+ maximum: 30,
816
+ description: "Max fallback candidates when nothing matches directly (default 8)."
817
+ }
818
+ },
819
+ required: ["task"],
820
+ additionalProperties: false
821
+ }
822
+ },
823
+ {
824
+ name: "tigercat_reference",
825
+ description: 'Read one allow-listed Tigercat skill reference file verbatim. Use only for paths returned by other tigercat tools \u2014 typically to read an "inlined": false pointer source once per session.',
826
+ inputSchema: {
827
+ type: "object",
828
+ properties: {
829
+ path: {
830
+ type: "string",
831
+ description: 'Repo-relative path under skills/tigercat/, exactly as returned in a source "path" field, e.g. skills/tigercat/references/shared/glossary.md.'
832
+ },
833
+ maxBytes: MAX_BYTES_SCHEMA
834
+ },
835
+ required: ["path"],
836
+ additionalProperties: false
837
+ }
838
+ }
839
+ ];
527
840
  function createTigercatMcpServer(options = {}) {
528
841
  const server = new Server(
529
842
  {
530
843
  name: "@expcat/tigercat-mcp",
531
- version: "2.0.0-rc.1"
844
+ version: PACKAGE_VERSION
532
845
  },
533
846
  {
534
847
  capabilities: {
535
848
  tools: {},
536
849
  resources: {},
537
850
  prompts: {}
538
- }
851
+ },
852
+ instructions: TIGERCAT_SERVER_INSTRUCTIONS
539
853
  }
540
854
  );
541
855
  let indexPromise;
542
856
  const getIndex = () => {
543
- indexPromise ??= loadSkillIndex(options.root);
857
+ indexPromise ??= loadSkillIndex(options);
544
858
  return indexPromise;
545
859
  };
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
- }));
860
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TIGERCAT_TOOLS }));
615
861
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
616
862
  const index = await getIndex();
617
863
  const args = request.params.arguments ?? {};
618
864
  try {
619
865
  if (request.params.name === "tigercat_search") {
620
- return jsonContent(
866
+ return renderToolResult(
621
867
  await searchTigercat(index, {
622
868
  query: stringArg(args.query),
623
869
  framework: frameworkArg(args.framework),
@@ -626,7 +872,7 @@ function createTigercatMcpServer(options = {}) {
626
872
  );
627
873
  }
628
874
  if (request.params.name === "tigercat_component") {
629
- return jsonContent(
875
+ return renderToolResult(
630
876
  await getTigercatComponent(index, {
631
877
  component: stringArg(args.component),
632
878
  framework: frameworkArg(args.framework),
@@ -635,7 +881,7 @@ function createTigercatMcpServer(options = {}) {
635
881
  );
636
882
  }
637
883
  if (request.params.name === "tigercat_route") {
638
- return jsonContent(
884
+ return renderToolResult(
639
885
  await routeTigercatTask(index, {
640
886
  task: stringArg(args.task),
641
887
  framework: frameworkArg(args.framework),
@@ -645,7 +891,7 @@ function createTigercatMcpServer(options = {}) {
645
891
  );
646
892
  }
647
893
  if (request.params.name === "tigercat_reference") {
648
- return jsonContent(
894
+ return renderToolResult(
649
895
  await readReferenceSource(
650
896
  index,
651
897
  stringArg(args.path),
@@ -701,22 +947,22 @@ function createTigercatMcpServer(options = {}) {
701
947
  const index = await getIndex();
702
948
  const uri = request.params.uri;
703
949
  if (uri === "tigercat://inventory") {
704
- return resourceText(uri, JSON.stringify(getInventory(index), null, 2), JSON_MIME);
950
+ return resourceText(uri, JSON.stringify(getInventory(index)), JSON_MIME);
705
951
  }
706
952
  if (uri.startsWith("tigercat://component/")) {
707
953
  const component = decodeURIComponent(uri.slice("tigercat://component/".length));
708
954
  const lookup = await getTigercatComponent(index, { component });
709
- return resourceText(uri, JSON.stringify(lookup, null, 2), JSON_MIME);
955
+ return resourceText(uri, JSON.stringify(lookup), JSON_MIME);
710
956
  }
711
957
  if (uri.startsWith("tigercat://category/")) {
712
958
  const category = decodeURIComponent(uri.slice("tigercat://category/".length));
713
959
  const components = getCategoryComponents(index, category);
714
- return resourceText(uri, JSON.stringify({ category, components }, null, 2), JSON_MIME);
960
+ return resourceText(uri, JSON.stringify({ category, components }), JSON_MIME);
715
961
  }
716
962
  if (uri.startsWith("tigercat://topic/")) {
717
963
  const topic = decodeURIComponent(uri.slice("tigercat://topic/".length));
718
964
  const route = await createTopicRoute(index, topic);
719
- return resourceText(uri, JSON.stringify(route, null, 2), JSON_MIME);
965
+ return resourceText(uri, JSON.stringify(route), JSON_MIME);
720
966
  }
721
967
  if (uri.startsWith("tigercat://reference/")) {
722
968
  const path = decodeURIComponent(uri.slice("tigercat://reference/".length));
@@ -762,7 +1008,7 @@ function createTigercatMcpServer(options = {}) {
762
1008
  text: [
763
1009
  `Use tigercat_route with framework "${framework}" before answering.`,
764
1010
  "Use tigercat_component for exact component imports and props when route results mention components.",
765
- "Read only the returned sources needed for the task.",
1011
+ 'Read only the returned sources; sources marked "inlined": false are optional background \u2014 fetch each at most once via tigercat_reference.',
766
1012
  `Task: ${task}`,
767
1013
  "Answer with exact import paths, key props/events, and React/Vue binding differences."
768
1014
  ].join("\n")
@@ -779,11 +1025,49 @@ function jsonContent(value, isError = false) {
779
1025
  content: [
780
1026
  {
781
1027
  type: "text",
782
- text: JSON.stringify(value, null, 2)
1028
+ text: JSON.stringify(value)
783
1029
  }
784
1030
  ]
785
1031
  };
786
1032
  }
1033
+ function renderToolResult(value) {
1034
+ const blocks = [];
1035
+ const seen = /* @__PURE__ */ new Set();
1036
+ const visit = (node) => {
1037
+ if (Array.isArray(node)) return node.map(visit);
1038
+ if (!node || typeof node !== "object") return node;
1039
+ const record = node;
1040
+ if (isReferenceSource(record)) {
1041
+ const { text, ...rest } = record;
1042
+ if (typeof text !== "string") return { ...rest, inlined: false };
1043
+ const section = typeof record.section === "string" ? record.section : void 0;
1044
+ const key = `${record.path}#${section ?? ""}`;
1045
+ if (!seen.has(key)) {
1046
+ seen.add(key);
1047
+ blocks.push({
1048
+ type: "text",
1049
+ text: formatSourceBlock(record.path, section, record.truncated, text)
1050
+ });
1051
+ }
1052
+ return { ...rest, inlined: true };
1053
+ }
1054
+ return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, visit(v)]));
1055
+ };
1056
+ const summary = visit(value);
1057
+ return {
1058
+ isError: false,
1059
+ content: [{ type: "text", text: JSON.stringify(summary) }, ...blocks]
1060
+ };
1061
+ }
1062
+ function isReferenceSource(record) {
1063
+ return typeof record.path === "string" && typeof record.reason === "string" && typeof record.truncated === "boolean";
1064
+ }
1065
+ function formatSourceBlock(path, section, truncated, text) {
1066
+ const heading = section ? `${path} \xA7 ${section}` : path;
1067
+ const truncatedNote = truncated ? " [truncated: raise maxBytes to read more]" : "";
1068
+ return `===== source: ${heading}${truncatedNote} =====
1069
+ ${text}`;
1070
+ }
787
1071
  function resourceText(uri, text, mimeType) {
788
1072
  return {
789
1073
  contents: [
@@ -821,17 +1105,20 @@ function formatError(error) {
821
1105
 
822
1106
  // src/index.ts
823
1107
  async function main() {
824
- const options = parseArgs(process.argv.slice(2));
825
- if (options.help) {
1108
+ const parsed = parseArgs(process.argv.slice(2));
1109
+ if (parsed.help) {
826
1110
  console.log(helpText());
827
1111
  return;
828
1112
  }
829
- if (options.doctor) {
830
- const result = await diagnoseTigercatMcp(options.root);
1113
+ const options = resolveOptions(parsed);
1114
+ if (parsed.doctor) {
1115
+ const result = await diagnoseTigercatMcp(options);
831
1116
  console.log(
832
1117
  [
833
1118
  `Tigercat MCP doctor: ${result.ok ? "ok" : "failed"}`,
834
- `root: ${result.root}`,
1119
+ `mode: ${result.mode === "http" ? "remote" : "local"}`,
1120
+ `${result.mode === "http" ? "base url" : "root"}: ${result.root}`,
1121
+ ...result.remoteVersion ? [`remote version: ${result.remoteVersion}`] : [],
835
1122
  `components: ${result.componentCount}`,
836
1123
  `aliases: ${result.aliasCount}`,
837
1124
  `topics: ${result.topicCount}`,
@@ -842,12 +1129,13 @@ async function main() {
842
1129
  if (!result.ok) process.exitCode = 1;
843
1130
  return;
844
1131
  }
845
- const server = createTigercatMcpServer({ root: options.root });
1132
+ const server = createTigercatMcpServer(options);
846
1133
  const transport = new StdioServerTransport();
847
1134
  await server.connect(transport);
848
1135
  }
849
1136
  function parseArgs(args) {
850
1137
  let root;
1138
+ let baseUrl;
851
1139
  let help = false;
852
1140
  let doctor = false;
853
1141
  for (let index = 0; index < args.length; index++) {
@@ -869,20 +1157,47 @@ function parseArgs(args) {
869
1157
  index++;
870
1158
  continue;
871
1159
  }
1160
+ if (arg === "--base-url") {
1161
+ const value = args[index + 1];
1162
+ if (!value) {
1163
+ throw new Error("Usage: tigercat-mcp --base-url <skills-base-url>");
1164
+ }
1165
+ baseUrl = value;
1166
+ index++;
1167
+ continue;
1168
+ }
872
1169
  throw new Error(`Unknown argument: ${arg}`);
873
1170
  }
874
- return { root, help, doctor };
1171
+ return { root, baseUrl, help, doctor };
1172
+ }
1173
+ function resolveOptions(parsed) {
1174
+ if (parsed.root) {
1175
+ if (parsed.baseUrl) {
1176
+ console.error("tigercat-mcp: --root takes precedence over --base-url; ignoring --base-url.");
1177
+ }
1178
+ return { root: parsed.root };
1179
+ }
1180
+ const envBaseUrl = process.env.TIGERCAT_MCP_BASE_URL?.trim();
1181
+ return { baseUrl: parsed.baseUrl ?? (envBaseUrl || void 0) ?? DEFAULT_REMOTE_BASE_URL };
875
1182
  }
876
1183
  function helpText() {
877
1184
  return [
878
- "Usage: tigercat-mcp [--root <repo-root>] [--doctor]",
1185
+ "Usage: tigercat-mcp [--root <repo-root>] [--base-url <skills-base-url>] [--doctor]",
879
1186
  "",
880
1187
  "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.",
1188
+ `By default skill references are fetched from ${DEFAULT_REMOTE_BASE_URL} (GitHub Pages).`,
1189
+ "",
1190
+ " --root <repo-root> read skills from a local Tigercat checkout (dev/offline mode).",
1191
+ " --base-url <url> fetch skills from a mirror of the /mcp/ Pages route.",
1192
+ " --doctor validate the skill inventory and exit without starting stdio.",
1193
+ " TIGERCAT_MCP_BASE_URL environment fallback for --base-url.",
1194
+ "",
1195
+ "Examples:",
1196
+ " tigercat-mcp # remote skills (default)",
1197
+ " tigercat-mcp --root /path/to/Tigercat # local checkout",
1198
+ " tigercat-mcp --base-url https://mirror.example.com/mcp/",
883
1199
  "",
884
- "Example MCP client command:",
885
- " tigercat-mcp --root /path/to/Tigercat"
1200
+ "If GitHub Pages is unreachable (offline, proxy, regional block), use --root or --base-url."
886
1201
  ].join("\n");
887
1202
  }
888
1203
  var isDirectRun = (() => {
@@ -900,4 +1215,4 @@ if (isDirectRun) {
900
1215
  });
901
1216
  }
902
1217
 
903
- export { createTigercatMcpServer, diagnoseTigercatMcp, getCategoryComponents, getInventory, getTigercatComponent, loadSkillIndex, routeTigercatTask, searchTigercat };
1218
+ export { DEFAULT_REMOTE_BASE_URL, createTigercatMcpServer, diagnoseTigercatMcp, getCategoryComponents, getInventory, getTigercatComponent, loadSkillIndex, routeTigercatTask, searchTigercat };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@expcat/tigercat-mcp",
3
- "version": "2.0.4",
3
+ "version": "2.0.19",
4
4
  "type": "module",
5
- "description": "Local MCP server for routing LLMs to Tigercat skill references",
5
+ "description": "MCP server routing LLMs to Tigercat skill references, fetched from GitHub Pages by default with a local checkout fallback",
6
6
  "license": "MIT",
7
7
  "author": "Yizhe Wang",
8
8
  "repository": {
@@ -10,7 +10,7 @@
10
10
  "url": "https://github.com/expcat/Tigercat",
11
11
  "directory": "packages/mcp"
12
12
  },
13
- "homepage": "https://github.com/expcat/Tigercat#readme",
13
+ "homepage": "https://expcat.github.io/Tigercat/mcp/",
14
14
  "bugs": {
15
15
  "url": "https://github.com/expcat/Tigercat/issues"
16
16
  },