Skip to content

Commit 5bd6928

Browse files
Askirclaude
andcommitted
feat(mcp): add get_skill_guide tool for on-demand skill content
Exposes skill guides (create-workflow, compile-workflow, refine-node, integrations) as an MCP tool so local Claude Code can fetch detailed procedural instructions on demand without bloating the base context. - New get_skill_guide MCP tool: list guides or fetch by name - Skills bundled into npm package during build (cp from monorepo root) - Updated CRAYON_CONTEXT to reference available guides - Fallback path resolution for monorepo dev mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2243ed1 commit 5bd6928

File tree

7 files changed

+268
-3
lines changed

7 files changed

+268
-3
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ coverage/
3333

3434
# Turbo
3535
.turbo/
36+
37+
# Build artifact: skills copied into packages/core during build
38+
packages/core/skills/

packages/core/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323
},
2424
"files": [
2525
"dist",
26-
"templates"
26+
"templates",
27+
"skills"
2728
],
2829
"scripts": {
29-
"build": "tsc --build && vite build dev-ui && chmod +x dist/cli/index.js",
30+
"build": "tsc --build && vite build dev-ui && chmod +x dist/cli/index.js && rm -rf ./skills && cp -r ../../skills ./skills",
3031
"test": "vitest run",
3132
"test:watch": "vitest"
3233
},

packages/core/src/cli/mcp/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export const packageRoot = join(__dirname, "..", "..", "..");
1010
// Templates directory (bundled with the package)
1111
export const templatesDir = join(packageRoot, "templates");
1212

