Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ out-tsc
.junie

.env
.env.local

# dependencies
node_modules
Expand Down
45 changes: 45 additions & 0 deletions docs/frontmcp/sdk-reference/contexts/resource-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,51 @@ class DocsApp {}
export default class DocServer {}
```

## Argument Autocompletion

Resource templates can provide autocompletion for their URI parameters. There are two approaches, both with full DI access via `this.get()`:

### Convention-Based (Preferred)

Define a method named `${argName}Completer` on your resource class. The framework discovers it automatically.

```typescript
@ResourceTemplate({
name: 'user-profile',
uriTemplate: 'users://{userId}/profile',
mimeType: 'application/json',
})
class UserProfileResource extends ResourceContext<{ userId: string }> {
async execute(uri: string, params: { userId: string }) {
const user = await this.get(UserService).findById(params.userId);
return { id: user.id, name: user.name };
}

async userIdCompleter(partial: string) {
const users = await this.get(UserService).search(partial);
return { values: users.map(u => u.id), total: users.length };
}
}
```

### Override-Based

Override `getArgumentCompleter(argName)` for dynamic dispatch across multiple parameters.

```typescript
getArgumentCompleter(argName: string): ResourceArgumentCompleter | null {
if (argName === 'userId') {
return async (partial) => {
const users = await this.get(UserService).search(partial);
return { values: users.map(u => u.id) };
};
}
return null;
}
```

Convention-based completers take priority when both are present.

## Related

<CardGroup cols={2}>
Expand Down
72 changes: 71 additions & 1 deletion libs/cli/src/commands/skills/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function stripFrontmatter(content: string): string {

export async function readSkill(
nameOrPath: string,
options: { reference?: string; listRefs?: boolean },
options: { reference?: string; listRefs?: boolean; listExamples?: boolean; examplesForRef?: string },
): Promise<void> {
// Support colon syntax: "skillName:path/to/file.ext"
let skillName = nameOrPath;
Expand Down Expand Up @@ -61,6 +61,68 @@ export async function readSkill(
return;
}

// Mode 1b: List examples
const refFilter = options.examplesForRef?.trim();
if (options.listExamples || options.examplesForRef !== undefined) {
if (options.examplesForRef !== undefined && !refFilter) {
console.error(c('red', 'Reference name for --examples cannot be empty.'));
process.exit(1);
}
const refs = entry.references ?? [];

// Collect all examples, optionally filtered by reference
const allExamples: Array<{ ref: string; name: string; level: string; description: string }> = [];
for (const ref of refs) {
if (refFilter && ref.name !== refFilter) continue;
if (!ref.examples || ref.examples.length === 0) continue;
for (const ex of ref.examples) {
allExamples.push({ ref: ref.name, name: ex.name, level: ex.level, description: ex.description });
}
}

if (refFilter && !refs.some((r) => r.name === refFilter)) {
console.error(c('red', `Reference "${refFilter}" not found in skill "${skillName}".`));
console.log(c('gray', `Use 'frontmcp skills read ${skillName} --refs' to list available references.`));
process.exit(1);
}

if (allExamples.length === 0) {
const scope = refFilter ? `reference "${refFilter}"` : `skill "${skillName}"`;
console.log(c('yellow', `No examples found for ${scope}.`));
return;
}

const title = refFilter ? `Examples for ${skillName} > ${refFilter}` : `Examples for ${skillName}`;
console.log(c('bold', `\n ${title}:\n`));

let currentRef = '';
for (const ex of allExamples) {
if (ex.ref !== currentRef) {
currentRef = ex.ref;
console.log(` ${c('cyan', currentRef)}`);
}
const levelTag =
ex.level === 'advanced'
? c('red', ex.level)
: ex.level === 'intermediate'
? c('yellow', ex.level)
: c('green', ex.level);
console.log(` ${c('green', ex.name)} ${c('gray', `[${levelTag}]`)}`);
if (ex.description) {
console.log(` ${c('gray', ex.description)}`);
}
}
console.log('');
console.log(
c(
'gray',
` ${allExamples.length} example(s). Read with: frontmcp skills read ${skillName}:examples/<reference>/<example>.md`,
),
);
console.log('');
return;
}

// Mode 2: Read a specific file (reference or any file in skill dir)
if (filePath) {
// Try exact path first, then references/<name>.md fallback
Expand Down Expand Up @@ -122,6 +184,10 @@ export async function readSkill(
console.log(c('gray', ` Has resources: ${entry.hasResources}`));
if (entry.references && entry.references.length > 0) {
console.log(c('gray', ` References: ${entry.references.length} (use --refs to list)`));
const exampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0);
if (exampleCount > 0) {
console.log(c('gray', ` Examples: ${exampleCount} (use --examples to list)`));
}
}
console.log('');
console.log(c('gray', ' ─────────────────────────────────────'));
Expand All @@ -134,5 +200,9 @@ export async function readSkill(
console.log(c('gray', ` Install: frontmcp skills install ${skillName} --provider claude`));
if (entry.references && entry.references.length > 0) {
console.log(c('gray', ` References: frontmcp skills read ${skillName} --refs`));
const footerExampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0);
if (footerExampleCount > 0) {
console.log(c('gray', ` Examples: frontmcp skills read ${skillName} --examples`));
}
}
}
16 changes: 12 additions & 4 deletions libs/cli/src/commands/skills/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,16 @@ export function registerSkillsCommands(program: Command): void {
.argument('<nameOrPath>', 'Skill name or skill:filepath (e.g., frontmcp-dev:references/create-tool.md)')
.argument('[reference]', 'Reference name to read (e.g., create-tool)')
.option('--refs', 'List all available references for the skill')
.action(async (name: string, reference: string | undefined, options: { refs?: boolean }) => {
const { readSkill } = await import('./read.js');
await readSkill(name, { reference, listRefs: options.refs });
});
.option('--examples [reference]', 'List examples for the skill, optionally filtered by reference name')
.action(
async (name: string, reference: string | undefined, options: { refs?: boolean; examples?: boolean | string }) => {
const { readSkill } = await import('./read.js');
await readSkill(name, {
reference,
listRefs: options.refs,
listExamples: options.examples === true ? true : undefined,
examplesForRef: typeof options.examples === 'string' ? options.examples : undefined,
});
},
);
}
22 changes: 22 additions & 0 deletions libs/sdk/src/common/interfaces/skill.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ export interface SkillReferenceInfo {
filename: string;
}

/**
* Metadata for a resolved example file within a skill's examples/ directory.
*/
export interface SkillExampleInfo {
/** Example name (filename without .md) */
name: string;
/** Short description from frontmatter */
description: string;
/** Parent reference name (examples are grouped by reference) */
reference: string;
/** Complexity level */
level: string;
/** Filename relative to the examples directory */
filename: string;
}

/**
* Full content returned when loading a skill.
* Contains all information needed for an LLM to execute the skill.
Expand Down Expand Up @@ -97,6 +113,12 @@ export interface SkillContent {
* Each entry contains name, description, and filename for the reference.
*/
resolvedReferences?: SkillReferenceInfo[];

/**
* Resolved example metadata from the skill's examples/ directory.
* Examples are grouped by reference and contain name, description, level, and filename.
*/
resolvedExamples?: SkillExampleInfo[];
}

/**
Expand Down
3 changes: 3 additions & 0 deletions libs/sdk/src/common/metadata/skill.metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface SkillResources {
references?: string;
/** Path to assets directory */
assets?: string;
/** Path to examples directory */
examples?: string;
}

/**
Expand Down Expand Up @@ -411,6 +413,7 @@ const skillResourcesSchema = z.object({
scripts: z.string().optional(),
references: z.string().optional(),
assets: z.string().optional(),
examples: z.string().optional(),
});

export const skillMetadataSchema = z
Expand Down
Loading
Loading