@cubis/foundry 0.3.46 → 0.3.48
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/README.md +19 -0
- package/mcp/dist/index.js +337 -105
- package/mcp/src/server.ts +41 -179
- package/mcp/src/tools/future/README.md +3 -3
- package/mcp/src/tools/index.ts +14 -0
- package/mcp/src/tools/registry.test.ts +121 -0
- package/mcp/src/tools/registry.ts +318 -0
- package/mcp/src/tools/skillGet.ts +36 -5
- package/mcp/src/tools/skillTools.test.ts +143 -1
- package/mcp/src/vault/manifest.test.ts +71 -1
- package/mcp/src/vault/manifest.ts +128 -3
- package/mcp/src/vault/scanner.test.ts +35 -0
- package/mcp/src/vault/scanner.ts +91 -1
- package/package.json +17 -1
- package/workflows/workflows/agent-environment-setup/platforms/antigravity/rules/GEMINI.md +189 -36
- package/workflows/workflows/agent-environment-setup/platforms/codex/rules/AGENTS.md +195 -40
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +189 -36
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +189 -35
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cubis Foundry MCP Server – declarative tool registry.
|
|
3
|
+
*
|
|
4
|
+
* Defines all built-in tools in a single registry array.
|
|
5
|
+
* Server.ts reads from this registry to auto-register tools,
|
|
6
|
+
* eliminating per-tool import boilerplate.
|
|
7
|
+
*
|
|
8
|
+
* When adding a new tool:
|
|
9
|
+
* 1. Create toolName.ts with name/description/schema/handler exports
|
|
10
|
+
* 2. Add a ToolRegistryEntry here
|
|
11
|
+
* 3. Done — server.ts picks it up automatically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import type { VaultManifest } from "../vault/types.js";
|
|
16
|
+
import type { ConfigScope } from "../cbxConfig/types.js";
|
|
17
|
+
|
|
18
|
+
// ─── Core types ─────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type ToolCategory = "skill" | "postman" | "stitch";
|
|
21
|
+
|
|
22
|
+
export interface ToolRegistryEntry {
|
|
23
|
+
/** Tool name exposed to MCP clients. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** One-line description for MCP discovery. */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Zod schema (the `.shape` is extracted at registration time). */
|
|
28
|
+
schema: z.ZodObject<z.ZodRawShape>;
|
|
29
|
+
/** Tool category for grouping and documentation. */
|
|
30
|
+
category: ToolCategory;
|
|
31
|
+
/**
|
|
32
|
+
* Handler factory. Receives shared runtime context and returns the
|
|
33
|
+
* concrete async handler that server.tool() expects.
|
|
34
|
+
*/
|
|
35
|
+
createHandler: (
|
|
36
|
+
ctx: ToolRuntimeContext,
|
|
37
|
+
) => (args: unknown) => Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolRuntimeContext {
|
|
41
|
+
manifest: VaultManifest;
|
|
42
|
+
charsPerToken: number;
|
|
43
|
+
summaryMaxLength: number;
|
|
44
|
+
defaultConfigScope: ConfigScope | "auto";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Tool imports ───────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
skillListCategoriesName,
|
|
51
|
+
skillListCategoriesDescription,
|
|
52
|
+
skillListCategoriesSchema,
|
|
53
|
+
handleSkillListCategories,
|
|
54
|
+
} from "./skillListCategories.js";
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
skillBrowseCategoryName,
|
|
58
|
+
skillBrowseCategoryDescription,
|
|
59
|
+
skillBrowseCategorySchema,
|
|
60
|
+
handleSkillBrowseCategory,
|
|
61
|
+
} from "./skillBrowseCategory.js";
|
|
62
|
+
|
|
63
|
+
import {
|
|
64
|
+
skillSearchName,
|
|
65
|
+
skillSearchDescription,
|
|
66
|
+
skillSearchSchema,
|
|
67
|
+
handleSkillSearch,
|
|
68
|
+
} from "./skillSearch.js";
|
|
69
|
+
|
|
70
|
+
import {
|
|
71
|
+
skillGetName,
|
|
72
|
+
skillGetDescription,
|
|
73
|
+
skillGetSchema,
|
|
74
|
+
handleSkillGet,
|
|
75
|
+
} from "./skillGet.js";
|
|
76
|
+
|
|
77
|
+
import {
|
|
78
|
+
skillBudgetReportName,
|
|
79
|
+
skillBudgetReportDescription,
|
|
80
|
+
skillBudgetReportSchema,
|
|
81
|
+
handleSkillBudgetReport,
|
|
82
|
+
} from "./skillBudgetReport.js";
|
|
83
|
+
|
|
84
|
+
import {
|
|
85
|
+
postmanGetModeName,
|
|
86
|
+
postmanGetModeDescription,
|
|
87
|
+
postmanGetModeSchema,
|
|
88
|
+
handlePostmanGetMode,
|
|
89
|
+
} from "./postmanGetMode.js";
|
|
90
|
+
|
|
91
|
+
import {
|
|
92
|
+
postmanSetModeName,
|
|
93
|
+
postmanSetModeDescription,
|
|
94
|
+
postmanSetModeSchema,
|
|
95
|
+
handlePostmanSetMode,
|
|
96
|
+
} from "./postmanSetMode.js";
|
|
97
|
+
|
|
98
|
+
import {
|
|
99
|
+
postmanGetStatusName,
|
|
100
|
+
postmanGetStatusDescription,
|
|
101
|
+
postmanGetStatusSchema,
|
|
102
|
+
handlePostmanGetStatus,
|
|
103
|
+
} from "./postmanGetStatus.js";
|
|
104
|
+
|
|
105
|
+
import {
|
|
106
|
+
stitchGetModeName,
|
|
107
|
+
stitchGetModeDescription,
|
|
108
|
+
stitchGetModeSchema,
|
|
109
|
+
handleStitchGetMode,
|
|
110
|
+
} from "./stitchGetMode.js";
|
|
111
|
+
|
|
112
|
+
import {
|
|
113
|
+
stitchSetProfileName,
|
|
114
|
+
stitchSetProfileDescription,
|
|
115
|
+
stitchSetProfileSchema,
|
|
116
|
+
handleStitchSetProfile,
|
|
117
|
+
} from "./stitchSetProfile.js";
|
|
118
|
+
|
|
119
|
+
import {
|
|
120
|
+
stitchGetStatusName,
|
|
121
|
+
stitchGetStatusDescription,
|
|
122
|
+
stitchGetStatusSchema,
|
|
123
|
+
handleStitchGetStatus,
|
|
124
|
+
} from "./stitchGetStatus.js";
|
|
125
|
+
|
|
126
|
+
// ─── Scope helper ───────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function withDefaultScope(
|
|
129
|
+
args: unknown,
|
|
130
|
+
defaultScope: ConfigScope | "auto",
|
|
131
|
+
): Record<string, unknown> {
|
|
132
|
+
const safeArgs =
|
|
133
|
+
args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
134
|
+
return {
|
|
135
|
+
...safeArgs,
|
|
136
|
+
scope: typeof safeArgs.scope === "string" ? safeArgs.scope : defaultScope,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Registry ───────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export const TOOL_REGISTRY: readonly ToolRegistryEntry[] = [
|
|
143
|
+
// ── Skill vault tools ─────────────────────────────────────
|
|
144
|
+
{
|
|
145
|
+
name: skillListCategoriesName,
|
|
146
|
+
description: skillListCategoriesDescription,
|
|
147
|
+
schema: skillListCategoriesSchema,
|
|
148
|
+
category: "skill",
|
|
149
|
+
createHandler: (ctx) => async () =>
|
|
150
|
+
handleSkillListCategories(ctx.manifest, ctx.charsPerToken),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: skillBrowseCategoryName,
|
|
154
|
+
description: skillBrowseCategoryDescription,
|
|
155
|
+
schema: skillBrowseCategorySchema,
|
|
156
|
+
category: "skill",
|
|
157
|
+
createHandler: (ctx) => async (args) =>
|
|
158
|
+
handleSkillBrowseCategory(
|
|
159
|
+
args as z.infer<typeof skillBrowseCategorySchema>,
|
|
160
|
+
ctx.manifest,
|
|
161
|
+
ctx.summaryMaxLength,
|
|
162
|
+
ctx.charsPerToken,
|
|
163
|
+
),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: skillSearchName,
|
|
167
|
+
description: skillSearchDescription,
|
|
168
|
+
schema: skillSearchSchema,
|
|
169
|
+
category: "skill",
|
|
170
|
+
createHandler: (ctx) => async (args) =>
|
|
171
|
+
handleSkillSearch(
|
|
172
|
+
args as z.infer<typeof skillSearchSchema>,
|
|
173
|
+
ctx.manifest,
|
|
174
|
+
ctx.summaryMaxLength,
|
|
175
|
+
ctx.charsPerToken,
|
|
176
|
+
),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: skillGetName,
|
|
180
|
+
description: skillGetDescription,
|
|
181
|
+
schema: skillGetSchema,
|
|
182
|
+
category: "skill",
|
|
183
|
+
createHandler: (ctx) => async (args) =>
|
|
184
|
+
handleSkillGet(
|
|
185
|
+
args as z.infer<typeof skillGetSchema>,
|
|
186
|
+
ctx.manifest,
|
|
187
|
+
ctx.charsPerToken,
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: skillBudgetReportName,
|
|
192
|
+
description: skillBudgetReportDescription,
|
|
193
|
+
schema: skillBudgetReportSchema,
|
|
194
|
+
category: "skill",
|
|
195
|
+
createHandler: (ctx) => async (args) =>
|
|
196
|
+
handleSkillBudgetReport(
|
|
197
|
+
args as z.infer<typeof skillBudgetReportSchema>,
|
|
198
|
+
ctx.manifest,
|
|
199
|
+
ctx.charsPerToken,
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// ── Postman tools ─────────────────────────────────────────
|
|
204
|
+
{
|
|
205
|
+
name: postmanGetModeName,
|
|
206
|
+
description: postmanGetModeDescription,
|
|
207
|
+
schema: postmanGetModeSchema,
|
|
208
|
+
category: "postman",
|
|
209
|
+
createHandler: (ctx) => async (args) =>
|
|
210
|
+
handlePostmanGetMode(
|
|
211
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
212
|
+
typeof postmanGetModeSchema
|
|
213
|
+
>,
|
|
214
|
+
),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: postmanSetModeName,
|
|
218
|
+
description: postmanSetModeDescription,
|
|
219
|
+
schema: postmanSetModeSchema,
|
|
220
|
+
category: "postman",
|
|
221
|
+
createHandler: (ctx) => async (args) =>
|
|
222
|
+
handlePostmanSetMode(
|
|
223
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
224
|
+
typeof postmanSetModeSchema
|
|
225
|
+
>,
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: postmanGetStatusName,
|
|
230
|
+
description: postmanGetStatusDescription,
|
|
231
|
+
schema: postmanGetStatusSchema,
|
|
232
|
+
category: "postman",
|
|
233
|
+
createHandler: (ctx) => async (args) =>
|
|
234
|
+
handlePostmanGetStatus(
|
|
235
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
236
|
+
typeof postmanGetStatusSchema
|
|
237
|
+
>,
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// ── Stitch tools ──────────────────────────────────────────
|
|
242
|
+
{
|
|
243
|
+
name: stitchGetModeName,
|
|
244
|
+
description: stitchGetModeDescription,
|
|
245
|
+
schema: stitchGetModeSchema,
|
|
246
|
+
category: "stitch",
|
|
247
|
+
createHandler: (ctx) => async (args) =>
|
|
248
|
+
handleStitchGetMode(
|
|
249
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
250
|
+
typeof stitchGetModeSchema
|
|
251
|
+
>,
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: stitchSetProfileName,
|
|
256
|
+
description: stitchSetProfileDescription,
|
|
257
|
+
schema: stitchSetProfileSchema,
|
|
258
|
+
category: "stitch",
|
|
259
|
+
createHandler: (ctx) => async (args) =>
|
|
260
|
+
handleStitchSetProfile(
|
|
261
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
262
|
+
typeof stitchSetProfileSchema
|
|
263
|
+
>,
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: stitchGetStatusName,
|
|
268
|
+
description: stitchGetStatusDescription,
|
|
269
|
+
schema: stitchGetStatusSchema,
|
|
270
|
+
category: "stitch",
|
|
271
|
+
createHandler: (ctx) => async (args) =>
|
|
272
|
+
handleStitchGetStatus(
|
|
273
|
+
withDefaultScope(args, ctx.defaultConfigScope) as z.infer<
|
|
274
|
+
typeof stitchGetStatusSchema
|
|
275
|
+
>,
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
// ─── Helpers ────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/** Get tools filtered by category. */
|
|
283
|
+
export function getToolsByCategory(
|
|
284
|
+
category: ToolCategory,
|
|
285
|
+
): ToolRegistryEntry[] {
|
|
286
|
+
return TOOL_REGISTRY.filter((t) => t.category === category);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Get all registered tool names. */
|
|
290
|
+
export function getRegisteredToolNames(): string[] {
|
|
291
|
+
return TOOL_REGISTRY.map((t) => t.name);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Build a summary of the registry for documentation/rule-file generation. */
|
|
295
|
+
export function buildRegistrySummary(): {
|
|
296
|
+
categories: Record<
|
|
297
|
+
string,
|
|
298
|
+
{ tools: Array<{ name: string; description: string }> }
|
|
299
|
+
>;
|
|
300
|
+
totalTools: number;
|
|
301
|
+
} {
|
|
302
|
+
const categories: Record<
|
|
303
|
+
string,
|
|
304
|
+
{ tools: Array<{ name: string; description: string }> }
|
|
305
|
+
> = {};
|
|
306
|
+
|
|
307
|
+
for (const tool of TOOL_REGISTRY) {
|
|
308
|
+
if (!categories[tool.category]) {
|
|
309
|
+
categories[tool.category] = { tools: [] };
|
|
310
|
+
}
|
|
311
|
+
categories[tool.category].tools.push({
|
|
312
|
+
name: tool.name,
|
|
313
|
+
description: tool.description,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { categories, totalTools: TOOL_REGISTRY.length };
|
|
318
|
+
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import type { VaultManifest } from "../vault/types.js";
|
|
10
|
-
import {
|
|
11
|
-
import { notFound } from "../utils/errors.js";
|
|
10
|
+
import { readSkillContentWithReferences } from "../vault/manifest.js";
|
|
11
|
+
import { invalidInput, notFound } from "../utils/errors.js";
|
|
12
12
|
import {
|
|
13
13
|
buildSkillToolMetrics,
|
|
14
14
|
estimateTokensFromText,
|
|
@@ -17,10 +17,16 @@ import {
|
|
|
17
17
|
export const skillGetName = "skill_get";
|
|
18
18
|
|
|
19
19
|
export const skillGetDescription =
|
|
20
|
-
"Get
|
|
20
|
+
"Get full content of a specific skill by ID. Returns SKILL.md content and optionally direct referenced markdown files.";
|
|
21
21
|
|
|
22
22
|
export const skillGetSchema = z.object({
|
|
23
23
|
id: z.string().describe("The skill ID (directory name) to retrieve"),
|
|
24
|
+
includeReferences: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(
|
|
28
|
+
"Whether to include direct local markdown references from SKILL.md (default: true)",
|
|
29
|
+
),
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
export async function handleSkillGet(
|
|
@@ -28,14 +34,38 @@ export async function handleSkillGet(
|
|
|
28
34
|
manifest: VaultManifest,
|
|
29
35
|
charsPerToken: number,
|
|
30
36
|
) {
|
|
31
|
-
const { id } = args;
|
|
37
|
+
const { id, includeReferences = true } = args;
|
|
38
|
+
|
|
39
|
+
if (id.startsWith("workflow-") || id.startsWith("agent-")) {
|
|
40
|
+
invalidInput(
|
|
41
|
+
`Skill id "${id}" appears to be a wrapper id. Use workflow/agent routing (for example $workflow-implement-track or $agent-backend-specialist) and call skill_get only for concrete skill ids.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
32
44
|
|
|
33
45
|
const skill = manifest.skills.find((s) => s.id === id);
|
|
34
46
|
if (!skill) {
|
|
35
47
|
notFound("Skill", id);
|
|
36
48
|
}
|
|
37
49
|
|
|
38
|
-
const
|
|
50
|
+
const { skillContent, references } = await readSkillContentWithReferences(
|
|
51
|
+
skill.path,
|
|
52
|
+
includeReferences,
|
|
53
|
+
);
|
|
54
|
+
const referenceSection =
|
|
55
|
+
references.length > 0
|
|
56
|
+
? [
|
|
57
|
+
"",
|
|
58
|
+
"## Referenced Files",
|
|
59
|
+
"",
|
|
60
|
+
...references.flatMap((ref) => [
|
|
61
|
+
`### ${ref.relativePath}`,
|
|
62
|
+
"",
|
|
63
|
+
ref.content.trimEnd(),
|
|
64
|
+
"",
|
|
65
|
+
]),
|
|
66
|
+
].join("\n")
|
|
67
|
+
: "";
|
|
68
|
+
const content = `${skillContent}${referenceSection}`;
|
|
39
69
|
const loadedSkillEstimatedTokens = estimateTokensFromText(
|
|
40
70
|
content,
|
|
41
71
|
charsPerToken,
|
|
@@ -55,6 +85,7 @@ export async function handleSkillGet(
|
|
|
55
85
|
},
|
|
56
86
|
],
|
|
57
87
|
structuredContent: {
|
|
88
|
+
references: references.map((ref) => ({ path: ref.relativePath })),
|
|
58
89
|
metrics,
|
|
59
90
|
},
|
|
60
91
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { mkdtempSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { VaultManifest } from "../vault/types.js";
|
|
@@ -147,6 +147,134 @@ describe("skill tools", () => {
|
|
|
147
147
|
expect(toolMetrics.loadedSkillEstimatedTokens).toBeGreaterThan(0);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
it("includes referenced markdown files in skill_get by default", async () => {
|
|
151
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "mcp-skill-ref-default-"));
|
|
152
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
153
|
+
const referencesDir = path.join(dir, "references");
|
|
154
|
+
mkdirSync(referencesDir, { recursive: true });
|
|
155
|
+
writeFileSync(
|
|
156
|
+
skillFile,
|
|
157
|
+
[
|
|
158
|
+
"---",
|
|
159
|
+
"name: referenced-skill",
|
|
160
|
+
"description: skill with refs",
|
|
161
|
+
"---",
|
|
162
|
+
"# Skill",
|
|
163
|
+
"See [Guide](references/guide.md).",
|
|
164
|
+
].join("\n"),
|
|
165
|
+
"utf8",
|
|
166
|
+
);
|
|
167
|
+
writeFileSync(
|
|
168
|
+
path.join(referencesDir, "guide.md"),
|
|
169
|
+
"# Guide\nReferenced content",
|
|
170
|
+
"utf8",
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const skillBytes = statSync(skillFile).size;
|
|
174
|
+
const manifest: VaultManifest = {
|
|
175
|
+
categories: ["general"],
|
|
176
|
+
skills: [
|
|
177
|
+
{
|
|
178
|
+
id: "referenced-skill",
|
|
179
|
+
category: "general",
|
|
180
|
+
path: skillFile,
|
|
181
|
+
fileBytes: skillBytes,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
fullCatalogBytes: skillBytes,
|
|
185
|
+
fullCatalogEstimatedTokens: Math.ceil(skillBytes / 4),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = await handleSkillGet({ id: "referenced-skill" }, manifest, 4);
|
|
189
|
+
expect(result.content[0].text).toContain("## Referenced Files");
|
|
190
|
+
expect(result.content[0].text).toContain("### references/guide.md");
|
|
191
|
+
expect(result.content[0].text).toContain("Referenced content");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("skips referenced markdown files when includeReferences is false", async () => {
|
|
195
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "mcp-skill-ref-skip-"));
|
|
196
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
197
|
+
const referencesDir = path.join(dir, "references");
|
|
198
|
+
mkdirSync(referencesDir, { recursive: true });
|
|
199
|
+
writeFileSync(
|
|
200
|
+
skillFile,
|
|
201
|
+
[
|
|
202
|
+
"---",
|
|
203
|
+
"name: referenced-skill-no-refs",
|
|
204
|
+
"description: skill with refs",
|
|
205
|
+
"---",
|
|
206
|
+
"# Skill",
|
|
207
|
+
"See [Guide](references/guide.md).",
|
|
208
|
+
].join("\n"),
|
|
209
|
+
"utf8",
|
|
210
|
+
);
|
|
211
|
+
writeFileSync(path.join(referencesDir, "guide.md"), "# Guide", "utf8");
|
|
212
|
+
|
|
213
|
+
const skillBytes = statSync(skillFile).size;
|
|
214
|
+
const manifest: VaultManifest = {
|
|
215
|
+
categories: ["general"],
|
|
216
|
+
skills: [
|
|
217
|
+
{
|
|
218
|
+
id: "referenced-skill-no-refs",
|
|
219
|
+
category: "general",
|
|
220
|
+
path: skillFile,
|
|
221
|
+
fileBytes: skillBytes,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
fullCatalogBytes: skillBytes,
|
|
225
|
+
fullCatalogEstimatedTokens: Math.ceil(skillBytes / 4),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = await handleSkillGet(
|
|
229
|
+
{ id: "referenced-skill-no-refs", includeReferences: false },
|
|
230
|
+
manifest,
|
|
231
|
+
4,
|
|
232
|
+
);
|
|
233
|
+
expect(result.content[0].text).not.toContain("## Referenced Files");
|
|
234
|
+
expect(result.content[0].text).not.toContain("### references/guide.md");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("loads sibling markdown files when SKILL.md has no explicit links", async () => {
|
|
238
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), "mcp-skill-ref-fallback-"));
|
|
239
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
240
|
+
writeFileSync(
|
|
241
|
+
skillFile,
|
|
242
|
+
[
|
|
243
|
+
"---",
|
|
244
|
+
"name: sibling-fallback-skill",
|
|
245
|
+
"description: fallback sibling",
|
|
246
|
+
"---",
|
|
247
|
+
"# Skill",
|
|
248
|
+
].join("\n"),
|
|
249
|
+
"utf8",
|
|
250
|
+
);
|
|
251
|
+
writeFileSync(path.join(dir, "overview.md"), "# Overview\nSibling", "utf8");
|
|
252
|
+
|
|
253
|
+
const skillBytes = statSync(skillFile).size;
|
|
254
|
+
const manifest: VaultManifest = {
|
|
255
|
+
categories: ["general"],
|
|
256
|
+
skills: [
|
|
257
|
+
{
|
|
258
|
+
id: "sibling-fallback-skill",
|
|
259
|
+
category: "general",
|
|
260
|
+
path: skillFile,
|
|
261
|
+
fileBytes: skillBytes,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
fullCatalogBytes: skillBytes,
|
|
265
|
+
fullCatalogEstimatedTokens: Math.ceil(skillBytes / 4),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const result = await handleSkillGet(
|
|
269
|
+
{ id: "sibling-fallback-skill" },
|
|
270
|
+
manifest,
|
|
271
|
+
4,
|
|
272
|
+
);
|
|
273
|
+
expect(result.content[0].text).toContain("## Referenced Files");
|
|
274
|
+
expect(result.content[0].text).toContain("### overview.md");
|
|
275
|
+
expect(result.content[0].text).toContain("Sibling");
|
|
276
|
+
});
|
|
277
|
+
|
|
150
278
|
it("throws when skill_get cannot find the requested skill", async () => {
|
|
151
279
|
const manifest = createManifest();
|
|
152
280
|
await expect(handleSkillGet({ id: "missing" }, manifest, 4)).rejects.toThrow(
|
|
@@ -154,6 +282,20 @@ describe("skill tools", () => {
|
|
|
154
282
|
);
|
|
155
283
|
});
|
|
156
284
|
|
|
285
|
+
it("throws a wrapper guidance error when skill_get receives workflow id", async () => {
|
|
286
|
+
const manifest = createManifest();
|
|
287
|
+
await expect(
|
|
288
|
+
handleSkillGet({ id: "workflow-implement-track" }, manifest, 4),
|
|
289
|
+
).rejects.toThrow("appears to be a wrapper id");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("throws a wrapper guidance error when skill_get receives agent id", async () => {
|
|
293
|
+
const manifest = createManifest();
|
|
294
|
+
await expect(
|
|
295
|
+
handleSkillGet({ id: "agent-backend-specialist" }, manifest, 4),
|
|
296
|
+
).rejects.toThrow("appears to be a wrapper id");
|
|
297
|
+
});
|
|
298
|
+
|
|
157
299
|
it("returns consolidated budget rollup for selected and loaded skills", () => {
|
|
158
300
|
const manifest = createManifest();
|
|
159
301
|
const result = handleSkillBudgetReport(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
extractDescription,
|
|
9
9
|
parseDescriptionFromFrontmatter,
|
|
10
10
|
readFullSkillContent,
|
|
11
|
+
readSkillContentWithReferences,
|
|
11
12
|
} from "./manifest.js";
|
|
12
13
|
|
|
13
14
|
const tempDirs: string[] = [];
|
|
@@ -90,6 +91,75 @@ describe("skill content IO", () => {
|
|
|
90
91
|
|
|
91
92
|
await expect(readFullSkillContent(file)).resolves.toBe(body);
|
|
92
93
|
});
|
|
94
|
+
|
|
95
|
+
it("loads direct local markdown references from SKILL.md", async () => {
|
|
96
|
+
const dir = createTempDir("mcp-refs-read-");
|
|
97
|
+
const file = path.join(dir, "SKILL.md");
|
|
98
|
+
const refsDir = path.join(dir, "references");
|
|
99
|
+
const refFile = path.join(refsDir, "guide.md");
|
|
100
|
+
const nestedRefFile = path.join(dir, "notes.md");
|
|
101
|
+
|
|
102
|
+
writeFileSync(
|
|
103
|
+
file,
|
|
104
|
+
[
|
|
105
|
+
"---",
|
|
106
|
+
"name: ref-read",
|
|
107
|
+
"description: Read refs",
|
|
108
|
+
"---",
|
|
109
|
+
"# Skill",
|
|
110
|
+
"See [Guide](references/guide.md).",
|
|
111
|
+
"See [Notes](notes.md#section).",
|
|
112
|
+
"Ignore [External](https://example.com).",
|
|
113
|
+
].join("\n"),
|
|
114
|
+
"utf8",
|
|
115
|
+
);
|
|
116
|
+
mkdirSync(refsDir, { recursive: true });
|
|
117
|
+
writeFileSync(refFile, "# Guide\nReference body", "utf8");
|
|
118
|
+
writeFileSync(nestedRefFile, "# Notes\nMore details", "utf8");
|
|
119
|
+
|
|
120
|
+
const result = await readSkillContentWithReferences(file, true);
|
|
121
|
+
expect(result.skillContent).toContain("# Skill");
|
|
122
|
+
expect(result.references).toEqual([
|
|
123
|
+
{ relativePath: "references/guide.md", content: "# Guide\nReference body" },
|
|
124
|
+
{ relativePath: "notes.md", content: "# Notes\nMore details" },
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("skips references when includeReferences is false", async () => {
|
|
129
|
+
const dir = createTempDir("mcp-refs-skip-");
|
|
130
|
+
const file = path.join(dir, "SKILL.md");
|
|
131
|
+
const refFile = path.join(dir, "reference.md");
|
|
132
|
+
writeFileSync(
|
|
133
|
+
file,
|
|
134
|
+
["---", "name: skip", "description: skip", "---", "[Ref](reference.md)"].join(
|
|
135
|
+
"\n",
|
|
136
|
+
),
|
|
137
|
+
"utf8",
|
|
138
|
+
);
|
|
139
|
+
writeFileSync(refFile, "ref", "utf8");
|
|
140
|
+
|
|
141
|
+
const result = await readSkillContentWithReferences(file, false);
|
|
142
|
+
expect(result.references).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("falls back to sibling markdown files when SKILL.md has no links", async () => {
|
|
146
|
+
const dir = createTempDir("mcp-refs-fallback-");
|
|
147
|
+
const file = path.join(dir, "SKILL.md");
|
|
148
|
+
const siblingRef = path.join(dir, "overview.md");
|
|
149
|
+
writeFileSync(
|
|
150
|
+
file,
|
|
151
|
+
["---", "name: fallback", "description: fallback", "---", "# Skill"].join(
|
|
152
|
+
"\n",
|
|
153
|
+
),
|
|
154
|
+
"utf8",
|
|
155
|
+
);
|
|
156
|
+
writeFileSync(siblingRef, "# Overview\nSibling content", "utf8");
|
|
157
|
+
|
|
158
|
+
const result = await readSkillContentWithReferences(file, true);
|
|
159
|
+
expect(result.references).toEqual([
|
|
160
|
+
{ relativePath: "overview.md", content: "# Overview\nSibling content" },
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
93
163
|
});
|
|
94
164
|
|
|
95
165
|
describe("manifest enrichment", () => {
|