Skip to content

Commit 8c15da5

Browse files
agentfront[bot]frontegg-david
andcommitted
Cherry-pick: feat: add examples directory support and update metadata schema in skill files
Cherry-picked from #323 (merged to release/1.0.x) Original commit: 6b99368 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
1 parent 4da37fd commit 8c15da5

271 files changed

Lines changed: 20819 additions & 158 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ out-tsc
99
.junie
1010

1111
.env
12+
.env.local
1213

1314
# dependencies
1415
node_modules

docs/frontmcp/sdk-reference/contexts/resource-context.mdx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,51 @@ class DocsApp {}
309309
export default class DocServer {}
310310
```
311311

312+
## Argument Autocompletion
313+
314+
Resource templates can provide autocompletion for their URI parameters. There are two approaches, both with full DI access via `this.get()`:
315+
316+
### Convention-Based (Preferred)
317+
318+
Define a method named `${argName}Completer` on your resource class. The framework discovers it automatically.
319+
320+
```typescript
321+
@ResourceTemplate({
322+
name: 'user-profile',
323+
uriTemplate: 'users://{userId}/profile',
324+
mimeType: 'application/json',
325+
})
326+
class UserProfileResource extends ResourceContext<{ userId: string }> {
327+
async execute(uri: string, params: { userId: string }) {
328+
const user = await this.get(UserService).findById(params.userId);
329+
return { id: user.id, name: user.name };
330+
}
331+
332+
async userIdCompleter(partial: string) {
333+
const users = await this.get(UserService).search(partial);
334+
return { values: users.map(u => u.id), total: users.length };
335+
}
336+
}
337+
```
338+
339+
### Override-Based
340+
341+
Override `getArgumentCompleter(argName)` for dynamic dispatch across multiple parameters.
342+
343+
```typescript
344+
getArgumentCompleter(argName: string): ResourceArgumentCompleter | null {
345+
if (argName === 'userId') {
346+
return async (partial) => {
347+
const users = await this.get(UserService).search(partial);
348+
return { values: users.map(u => u.id) };
349+
};
350+
}
351+
return null;
352+
}
353+
```
354+
355+
Convention-based completers take priority when both are present.
356+
312357
## Related
313358

314359
<CardGroup cols={2}>

libs/cli/src/commands/skills/read.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function stripFrontmatter(content: string): string {
1616

1717
export async function readSkill(
1818
nameOrPath: string,
19-
options: { reference?: string; listRefs?: boolean },
19+
options: { reference?: string; listRefs?: boolean; listExamples?: boolean; examplesForRef?: string },
2020
): Promise<void> {
2121
// Support colon syntax: "skillName:path/to/file.ext"
2222
let skillName = nameOrPath;
@@ -61,6 +61,68 @@ export async function readSkill(
6161
return;
6262
}
6363

64+
// Mode 1b: List examples
65+
const refFilter = options.examplesForRef?.trim();
66+
if (options.listExamples || options.examplesForRef !== undefined) {
67+
if (options.examplesForRef !== undefined && !refFilter) {
68+
console.error(c('red', 'Reference name for --examples cannot be empty.'));
69+
process.exit(1);
70+
}
71+
const refs = entry.references ?? [];
72+
73+
// Collect all examples, optionally filtered by reference
74+
const allExamples: Array<{ ref: string; name: string; level: string; description: string }> = [];
75+
for (const ref of refs) {
76+
if (refFilter && ref.name !== refFilter) continue;
77+
if (!ref.examples || ref.examples.length === 0) continue;
78+
for (const ex of ref.examples) {
79+
allExamples.push({ ref: ref.name, name: ex.name, level: ex.level, description: ex.description });
80+
}
81+
}
82+
83+
if (refFilter && !refs.some((r) => r.name === refFilter)) {
84+
console.error(c('red', `Reference "${refFilter}" not found in skill "${skillName}".`));
85+
console.log(c('gray', `Use 'frontmcp skills read ${skillName} --refs' to list available references.`));
86+
process.exit(1);
87+
}
88+
89+
if (allExamples.length === 0) {
90+
const scope = refFilter ? `reference "${refFilter}"` : `skill "${skillName}"`;
91+
console.log(c('yellow', `No examples found for ${scope}.`));
92+
return;
93+
}
94+
95+
const title = refFilter ? `Examples for ${skillName} > ${refFilter}` : `Examples for ${skillName}`;
96+
console.log(c('bold', `\n ${title}:\n`));
97+
98+
let currentRef = '';
99+
for (const ex of allExamples) {
100+
if (ex.ref !== currentRef) {
101+
currentRef = ex.ref;
102+
console.log(` ${c('cyan', currentRef)}`);
103+
}
104+
const levelTag =
105+
ex.level === 'advanced'
106+
? c('red', ex.level)
107+
: ex.level === 'intermediate'
108+
? c('yellow', ex.level)
109+
: c('green', ex.level);
110+
console.log(` ${c('green', ex.name)} ${c('gray', `[${levelTag}]`)}`);
111+
if (ex.description) {
112+
console.log(` ${c('gray', ex.description)}`);
113+
}
114+
}
115+
console.log('');
116+
console.log(
117+
c(
118+
'gray',
119+
` ${allExamples.length} example(s). Read with: frontmcp skills read ${skillName}:examples/<reference>/<example>.md`,
120+
),
121+
);
122+
console.log('');
123+
return;
124+
}
125+
64126
// Mode 2: Read a specific file (reference or any file in skill dir)
65127
if (filePath) {
66128
// Try exact path first, then references/<name>.md fallback
@@ -122,6 +184,10 @@ export async function readSkill(
122184
console.log(c('gray', ` Has resources: ${entry.hasResources}`));
123185
if (entry.references && entry.references.length > 0) {
124186
console.log(c('gray', ` References: ${entry.references.length} (use --refs to list)`));
187+
const exampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0);
188+
if (exampleCount > 0) {
189+
console.log(c('gray', ` Examples: ${exampleCount} (use --examples to list)`));
190+
}
125191
}
126192
console.log('');
127193
console.log(c('gray', ' ─────────────────────────────────────'));
@@ -134,5 +200,9 @@ export async function readSkill(
134200
console.log(c('gray', ` Install: frontmcp skills install ${skillName} --provider claude`));
135201
if (entry.references && entry.references.length > 0) {
136202
console.log(c('gray', ` References: frontmcp skills read ${skillName} --refs`));
203+
const footerExampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0);
204+
if (footerExampleCount > 0) {
205+
console.log(c('gray', ` Examples: frontmcp skills read ${skillName} --examples`));
206+
}
137207
}
138208
}

libs/cli/src/commands/skills/register.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ export function registerSkillsCommands(program: Command): void {
6868
.argument('<nameOrPath>', 'Skill name or skill:filepath (e.g., frontmcp-dev:references/create-tool.md)')
6969
.argument('[reference]', 'Reference name to read (e.g., create-tool)')
7070
.option('--refs', 'List all available references for the skill')
71-
.action(async (name: string, reference: string | undefined, options: { refs?: boolean }) => {
72-
const { readSkill } = await import('./read.js');
73-
await readSkill(name, { reference, listRefs: options.refs });
74-
});
71+
.option('--examples [reference]', 'List examples for the skill, optionally filtered by reference name')
72+
.action(
73+
async (name: string, reference: string | undefined, options: { refs?: boolean; examples?: boolean | string }) => {
74+
const { readSkill } = await import('./read.js');
75+
await readSkill(name, {
76+
reference,
77+
listRefs: options.refs,
78+
listExamples: options.examples === true ? true : undefined,
79+
examplesForRef: typeof options.examples === 'string' ? options.examples : undefined,
80+
});
81+
},
82+
);
7583
}

libs/sdk/src/common/interfaces/skill.interface.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ export interface SkillReferenceInfo {
2222
filename: string;
2323
}
2424

25+
/**
26+
* Metadata for a resolved example file within a skill's examples/ directory.
27+
*/
28+
export interface SkillExampleInfo {
29+
/** Example name (filename without .md) */
30+
name: string;
31+
/** Short description from frontmatter */
32+
description: string;
33+
/** Parent reference name (examples are grouped by reference) */
34+
reference: string;
35+
/** Complexity level */
36+
level: string;
37+
/** Filename relative to the examples directory */
38+
filename: string;
39+
}
40+
2541
/**
2642
* Full content returned when loading a skill.
2743
* Contains all information needed for an LLM to execute the skill.
@@ -97,6 +113,12 @@ export interface SkillContent {
97113
* Each entry contains name, description, and filename for the reference.
98114
*/
99115
resolvedReferences?: SkillReferenceInfo[];
116+
117+
/**
118+
* Resolved example metadata from the skill's examples/ directory.
119+
* Examples are grouped by reference and contain name, description, level, and filename.
120+
*/
121+
resolvedExamples?: SkillExampleInfo[];
100122
}
101123

102124
/**

libs/sdk/src/common/metadata/skill.metadata.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface SkillResources {
2828
references?: string;
2929
/** Path to assets directory */
3030
assets?: string;
31+
/** Path to examples directory */
32+
examples?: string;
3133
}
3234

3335
/**
@@ -411,6 +413,7 @@ const skillResourcesSchema = z.object({
411413
scripts: z.string().optional(),
412414
references: z.string().optional(),
413415
assets: z.string().optional(),
416+
examples: z.string().optional(),
414417
});
415418

416419
export const skillMetadataSchema = z

0 commit comments

Comments
 (0)