13+
// Skills directory (bundled with package at build time, copied from monorepo root)
14+
export const skillsDir = join(packageRoot, "skills");
15+
1316
// Read version from package.json
1417
const pkg = JSON.parse(
1518
readFileSync(join(packageRoot, "package.json"), "utf-8"),
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { readdir, readFile } from "node:fs/promises";
2+
import { existsSync } from "node:fs";
3+
import { join, relative } from "node:path";
4+
import matter from "gray-matter";
5+
import { skillsDir } from "../config.js";
6+
7+
export interface SkillGuideEntry {
8+
name: string;
9+
description: string;
10+
}
11+
12+
export interface SkillGuideContent {
13+
name: string;
14+
description: string;
15+
content: string;
16+
}
17+
18+
/**
19+
* Resolve the skills directory. Checks the package-bundled location first,
20+
* then falls back to the monorepo root (for dev without build).
21+
*/
22+
function resolveSkillsDir(): string | null {
23+
if (existsSync(skillsDir)) return skillsDir;
24+
// Dev mode fallback: monorepo root
25+
const monorepoSkills = join(skillsDir, "..", "..", "skills");
26+
if (existsSync(monorepoSkills)) return monorepoSkills;
27+
return null;
28+
}
29+
30+
/**
31+
* Recursively collect files from a directory.
32+
*/
33+
async function walkDir(dir: string): Promise<string[]> {
34+
const entries = await readdir(dir, { withFileTypes: true });
35+
const files: string[] = [];
36+
for (const entry of entries) {
37+
const full = join(dir, entry.name);
38+
if (entry.isDirectory()) {
39+
files.push(...(await walkDir(full)));
40+
} else {
41+
files.push(full);
42+
}
43+
}
44+
return files;
45+
}
46+
47+
/**
48+
* Derive a guide name from a file path relative to the skills directory.
49+
*
50+
* - `create-workflow/SKILL.md` → `"create-workflow"`
51+
* - `integrations/SKILL.md` → `"integrations"`
52+
* - `integrations/salesforce.md` → `"integrations/salesforce"`
53+
* - `integrations/scripts/fetch-schema.ts` → `"integrations/scripts/fetch-schema"`
54+
*/
55+
function deriveGuideName(relPath: string): string {
56+
// SKILL.md → use parent directory name
57+
if (relPath.endsWith("/SKILL.md") || relPath === "SKILL.md") {
58+
const dir = relPath.replace(/\/?SKILL\.md$/, "");
59+
return dir || "index";
60+
}
61+
// Strip extension
62+
return relPath.replace(/\.(md|ts)$/, "");
63+
}
64+
65+
/**
66+
* List all available skill guides with names and descriptions.
67+
*/
68+
export async function listSkillGuides(): Promise<SkillGuideEntry[]> {
69+
const dir = resolveSkillsDir();
70+
if (!dir) return [];
71+
72+
const allFiles = await walkDir(dir);
73+
const guides: SkillGuideEntry[] = [];
74+
75+
for (const filePath of allFiles) {
76+
const rel = relative(dir, filePath);
77+
78+
// Skip disabled files
79+
if (rel.includes(".disabled")) continue;
80+
81+
// Only include .md and .ts files
82+
if (!rel.endsWith(".md") && !rel.endsWith(".ts")) continue;
83+
84+
const name = deriveGuideName(rel);
85+
let description = "";
86+
87+
if (rel.endsWith(".md")) {
88+
try {
89+
const raw = await readFile(filePath, "utf-8");
90+
const { data } = matter(raw);
91+
description = (data.description as string) || "";
92+
} catch {
93+
// skip files we can't parse
94+
}
95+
} else {
96+
// For .ts files, use the filename as a description hint
97+
description = `Script template: ${rel}`;
98+
}
99+
100+
guides.push({ name, description });
101+
}
102+
103+
// Sort: top-level skills first, then sub-guides
104+
guides.sort((a, b) => {
105+
const aDepth = a.name.split("/").length;
106+
const bDepth = b.name.split("/").length;
107+
if (aDepth !== bDepth) return aDepth - bDepth;
108+
return a.name.localeCompare(b.name);
109+
});
110+
111+
return guides;
112+
}
113+
114+
/**
115+
* Get the full content of a skill guide by name.
116+
*/
117+
export async function getSkillGuideContent(
118+
name: string,
119+
): Promise<SkillGuideContent | null> {
120+
const dir = resolveSkillsDir();
121+
if (!dir) return null;
122+
123+
// Try resolving the name to a file path
124+
const candidates = [
125+
join(dir, name, "SKILL.md"), // "create-workflow" → create-workflow/SKILL.md
126+
join(dir, `${name}.md`), // "integrations/salesforce" → integrations/salesforce.md
127+
join(dir, `${name}.ts`), // "integrations/scripts/fetch-schema" → integrations/scripts/fetch-schema.ts
128+
];
129+
130+
for (const filePath of candidates) {
131+
if (!existsSync(filePath)) continue;
132+
133+
const raw = await readFile(filePath, "utf-8");
134+
135+
if (filePath.endsWith(".md")) {
136+
const { data, content } = matter(raw);
137+
return {
138+
name,
139+
description: (data.description as string) || "",
140+
content: content.trim(),
141+
};
142+
}
143+
144+
// .ts file — return raw content
145+
return {
146+
name,
147+
description: `Script template: ${relative(dir, filePath)}`,
148+
content: raw,
149+
};
150+
}
151+
152+
return null;
153+
}

packages/core/src/cli/mcp/sandbox-server.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ You have access to crayon MCP tools for building and running AI-native workflows
8080
- \`create_database\` — set up a database
8181
- \`setup_app_schema\` — initialize crayon tables in existing DB
8282
83+
**Skill guides:**
84+
- \`get_skill_guide\` — detailed procedural guides for workflow development
85+
8386
### Workflow Development Pipeline
8487
8588
1. **Design** — create \`src/crayon/workflows/<name>.ts\` with a \`description\` field that captures the flow (task ordering, conditions, loops)
@@ -180,7 +183,23 @@ export const myAgent = Agent.create({
180183
181184
### Integration Gate
182185
183-
Before implementing nodes that use external services, call \`get_connection_info\` to verify the connection exists. If it fails, tell the user to set up the connection in the Dev UI first.`;
186+
Before implementing nodes that use external services, call \`get_connection_info\` to verify the connection exists. If it fails, tell the user to set up the connection in the Dev UI first.
187+
188+
### Skill Guides
189+
190+
Detailed procedural guides are available for complex tasks. Call \`get_skill_guide\` to load them:
191+
192+
| Guide | When to fetch |
193+
|-------|---------------|
194+
| \`create-workflow\` | Designing a new workflow from scratch |
195+
| \`compile-workflow\` | Updating a workflow's run() from descriptions |
196+
| \`refine-node\` | Adding schemas, tools, implementation to nodes |
197+
| \`integrations\` | Index of integration setup guides |
198+
| \`integrations/salesforce\` | Salesforce GraphQL integration setup |
199+
| \`integrations/postgres\` | PostgreSQL query integration setup |
200+
| \`integrations/unlisted\` | Custom integration for unlisted systems |
201+
202+
**Always fetch the relevant guide before starting a complex workflow task.** The guides contain critical steps, connection gates, and templates that ensure correct implementation.`;
184203

185204
/**
186205
* Start the sandbox MCP server in stdio mode.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { ApiFactory } from "@tigerdata/mcp-boilerplate";
2+
import { z } from "zod";
3+
import type { ServerContext } from "../types.js";
4+
import { listSkillGuides, getSkillGuideContent } from "../lib/skills.js";
5+
6+
const inputSchema = {
7+
name: z
8+
.string()
9+
.optional()
10+
.describe(
11+
"Skill guide name (e.g., 'create-workflow', 'integrations/salesforce'). " +
12+
"Omit to list all available guides.",
13+
),
14+
} as const;
15+
16+
const outputSchema = {
17+
guides: z
18+
.array(
19+
z.object({
20+
name: z.string(),
21+
description: z.string(),
22+
}),
23+
)
24+
.optional()
25+
.describe("List of available guides (returned when name is omitted)"),
26+
content: z
27+
.string()
28+
.optional()
29+
.describe("Full guide content (returned when name is provided)"),
30+
error: z.string().optional().describe("Error message if guide not found"),
31+
} as const;
32+
33+
type OutputSchema = {
34+
guides?: { name: string; description: string }[];
35+
content?: string;
36+
error?: string;
37+
};
38+
39+
export const getSkillGuideFactory: ApiFactory<
40+
ServerContext,
41+
typeof inputSchema,
42+
typeof outputSchema
43+
> = () => {
44+
return {
45+
name: "get_skill_guide",
46+
config: {
47+
title: "Get Skill Guide",
48+
description:
49+
"Get crayon skill guides with detailed procedural instructions for workflow development. " +
50+
"Call without a name to list available guides, or with a name to get the full content. " +
51+
"Guides: create-workflow, compile-workflow, refine-node, integrations, " +
52+
"integrations/salesforce, integrations/postgres, integrations/unlisted.",
53+
inputSchema,
54+
outputSchema,
55+
},
56+
fn: async ({ name }): Promise<OutputSchema> => {
57+
try {
58+
if (!name) {
59+
const guides = await listSkillGuides();
60+
if (guides.length === 0) {
61+
return { error: "No skill guides found. The skills directory may not be bundled." };
62+
}
63+
return { guides };
64+
}
65+
66+
const guide = await getSkillGuideContent(name);
67+
if (!guide) {
68+
const guides = await listSkillGuides();
69+
return {
70+
error: `Guide "${name}" not found. Available guides: ${guides.map((g) => g.name).join(", ")}`,
71+
guides,
72+
};
73+
}
74+
75+
return { content: guide.content };
76+
} catch (err) {
77+
return {
78+
error: err instanceof Error ? err.message : String(err),
79+
};
80+
}
81+
},
82+
};
83+
};

packages/core/src/cli/mcp/tools/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { runNodeFactory } from "./runNode.js";
1010
import { listRunsFactory } from "./listRuns.js";
1111
import { getRunFactory } from "./getRun.js";
1212
import { getTraceFactory } from "./getTrace.js";
13+
import { getSkillGuideFactory } from "./getSkillGuide.js";
1314

1415
export async function getApiFactories() {
1516
return [
@@ -25,5 +26,7 @@ export async function getApiFactories() {
2526
listRunsFactory,
2627
getRunFactory,
2728
getTraceFactory,
29+
30+
getSkillGuideFactory,
2831
] as const;
2932
}

0 commit comments

Comments
 (0)