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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,17 @@ writes nothing — so check the files landed rather than trusting the exit code.
`.agents/` is deliberately **not** in the generated `.gitignore`: the skill is
meant to be committed so the whole team gets the same copy.

### When the skill is absent

The generated README and `AGENTS.md` both have a skill section, and it must
describe what is on disk. `project-generator.ts` therefore copies the skill
**before** rendering any template and passes `withSkills: skillCopied` — the
copy's actual outcome, not the flag, so a failed copy is covered too. Without
the skill, both docs link upstream (`SKILL_URL`) and give `SKILLS_ADD_COMMAND`
instead of pointing at `.agents/`. That command omits `-a universal` on purpose:
whoever opted out gets to pick the agent. The README section lives in
`src/templates/common/skill-readme.ts`, shared by both transports.

## Examples

`examples/` holds complete servers built with the CLI, one folder per example, indexed in `examples/README.md`. They are not part of the published package (`files` is `dist` only) and the root `lint`, `format`, `tsc` and `test` scripts do not touch them.
Expand Down
41 changes: 22 additions & 19 deletions src/project-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,34 @@ export interface ProjectConfig {
export async function generateProject(config: ProjectConfig): Promise<void> {
const { projectName, packageManager, transport, withOAuth, withGitInit, withSkills } = config;

const templateOptions: CommonTemplateOptions = {
withOAuth: transport === 'http' ? withOAuth : false,
packageManager,
transport,
};

const projectPath = join(process.cwd(), projectName);
const srcPath = join(projectPath, 'src');

// Create directories
await mkdir(srcPath, { recursive: true });

// The skill is vendored, so this is a file copy. It never fails the scaffold:
// the project is complete without it. It runs before the templates so the
// README and AGENTS.md describe what is actually on disk — a project without
// the skill must not point at a directory that does not exist.
let skillCopied = false;
if (withSkills) {
const result = await copySkill(projectPath);
skillCopied = result.copied;

if (!result.copied) {
console.log(`\n Could not add the tool-design skill (${result.reason}).`);
console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`);
}
}

const templateOptions: CommonTemplateOptions = {
withOAuth: transport === 'http' ? withOAuth : false,
packageManager,
transport,
withSkills: skillCopied,
};

// Build list of files to write
const filesToWrite: Promise<void>[] = [];

Expand Down Expand Up @@ -132,19 +148,6 @@ export async function generateProject(config: ProjectConfig): Promise<void> {
}
}

// The skill is vendored, so this is a file copy. It never fails the scaffold:
// the project is complete without it.
let skillCopied = false;
if (withSkills) {
const result = await copySkill(projectPath);
skillCopied = result.copied;

if (!result.copied) {
console.log(`\n Could not add the tool-design skill (${result.reason}).`);
console.log(` To add it by hand: ${SKILLS_INSTALL_HINT}`);
}
}

const commands = packageManagerCommands[packageManager];
console.log(`\nCreated ${projectName} with MCP SDK (${transport}) at ${projectPath}`);
console.log(`\nNext steps:`);
Expand Down
11 changes: 11 additions & 0 deletions src/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export const SKILLS_UPDATE_COMMAND = 'npx skills update -p -y';
*/
export const SKILLS_INSTALL_HINT = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME} -a universal`;

/** Where the skill lives upstream, for projects scaffolded without it. */
export const SKILL_URL = `https://github.com/${SKILL_REPO}/tree/main/${SKILL_NAME}`;

/**
* Offered to projects scaffolded with `--no-skills`.
*
* No `-a universal` here: whoever opted out of the vendored copy gets to pick
* which agent the skills CLI installs for.
*/
export const SKILLS_ADD_COMMAND = `npx skills add ${SKILL_REPO} --skill ${SKILL_NAME}`;

/**
* The vendored skill, copied verbatim from `agentailor/skills`.
*
Expand Down
11 changes: 11 additions & 0 deletions src/templates/common/agents.md.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ describe('getAgentsMdTemplate', () => {
expect(template).toContain('Commit `.agents/`');
});

// With --no-skills there is no .agents/ to point at; send the reader upstream.
it('should link upstream and give the install command when the skill is absent', () => {
for (const options of [sdkHttp, sdkStdio]) {
const template = getAgentsMdTemplate(projectName, { ...options, withSkills: false });
expect(template).not.toContain('.agents/');
expect(template).not.toContain('npx skills update');
expect(template).toContain('https://github.com/agentailor/skills/tree/main/tool-design');
expect(template).toContain('npx skills add agentailor/skills --skill tool-design');
}
});

// The copy is vendored, so updating is the user's choice, not automatic.
it("should frame the update as the reader's choice", () => {
const template = getAgentsMdTemplate(projectName, sdkHttp);
Expand Down
23 changes: 21 additions & 2 deletions src/templates/common/agents.md.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { CommonTemplateOptions } from './types.js';
import { SKILLS_UPDATE_COMMAND, SKILLS_INSTALL_HINT, SKILLS_DIR } from '../../skills.js';
import {
SKILLS_UPDATE_COMMAND,
SKILLS_INSTALL_HINT,
SKILLS_DIR,
SKILLS_ADD_COMMAND,
SKILL_URL,
} from '../../skills.js';

/**
* AGENTS.md for the generated project.
Expand All @@ -12,6 +18,7 @@ export function getAgentsMdTemplate(projectName: string, options?: CommonTemplat
const transport = options?.transport ?? 'http';
const withOAuth = options?.withOAuth ?? false;
const packageManager = options?.packageManager ?? 'npm';
const withSkills = options?.withSkills ?? true;

const isStdio = transport === 'stdio';

Expand Down Expand Up @@ -152,7 +159,8 @@ root, and domain logic stays free of MCP imports so it remains testable on its
own.
`;

const skillSection = `
const skillSection = withSkills
? `
## The tool-design skill

\`${SKILLS_DIR}/skills/tool-design/SKILL.md\` is the long form of the guidance above,
Expand All @@ -172,6 +180,17 @@ That is yours to run, not something this project does for you. If the directory
is missing entirely, add it with \`${SKILLS_INSTALL_HINT}\`.

Commit \`${SKILLS_DIR}/\` so everyone working on this project gets the same copy.
`
: `
## The tool-design skill

The guidance above is the short form of Agentailor's
[tool-design](${SKILL_URL}) skill, which has the long form with worked
examples. It is not installed in this project. To add it:

\`\`\`bash
${SKILLS_ADD_COMMAND}
\`\`\`
`;

const conventions = `
Expand Down
28 changes: 28 additions & 0 deletions src/templates/common/skill-readme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { getSkillReadmeSection } from './skill-readme.js';
import { getReadmeTemplate as getHttpReadme } from '../sdk/http/index.js';
import { getReadmeTemplate as getStdioReadme } from '../sdk/stdio/index.js';

describe('getSkillReadmeSection', () => {
it('should point at the installed skill and how to update it', () => {
const section = getSkillReadmeSection(true);
expect(section).toContain('.agents/skills/tool-design/');
expect(section).toContain('npx skills update -p -y');
});

// A --no-skills project has no .agents/; send the reader upstream instead.
it('should link upstream and give the install command when the skill is absent', () => {
const section = getSkillReadmeSection(false);
expect(section).not.toContain('.agents/');
expect(section).not.toContain('npx skills update');
expect(section).toContain('https://github.com/agentailor/skills/tree/main/tool-design');
expect(section).toContain('npx skills add agentailor/skills --skill tool-design');
});

it('should follow withSkills in both transport READMEs', () => {
for (const getReadme of [getHttpReadme, getStdioReadme]) {
expect(getReadme('p')).toContain('.agents/skills/tool-design/');
expect(getReadme('p', { withSkills: false })).not.toContain('.agents/');
}
});
});
37 changes: 37 additions & 0 deletions src/templates/common/skill-readme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SKILLS_ADD_COMMAND, SKILLS_DIR, SKILLS_UPDATE_COMMAND, SKILL_URL } from '../../skills.js';

/**
* The README's "Tool design skill" section, shared by every transport.
*
* Branches on whether the skill is actually in the project: pointing at
* `.agents/skills/tool-design/` in a project scaffolded with `--no-skills` sends
* the reader to a directory that does not exist.
*/
export function getSkillReadmeSection(withSkills: boolean): string {
if (!withSkills) {
return `## Tool design skill

Agentailor's [tool-design](${SKILL_URL}) skill gives a coding agent working here
detailed guidance on designing MCP tools. It is not installed in this project.
To add it:

\`\`\`bash
${SKILLS_ADD_COMMAND}
\`\`\`
`;
}

return `## Tool design skill

Agentailor's [tool-design](${SKILL_URL}) skill is
included at \`${SKILLS_DIR}/skills/tool-design/\`, so a coding agent working here has
the guidance on hand. Commit it so the whole team gets the same copy.

It is the version that shipped with the scaffolding CLI. To check for a newer
one — your call, not automatic:

\`\`\`bash
${SKILLS_UPDATE_COMMAND}
\`\`\`
`;
}
5 changes: 5 additions & 0 deletions src/templates/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type TransportType = 'http' | 'stdio';
*/
export interface BaseTemplateOptions {
packageManager?: PackageManager;
/**
* Whether the tool-design skill is present in the project. Defaults to true.
* The generator sets it from the copy's actual outcome, not just the flag.
*/
withSkills?: boolean;
}

/**
Expand Down
15 changes: 2 additions & 13 deletions src/templates/sdk/http/readme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TemplateOptions } from './index.js';
import { getSkillReadmeSection } from '../../common/skill-readme.js';

/**
* Shared README template, used by both SDK HTTP template types.
Expand Down Expand Up @@ -245,19 +246,7 @@ docker run -p 3000:3000 ${projectName}
instance is discarded between calls.
- Modify the HTTP handler and Express configuration in \`src/index.ts\`${customizationOAuthNote}

## Tool design skill

Agentailor's [tool-design](https://github.com/agentailor/skills) skill is
included at \`.agents/skills/tool-design/\`, so a coding agent working here has
the guidance on hand. Commit it so the whole team gets the same copy.

It is the version that shipped with the scaffolding CLI. To check for a newer
one — your call, not automatic:

\`\`\`bash
npx skills update -p -y
\`\`\`

${getSkillReadmeSection(options?.withSkills ?? true)}
## Learn More

- [Model Context Protocol](https://modelcontextprotocol.io/)
Expand Down
15 changes: 2 additions & 13 deletions src/templates/sdk/stdio/readme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TemplateOptions } from './index.js';
import { getSkillReadmeSection } from '../../common/skill-readme.js';

export function getReadmeTemplate(projectName: string, options?: TemplateOptions): string {
const packageManager = options?.packageManager ?? 'npm';
Expand Down Expand Up @@ -155,19 +156,7 @@ ${projectName}/
imports so it stays testable on its own.
- Modify transport configuration in \`src/index.ts\`

## Tool design skill

Agentailor's [tool-design](https://github.com/agentailor/skills) skill is
included at \`.agents/skills/tool-design/\`, so a coding agent working here has
the guidance on hand. Commit it so the whole team gets the same copy.

It is the version that shipped with the scaffolding CLI. To check for a newer
one — your call, not automatic:

\`\`\`bash
npx skills update -p -y
\`\`\`

${getSkillReadmeSection(options?.withSkills ?? true)}
## Learn More

- [Model Context Protocol](https://modelcontextprotocol.io/)
Expand Down
Loading