Auto-generated by
frontmcp skills install(v1.1.1) — do not edit manually.
This project uses FrontMCP skills installed in .claude/skills/.
Before writing code, search the installed skills for relevant guidance:
When you need to implement something, read the matching skill first — it contains patterns, examples, and common mistakes to avoid.
- Monorepo: Nx-based monorepo managing multiple TypeScript libraries
- Libraries Location:
/libs/* - each library is independent and publishable
Located in /libs/*:
- cli (
libs/cli) - Command-line interface - sdk (
libs/sdk) - Core FrontMCP SDK - adapters (
libs/adapters) - Framework adapters and integrations - plugins (
libs/plugins) - Plugin system and extensions - skills (
libs/skills) - Curated SKILL.md catalog for scaffold and install tooling
Note:
ast-guard,vectoriadb,enclave-vm,json-schema-to-zod-v3, andmcp-from-openapihave been moved to external repositories.
Located in /apps/*:
- demo (
apps/demo) - Demo application for development and testing
- Build System: Nx (commands:
nx build sdk,nx test ast-guard,nx run-many -t test) - Language: TypeScript with strict mode enabled
- Testing: Jest with 95%+ coverage requirement
- Package Manager: yarn
- Coverage Requirement: 95%+ across all metrics (statements, branches, functions, lines)
- No Warnings: Build must complete without TypeScript warnings
- All Tests Passing: 100% test pass rate required
- Strict TypeScript: Use strict type checking, no
anytypes without justification - Test File Naming: All test files MUST use
.spec.ts(or.spec.tsxfor React components) extension (NOT.test.ts). E2E tests use.e2e.spec.ts, perf tests use.perf.spec.ts, Playwright tests use.pw.spec.ts
// Export everything users need
export { JSAstValidator } from './validator';
export * from './interfaces';
export * from './rules';
export * from './presets';
export * from './errors';
// NO legacy exports!
// ❌ export { JSAstValidator as AstGuard } // WRONG
// ❌ export { AstGuardError as JSAstValidatorError } // WRONG- Purpose: Core FrontMCP SDK for building MCP servers and clients
- Scope: Main package that other libraries build upon
- Purpose: Command-line interface for FrontMCP
- Scope: Developer tooling and utilities
- Purpose: Framework adapters (Express, Fastify, etc.)
- Scope: Integration layer for different frameworks
- Purpose: Plugin system and extensions
- Scope: Extensibility framework
- Purpose: Authentication, session management, credential vault, CIMD, and OAuth extensions
- Scope: Standalone auth library used by SDK and other packages
- Note: All authentication-related code should be placed in this library, not in SDK
- Purpose: Curated SKILL.md catalog for scaffolding and future skill installation
- Scope: Publishable catalog of markdown-based skills organized by category and deployment target
- Structure:
catalog/contains SKILL.md directories;src/has manifest types and loader helpers - Build: Custom asset-aware build that copies
catalog/**into dist (not stock Nx lib generator) - Manifest:
catalog/skills-manifest.jsonis the single source of truth for scaffold filtering and future installer - Adding skills: Create dir in
catalog/<category>/<name>/, addSKILL.md, update manifest, runnx test skills
- Type: Demo application
- Purpose: Development and testing playground for FrontMCP packages
- Usage: Testing integrations, examples, and development workflows
FrontMCP is a TypeScript-first schema validation framework. All types should align with MCP protocol definitions.
- Tools:
execute()returnsPromise<Out>whereOutis the typed output schema - Prompts:
execute()returnsPromise<GetPromptResult>(MCP-defined type) - Resources:
read()returnsPromise<ReadResourceResult>(MCP-defined type)
// ✅ Good - strict MCP protocol types
abstract execute(args: Record<string, string>): Promise<GetPromptResult>;
// ❌ Bad - using unknown defeats TypeScript-first development
abstract execute(args: Record<string, string>): Promise<unknown>;execute/readmethods return strictly typed MCP responsesparseOutput/safeParseOutputnormalize various input shapes to the strict type- Flows finalize output using the entry's parse methods
// In flow finalize stage:
const parseResult = prompt.safeParseOutput(rawOutput);
if (!parseResult.success) throw new InvalidOutputError();
this.respond(parseResult.data); // data is GetPromptResultUse unknown instead of any for generic type defaults (NOT for MCP protocol types):
// ✅ Good - explicit constraint with unknown default
class ResourceContext<
Params extends Record<string, string> = Record<string, string>,
Out = unknown,
> extends ExecutionContextBase<Out>
// ❌ Bad - loose any types
class ResourceContext<In = any, Out = any>Use type parameters with constraints:
// ✅ Good - constrained generic
export type ResourceCtorArgs<Params extends Record<string, string> = Record<string, string>>
// ❌ Bad - unconstrained
export type ResourceCtorArgs<Params = any>Create shared base classes for common functionality:
// ExecutionContextBase provides: get(), tryGet(), scope, fail(), mark(), fetch()
export abstract class ToolContext extends ExecutionContextBase<Out>
export abstract class ResourceContext extends ExecutionContextBase<Out>Use specific error classes with MCP error codes:
export const MCP_ERROR_CODES = {
RESOURCE_NOT_FOUND: -32002,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
PARSE_ERROR: -32700,
} as const;
export class ResourceNotFoundError extends PublicMcpError {
readonly mcpErrorCode = MCP_ERROR_CODES.RESOURCE_NOT_FOUND;
toJsonRpcError() {
return { code: this.mcpErrorCode, message: this.getPublicMessage(), data: { uri: this.uri } };
}
}Avoid non-null assertions - use proper error handling:
// ✅ Good
const rec = this.defs.get(token);
if (!rec) {
throw new DependencyNotFoundError('AuthRegistry', tokenName(token));
}
// ❌ Bad - non-null assertion
const rec = this.defs.get(token)!;Use getCapabilities() for dynamic capability exposure:
// In registry
getCapabilities(): { subscribe: boolean; listChanged: boolean } {
return { subscribe: false, listChanged: this.hasAny() };
}
// In transport adapter
resources: this.scope.resources.getCapabilities(), // Not hardcoded!Use changeScope instead of scope to avoid confusion with Scope class:
export type ResourceChangeEvent = {
kind: ResourceChangeKind;
changeScope: ResourceChangeScope; // Not 'scope'!
// ...
};Validate URIs per RFC 3986 at metadata level:
uri: z.string().min(1).refine(isValidMcpUri, {
message: 'URI must have a valid scheme (e.g., file://, https://, custom://)',
}),Fail fast on invalid hook flows:
const validFlows = ['resources:read-resource', 'resources:list-resources'];
const invalidHooks = allHooks.filter((hook) => !validFlows.includes(hook.metadata.flow));
if (invalidHooks.length > 0) {
throw new InvalidHookFlowError(`Resource "${className}" has hooks for unsupported flows: ${invalidFlowNames}`);
}Centralize record types in common/records:
// Import from common, not from module-specific files
import { AnyResourceRecord } from '../common/records';
// In libs/sdk/src/common/records/resource.record.ts
export type AnyResourceRecord = ResourceRecord | ResourceTemplateRecord;IMPORTANT: Never run git commit or git push commands. The user handles all git operations themselves. Claude should only:
- Create/edit files
- Stage files with
git addif explicitly asked - Check status with
git status,git diff,git log
Never:
git commitgit pushgit commit --amend- Any command that modifies git history
Before completing a task, run the following cleanup:
# Remove unused imports from changed files
node scripts/fix-unused-imports.mjsThis script automatically:
- Finds all files changed in the current branch (compared to main)
- Removes unused imports using ESLint
- Supports custom base branch:
node scripts/fix-unused-imports.mjs <branch-name>
Plugins can extend ExecutionContextBase (ToolContext, etc.) to add properties like this.remember:
- Module Augmentation (TypeScript types):
declare module '@frontmcp/sdk' {
interface ExecutionContextBase {
readonly myProperty: MyType;
}
}- Runtime Extension (prototype modification):
export function installMyContextExtension(): void {
const { ExecutionContextBase } = require('@frontmcp/sdk');
Object.defineProperty(ExecutionContextBase.prototype, 'myProperty', {
get: function () {
return this.get(MyToken);
},
configurable: true,
enumerable: false,
});
}See plugins/plugin-remember/src/remember.context-extension.ts for a complete example.
IMPORTANT: Never import from @modelcontextprotocol/sdk (or any @modelcontextprotocol/* subpath) directly from libs/sdk, libs/adapters, libs/plugins, libs/auth, libs/storage-sqlite, tests, or any app. All MCP protocol types, schemas, error classes (e.g. McpError), transports, and client/server bindings must be imported from @frontmcp/protocol, which is the single, replaceable boundary between FrontMCP and the upstream MCP package.
// ✅ Good — routes through the boundary
// ❌ Bad — locks us into @modelcontextprotocol/sdk at every call site
import { McpError, ToolSchema } from '@modelcontextprotocol/sdk/types.js';
import { CallToolRequestSchema, McpError, type ServerCapabilities, type Tool } from '@frontmcp/protocol';Why this matters:
libs/protocol/src/types.tsis the ONLY file that transitively imports from@modelcontextprotocol/sdk. Every other import must go through@frontmcp/protocolso that dropping or swapping the upstream package is a single-file change.- The
@nx/dependency-checkslint rule innx run sdk:lintenforces this implicitly — adding a direct import tolibs/sdk/src/**will fail the build until the dep is declared onlibs/sdk/package.json, which we explicitly do NOT want to do. - If a type or schema you need isn't reachable via
@frontmcp/protocol, re-export it fromlibs/protocol/src/types.tsrather than reaching around the boundary.
Tests in libs/sdk/src/**/__tests__/*.spec.ts follow the same rule — lint treats them as part of the SDK package.
IMPORTANT: Always use @frontmcp/utils for cryptographic operations. Never use node:crypto directly or implement custom crypto functions.
import {
base64urlDecode, // Base64url decoding
// Encoding
base64urlEncode, // Base64url encoding
decryptAesGcm, // AES-256-GCM decryption
encryptAesGcm, // AES-256-GCM encryption
generateCodeChallenge, // PKCE code challenge (S256)
// PKCE (RFC 7636)
generateCodeVerifier, // PKCE code verifier (43-128 chars)
generatePkcePair, // Generate both verifier and challenge
// Encryption
hkdfSha256, // HKDF-SHA256 key derivation (RFC 5869)
// Random generation
randomBytes, // Cryptographic random bytes
randomUUID, // UUID v4 generation
// Hashing
sha256, // SHA-256 hash (Uint8Array)
sha256Base64url, // SHA-256 hash (base64url string)
sha256Hex, // SHA-256 hash (hex string)
} from '@frontmcp/utils';This ensures cross-platform support (Node.js and browser) with consistent behavior.
// ✅ Good - use utils for PKCE
import { generateCodeVerifier, sha256Base64url } from '@frontmcp/utils';
const verifier = generateCodeVerifier();
const challenge = sha256Base64url(verifier);
// ❌ Bad - custom implementation
private generatePkceVerifier(): string {
const chars = 'ABC...';
// Don't do this - use generateCodeVerifier() instead
}IMPORTANT: Always use @frontmcp/utils for file system operations. Never use fs/promises or node:fs directly.
import {
access, // Check file accessibility
copyFile, // Copy file
cp, // Copy file or directory recursively
ensureDir, // Ensure directory exists
fileExists, // Check if file exists (returns boolean)
isDirEmpty, // Check if directory is empty
mkdir, // Create directory
mkdtemp, // Create temporary directory
readdir, // List directory contents
readFile, // Read file as string
readFileBuffer, // Read file as Buffer
readJSON, // Read and parse JSON file
rename, // Rename/move file or directory
rm, // Remove file or directory
runCmd, // Run command as child process
stat, // Get file/directory stats
unlink, // Delete file
writeFile, // Write content to file
writeJSON, // Write object as JSON
} from '@frontmcp/utils';Benefits:
- Cross-platform support (Node.js only, throws in browser)
- Lazy-loaded modules to avoid import errors in browser builds
- Consistent API across the codebase
- Centralized error handling and logging
IMPORTANT: When creating stores (session stores, elicitation stores, etc.), always use the factory pattern. Never construct stores directly with raw Redis clients.
// ✅ Good - Use factory function
import { createSessionStore } from '@frontmcp/sdk/auth/session';
import { createElicitationStore } from '@frontmcp/sdk/elicitation';
const sessionStore = await createSessionStore({
provider: 'redis',
host: 'localhost',
port: 6379,
keyPrefix: 'mcp:session:',
});
const { store, type } = createElicitationStore({
redis: { provider: 'redis', host: 'localhost', port: 6379 },
keyPrefix: 'mcp:elicit:',
logger,
});
// ❌ Bad - Direct construction with raw Redis client
const Redis = require('ioredis');
const client = new Redis({ host: 'localhost' });
const store = new RedisElicitationStore(client, logger); // DON'T DO THISFactory Pattern Benefits:
- Automatic provider detection (Redis, Vercel KV, etc.)
- Consistent key prefix handling
- Lazy-loading of dependencies (avoids bundling ioredis when not used)
- Built-in error handling and logging
- Support for fallback to memory store in development
- Edge runtime detection and appropriate error messages
Creating New Store Factories:
- Create a factory file (e.g.,
my-store.factory.ts) - Use
RedisOptionstype for configuration - Use type guards (
isRedisProvider(),isVercelKvProvider()) for provider detection - Lazy-require implementations:
const { MyStore } = require('./my.store') - Return store type information:
{ store, type: 'redis' | 'memory' } - Handle Edge runtime restrictions (throw if memory not supported)
See libs/sdk/src/elicitation/elicitation-store.factory.ts for a complete example.
When RememberPlugin is installed, tools can use this.remember and this.approval:
@Tool({ name: 'my_tool' })
class MyTool extends ToolContext {
async execute(input) {
// Store/retrieve session memory
await this.remember.set('key', 'value');
const val = await this.remember.get('key');
// Check tool approval
const approved = await this.approval.isApproved('other-tool');
}
}Use helper functions for feature-specific registration logic:
// ✅ Good - use helper from skill module
import { registerSkillCapabilities } from '../skill/skill-scope.helper';
await registerSkillCapabilities({
skillRegistry: this.scopeSkills,
flowRegistry: this.scopeFlows,
toolRegistry: this.scopeTools,
providers: this.scopeProviders,
skillsConfig: this.metadata.skillsConfig,
logger: this.logger,
});
// ❌ Bad - inline 40+ lines of skill registration logic in scope.instance.tsUse the utility from skill module instead of inline detection:
// ✅ Good - use utility
import { detectSkillsOnlyMode } from '../../skill/skill-mode.utils';
const skillsOnlyMode = detectSkillsOnlyMode(query);
// ❌ Bad - duplicated inline logic
const mode = query?.['mode'];
const skillsOnlyMode = mode === 'skills_only' || (Array.isArray(mode) && mode.includes('skills_only'));Use the SkillVisibility type from common/metadata instead of inline literals:
// ✅ Good - use exported type
import { SkillVisibility } from '../common/metadata/skill.metadata';
private readonly visibility: SkillVisibility;
// ❌ Bad - inline literal union
private readonly visibility: 'mcp' | 'http' | 'both';Use private keyword without underscore prefix, expose via getters:
// ✅ Good - idiomatic TypeScript
private readonly tags: string[];
private readonly priority: number;
private cachedContent?: CachedSkillContent;
getTags(): string[] { return this.tags; }
getPriority(): number { return this.priority; }
// ❌ Bad - underscore prefix pattern
private readonly _tags: string[];
getTags(): string[] { return this._tags; }Use ToolEntry.getInputJsonSchema() for single source of truth:
// ✅ Good - use entry method
const inputSchema = tool.getInputJsonSchema();
// ❌ Bad - duplicated conversion logic
if (tool.rawInputSchema) { ... }
else if (tool.inputSchema) { try { toJSONSchema(z.object(...)) } }Configure via skillsConfig.cache option, supports memory (default) and Redis:
@FrontMcp({
skillsConfig: {
enabled: true,
cache: {
enabled: true,
redis: { provider: 'redis', host: 'localhost' },
ttlMs: 60000,
},
},
})Configure via skillsConfig.auth option:
// API key auth
@FrontMcp({
skillsConfig: {
enabled: true,
auth: 'api-key',
apiKeys: ['sk-xxx', 'sk-yyy'],
},
})
// JWT bearer auth
@FrontMcp({
skillsConfig: {
enabled: true,
auth: 'bearer',
jwt: {
issuer: 'https://auth.example.com',
audience: 'skills-api',
},
},
})❌ Don't: Import from @modelcontextprotocol/sdk directly (use @frontmcp/protocol — that's the only place allowed to touch the upstream package)
❌ Don't: Use node:crypto directly (use @frontmcp/utils for cross-platform support)
❌ Don't: Use fs/promises or node:fs directly (use @frontmcp/utils for consistent file ops)
❌ Don't: Add backwards compatibility exports in new libraries
❌ Don't: Use prefixes like "PT-001" in test names
❌ Don't: Skip constructor validation tests
❌ Don't: Ignore error class instanceof checks in tests
❌ Don't: Use any type without strong justification
❌ Don't: Commit code with test failures or build warnings
❌ Don't: Use non-null assertions (!) - throw proper errors instead
❌ Don't: Mutate rawInput in flows - use state.set() for flow state
❌ Don't: Hardcode capabilities in adapters - use registry.getCapabilities()
❌ Don't: Name event properties scope when they don't refer to Scope class
❌ Don't: Put auth-related code in libs/sdk/src/auth (use libs/auth instead)
❌ Don't: Name test files with .test.ts extension (use .spec.ts instead)
❌ Don't: Use ToolContext<typeof inputSchema> — types are auto-inferred from the @Tool decorator; use plain ToolContext
✅ Do: Use clean, descriptive names for everything
✅ Do: Import all MCP protocol types, schemas, and McpError from @frontmcp/protocol
✅ Do: Use @frontmcp/utils for file system and crypto operations
✅ Do: Test all code paths including errors
✅ Do: Document known limitations clearly
✅ Do: Follow the preset pattern for hierarchical configurations
✅ Do: Achieve 95%+ test coverage
✅ Do: Use strict TypeScript settings
✅ Do: Write comprehensive security documentation
✅ Do: Use unknown instead of any for generic type defaults
✅ Do: Validate hooks match their entry type (fail fast)
✅ Do: Use specific MCP error classes with JSON-RPC codes
✅ Do: Place authentication logic in libs/auth, import via @frontmcp/auth