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
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# Skills and Tools

This project uses **FrontMCP skills** installed in `.claude/skills/`.
Before writing code, search the installed skills for relevant guidance:

- **Building components** (tools, resources, prompts, plugins, adapters) — check `frontmcp-development`
- **Testing** — check `frontmcp-testing`
- **Configuration** (auth, CORS, transport, sessions) — check `frontmcp-config`
- **Deployment** (Docker, Vercel, Lambda, Cloudflare) — check `frontmcp-deployment`
- **Production readiness** (security, performance, reliability) — check `frontmcp-production-readiness`

When you need to implement something, **read the matching skill first** — it contains patterns, examples, verification checklists, and common mistakes to avoid.

# FrontMCP Monorepo - Development Guide

## Repository Structure
Expand Down
17 changes: 16 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@ export default [
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
ignores: ['**/dist', '**/*.d.ts', '**/*.d.ts.map', '**/fixture/libs', 'apps/e2e/**'],
ignores: [
'**/dist',
'**/*.d.ts',
'**/*.d.ts.map',
'**/fixture/libs',
'apps/e2e/**',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.e2e.spec.ts',
'**/*.perf.spec.ts',
'**/*.pw.spec.ts',
'**/__tests__/**',
'**/__test-utils__/**',
],
},
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
Expand Down
4 changes: 2 additions & 2 deletions libs/auth/src/machine-id/machine-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function isDevPersistenceEnabled(): boolean {
function resolveMachineIdPath(): string {
if (isBrowser()) return DEFAULT_MACHINE_ID_PATH;
// Lazy-load path to avoid browser bundling issues
// eslint-disable-next-line @typescript-eslint/no-require-imports

const path = require('path') as typeof import('path');
const machineIdPath = getEnv('MACHINE_ID_PATH') ?? DEFAULT_MACHINE_ID_PATH;
return path.isAbsolute(machineIdPath) ? machineIdPath : path.resolve(getCwd(), machineIdPath);
Expand Down Expand Up @@ -77,7 +77,7 @@ function saveMachineIdAsync(machineId: string): void {
}

const machineIdPath = resolveMachineIdPath();
// eslint-disable-next-line @typescript-eslint/no-require-imports

const path = require('path') as typeof import('path');
const dir = path.dirname(machineIdPath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* TypeScript-only constructs at runtime (the .ts extension is for build-time only).
*/

/* eslint-disable @typescript-eslint/no-require-imports */

/**
* Generate the daemon-client JavaScript source code (CJS module).
Expand Down
53 changes: 47 additions & 6 deletions libs/cli/src/commands/scaffold/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@frontmcp/utils';
import { runInit } from '../../core/tsconfig';
import { getSelfVersion } from '../../core/version';
import { buildSkillsSection } from '../skills/install';
import { clack } from '../../shared/prompts';
// Inline skill manifest types to avoid build dependency on @frontmcp/skills source
interface SkillCatalogEntry {
Expand Down Expand Up @@ -308,16 +309,19 @@ LICENSE

// jest.e2e.config.ts and tsconfig.e2e.json removed — `frontmcp test` auto-generates the correct config

function generateClaudeMd(projectName: string, pm: PackageManager): string {
function generateClaudeMd(
projectName: string,
pm: PackageManager,
skillEntries: { name: string; description: string }[],
): string {
const cfg = PM_CONFIG[pm];
const version = getSelfVersion();
const skillsBlock = buildSkillsSection(version, skillEntries);
return `# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

# Skills and Tools
Use frontmcp skills for code planning, generation, and testing.


${skillsBlock}
## Project Overview

TypeScript MCP (Model Context Protocol) server built with the **FrontMCP** framework. Uses decorator-based architecture with \`@FrontMcp\`, \`@App\`, and \`@Tool\` decorators from \`@frontmcp/sdk\`. Requires Node >= 22.
Expand Down Expand Up @@ -1397,6 +1401,38 @@ async function collectOptions(projectArg?: string, flags?: CreateFlags): Promise
};
}

async function loadSkillEntriesForClaudeMd(
options: Pick<CreateOptions, 'deploymentTarget' | 'skillsBundle'>,
): Promise<{ name: string; description: string }[]> {
const bundle = options.skillsBundle ?? 'recommended';
if (bundle === 'none') return [];

try {
const catalogDir = path.resolve(__dirname, '..', '..', '..', '..', 'skills', 'catalog');
const manifestPath = path.join(catalogDir, 'skills-manifest.json');

let manifestContent: string;
if (await fileExists(manifestPath)) {
manifestContent = await readFile(manifestPath);
} else {
const require_ = createRequire(__filename);
const pkgManifest = require_.resolve('@frontmcp/skills/catalog/skills-manifest.json');
manifestContent = await readFile(pkgManifest);
}
const manifest = JSON.parse(manifestContent) as SkillManifest;
const target = options.deploymentTarget;
return manifest.skills
.filter((s) => {
const targetMatch = s.targets.includes('all') || s.targets.includes(target);
const bundleMatch = s.bundle?.includes(bundle);
return targetMatch && bundleMatch;
})
.map((s) => ({ name: s.name, description: s.description }));
} catch {
return [];
}
}

async function scaffoldSkills(targetDir: string, options: CreateOptions): Promise<void> {
const bundle = options.skillsBundle ?? 'recommended';
if (bundle === 'none') return;
Expand Down Expand Up @@ -1650,7 +1686,12 @@ async function scaffoldProject(options: CreateOptions): Promise<void> {
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'README.md'), generateReadme(options));

// CLAUDE.md and AGENTS.md for AI coding assistants
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'CLAUDE.md'), generateClaudeMd(pkgName, packageManager));
const skillEntries = await loadSkillEntriesForClaudeMd(options);
await scaffoldFileIfMissing(
targetDir,
path.join(targetDir, 'CLAUDE.md'),
generateClaudeMd(pkgName, packageManager, skillEntries),
);
await scaffoldFileIfMissing(targetDir, path.join(targetDir, 'AGENTS.md'), generateAgentsMd(pkgName, packageManager));

// Initialize git repository
Expand Down
221 changes: 221 additions & 0 deletions libs/cli/src/commands/skills/__tests__/install.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/**
* Unit tests for skills install CLAUDE.md generation
*/

import { buildSkillsSection, ensureClaudeMdSkillsInstructions } from '../install';

// Mock dependencies
jest.mock('../../../core/version', () => ({
getSelfVersion: () => '1.0.0-test',
}));

jest.mock('../catalog', () => ({
loadCatalog: () => ({
version: 1,
skills: [
{ name: 'skill-alpha', description: 'Use when you want to build alpha components', tags: [], category: 'dev' },
{ name: 'skill-beta', description: 'Testing helpers and utilities', tags: [], category: 'test' },
],
}),
getCatalogDir: () => '/mock/catalog',
}));

let mockFiles: Record<string, string> = {};

jest.mock('@frontmcp/utils', () => ({
fileExists: jest.fn(async (p: string) => p in mockFiles),
readFile: jest.fn(async (p: string) => mockFiles[p] ?? ''),
writeFile: jest.fn(async (p: string, content: string) => {
mockFiles[p] = content;
}),
ensureDir: jest.fn(),
cp: jest.fn(),
}));

/** Helper: mark both catalog skills as installed under cwd */
function mockInstalledSkills(cwd: string) {
mockFiles[`${cwd}/.claude/skills/skill-alpha/SKILL.md`] = '# skill-alpha';
mockFiles[`${cwd}/.claude/skills/skill-beta/SKILL.md`] = '# skill-beta';
}

beforeEach(() => {
mockFiles = {};
jest.clearAllMocks();
});

describe('buildSkillsSection', () => {
const skills = [
{ name: 'frontmcp-development', description: 'Use when you want to create tools, resources, and prompts' },
{ name: 'frontmcp-testing', description: 'Testing utilities and e2e helpers' },
];

it('should generate section with version markers', () => {
const section = buildSkillsSection('1.2.3', skills);

expect(section).toContain('<!-- frontmcp:skills-start v1.2.3 -->');
expect(section).toContain('<!-- frontmcp:skills-end -->');
});

it('should include auto-generated attribution with version', () => {
const section = buildSkillsSection('1.2.3', skills);

expect(section).toContain('Auto-generated by `frontmcp skills install` (v1.2.3)');
expect(section).toContain('do not edit manually');
});

it('should list all skills from input', () => {
const section = buildSkillsSection('1.0.0', skills);

expect(section).toContain('**frontmcp-development**');
expect(section).toContain('**frontmcp-testing**');
});

it('should strip "Use when you want to" prefix from descriptions', () => {
const section = buildSkillsSection('1.0.0', skills);

expect(section).toContain('create tools, resources, and prompts');
expect(section).not.toContain('Use when you want to create');
});

it('should include the Skills and Tools heading', () => {
const section = buildSkillsSection('1.0.0', skills);

expect(section).toContain('# Skills and Tools');
});

it('should handle empty skills list', () => {
const section = buildSkillsSection('1.0.0', []);

expect(section).toContain('<!-- frontmcp:skills-start');
expect(section).toContain('<!-- frontmcp:skills-end -->');
expect(section).not.toContain('- **');
});
});

describe('ensureClaudeMdSkillsInstructions', () => {
it('should create new CLAUDE.md with installed skills only', async () => {
mockInstalledSkills('/test/project');

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toBeDefined();
expect(content).toContain('<!-- frontmcp:skills-start v1.0.0-test -->');
expect(content).toContain('<!-- frontmcp:skills-end -->');
expect(content).toContain('**skill-alpha**');
expect(content).toContain('**skill-beta**');
});

it('should only list installed skills, not full catalog', async () => {
// Only install skill-alpha, not skill-beta
mockFiles['/test/project/.claude/skills/skill-alpha/SKILL.md'] = '# skill-alpha';

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toContain('**skill-alpha**');
expect(content).not.toContain('**skill-beta**');
});

it('should replace existing marker-bounded block on re-run', async () => {
mockInstalledSkills('/test/project');
mockFiles['/test/project/CLAUDE.md'] = [
'# My Project',
'',
'<!-- frontmcp:skills-start v0.9.0 -->',
'# Skills and Tools',
'Old content here',
'<!-- frontmcp:skills-end -->',
'',
'## Other Section',
'User content preserved.',
].join('\n');

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toContain('<!-- frontmcp:skills-start v1.0.0-test -->');
expect(content).not.toContain('v0.9.0');
expect(content).not.toContain('Old content here');
expect(content).toContain('**skill-alpha**');
expect(content).toContain('## Other Section');
expect(content).toContain('User content preserved.');
});

it('should migrate legacy "# Skills and Tools" header to marker format', async () => {
mockInstalledSkills('/test/project');
mockFiles['/test/project/CLAUDE.md'] = [
'# Skills and Tools',
'Some old hardcoded content.',
'',
'# Other Heading',
'User content here.',
].join('\n');

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toContain('<!-- frontmcp:skills-start v1.0.0-test -->');
expect(content).toContain('<!-- frontmcp:skills-end -->');
expect(content).not.toContain('Some old hardcoded content');
expect(content).toContain('# Other Heading');
expect(content).toContain('User content here.');
});

it('should migrate legacy "# Skills and Tools" when next heading is ## level', async () => {
mockInstalledSkills('/test/project');
mockFiles['/test/project/CLAUDE.md'] = [
'# Skills and Tools',
'Some old hardcoded content.',
'',
'## Other Heading',
'User content here.',
].join('\n');

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toContain('<!-- frontmcp:skills-start v1.0.0-test -->');
expect(content).toContain('<!-- frontmcp:skills-end -->');
expect(content).not.toContain('Some old hardcoded content');
expect(content).toContain('## Other Heading');
expect(content).toContain('User content here.');
});

it('should prepend block when CLAUDE.md exists without any skills section', async () => {
mockInstalledSkills('/test/project');
mockFiles['/test/project/CLAUDE.md'] = ['# My Project', '', '## Commands', 'Run yarn dev.'].join('\n');

await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
// Skills block should be at the top
expect(content.indexOf('<!-- frontmcp:skills-start')).toBeLessThan(content.indexOf('# My Project'));
// Original content preserved
expect(content).toContain('## Commands');
expect(content).toContain('Run yarn dev.');
});

it('should not produce duplicate marker blocks', async () => {
mockInstalledSkills('/test/project');
// Run twice
await ensureClaudeMdSkillsInstructions('/test/project');
await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
const startCount = (content.match(/<!-- frontmcp:skills-start/g) ?? []).length;
const endCount = (content.match(/<!-- frontmcp:skills-end -->/g) ?? []).length;
expect(startCount).toBe(1);
expect(endCount).toBe(1);
});

it('should produce empty skills list when no skills are installed', async () => {
// No skills installed (no SKILL.md files in mockFiles)
await ensureClaudeMdSkillsInstructions('/test/project');

const content = mockFiles['/test/project/CLAUDE.md'];
expect(content).toContain('<!-- frontmcp:skills-start');
expect(content).not.toContain('**skill-alpha**');
expect(content).not.toContain('**skill-beta**');
});
});
Loading
Loading