diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ef209cfc3..f7055e6e4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,15 @@ name: "CodeQL Advanced" on: push: - branches: ["main"] + branches: + - main + - "next/**" + - "release/**" pull_request: - branches: ["main"] + branches: + - main + - "next/**" + - "release/**" schedule: - cron: "16 7 * * 4" diff --git a/apps/e2e/demo-e2e-resource-providers/e2e/resource-providers.e2e.spec.ts b/apps/e2e/demo-e2e-resource-providers/e2e/resource-providers.e2e.spec.ts new file mode 100644 index 000000000..d902e5e1b --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/e2e/resource-providers.e2e.spec.ts @@ -0,0 +1,173 @@ +/** + * E2E Tests: Resource Provider Resolution & Plugin Context Extensions + * + * Verifies that: + * 1. App-level providers registered via @App({ providers: [...] }) are accessible + * from resources via this.get(Token), sharing the same GLOBAL singleton as tools. + * 2. Plugin providers exposed via contextExtensions (e.g., this.counter) work + * correctly in resource contexts, not just tool contexts. + */ +import { test, expect } from '@frontmcp/testing'; + +/** + * Extract JSON content from a resource read result. + * Resources return { contents: [{ text: '...' }] } so we parse the text. + */ +function extractResourceJson(result: unknown): T { + const raw = result as { raw?: { contents?: Array<{ text?: string }> } }; + const text = raw?.raw?.contents?.[0]?.text; + if (!text) throw new Error('No text content in resource result'); + return JSON.parse(text) as T; +} + +/** + * Extract structured content from a tool call result. + */ +function extractToolJson(result: unknown): T { + const raw = result as { raw?: { structuredContent?: T; content?: Array<{ text?: string }> } }; + if (raw?.raw?.structuredContent) return raw.raw.structuredContent; + const text = raw?.raw?.content?.[0]?.text; + if (!text) throw new Error('No content in tool result'); + return JSON.parse(text) as T; +} + +test.describe('Resource Provider Resolution E2E', () => { + test.use({ + server: 'apps/e2e/demo-e2e-resource-providers/src/main.ts', + project: 'demo-e2e-resource-providers', + publicMode: true, + }); + + // ─── Discovery ────────────────────────────────────────────────────────── + + test.describe('Discovery', () => { + test('should list all tools', async ({ mcp }) => { + const tools = await mcp.tools.list(); + expect(tools).toContainTool('store_set'); + expect(tools).toContainTool('store_get'); + expect(tools).toContainTool('counter_increment'); + expect(tools).toContainTool('debug_providers'); + }); + + test('should list all resources', async ({ mcp }) => { + const resources = await mcp.resources.list(); + expect(resources).toContainResource('store://contents'); + expect(resources).toContainResource('counter://status'); + expect(resources).toContainResource('debug://providers'); + }); + }); + + // ─── App-level provider in resource via this.get() ───────────────────── + + test.describe('App Provider in Resource', () => { + test('tool can resolve app provider via this.get()', async ({ mcp }) => { + const result = await mcp.tools.call('store_set', { key: 'test', value: 'hello' }); + expect(result).toBeSuccessful(); + expect(result).toHaveTextContent('storeInstanceId'); + }); + + test('resource can resolve same app provider via this.get()', async ({ mcp }) => { + const resource = await mcp.resources.read('store://contents'); + expect(resource).toBeSuccessful(); + expect(resource).toHaveTextContent('storeInstanceId'); + }); + + test('resource and tool share the same GLOBAL provider instance', async ({ mcp }) => { + // Store a value via tool + const setResult = await mcp.tools.call('store_set', { key: 'shared-test', value: 'from-tool' }); + expect(setResult).toBeSuccessful(); + + // Read back via resource — should see the same data (same singleton) + const resource = await mcp.resources.read('store://contents'); + expect(resource).toBeSuccessful(); + expect(resource).toHaveTextContent('shared-test'); + expect(resource).toHaveTextContent('from-tool'); + + // Compare storeInstanceId + const toolData = extractToolJson<{ storeInstanceId: string }>(setResult); + const resourceData = extractResourceJson<{ storeInstanceId: string }>(resource); + + expect(toolData.storeInstanceId).toBeDefined(); + expect(resourceData.storeInstanceId).toBeDefined(); + expect(toolData.storeInstanceId).toBe(resourceData.storeInstanceId); + }); + + test('resource sees data written by tool (shared state)', async ({ mcp }) => { + await mcp.tools.call('store_set', { key: 'cross-check', value: 'works' }); + const resource = await mcp.resources.read('store://contents'); + expect(resource).toBeSuccessful(); + expect(resource).toHaveTextContent('cross-check'); + + const getResult = await mcp.tools.call('store_get', { key: 'cross-check' }); + expect(getResult).toBeSuccessful(); + expect(getResult).toHaveTextContent('works'); + }); + }); + + // ─── Plugin context extension in resource ────────────────────────────── + + test.describe('Plugin Context Extension in Resource', () => { + test('tool can access plugin context extension (this.counter)', async ({ mcp }) => { + const result = await mcp.tools.call('counter_increment', {}); + expect(result).toBeSuccessful(); + expect(result).toHaveTextContent('counterInstanceId'); + }); + + test('resource can access plugin context extension (this.counter)', async ({ mcp }) => { + const resource = await mcp.resources.read('counter://status'); + expect(resource).toBeSuccessful(); + expect(resource).toHaveTextContent('counterInstanceId'); + }); + + test('resource and tool share same plugin provider instance', async ({ mcp }) => { + // Increment via tool + const inc1 = await mcp.tools.call('counter_increment', {}); + expect(inc1).toBeSuccessful(); + const inc2 = await mcp.tools.call('counter_increment', {}); + expect(inc2).toBeSuccessful(); + + // Read counter status via resource + const resource = await mcp.resources.read('counter://status'); + expect(resource).toBeSuccessful(); + + // Counter was incremented twice, so count should be >= 2 + const resourceData = extractResourceJson<{ count: number; counterInstanceId: string }>(resource); + expect(resourceData.count).toBeGreaterThanOrEqual(2); + + // Verify same plugin instance + const toolData = extractToolJson<{ counterInstanceId: string }>(inc1); + expect(toolData.counterInstanceId).toBe(resourceData.counterInstanceId); + }); + }); + + // ─── Cross-component consistency ─────────────────────────────────────── + + test.describe('Cross-Component Provider Consistency', () => { + test('multiple resource reads use same provider instance', async ({ mcp }) => { + const res1 = await mcp.resources.read('store://contents'); + const res2 = await mcp.resources.read('store://contents'); + + expect(res1).toBeSuccessful(); + expect(res2).toBeSuccessful(); + + const data1 = extractResourceJson<{ storeInstanceId: string }>(res1); + const data2 = extractResourceJson<{ storeInstanceId: string }>(res2); + expect(data1.storeInstanceId).toBe(data2.storeInstanceId); + }); + + test('debug tool and resource resolve same provider instance', async ({ mcp }) => { + const toolDebug = await mcp.tools.call('debug_providers', {}); + const resourceDebug = await mcp.resources.read('debug://providers'); + + expect(toolDebug).toBeSuccessful(); + expect(resourceDebug).toBeSuccessful(); + + const toolData = extractToolJson<{ storeInstanceId: string }>(toolDebug); + const resourceData = extractResourceJson<{ storeInstanceId: string }>(resourceDebug); + + expect(toolData.storeInstanceId).toBeDefined(); + expect(resourceData.storeInstanceId).toBeDefined(); + expect(toolData.storeInstanceId).toBe(resourceData.storeInstanceId); + }); + }); +}); diff --git a/apps/e2e/demo-e2e-resource-providers/jest.e2e.config.ts b/apps/e2e/demo-e2e-resource-providers/jest.e2e.config.ts new file mode 100644 index 000000000..9abe66e9b --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/jest.e2e.config.ts @@ -0,0 +1,44 @@ +import type { Config } from '@jest/types'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const e2eCoveragePreset = require('../../../jest.e2e.coverage.preset.js'); + +const config: Config.InitialOptions = { + displayName: 'demo-e2e-resource-providers', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + testMatch: ['/e2e/**/*.e2e.spec.ts'], + testTimeout: 60000, + maxWorkers: 1, + setupFilesAfterEnv: ['/../../../libs/testing/src/setup.ts'], + transformIgnorePatterns: ['node_modules/(?!(jose)/)'], + transform: { + '^.+\\.[tj]s$': [ + '@swc/jest', + { + jsc: { + parser: { + syntax: 'typescript', + decorators: true, + }, + transform: { + decoratorMetadata: true, + }, + target: 'es2022', + }, + }, + ], + }, + moduleNameMapper: { + '^@frontmcp/testing$': '/../../../libs/testing/src/index.ts', + '^@frontmcp/sdk$': '/../../../libs/sdk/src/index.ts', + '^@frontmcp/adapters$': '/../../../libs/adapters/src/index.ts', + '^@frontmcp/auth$': '/../../../libs/auth/src/index.ts', + '^@frontmcp/utils$': '/../../../libs/utils/src/index.ts', + }, + coverageDirectory: '../../../coverage/e2e/demo-e2e-resource-providers', + ...e2eCoveragePreset, +}; + +export default config; diff --git a/apps/e2e/demo-e2e-resource-providers/project.json b/apps/e2e/demo-e2e-resource-providers/project.json new file mode 100644 index 000000000..06bb4ae23 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/project.json @@ -0,0 +1,54 @@ +{ + "name": "demo-e2e-resource-providers", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/e2e/demo-e2e-resource-providers/src", + "projectType": "application", + "tags": ["scope:demo", "type:e2e", "feature:resource-providers"], + "targets": { + "build": { + "executor": "@nx/webpack:webpack", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "development", + "options": { + "target": "node", + "compiler": "tsc", + "outputPath": "dist/apps/e2e/demo-e2e-resource-providers", + "main": "apps/e2e/demo-e2e-resource-providers/src/main.ts", + "tsConfig": "apps/e2e/demo-e2e-resource-providers/tsconfig.app.json", + "webpackConfig": "apps/e2e/demo-e2e-resource-providers/webpack.config.js", + "generatePackageJson": true + }, + "configurations": { + "development": {}, + "production": { + "optimization": true + } + } + }, + "serve": { + "executor": "nx:run-commands", + "dependsOn": ["build"], + "options": { + "command": "node dist/apps/e2e/demo-e2e-resource-providers/main.js", + "cwd": "{workspaceRoot}" + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/apps/e2e/demo-e2e-resource-providers"], + "options": { + "jestConfig": "apps/e2e/demo-e2e-resource-providers/jest.e2e.config.ts", + "passWithNoTests": true + } + }, + "test:e2e": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/apps/e2e/demo-e2e-resource-providers-e2e"], + "options": { + "jestConfig": "apps/e2e/demo-e2e-resource-providers/jest.e2e.config.ts", + "runInBand": true, + "passWithNoTests": true + } + } + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/index.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/index.ts new file mode 100644 index 000000000..82ac57892 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/index.ts @@ -0,0 +1,19 @@ +import { App } from '@frontmcp/sdk'; +import { DataStoreService } from './providers/data-store.provider'; +import { CounterPlugin } from '../../plugins/counter/counter.plugin'; +import StoreSetTool from './tools/store-set.tool'; +import StoreGetTool from './tools/store-get.tool'; +import CounterIncrementTool from './tools/counter-increment.tool'; +import DebugProvidersTool from './tools/debug-providers.tool'; +import StoreContentsResource from './resources/store-contents.resource'; +import CounterStatusResource from './resources/counter-status.resource'; +import DebugProvidersResource from './resources/debug-providers.resource'; + +@App({ + name: 'main', + providers: [DataStoreService], + plugins: [CounterPlugin], + tools: [StoreSetTool, StoreGetTool, CounterIncrementTool, DebugProvidersTool], + resources: [StoreContentsResource, CounterStatusResource, DebugProvidersResource], +}) +export class MainApp {} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/providers/data-store.provider.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/providers/data-store.provider.ts new file mode 100644 index 000000000..f8b8bdace --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/providers/data-store.provider.ts @@ -0,0 +1,35 @@ +import { Provider, ProviderScope } from '@frontmcp/sdk'; +import type { Token } from '@frontmcp/di'; + +export const DATA_STORE_TOKEN: Token = Symbol('DataStore'); + +export interface DataStoreEntry { + key: string; + value: string; + createdAt: number; +} + +@Provider({ + name: 'DataStoreService', + scope: ProviderScope.GLOBAL, +}) +export class DataStoreService { + private readonly store = new Map(); + readonly instanceId = `store-${Math.random().toString(36).substring(2, 10)}`; + + set(key: string, value: string): void { + this.store.set(key, { key, value, createdAt: Date.now() }); + } + + get(key: string): DataStoreEntry | undefined { + return this.store.get(key); + } + + getAll(): DataStoreEntry[] { + return Array.from(this.store.values()); + } + + getInstanceId(): string { + return this.instanceId; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/counter-status.resource.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/counter-status.resource.ts new file mode 100644 index 000000000..eb46faaf0 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/counter-status.resource.ts @@ -0,0 +1,23 @@ +import { Resource, ResourceContext } from '@frontmcp/sdk'; + +/** + * Resource that accesses the CounterPlugin via context extension (this.counter). + * + * BUG UNDER TEST: Plugin context extensions should work in resources the same + * way they work in tools. If the plugin's exported provider is not in the + * resource's provider hierarchy, this.counter will throw + * ProviderNotRegisteredError. + */ +@Resource({ + uri: 'counter://status', + name: 'Counter Status', + description: 'Reads counter status via plugin context extension', + mimeType: 'application/json', +}) +export default class CounterStatusResource extends ResourceContext { + async execute() { + const count = this.counter.getCount(); + const instanceId = this.counter.getInstanceId(); + return { count, counterInstanceId: instanceId }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/debug-providers.resource.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/debug-providers.resource.ts new file mode 100644 index 000000000..dc1046ba6 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/debug-providers.resource.ts @@ -0,0 +1,27 @@ +import { Resource, ResourceContext } from '@frontmcp/sdk'; +import { DataStoreService } from '../providers/data-store.provider'; + +@Resource({ + uri: 'debug://providers', + name: 'Debug Providers', + description: 'Debug resource that reports provider resolution details', + mimeType: 'application/json', +}) +export default class DebugProvidersResource extends ResourceContext { + async execute() { + let storeInstanceId = 'NOT_RESOLVED'; + let error = ''; + + try { + const store = this.get(DataStoreService); + storeInstanceId = store.getInstanceId(); + } catch (e: unknown) { + error = e instanceof Error ? `${e.constructor.name}: ${e.message}` : String(e); + } + + return { + storeInstanceId, + error: error || undefined, + }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/store-contents.resource.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/store-contents.resource.ts new file mode 100644 index 000000000..ecd7f1f98 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/resources/store-contents.resource.ts @@ -0,0 +1,27 @@ +import { Resource, ResourceContext } from '@frontmcp/sdk'; +import { DataStoreService } from '../providers/data-store.provider'; + +/** + * Static resource that reads from the GLOBAL DataStoreService via DI. + * + * BUG UNDER TEST: Resources should be able to use this.get() to resolve + * app-level providers, the same way tools do. If DI doesn't work in resources, + * this resource will throw ProviderNotRegisteredError. + */ +@Resource({ + uri: 'store://contents', + name: 'Store Contents', + description: 'Lists all entries in the data store using DI-resolved provider', + mimeType: 'application/json', +}) +export default class StoreContentsResource extends ResourceContext { + async execute() { + const store = this.get(DataStoreService); + const entries = store.getAll(); + return { + entries, + storeInstanceId: store.getInstanceId(), + count: entries.length, + }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/counter-increment.tool.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/counter-increment.tool.ts new file mode 100644 index 000000000..9f48d291a --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/counter-increment.tool.ts @@ -0,0 +1,21 @@ +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'counter_increment', + description: 'Increment the counter using plugin context extension (this.counter)', + inputSchema: {}, + outputSchema: { + count: z.number(), + counterInstanceId: z.string(), + }, +}) +export default class CounterIncrementTool extends ToolContext { + async execute(_input: Record) { + const newCount = this.counter.increment(); + return { + count: newCount, + counterInstanceId: this.counter.getInstanceId(), + }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/debug-providers.tool.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/debug-providers.tool.ts new file mode 100644 index 000000000..cf9935d7c --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/debug-providers.tool.ts @@ -0,0 +1,31 @@ +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { DataStoreService } from '../providers/data-store.provider'; + +@Tool({ + name: 'debug_providers', + description: 'Debug tool that reports provider resolution details', + inputSchema: {}, + outputSchema: { + storeInstanceId: z.string(), + error: z.string().optional(), + }, +}) +export default class DebugProvidersTool extends ToolContext { + async execute(_input: Record) { + let storeInstanceId = 'NOT_RESOLVED'; + let error = ''; + + try { + const store = this.get(DataStoreService); + storeInstanceId = store.getInstanceId(); + } catch (e: unknown) { + error = e instanceof Error ? `${e.constructor.name}: ${e.message}` : String(e); + } + + return { + storeInstanceId, + error: error || undefined, + }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-get.tool.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-get.tool.ts new file mode 100644 index 000000000..030567e49 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-get.tool.ts @@ -0,0 +1,27 @@ +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { DataStoreService } from '../providers/data-store.provider'; + +@Tool({ + name: 'store_get', + description: 'Get a value from the GLOBAL DataStoreService provider', + inputSchema: { + key: z.string().describe('Key to retrieve'), + }, + outputSchema: { + found: z.boolean(), + value: z.string().optional(), + storeInstanceId: z.string(), + }, +}) +export default class StoreGetTool extends ToolContext { + async execute(input: { key: string }) { + const store = this.get(DataStoreService); + const entry = store.get(input.key); + return { + found: !!entry, + value: entry?.value, + storeInstanceId: store.getInstanceId(), + }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-set.tool.ts b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-set.tool.ts new file mode 100644 index 000000000..c9652c8ce --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/apps/main/tools/store-set.tool.ts @@ -0,0 +1,23 @@ +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { DataStoreService } from '../providers/data-store.provider'; + +@Tool({ + name: 'store_set', + description: 'Store a key-value pair using the GLOBAL DataStoreService provider', + inputSchema: { + key: z.string().describe('Key to store'), + value: z.string().describe('Value to store'), + }, + outputSchema: { + success: z.boolean(), + storeInstanceId: z.string(), + }, +}) +export default class StoreSetTool extends ToolContext { + async execute(input: { key: string; value: string }) { + const store = this.get(DataStoreService); + store.set(input.key, input.value); + return { success: true, storeInstanceId: store.getInstanceId() }; + } +} diff --git a/apps/e2e/demo-e2e-resource-providers/src/main.ts b/apps/e2e/demo-e2e-resource-providers/src/main.ts new file mode 100644 index 000000000..fa77dedf7 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/main.ts @@ -0,0 +1,20 @@ +import { FrontMcp, LogLevel } from '@frontmcp/sdk'; +import { MainApp } from './apps/main'; + +const port = parseInt(process.env['PORT'] ?? '3121', 10); + +@FrontMcp({ + info: { name: 'Demo E2E Resource Providers', version: '0.1.0' }, + apps: [MainApp], + logging: { level: LogLevel.Warn }, + http: { port }, + auth: { + mode: 'public', + sessionTtl: 3600, + anonymousScopes: ['anonymous'], + }, + transport: { + protocol: { json: true, legacy: true, strictSession: false }, + }, +}) +export default class Server {} diff --git a/apps/e2e/demo-e2e-resource-providers/src/plugins/counter/counter.plugin.ts b/apps/e2e/demo-e2e-resource-providers/src/plugins/counter/counter.plugin.ts new file mode 100644 index 000000000..afaaa4d3e --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/src/plugins/counter/counter.plugin.ts @@ -0,0 +1,59 @@ +import { Plugin, Provider, DynamicPlugin, ProviderScope } from '@frontmcp/sdk'; + +// ── Provider ──────────────────────────────────────────────────────────────── + +@Provider({ + name: 'CounterService', + scope: ProviderScope.GLOBAL, +}) +export class CounterService { + private count = 0; + readonly instanceId = `counter-${Math.random().toString(36).substring(2, 10)}`; + + increment(): number { + return ++this.count; + } + + getCount(): number { + return this.count; + } + + getInstanceId(): string { + return this.instanceId; + } +} + +// ── Module Augmentation ───────────────────────────────────────────────────── + +declare module '@frontmcp/sdk' { + interface ExecutionContextBase { + readonly counter: CounterService; + } +} + +// ── Plugin ────────────────────────────────────────────────────────────────── + +/** + * Plugin that provides a CounterService via DI and exposes it as a context + * extension (`this.counter`). + * + * BUG UNDER TEST: When a plugin declares `providers` and `contextExtensions` + * that reference the same token, the provider must be resolvable from both + * tools AND resources. If plugin-exported providers are not merged into the + * resource provider registry, `this.counter` will throw + * ProviderNotRegisteredError in resource contexts. + */ +@Plugin({ + name: 'counter', + description: 'Counter plugin with context extension for both tools and resources', + providers: [CounterService], + exports: [CounterService], + contextExtensions: [ + { + property: 'counter', + token: CounterService, + errorMessage: 'CounterPlugin is not installed. Add it to your app plugins.', + }, + ], +}) +export class CounterPlugin extends DynamicPlugin> {} diff --git a/apps/e2e/demo-e2e-resource-providers/tsconfig.app.json b/apps/e2e/demo-e2e-resource-providers/tsconfig.app.json new file mode 100644 index 000000000..ac03a0d92 --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "types": ["node"], + "emitDecoratorMetadata": true, + "experimentalDecorators": true + }, + "exclude": ["jest.config.ts", "jest.e2e.config.ts", "src/**/*.spec.ts", "src/**/*.spec.tsx", "e2e/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/apps/e2e/demo-e2e-resource-providers/tsconfig.json b/apps/e2e/demo-e2e-resource-providers/tsconfig.json new file mode 100644 index 000000000..f2fd67cbf --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/apps/e2e/demo-e2e-resource-providers/webpack.config.js b/apps/e2e/demo-e2e-resource-providers/webpack.config.js new file mode 100644 index 000000000..d27dc19ae --- /dev/null +++ b/apps/e2e/demo-e2e-resource-providers/webpack.config.js @@ -0,0 +1,28 @@ +const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); +const { join } = require('path'); + +module.exports = { + output: { + path: join(__dirname, '../../../dist/apps/e2e/demo-e2e-resource-providers'), + ...(process.env.NODE_ENV !== 'production' && { + devtoolModuleFilenameTemplate: '[absolute-resource-path]', + }), + }, + mode: 'development', + devtool: 'eval-cheap-module-source-map', + plugins: [ + new NxAppWebpackPlugin({ + target: 'node', + compiler: 'tsc', + main: './src/main.ts', + sourceMap: true, + tsConfig: './tsconfig.app.json', + assets: [], + externalDependencies: 'all', + optimization: false, + outputHashing: 'none', + generatePackageJson: true, + buildLibsFromSource: true, + }), + ], +}; diff --git a/libs/cli/src/commands/skills/catalog.ts b/libs/cli/src/commands/skills/catalog.ts index 8dfd92c72..286400446 100644 --- a/libs/cli/src/commands/skills/catalog.ts +++ b/libs/cli/src/commands/skills/catalog.ts @@ -8,6 +8,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { TFIDFVectoria } from 'vectoriadb'; +import type { SkillReferenceEntry } from '@frontmcp/skills'; interface SkillEntry { name: string; @@ -18,6 +19,7 @@ interface SkillEntry { hasResources: boolean; tags: string[]; bundle?: string[]; + references?: SkillReferenceEntry[]; } interface SkillManifest { @@ -194,7 +196,6 @@ export function loadCatalog(): SkillManifest { function resolveManifestPath(): string { // Primary: resolve directly from the @frontmcp/skills package try { - // eslint-disable-next-line @typescript-eslint/no-require-imports return require.resolve('@frontmcp/skills/catalog/skills-manifest.json'); } catch { // Not resolvable via subpath — try via package root @@ -202,7 +203,6 @@ function resolveManifestPath(): string { // Fallback: find the package root and navigate to catalog/ try { - // eslint-disable-next-line @typescript-eslint/no-require-imports const pkgJsonPath = require.resolve('@frontmcp/skills/package.json'); const pkgRoot = path.dirname(pkgJsonPath); const manifestPath = path.join(pkgRoot, 'catalog', 'skills-manifest.json'); @@ -286,6 +286,21 @@ function buildSearchableText(skill: SkillEntry): string { // Category (1x weight) parts.push(skill.category); + // Reference names and descriptions (1x weight each) + if (skill.references) { + for (const ref of skill.references) { + const refNameParts = ref.name.split(/[-_.\s]/).filter(Boolean); + parts.push(...refNameParts); + if (ref.description) { + const refTerms = ref.description + .toLowerCase() + .split(/\s+/) + .filter((word) => word.length >= 4 && !STOP_WORDS.has(word)); + parts.push(...refTerms); + } + } + } + return parts.join(' '); } diff --git a/libs/cli/src/commands/skills/install.ts b/libs/cli/src/commands/skills/install.ts index 4952ad844..802a89ecc 100644 --- a/libs/cli/src/commands/skills/install.ts +++ b/libs/cli/src/commands/skills/install.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { c } from '../../core/colors'; -import { ensureDir, fileExists, cp } from '@frontmcp/utils'; +import { ensureDir, fileExists, cp, readFile, writeFile } from '@frontmcp/utils'; import { loadCatalog, getCatalogDir } from './catalog'; const PROVIDER_DIRS: Record = { @@ -8,6 +8,24 @@ const PROVIDER_DIRS: Record = { codex: '.codex/skills', }; +/** The marker we look for in CLAUDE.md to know skills instructions are present. */ +const SKILLS_MARKER = '# Skills and Tools'; + +/** Minimal CLAUDE.md content that instructs Claude to use installed skills. */ +const CLAUDE_MD_SKILLS_SECTION = `# 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. +`; + export interface InstallOptions { provider?: 'claude' | 'codex'; dir?: string; @@ -16,6 +34,31 @@ export interface InstallOptions { category?: string; } +/** + * Ensure CLAUDE.md exists and contains skills usage instructions. + * If the file doesn't exist, creates it with the skills section. + * If it exists but lacks the skills marker, prepends the section. + */ +async function ensureClaudeMdSkillsInstructions(cwd: string): Promise { + const claudeMdPath = path.join(cwd, 'CLAUDE.md'); + + if (await fileExists(claudeMdPath)) { + const content = await readFile(claudeMdPath); + if (content.includes(SKILLS_MARKER)) { + // Already has skills instructions — nothing to do + return; + } + // Exists but missing skills section — prepend it + const updated = CLAUDE_MD_SKILLS_SECTION + '\n' + content; + await writeFile(claudeMdPath, updated); + console.log(`${c('green', '✓')} Updated ${c('cyan', 'CLAUDE.md')} with skills usage instructions`); + } else { + // Create new CLAUDE.md with skills section + await writeFile(claudeMdPath, CLAUDE_MD_SKILLS_SECTION); + console.log(`${c('green', '✓')} Created ${c('cyan', 'CLAUDE.md')} with skills usage instructions`); + } +} + export async function installSkill(name: string | undefined, options: InstallOptions): Promise { const manifest = loadCatalog(); const provider = options.provider ?? 'claude'; @@ -92,4 +135,9 @@ export async function installSkill(name: string | undefined, options: InstallOpt if (skills.length > 1) { console.log(`\n${c('green', '✓')} Installed ${installed}/${skills.length} skills (provider: ${provider})`); } + + // For Claude provider: ensure CLAUDE.md has skills usage instructions + if (provider === 'claude') { + await ensureClaudeMdSkillsInstructions(process.cwd()); + } } diff --git a/libs/cli/src/commands/skills/read.ts b/libs/cli/src/commands/skills/read.ts new file mode 100644 index 000000000..b46b77b4c --- /dev/null +++ b/libs/cli/src/commands/skills/read.ts @@ -0,0 +1,138 @@ +import * as path from 'path'; +import { c } from '../../core/colors'; +import { fileExists, readFile } from '@frontmcp/utils'; +import { loadCatalog, getCatalogDir } from './catalog'; + +/** + * Strip YAML frontmatter from markdown content. + * Returns the body content after the closing `---`. + */ +function stripFrontmatter(content: string): string { + if (!content.startsWith('---')) return content; + const endIdx = content.indexOf('---', 3); + if (endIdx === -1) return content; + return content.substring(endIdx + 3).trim(); +} + +export async function readSkill( + nameOrPath: string, + options: { reference?: string; listRefs?: boolean }, +): Promise { + // Support colon syntax: "skillName:path/to/file.ext" + let skillName = nameOrPath; + let filePath = options.reference; + + if (!filePath && nameOrPath.includes(':')) { + const colonIdx = nameOrPath.indexOf(':'); + skillName = nameOrPath.substring(0, colonIdx); + filePath = nameOrPath.substring(colonIdx + 1); + } + + const manifest = loadCatalog(); + const entry = manifest.skills.find((s) => s.name === skillName); + + if (!entry) { + console.error(c('red', `Skill "${skillName}" not found in catalog.`)); + console.log(c('gray', "Use 'frontmcp skills list' to see available skills.")); + process.exit(1); + } + + const catalogDir = getCatalogDir(); + const skillDir = path.join(catalogDir, entry.path); + + // Mode 1: List references + if (options.listRefs) { + const refs = entry.references ?? []; + if (refs.length === 0) { + console.log(c('yellow', `Skill "${skillName}" has no references.`)); + return; + } + + console.log(c('bold', `\n References for ${skillName}:\n`)); + for (const ref of refs) { + console.log(` ${c('green', ref.name)}`); + if (ref.description) { + console.log(` ${c('gray', ref.description)}`); + } + } + console.log(''); + console.log(c('gray', ` ${refs.length} reference(s). Read with: frontmcp skills read ${skillName} `)); + console.log(c('gray', ` Or use colon syntax: frontmcp skills read ${skillName}:\n`)); + return; + } + + // Mode 2: Read a specific file (reference or any file in skill dir) + if (filePath) { + // Try exact path first, then references/.md fallback + let targetPath = path.join(skillDir, filePath); + + if (!(await fileExists(targetPath))) { + // Try with .md extension in references/ + const refPath = path.join(skillDir, 'references', `${filePath}.md`); + if (await fileExists(refPath)) { + targetPath = refPath; + } else { + // Try with .md extension at the given path + const withMd = path.join(skillDir, `${filePath}.md`); + if (await fileExists(withMd)) { + targetPath = withMd; + } else { + console.error(c('red', `File "${filePath}" not found in skill "${skillName}".`)); + if (entry.references && entry.references.length > 0) { + console.log(c('gray', `Available references: ${entry.references.map((r) => r.name).join(', ')}`)); + } + console.log(c('gray', `Use 'frontmcp skills read ${skillName} --refs' to list all references.`)); + process.exit(1); + } + } + } + + const content = await readFile(targetPath); + const displayName = path.relative(skillDir, targetPath); + + console.log(c('bold', `\n ${skillName} > ${displayName}`)); + console.log(c('gray', ' ─────────────────────────────────────')); + console.log(''); + + // Strip frontmatter for .md files + if (targetPath.endsWith('.md')) { + console.log(stripFrontmatter(content)); + } else { + console.log(content); + } + console.log(''); + return; + } + + // Mode 3: Read main SKILL.md (default) + const skillMd = path.join(skillDir, 'SKILL.md'); + + if (!(await fileExists(skillMd))) { + console.error(c('red', `SKILL.md not found at ${skillMd}`)); + process.exit(1); + } + + const content = await readFile(skillMd); + + console.log(c('bold', `\n ${entry.name}`)); + console.log(c('gray', ` Category: ${entry.category}`)); + console.log(c('gray', ` Tags: ${entry.tags.join(', ')}`)); + console.log(c('gray', ` Targets: ${entry.targets.join(', ')}`)); + console.log(c('gray', ` Bundle: ${entry.bundle?.join(', ') ?? 'none'}`)); + 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)`)); + } + console.log(''); + console.log(c('gray', ' ─────────────────────────────────────')); + console.log(''); + + // Print body (skip frontmatter) + console.log(stripFrontmatter(content)); + + console.log(''); + 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`)); + } +} diff --git a/libs/cli/src/commands/skills/register.ts b/libs/cli/src/commands/skills/register.ts index d8f6918f2..fe059d31d 100644 --- a/libs/cli/src/commands/skills/register.ts +++ b/libs/cli/src/commands/skills/register.ts @@ -63,11 +63,13 @@ export function registerSkillsCommands(program: Command): void { ); skills - .command('show') - .description('Show full details of a skill including instructions') - .argument('', 'Skill name') - .action(async (name: string) => { - const { showSkill } = await import('./show.js'); - await showSkill(name); + .command('read') + .description('Read a skill, its references, or any file in the skill directory') + .argument('', '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 }); }); } diff --git a/libs/cli/src/commands/skills/search.ts b/libs/cli/src/commands/skills/search.ts index 1ba4194ee..76821ce9a 100644 --- a/libs/cli/src/commands/skills/search.ts +++ b/libs/cli/src/commands/skills/search.ts @@ -23,6 +23,6 @@ export async function searchSkills( console.log(''); } - console.log(c('gray', ` ${results.length} result(s). Use 'frontmcp skills show ' for full details.`)); + console.log(c('gray', ` ${results.length} result(s). Use 'frontmcp skills read ' for full details.`)); console.log(c('gray', ` Install: 'frontmcp skills install --provider claude'\n`)); } diff --git a/libs/cli/src/commands/skills/show.ts b/libs/cli/src/commands/skills/show.ts deleted file mode 100644 index 360e4dacf..000000000 --- a/libs/cli/src/commands/skills/show.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as path from 'path'; -import { c } from '../../core/colors'; -import { fileExists, readFile } from '@frontmcp/utils'; -import { loadCatalog, getCatalogDir } from './catalog'; - -export async function showSkill(name: string): Promise { - const manifest = loadCatalog(); - const entry = manifest.skills.find((s) => s.name === name); - - if (!entry) { - console.error(c('red', `Skill "${name}" not found in catalog.`)); - console.log(c('gray', "Use 'frontmcp skills list' to see available skills.")); - process.exit(1); - } - - const catalogDir = getCatalogDir(); - const skillDir = path.join(catalogDir, entry.path); - const skillMd = path.join(skillDir, 'SKILL.md'); - - if (!(await fileExists(skillMd))) { - console.error(c('red', `SKILL.md not found at ${skillMd}`)); - process.exit(1); - } - - const content = await readFile(skillMd); - - console.log(c('bold', `\n ${entry.name}`)); - console.log(c('gray', ` Category: ${entry.category}`)); - console.log(c('gray', ` Tags: ${entry.tags.join(', ')}`)); - console.log(c('gray', ` Targets: ${entry.targets.join(', ')}`)); - console.log(c('gray', ` Bundle: ${entry.bundle?.join(', ') ?? 'none'}`)); - console.log(c('gray', ` Has resources: ${entry.hasResources}`)); - console.log(''); - console.log(c('gray', ' ─────────────────────────────────────')); - console.log(''); - - // Print body (skip frontmatter) - const bodyStart = content.indexOf('---', 3); - if (bodyStart !== -1) { - const body = content.substring(bodyStart + 3).trim(); - console.log(body); - } else { - console.log(content); - } - - console.log(''); - console.log(c('gray', ` Install: frontmcp skills install ${name} --provider claude`)); -} diff --git a/libs/cli/src/core/__tests__/colors.spec.ts b/libs/cli/src/core/__tests__/colors.spec.ts index 956c9ccfc..8e1ba4344 100644 --- a/libs/cli/src/core/__tests__/colors.spec.ts +++ b/libs/cli/src/core/__tests__/colors.spec.ts @@ -2,6 +2,16 @@ import { COLORS, c } from '../colors'; +const originalForceColor = process.env['FORCE_COLOR']; +const originalNoColor = process.env['NO_COLOR']; + +function restoreColorEnv(): void { + if (originalForceColor === undefined) delete process.env['FORCE_COLOR']; + else process.env['FORCE_COLOR'] = originalForceColor; + if (originalNoColor === undefined) delete process.env['NO_COLOR']; + else process.env['NO_COLOR'] = originalNoColor; +} + describe('colors', () => { describe('COLORS', () => { it('should have reset code', () => { @@ -42,6 +52,14 @@ describe('colors', () => { }); describe('c', () => { + beforeEach(() => { + process.env['FORCE_COLOR'] = '1'; + }); + + afterEach(() => { + restoreColorEnv(); + }); + it('should wrap text with red color', () => { const result = c('red', 'error text'); expect(result).toBe('\x1b[31merror text\x1b[0m'); @@ -87,4 +105,58 @@ describe('colors', () => { expect(result).toBe('\x1b[31m\x1b[0m'); }); }); + + describe('NO_COLOR support', () => { + beforeEach(() => { + delete process.env['FORCE_COLOR']; + delete process.env['NO_COLOR']; + }); + + afterEach(() => { + restoreColorEnv(); + }); + + it('should return plain text when NO_COLOR is set', () => { + process.env['NO_COLOR'] = '1'; + const result = c('red', 'error text'); + expect(result).toBe('error text'); + }); + + it('should treat NO_COLOR empty string as set', () => { + process.env['NO_COLOR'] = ''; + const result = c('red', 'error text'); + expect(result).toBe('error text'); + }); + + it('should respect FORCE_COLOR to enable colors', () => { + process.env['FORCE_COLOR'] = '1'; + const result = c('red', 'error text'); + expect(result).toBe('\x1b[31merror text\x1b[0m'); + }); + + it('should disable colors when FORCE_COLOR is "0"', () => { + process.env['FORCE_COLOR'] = '0'; + const result = c('red', 'error text'); + expect(result).toBe('error text'); + }); + + it('should disable colors when FORCE_COLOR is "false"', () => { + process.env['FORCE_COLOR'] = 'false'; + const result = c('red', 'error text'); + expect(result).toBe('error text'); + }); + + it('should prioritize NO_COLOR over FORCE_COLOR', () => { + process.env['NO_COLOR'] = '1'; + process.env['FORCE_COLOR'] = '1'; + const result = c('red', 'error text'); + expect(result).toBe('error text'); + }); + + it('should return plain text when no TTY and no FORCE_COLOR', () => { + // In test environment, process.stdout.isTTY is undefined (not a TTY) + const result = c('bold', 'heading'); + expect(result).toBe('heading'); + }); + }); }); diff --git a/libs/cli/src/core/__tests__/help.spec.ts b/libs/cli/src/core/__tests__/help.spec.ts index e623add57..ef5a06561 100644 --- a/libs/cli/src/core/__tests__/help.spec.ts +++ b/libs/cli/src/core/__tests__/help.spec.ts @@ -10,6 +10,11 @@ describe('customizeHelp', () => { return output; } + it('should contain "Getting Started" section header', () => { + const help = getHelpOutput(); + expect(help).toContain('Getting Started'); + }); + it('should contain "Development" section header', () => { const help = getHelpOutput(); expect(help).toContain('Development'); @@ -25,8 +30,37 @@ describe('customizeHelp', () => { expect(help).toContain('Package Manager'); }); + it('should contain "Skills" section header', () => { + const help = getHelpOutput(); + expect(help).toContain('Skills'); + }); + + it('should place "socket" after Process Manager header', () => { + const help = getHelpOutput(); + const pmIdx = help.indexOf('Process Manager'); + const socketIdx = help.indexOf('socket'); + expect(pmIdx).toBeGreaterThan(-1); + expect(socketIdx).toBeGreaterThan(-1); + expect(socketIdx).toBeGreaterThan(pmIdx); + }); + + it('should show skills subcommands inline', () => { + const help = getHelpOutput(); + expect(help).toContain('skills search'); + expect(help).toContain('skills list'); + expect(help).toContain('skills install'); + expect(help).toContain('skills read'); + }); + it('should list all commands in help output', () => { const help = getHelpOutput(); + // Strip ANSI escape codes before line matching + // eslint-disable-next-line no-control-regex + const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ''); + const helpLines = help.split(/\r?\n/).map((l) => stripAnsi(l).trim()); + + const hasCommandLine = (cmd: string) => helpLines.some((line) => line === cmd || line.startsWith(cmd + ' ')); + const expectedCommands = [ 'dev', 'build', @@ -46,10 +80,16 @@ describe('customizeHelp', () => { 'install', 'uninstall', 'configure', + 'skills search', + 'skills list', + 'skills install', + 'skills read', ]; for (const cmd of expectedCommands) { - expect(help).toContain(cmd); + if (!hasCommandLine(cmd)) { + fail(`Help output missing command '${cmd}'`); + } } }); @@ -60,6 +100,16 @@ describe('customizeHelp', () => { expect(help).toContain('frontmcp build --target node'); }); + it('should contain discoverability footer', () => { + const help = getHelpOutput(); + expect(help).toContain('frontmcp --help'); + }); + + it('should show the improved description', () => { + const help = getHelpOutput(); + expect(help).toContain('Build, test, and deploy MCP servers with FrontMCP'); + }); + it('should show subcommand help for build', () => { const program = createProgram(); const buildCmd = program.commands.find((c) => c.name() === 'build'); diff --git a/libs/cli/src/core/colors.ts b/libs/cli/src/core/colors.ts index 4297c9e29..cbede41b2 100644 --- a/libs/cli/src/core/colors.ts +++ b/libs/cli/src/core/colors.ts @@ -10,4 +10,12 @@ export const COLORS = { gray: '\x1b[90m', } as const; -export const c = (color: keyof typeof COLORS, s: string) => COLORS[color] + s + COLORS.reset; +function colorsEnabled(): boolean { + if (process.env['NO_COLOR'] !== undefined) return false; + const fc = process.env['FORCE_COLOR']; + if (fc !== undefined) return fc !== '0' && fc.toLowerCase() !== 'false'; + return process.stdout?.isTTY === true; +} + +export const c = (color: keyof typeof COLORS, s: string): string => + colorsEnabled() ? COLORS[color] + s + COLORS.reset : s; diff --git a/libs/cli/src/core/help.ts b/libs/cli/src/core/help.ts index f7cda52b0..c00bd5ddf 100644 --- a/libs/cli/src/core/help.ts +++ b/libs/cli/src/core/help.ts @@ -1,25 +1,79 @@ import { Command } from 'commander'; import { c } from './colors'; -/** Group labels in display order, mapped to the command names they contain. */ +/** + * Command groups in display order, mapped to the command names they contain. + * The `skills` group is handled separately to show subcommands inline. + */ const GROUPS: [label: string, commands: string[]][] = [ - ['Development', ['dev', 'build', 'test', 'init', 'doctor', 'inspector', 'create', 'socket']], - ['Skills', ['skills']], - ['Process Manager', ['start', 'stop', 'restart', 'status', 'list', 'logs', 'service']], + ['Getting Started', ['create', 'init', 'doctor']], + ['Development', ['dev', 'build', 'test', 'inspector']], + ['Process Manager', ['start', 'stop', 'restart', 'status', 'list', 'logs', 'socket', 'service']], ['Package Manager', ['install', 'uninstall', 'configure']], ]; +const EXAMPLES: string[] = [ + 'npx frontmcp create my-mcp # Scaffold a new project', + 'frontmcp dev # Start dev server with hot-reload', + 'frontmcp build --target node # Build for Node.js deployment', + 'frontmcp start my-app --port 3005 # Start managed server', + 'frontmcp install @company/my-mcp # Install from npm registry', + 'frontmcp skills search "openapi" # Find skills in catalog', +]; + +/** Format a top-level command line with cyan name and dim arg placeholders. */ +function formatCommandLine(sub: Command, padWidth: number): string { + const rawName = sub.name(); + const rawArgs = sub.registeredArguments.map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`)).join(' '); + const rawTerm = rawArgs ? `${rawName} ${rawArgs}` : rawName; + + const name = c('cyan', rawName); + const args = sub.registeredArguments.map((a) => c('dim', a.required ? `<${a.name()}>` : `[${a.name()}]`)).join(' '); + const term = args ? `${name} ${args}` : name; + + const padding = ' '.repeat(Math.max(2, padWidth - rawTerm.length + 2)); + return ` ${term}${padding}${sub.description()}`; +} + +/** Format a skills subcommand as "skills ". */ +function formatSkillsLine(sub: Command, padWidth: number): string { + const rawPrefix = `skills ${sub.name()}`; + const rawArgs = sub.registeredArguments.map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`)).join(' '); + const rawTerm = rawArgs ? `${rawPrefix} ${rawArgs}` : rawPrefix; + + const prefix = c('cyan', rawPrefix); + const args = sub.registeredArguments.map((a) => c('dim', a.required ? `<${a.name()}>` : `[${a.name()}]`)).join(' '); + const term = args ? `${prefix} ${args}` : prefix; + + const padding = ' '.repeat(Math.max(2, padWidth - rawTerm.length + 2)); + return ` ${term}${padding}${sub.description()}`; +} + /** * Apply custom help formatting to the top-level program so that - * `frontmcp --help` groups commands under section headers and appends - * an Examples block. + * `frontmcp --help` groups commands under section headers, shows skills + * subcommands inline, and appends a concise Examples block. */ export function customizeHelp(program: Command): void { program.configureHelp({ formatHelp(cmd, helper) { - const termWidth = helper.padWidth(cmd, helper); + let termWidth = helper.padWidth(cmd, helper); const lines: string[] = []; + // Adjust padWidth to account for skills subcommand terms (e.g. "skills search ") + const allCommands = helper.visibleCommands(cmd); + const skillsCmd = allCommands.find((c) => c.name() === 'skills'); + const skillsSubs = skillsCmd ? helper.visibleCommands(skillsCmd) : []; + if (skillsCmd) { + for (const sub of skillsSubs) { + const rawArgs = sub.registeredArguments + .map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`)) + .join(' '); + const rawTerm = rawArgs ? `skills ${sub.name()} ${rawArgs}` : `skills ${sub.name()}`; + termWidth = Math.max(termWidth, rawTerm.length); + } + } + // Description const desc = helper.commandDescription(cmd); if (desc) lines.push(desc, ''); @@ -29,18 +83,33 @@ export function customizeHelp(program: Command): void { lines.push(` ${helper.commandUsage(cmd)}`, ''); // Grouped commands - const allCommands = cmd.commands; for (const [label, names] of GROUPS) { const matching = names.map((n) => allCommands.find((c) => c.name() === n)).filter(Boolean) as Command[]; if (matching.length === 0) continue; lines.push(c('bold', label)); for (const sub of matching) { - const name = sub.name(); - const args = sub.registeredArguments.map((a) => (a.required ? `<${a.name()}>` : `[${a.name()}]`)).join(' '); - const term = args ? `${name} ${args}` : name; - const padding = ' '.repeat(Math.max(2, termWidth - term.length + 2)); - lines.push(` ${term}${padding}${sub.description()}`); + lines.push(formatCommandLine(sub, termWidth)); + } + lines.push(''); + } + + // Skills — show subcommands inline + if (skillsCmd && skillsSubs.length > 0) { + lines.push(c('bold', 'Skills')); + for (const sub of skillsSubs) { + lines.push(formatSkillsLine(sub, termWidth)); + } + lines.push(''); + } + + // Other commands not in any group + const renderedNames = new Set([...GROUPS.flatMap(([, names]) => names), 'skills']); + const other = allCommands.filter((sc) => !renderedNames.has(sc.name())); + if (other.length > 0) { + lines.push(c('bold', 'Other')); + for (const sub of other) { + lines.push(formatCommandLine(sub, termWidth)); } lines.push(''); } @@ -61,35 +130,14 @@ export function customizeHelp(program: Command): void { }, }); - program.addHelpText( - 'after', - ` -${c('bold', 'Examples')} - frontmcp dev - frontmcp build --target node - frontmcp build --target cli - frontmcp build --target cli --js - frontmcp test --runInBand - frontmcp init - frontmcp doctor - frontmcp inspector - npx frontmcp create # Interactive mode - npx frontmcp create my-mcp --yes # Use defaults - npx frontmcp create my-mcp --target vercel # Vercel deployment - frontmcp socket ./src/main.ts --socket /tmp/my-app.sock - frontmcp start my-app --entry ./src/main.ts --port 3005 - frontmcp stop my-app - frontmcp logs my-app --follow - frontmcp service install my-app - frontmcp skills list - frontmcp skills search "openapi adapter" - frontmcp skills install frontmcp-development -p claude - frontmcp skills install --all -p claude - frontmcp install @company/my-mcp --registry https://npm.company.com - frontmcp install ./my-local-app - frontmcp install github:user/repo - frontmcp configure my-app - frontmcp uninstall my-app -`, - ); + program.addHelpText('after', () => { + const lines = [ + c('bold', 'Examples'), + ...EXAMPLES.map((ex) => ` ${ex}`), + '', + c('dim', `Use ${c('cyan', 'frontmcp --help')} for detailed usage of any command.`), + '', + ]; + return '\n' + lines.join('\n'); + }); } diff --git a/libs/cli/src/core/program.ts b/libs/cli/src/core/program.ts index 166a69dfe..874b3a2f3 100644 --- a/libs/cli/src/core/program.ts +++ b/libs/cli/src/core/program.ts @@ -11,7 +11,10 @@ import { customizeHelp } from './help'; export function createProgram(): Command { const program = new Command(); - program.name('frontmcp').description('FrontMCP command line interface').version(getSelfVersion(), '-V, --version'); + program + .name('frontmcp') + .description('Build, test, and deploy MCP servers with FrontMCP') + .version(getSelfVersion(), '-V, --version'); registerDevCommands(program); registerBuildCommands(program); diff --git a/libs/sdk/src/common/entries/resource.entry.ts b/libs/sdk/src/common/entries/resource.entry.ts index 2d66d0108..1bdab6954 100644 --- a/libs/sdk/src/common/entries/resource.entry.ts +++ b/libs/sdk/src/common/entries/resource.entry.ts @@ -8,6 +8,8 @@ import { ReadResourceResult, Request, Notification } from '@frontmcp/protocol'; import { RequestHandlerExtra } from '@frontmcp/protocol'; import { AuthInfo } from '@frontmcp/protocol'; import { ProviderRegistryInterface } from '../interfaces/internal'; +import type { ResourceArgumentCompleter } from '../interfaces/resource.interface'; +import ProviderRegistry from '../../provider/provider.registry'; export type ResourceReadExtra = RequestHandlerExtra & { authInfo: AuthInfo; @@ -57,6 +59,12 @@ export abstract class ResourceEntry< */ isTemplate: boolean; + /** + * Get the provider registry for this resource. + * Used by flows to build context-aware providers for CONTEXT-scoped dependencies. + */ + abstract get providers(): ProviderRegistry; + /** * Create a resource context (class or function wrapper). * @param uri The actual URI being read (for templates, this includes resolved params) @@ -81,4 +89,13 @@ export abstract class ResourceEntry< * For templates: pattern match and extract parameters */ abstract matchUri(uri: string): { matches: boolean; params: Params }; + + /** + * Get an argument completer for resource template autocompletion. + * Override in subclasses to provide suggestions for template parameters. + * Returns null by default (no completion available). + */ + getArgumentCompleter(_argName: string): ResourceArgumentCompleter | null { + return null; + } } diff --git a/libs/sdk/src/common/entries/scope.entry.ts b/libs/sdk/src/common/entries/scope.entry.ts index 955bdd4cc..bb68f7681 100644 --- a/libs/sdk/src/common/entries/scope.entry.ts +++ b/libs/sdk/src/common/entries/scope.entry.ts @@ -67,6 +67,31 @@ export abstract class ScopeEntry extends BaseEntry void | Promise> = []; + + /** + * Register a callback to run after the server has started. + * Plugins can use this for post-startup initialization (e.g., warming caches, + * starting background jobs, logging readiness). + */ + onServerStarted(callback: () => void | Promise): void { + this.lifecycleCallbacks.push(callback); + } + + /** + * Emit the server-started lifecycle event. Called by FrontMcpInstance after server.start(). + * @internal + */ + async emitServerStarted(): Promise { + for (const cb of this.lifecycleCallbacks) { + await cb(); + } + } + abstract registryFlows(...flows: FlowType[]): Promise; abstract runFlow( diff --git a/libs/sdk/src/common/interfaces/resource.interface.ts b/libs/sdk/src/common/interfaces/resource.interface.ts index 57add6f2d..4211439b3 100644 --- a/libs/sdk/src/common/interfaces/resource.interface.ts +++ b/libs/sdk/src/common/interfaces/resource.interface.ts @@ -14,6 +14,27 @@ export interface ResourceInterface = Recor execute(uri: string, params: Params): Promise; } +/** + * Result returned by a resource argument completer. + */ +export interface ResourceCompletionResult { + /** Completion suggestions matching the partial value */ + values: string[]; + /** Total number of matching values (for pagination) */ + total?: number; + /** Whether more results exist beyond what was returned */ + hasMore?: boolean; +} + +/** + * Function that provides completion suggestions for a resource template argument. + * @param partial - The partial value typed so far + * @returns Completion suggestions + */ +export type ResourceArgumentCompleter = ( + partial: string, +) => Promise | ResourceCompletionResult; + /** * Function-style resource type. * This represents resources created via resource() or resourceTemplate() builders. @@ -90,6 +111,30 @@ export abstract class ResourceContext< abstract execute(uri: string, params: Params): Promise; + /** + * Override to provide autocompletion for resource template arguments. + * Called by the MCP `completion/complete` handler when a client requests + * suggestions for a template parameter. + * + * @param argName - The template parameter name (e.g., 'userId') + * @returns A completer function, or null if no completion is available for this argument + * + * @example + * ```typescript + * getArgumentCompleter(argName: string): ResourceArgumentCompleter | null { + * if (argName === 'userId') { + * return async (partial) => ({ + * values: await this.searchUsers(partial), + * }); + * } + * return null; + * } + * ``` + */ + getArgumentCompleter(_argName: string): ResourceArgumentCompleter | null { + return null; + } + public get output(): Out | undefined { return this._output; } diff --git a/libs/sdk/src/common/interfaces/skill.interface.ts b/libs/sdk/src/common/interfaces/skill.interface.ts index 4f0a45163..3281e63e8 100644 --- a/libs/sdk/src/common/interfaces/skill.interface.ts +++ b/libs/sdk/src/common/interfaces/skill.interface.ts @@ -10,6 +10,18 @@ import { SkillRecord } from '../records'; */ export type SkillType = Type | SkillRecord | string; +/** + * Metadata for a resolved reference file within a skill's references/ directory. + */ +export interface SkillReferenceInfo { + /** Reference name (typically filename without .md) */ + name: string; + /** Short description from frontmatter or first paragraph */ + description: string; + /** Filename relative to the skill directory */ + filename: string; +} + /** * Full content returned when loading a skill. * Contains all information needed for an LLM to execute the skill. @@ -79,6 +91,12 @@ export interface SkillContent { * Bundled resource directories (scripts/, references/, assets/). */ resources?: SkillResources; + + /** + * Resolved reference metadata from the skill's references/ directory. + * Each entry contains name, description, and filename for the reference. + */ + resolvedReferences?: SkillReferenceInfo[]; } /** diff --git a/libs/sdk/src/completion/flows/complete.flow.ts b/libs/sdk/src/completion/flows/complete.flow.ts index 71402f4fb..954cfae56 100644 --- a/libs/sdk/src/completion/flows/complete.flow.ts +++ b/libs/sdk/src/completion/flows/complete.flow.ts @@ -174,20 +174,17 @@ export default class CompleteFlow extends FlowBase { const resourceMatch = this.scope.resources.findResourceForUri(uri); if (resourceMatch) { - // Check if the resource instance has a completer for this argument - // Completion support is optional - resources can implement getArgumentCompleter to provide suggestions - const instance = resourceMatch.instance as any; // ResourceInstance may have completer method - if (typeof instance.getArgumentCompleter === 'function') { - const completer = instance.getArgumentCompleter(argName); - if (completer) { - try { - const result = await completer(argValue); - values = result.values || []; - total = result.total; - hasMore = result.hasMore; - } catch (e) { - this.logger.warn(`complete: completer failed for resource "${uri}" argument "${argName}": ${e}`); - } + // Check if the resource has a completer for this argument + // Completion support is optional — resources override getArgumentCompleter to provide suggestions + const completer = resourceMatch.instance.getArgumentCompleter(argName); + if (completer) { + try { + const result = await completer(argValue); + values = result.values || []; + total = result.total; + hasMore = result.hasMore; + } catch (e) { + this.logger.warn(`complete: completer failed for resource "${uri}" argument "${argName}": ${e}`); } } } else { diff --git a/libs/sdk/src/front-mcp/front-mcp.ts b/libs/sdk/src/front-mcp/front-mcp.ts index 772d63427..436e81f27 100644 --- a/libs/sdk/src/front-mcp/front-mcp.ts +++ b/libs/sdk/src/front-mcp/front-mcp.ts @@ -57,6 +57,11 @@ export class FrontMcpInstance implements FrontMcpInterface { throw new ServerNotFoundError(); } await server.start(); + + // Emit server-started lifecycle event to all scopes + for (const scope of this.getScopes()) { + await scope.emitServerStarted(); + } } /** diff --git a/libs/sdk/src/provider/provider.registry.ts b/libs/sdk/src/provider/provider.registry.ts index 13fc363ed..f38bdad18 100644 --- a/libs/sdk/src/provider/provider.registry.ts +++ b/libs/sdk/src/provider/provider.registry.ts @@ -669,12 +669,13 @@ export default class ProviderRegistry */ getProviderInfo(token: Token) { const def = this.defs.get(token); - const instance = this.instances.get(token); - if (!def || !instance) + if (!def) throw new ProviderNotRegisteredError( tokenName(token), 'not a registered DEFAULT provider and not a constructable class', ); + // Instance may be undefined for CONTEXT-scoped providers (built per-request, not at init) + const instance = this.instances.get(token); return { token, def, diff --git a/libs/sdk/src/resource/flows/read-resource.flow.ts b/libs/sdk/src/resource/flows/read-resource.flow.ts index 9d645b9d5..f91ecdb7b 100644 --- a/libs/sdk/src/resource/flows/read-resource.flow.ts +++ b/libs/sdk/src/resource/flows/read-resource.flow.ts @@ -13,6 +13,7 @@ import { } from '../../errors'; import { isUIResourceUri, handleUIResourceRead } from '../../tool/ui'; import { FlowContextProviders } from '../../provider/flow-context-providers'; +import { randomBytes } from '@frontmcp/utils'; const inputSchema = z.object({ request: ReadResourceRequestSchema, @@ -234,8 +235,24 @@ export default class ReadResourceFlow extends FlowBase { const { resource, input, params } = this.state.required; try { - // Create context-aware providers that include scoped providers from plugins - const contextProviders = new FlowContextProviders(this.scope.providers, this.deps); + // Build context-scoped providers from the resource's provider registry (app-level). + // This ensures CONTEXT-scoped providers registered at the app level are available, + // matching the same resolution chain that tools use. + const sessionKey = + this.state.sessionId ?? + ctx.authInfo?.sessionId ?? + `req-${Date.now()}-${Buffer.from(randomBytes(16)).toString('hex')}`; + const resourceViews = await resource.providers.buildViews(sessionKey, new Map(this.deps)); + + // Merge resource's context providers with flow's context deps + const mergedContextDeps = new Map(this.deps); + for (const [token, instance] of resourceViews.context) { + if (!mergedContextDeps.has(token)) { + mergedContextDeps.set(token, instance); + } + } + + const contextProviders = new FlowContextProviders(resource.providers, mergedContextDeps); const context = resource.create(input.uri, params, { ...ctx, contextProviders }); const resourceHooks = this.scope.hooks.getClsHooks(resource.record.provide).map((hook) => { hook.run = async () => { diff --git a/libs/sdk/src/resource/resource.instance.ts b/libs/sdk/src/resource/resource.instance.ts index fc10cf00c..53b05e87f 100644 --- a/libs/sdk/src/resource/resource.instance.ts +++ b/libs/sdk/src/resource/resource.instance.ts @@ -29,17 +29,25 @@ export class ResourceInstance< Params extends Record = Record, Out = unknown, > extends ResourceEntry { - private readonly providers: ProviderRegistry; + private readonly _providers: ProviderRegistry; readonly scope: ScopeEntry; readonly hooks: HookRegistry; + /** + * Get the provider registry for this resource. + * Used by flows to build context-aware providers for CONTEXT-scoped dependencies. + */ + get providers(): ProviderRegistry { + return this._providers; + } + /** Parsed URI template info for template resources */ private templateInfo?: { pattern: RegExp; paramNames: string[] }; constructor(record: ResourceRecord | ResourceTemplateRecord, providers: ProviderRegistry, owner: EntryOwnerRef) { super(record); this.owner = owner; - this.providers = providers; + this._providers = providers; this.name = record.metadata.name; this.fullName = this.owner.id + ':' + this.name; this.scope = this.providers.getActiveScope(); @@ -91,6 +99,29 @@ export class ResourceInstance< return this.record.metadata; } + /** + * Get an argument completer from the resource class prototype. + * Returns a completer function if the resource class overrides getArgumentCompleter, + * or null if no completer is available. + */ + override getArgumentCompleter( + argName: string, + ): + | (( + partial: string, + ) => + | Promise<{ values: string[]; total?: number; hasMore?: boolean }> + | { values: string[]; total?: number; hasMore?: boolean }) + | null { + const cls = this.record.provide; + if (typeof cls === 'function' && cls.prototype && typeof cls.prototype.getArgumentCompleter === 'function') { + // Call the method on the prototype — it doesn't need instance state for static completions + // For dynamic completions that need DI, the method should be overridden at instance level + return cls.prototype.getArgumentCompleter.call(cls.prototype, argName); + } + return null; + } + /** * Match a URI against this resource. * For static resources: exact match against uri diff --git a/libs/sdk/src/skill/skill.instance.ts b/libs/sdk/src/skill/skill.instance.ts index 07c33a604..4b48c8f60 100644 --- a/libs/sdk/src/skill/skill.instance.ts +++ b/libs/sdk/src/skill/skill.instance.ts @@ -1,11 +1,12 @@ // file: libs/sdk/src/skill/skill.instance.ts import { EntryOwnerRef, SkillEntry, SkillKind, SkillRecord, SkillToolRef, normalizeToolRef } from '../common'; -import { SkillContent } from '../common/interfaces'; +import { SkillContent, SkillReferenceInfo } from '../common/interfaces'; import { SkillVisibility } from '../common/metadata/skill.metadata'; import ProviderRegistry from '../provider/provider.registry'; import { ScopeEntry } from '../common'; -import { loadInstructions, buildSkillContent } from './skill.utils'; +import { loadInstructions, buildSkillContent, resolveReferences } from './skill.utils'; +import { dirname, pathResolve } from '@frontmcp/utils'; /** * Extended SkillContent with additional metadata for caching. @@ -120,13 +121,38 @@ export class SkillInstance extends SkillEntry { * Load the full skill content. * Results are cached after the first load. */ + /** + * Resolve the base directory for this skill (for file/reference resolution). + */ + private getBaseDir(): string | undefined { + if (this.record.kind === SkillKind.FILE) { + return dirname(this.record.filePath) || undefined; + } + if (this.record.kind === SkillKind.VALUE && this.record.callerDir) { + return this.record.callerDir; + } + return undefined; + } + override async load(): Promise { if (this.cachedContent !== undefined) { return this.cachedContent; } const instructions = await this.loadInstructions(); - const baseContent = buildSkillContent(this.metadata, instructions); + + // Resolve references from the references/ directory if it exists + const refsPath = this.metadata.resources?.references; + let resolvedRefs: SkillReferenceInfo[] | undefined; + if (refsPath) { + const baseDir = this.getBaseDir(); + const refsDir = refsPath.startsWith('/') ? refsPath : baseDir ? pathResolve(baseDir, refsPath) : undefined; + if (refsDir) { + resolvedRefs = await resolveReferences(refsDir); + } + } + + const baseContent = buildSkillContent(this.metadata, instructions, resolvedRefs); // Add additional metadata that's useful for search but not in base SkillContent this.cachedContent = { diff --git a/libs/sdk/src/skill/skill.utils.ts b/libs/sdk/src/skill/skill.utils.ts index 76f215a32..537acc3ea 100644 --- a/libs/sdk/src/skill/skill.utils.ts +++ b/libs/sdk/src/skill/skill.utils.ts @@ -15,8 +15,8 @@ import { SkillInstructionSource, normalizeToolRef, } from '../common'; -import { SkillContent } from '../common/interfaces'; -import { readFile } from '@frontmcp/utils'; +import { SkillContent, SkillReferenceInfo } from '../common/interfaces'; +import { readFile, readdir, fileExists } from '@frontmcp/utils'; import { InvalidSkillError, SkillInstructionFetchError, InvalidInstructionSourceError } from '../errors'; import { stripFrontmatter } from './skill-md-parser'; @@ -164,14 +164,25 @@ export async function loadInstructions(source: SkillInstructionSource, basePath? * * @param metadata - The skill metadata * @param instructions - The resolved instructions string + * @param resolvedReferences - Optional resolved reference metadata from references/ directory * @returns The full skill content */ -export function buildSkillContent(metadata: SkillMetadata, instructions: string): SkillContent { +export function buildSkillContent( + metadata: SkillMetadata, + instructions: string, + resolvedReferences?: SkillReferenceInfo[], +): SkillContent { + // Append references routing table to instructions if references exist + let finalInstructions = instructions; + if (resolvedReferences && resolvedReferences.length > 0) { + finalInstructions += buildReferencesTable(resolvedReferences); + } + return { id: metadata.id ?? metadata.name, name: metadata.name, description: metadata.description, - instructions, + instructions: finalInstructions, tools: normalizeToolRefs(metadata.tools), parameters: metadata.parameters, examples: metadata.examples, @@ -180,9 +191,107 @@ export function buildSkillContent(metadata: SkillMetadata, instructions: string) specMetadata: metadata.specMetadata, allowedTools: metadata.allowedTools, resources: metadata.resources, + resolvedReferences, }; } +/** + * Build a markdown references routing table to append to skill instructions. + * Follows the same format as SKILL.md scenario routing tables. + */ +function buildReferencesTable(refs: SkillReferenceInfo[]): string { + const lines: string[] = ['', '', '## References', '', '| Reference | Description |', '| --------- | ----------- |']; + + for (const ref of refs) { + lines.push(`| \`${ref.name}\` | ${ref.description} |`); + } + + return lines.join('\n'); +} + +/** + * Scan a references/ directory for .md files and extract metadata. + * Parses YAML frontmatter for name/description, falls back to heading/paragraph. + * + * @param refsDir - Absolute path to the references/ directory + * @returns Array of resolved reference info, or undefined if no references + */ +export async function resolveReferences(refsDir: string): Promise { + if (!(await fileExists(refsDir))) return undefined; + + let files: string[]; + try { + files = (await readdir(refsDir)).filter((f: string) => f.endsWith('.md')).sort(); + } catch { + return undefined; + } + + if (files.length === 0) return undefined; + + const refs: SkillReferenceInfo[] = []; + for (const file of files) { + const content = await readFile(`${refsDir}/${file}`, 'utf-8'); + const filenameWithoutExt = file.replace(/\.md$/, ''); + + let name = filenameWithoutExt; + let description = ''; + + // Try parsing frontmatter + const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (fmMatch) { + const fmLines = fmMatch[1].split(/\r?\n/); + for (const line of fmLines) { + const colonIdx = line.indexOf(':'); + if (colonIdx === -1) continue; + const key = line.slice(0, colonIdx).trim(); + const val = line + .slice(colonIdx + 1) + .trim() + .replace(/^["']|["']$/g, ''); + if (key === 'name' && val) name = val; + if (key === 'description' && val) description = val; + } + } + + // Fallback: extract description from first paragraph + if (!description) { + const body = fmMatch ? content.substring(content.indexOf('---', 3) + 3).trim() : content.trim(); + description = extractFirstParagraph(body); + } + + refs.push({ name, description, filename: file }); + } + + return refs; +} + +/** + * Extract the first non-empty paragraph after the heading from markdown content. + */ +function extractFirstParagraph(body: string): string { + const lines = body.split(/\r?\n/); + let foundHeading = false; + const paragraphLines: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!foundHeading && trimmed.startsWith('#')) { + foundHeading = true; + continue; + } + if (foundHeading) { + if (trimmed === '') { + if (paragraphLines.length > 0) break; + continue; + } + if (trimmed.startsWith('#') || trimmed.startsWith('|') || trimmed.startsWith('-')) break; + paragraphLines.push(trimmed); + } + } + + return paragraphLines.join(' ').slice(0, 200) || ''; +} + /** * Normalize tool references to the array format expected by SkillContent. * Handles all supported tool reference formats: string, class, SkillToolRef, SkillToolRefWithClass. diff --git a/libs/skills/__tests__/manifest.spec.ts b/libs/skills/__tests__/manifest.spec.ts index ec5427f43..ff69c6834 100644 --- a/libs/skills/__tests__/manifest.spec.ts +++ b/libs/skills/__tests__/manifest.spec.ts @@ -22,7 +22,8 @@ describe('manifest constants', () => { expect(VALID_CATEGORIES).toContain('testing'); expect(VALID_CATEGORIES).toContain('guides'); expect(VALID_CATEGORIES).toContain('production'); - expect(VALID_CATEGORIES).toHaveLength(7); + expect(VALID_CATEGORIES).toContain('extensibility'); + expect(VALID_CATEGORIES).toHaveLength(8); }); it('should export valid bundles', () => { diff --git a/libs/skills/catalog/frontmcp-config/SKILL.md b/libs/skills/catalog/frontmcp-config/SKILL.md index ef5429588..f5fe68d9a 100644 --- a/libs/skills/catalog/frontmcp-config/SKILL.md +++ b/libs/skills/catalog/frontmcp-config/SKILL.md @@ -52,16 +52,24 @@ Entry point for configuring FrontMCP servers. This skill helps you find the righ ## Scenario Routing Table -| Scenario | Skill | Description | -| ---------------------------------------------------------- | ----------------------- | ------------------------------------------------------------- | -| Choose between SSE, Streamable HTTP, or stdio | `configure-transport` | Transport protocol selection with distributed session options | -| Set up CORS, port, base path, or request limits | `configure-http` | HTTP server options for Streamable HTTP and SSE transports | -| Add rate limiting, concurrency, or IP filtering | `configure-throttle` | Server-level and per-tool throttle configuration | -| Enable tools to ask users for input | `configure-elicitation` | Elicitation schemas, stores, and multi-step flows | -| Set up authentication (public, transparent, local, remote) | `configure-auth` | OAuth flows, credential vault, multi-app auth | -| Configure session storage backends | `configure-session` | Memory, Redis, Vercel KV, and custom session stores | -| Add Redis for production storage | `setup-redis` | Docker Redis, Vercel KV, pub/sub for subscriptions | -| Add SQLite for local development | `setup-sqlite` | SQLite with WAL mode, migration helpers | +| Scenario | Skill | Description | +| -------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------- | +| Choose between SSE, Streamable HTTP, or stdio | `configure-transport` | Transport protocol selection with distributed session options | +| Set up CORS, port, base path, or request limits | `configure-http` | HTTP server options for Streamable HTTP and SSE transports | +| Add rate limiting, concurrency, or IP filtering | `configure-throttle` | Server-level and per-tool throttle configuration | +| Enable tools to ask users for input | `configure-elicitation` | Elicitation schemas, stores, and multi-step flows | +| Set up authentication (public, transparent, local, remote) | `configure-auth` | OAuth flows, credential vault, multi-app auth | +| Configure session storage backends | `configure-session` | Memory, Redis, Vercel KV, and custom session stores | +| Add Redis for production storage | `setup-redis` | Docker Redis, Vercel KV, pub/sub for subscriptions | +| Add SQLite for local development | `setup-sqlite` | SQLite with WAL mode, migration helpers | +| Understand auth mode details (public/transparent/local/remote) | `configure-auth-modes` | Authentication mode details (public, transparent, local, remote) | +| Fine-tune guard configuration for throttling | `configure-throttle-guard-config` | Advanced guard configuration for throttling | +| Use transport protocol presets | `configure-transport-protocol-presets` | Transport protocol preset configurations | +| Split apps into separate scopes (`splitByApp`) | `decorators-guide` | Per-app scope and basePath isolation on `@FrontMcp` | +| Enable widget-to-host communication (ext-apps) | `decorators-guide` | `extApps` host capabilities, session validation, widget comms | +| Enable background jobs and workflows | `decorators-guide` | `jobs: { enabled: true, store? }` on `@FrontMcp` | +| Configure pagination for list operations | `decorators-guide` | `pagination` defaults for `tools/list` endpoint | +| Configure npm/ESM package loader for remote apps | `decorators-guide` | `loader` config for `App.esm()` / `App.remote()` resolution | ## Configuration Layers @@ -73,14 +81,19 @@ Server (@FrontMcp) ← Global defaults └── Tool (@Tool) ← Per-tool overrides ``` -| Setting | Server | App | Tool | -| --------------------- | ------------ | --- | -------------- | -| Transport | Yes | No | No | -| HTTP (CORS, port) | Yes | No | No | -| Throttle (rate limit) | Yes (global) | No | Yes (per-tool) | -| Auth mode | Yes | Yes | No | -| Session store | Yes | No | No | -| Elicitation | No | No | Yes (per-tool) | +| Setting | Server (`@FrontMcp`) | App (`@App`) | Tool (`@Tool`) | +| --------------------- | -------------------------------- | --------------------- | ------------------------------------------- | +| Transport | Yes | No | No | +| HTTP (CORS, port) | Yes | No | No | +| Throttle (rate limit) | Yes (`throttle` global defaults) | No | Yes (`rateLimit`, `concurrency`, `timeout`) | +| Auth mode | Yes | Yes (override) | No | +| Auth providers | No | Yes (`authProviders`) | Yes (`authProviders`) | +| Session store | Yes | No | No | +| Elicitation | Yes (enable: `elicitation`) | No | Yes (usage: `this.elicit()`) | +| ExtApps | Yes | No | No | +| Jobs / Workflows | Yes (`jobs: { enabled }`) | No | No | +| Pagination | Yes | No | No | +| SplitByApp | Yes | No | No | ## Cross-Cutting Patterns diff --git a/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md b/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md index c9b789bd6..5b63c92cb 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md @@ -1,3 +1,8 @@ +--- +name: configure-auth-modes +description: Detailed comparison of public, transparent, remote, and managed auth modes +--- + # Auth Modes Detailed Comparison ## Public Mode diff --git a/libs/skills/catalog/frontmcp-config/references/configure-auth.md b/libs/skills/catalog/frontmcp-config/references/configure-auth.md index dc30d6732..15f309511 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-auth.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-auth.md @@ -1,3 +1,8 @@ +--- +name: configure-auth +description: Set up authentication modes, credential vault, and OAuth flows for FrontMCP servers +--- + # Configure Authentication for FrontMCP This skill covers setting up authentication in a FrontMCP server. FrontMCP supports four auth modes, each suited to different deployment scenarios. All authentication logic lives in the `@frontmcp/auth` library. diff --git a/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md b/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md index 0114e1d0f..c21327018 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md @@ -1,3 +1,8 @@ +--- +name: configure-elicitation +description: Configure interactive user input during tool execution for confirmations, choices, and forms +--- + # Configuring Elicitation Elicitation allows tools to request interactive input from users mid-execution — confirmations, choices, or structured form data. diff --git a/libs/skills/catalog/frontmcp-config/references/configure-http.md b/libs/skills/catalog/frontmcp-config/references/configure-http.md index efb903fe7..01dfb39da 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-http.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-http.md @@ -1,3 +1,8 @@ +--- +name: configure-http +description: Configure HTTP server port, CORS policy, unix sockets, and entry path prefix +--- + # Configuring HTTP Options Configure the HTTP server — port, CORS policy, unix sockets, and entry path prefix. diff --git a/libs/skills/catalog/frontmcp-config/references/configure-session.md b/libs/skills/catalog/frontmcp-config/references/configure-session.md index ed5427b76..eb3a67c14 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-session.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-session.md @@ -1,3 +1,8 @@ +--- +name: configure-session +description: Set up session storage with Redis or Vercel KV for persistent user state across requests +--- + # Configure Session Management This skill covers setting up session storage in FrontMCP. Sessions track authenticated user state, token storage, and request context across MCP interactions. diff --git a/libs/skills/catalog/frontmcp-config/references/configure-throttle-guard-config.md b/libs/skills/catalog/frontmcp-config/references/configure-throttle-guard-config.md index 7e6918a8f..1b82e9541 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-throttle-guard-config.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-throttle-guard-config.md @@ -1,3 +1,8 @@ +--- +name: configure-throttle-guard-config +description: Complete GuardConfig interface reference for rate limiting, concurrency, and IP filtering +--- + # GuardConfig Full Reference ## Complete Configuration diff --git a/libs/skills/catalog/frontmcp-config/references/configure-throttle.md b/libs/skills/catalog/frontmcp-config/references/configure-throttle.md index 5c29dfcb8..a5dcc7df4 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-throttle.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-throttle.md @@ -1,3 +1,8 @@ +--- +name: configure-throttle +description: Protect servers with rate limiting, concurrency control, execution timeouts, and IP filtering +--- + # Configuring Throttle, Rate Limits, and IP Filtering Protect your FrontMCP server with rate limiting, concurrency control, execution timeouts, and IP filtering — at both server and per-tool levels. diff --git a/libs/skills/catalog/frontmcp-config/references/configure-transport-protocol-presets.md b/libs/skills/catalog/frontmcp-config/references/configure-transport-protocol-presets.md index 0722cdbfd..684cf181b 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-transport-protocol-presets.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-transport-protocol-presets.md @@ -1,3 +1,8 @@ +--- +name: configure-transport-protocol-presets +description: Reference for legacy, modern, stateless, and minimal transport protocol presets +--- + # Transport Protocol Presets Reference ## Preset Configurations diff --git a/libs/skills/catalog/frontmcp-config/references/configure-transport.md b/libs/skills/catalog/frontmcp-config/references/configure-transport.md index a64da85ac..d5a45f293 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-transport.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-transport.md @@ -1,3 +1,8 @@ +--- +name: configure-transport +description: Configure client transport protocols including SSE, Streamable HTTP, and stateless API modes +--- + # Configuring Transport Configure how clients connect to your FrontMCP server — SSE, Streamable HTTP, stateless API, or a combination. diff --git a/libs/skills/catalog/frontmcp-config/references/setup-redis.md b/libs/skills/catalog/frontmcp-config/references/setup-redis.md index ed52feeaa..3d71342d6 100644 --- a/libs/skills/catalog/frontmcp-config/references/setup-redis.md +++ b/libs/skills/catalog/frontmcp-config/references/setup-redis.md @@ -1,3 +1,8 @@ +--- +name: setup-redis +description: Cross-reference to the full Redis configuration guide in frontmcp-setup +--- + # Redis Setup Reference > This reference is maintained in `frontmcp-setup/references/setup-redis.md`. diff --git a/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md b/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md index 0ef88a30a..11ce17b6e 100644 --- a/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md +++ b/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md @@ -1,3 +1,8 @@ +--- +name: setup-sqlite +description: Cross-reference to the full SQLite configuration guide in frontmcp-setup +--- + # SQLite Setup Reference > This reference is maintained in `frontmcp-setup/references/setup-sqlite.md`. diff --git a/libs/skills/catalog/frontmcp-deployment/SKILL.md b/libs/skills/catalog/frontmcp-deployment/SKILL.md index 428edf2b8..6db369a35 100644 --- a/libs/skills/catalog/frontmcp-deployment/SKILL.md +++ b/libs/skills/catalog/frontmcp-deployment/SKILL.md @@ -53,15 +53,40 @@ Entry point for deploying and building FrontMCP servers. This skill helps you ch ## Scenario Routing Table -| Scenario | Skill | Description | -| ------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------- | -| Long-running server on VPS, Docker, or bare metal | `deploy-to-node` | Node.js with stdio or HTTP transport, PM2/Docker for process management | -| Serverless with zero config and Vercel KV | `deploy-to-vercel` | Vercel Functions with Streamable HTTP, Vercel KV for storage | -| AWS serverless with API Gateway | `deploy-to-lambda` | Lambda + API Gateway with Streamable HTTP, DynamoDB or ElastiCache | -| Edge computing with global distribution | `deploy-to-cloudflare` | Cloudflare Workers with KV or Durable Objects for storage | -| Standalone executable binary for distribution | `build-for-cli` | Single-binary CLI with stdio transport, embedded storage | -| Run MCP in a web browser | `build-for-browser` | Browser-compatible bundle with in-memory transport | -| Embed MCP into an existing Node.js application | `build-for-sdk` | Library build for programmatic usage without standalone server | +| Scenario | Skill | Description | +| ------------------------------------------------- | --------------------------- | ----------------------------------------------------------------------- | +| Long-running server on VPS, Docker, or bare metal | `deploy-to-node` | Node.js with stdio or HTTP transport, PM2/Docker for process management | +| Serverless with zero config and Vercel KV | `deploy-to-vercel` | Vercel Functions with Streamable HTTP, Vercel KV for storage | +| AWS serverless with API Gateway | `deploy-to-lambda` | Lambda + API Gateway with Streamable HTTP, DynamoDB or ElastiCache | +| Edge computing with global distribution | `deploy-to-cloudflare` | Cloudflare Workers with KV or Durable Objects for storage | +| Standalone executable binary for distribution | `build-for-cli` | Single-binary CLI with stdio transport, embedded storage | +| Run MCP in a web browser | `build-for-browser` | Browser-compatible bundle with in-memory transport | +| Embed MCP into an existing Node.js application | `build-for-sdk` | Library build for programmatic usage without standalone server | +| Write a Dockerfile for Node.js deployment | `deploy-to-node-dockerfile` | Dockerfile configuration for Node.js deployment | +| Configure Vercel-specific settings (vercel.json) | `deploy-to-vercel-config` | Vercel-specific configuration (vercel.json) | + +### CLI Commands for Deployment and Operations + +Beyond `frontmcp build`, the CLI provides commands for the full deployment lifecycle: + +| Command | Description | +| ---------------------------- | ----------------------------------------------------------------------------------- | +| `frontmcp build -t ` | Build for target: `node`, `vercel`, `lambda`, `cloudflare`, `cli`, `browser`, `sdk` | +| `frontmcp build -t cli --js` | Build CLI as JS bundle (instead of native binary via SEA) | +| `frontmcp start ` | Start a named MCP server with supervisor (process management) | +| `frontmcp stop ` | Stop managed server (`-f` for force kill) | +| `frontmcp restart ` | Restart managed server | +| `frontmcp status [name]` | Show process status (detail if name given, table if omitted) | +| `frontmcp list` | List all managed processes | +| `frontmcp logs ` | Tail log output (`-F` follow, `-n` lines) | +| `frontmcp socket ` | Start Unix socket daemon for local MCP server | +| `frontmcp service ` | Install/uninstall systemd (Linux) or launchd (macOS) service | +| `frontmcp install ` | Install MCP app from npm, local path, or git | +| `frontmcp uninstall ` | Remove installed MCP app | +| `frontmcp configure ` | Re-run setup questionnaire for installed app | +| `frontmcp doctor` | Check Node.js/npm versions and tsconfig requirements | +| `frontmcp inspector` | Launch MCP Inspector for debugging | +| `frontmcp init` | Create or fix tsconfig.json for FrontMCP | ## Target Comparison diff --git a/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md b/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md index 5d518ec6e..3950dcd24 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md @@ -1,3 +1,8 @@ +--- +name: build-for-browser +description: Build a FrontMCP server or client for browser environments and frontend frameworks +--- + # Building for Browser Build your FrontMCP server or client for browser environments. diff --git a/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md b/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md index c6273230d..82210a92e 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md @@ -1,3 +1,8 @@ +--- +name: build-for-cli +description: Build a FrontMCP server as a standalone CLI binary using Node.js SEA or bundled JS +--- + # Building a CLI Binary Build your FrontMCP server as a distributable CLI binary using Node.js Single Executable Applications (SEA) or as a bundled JS file. @@ -92,6 +97,54 @@ frontmcp build --target cli node dist/my-server.cjs.js ``` +## Unix Socket Daemon Mode + +Run your MCP server as a local daemon accessible via Unix socket: + +```bash +# Start daemon in foreground +frontmcp socket ./src/main.ts -s ~/.frontmcp/sockets/my-app.sock + +# Start daemon in background +frontmcp socket ./src/main.ts -b --db ~/.my-tool/data.db + +# Default socket path: ~/.frontmcp/sockets/{app-name}.sock +``` + +The daemon accepts JSON-RPC requests over HTTP via the Unix socket, making it ideal for local MCP clients (Claude Code, IDE extensions) that need persistent tool access without a TCP port. + +## Process Management + +Manage long-running MCP server processes with built-in supervisor commands: + +```bash +# Start a named server (auto-restarts on crash) +frontmcp start my-server -e ./src/main.ts --max-restarts 5 + +# Monitor +frontmcp status my-server # Detailed status for one server +frontmcp status # Table of all managed servers +frontmcp list # List all managed processes +frontmcp logs my-server -F # Tail logs (follow mode) + +# Control +frontmcp restart my-server +frontmcp stop my-server # Graceful shutdown (SIGTERM) +frontmcp stop my-server -f # Force kill (SIGKILL) +``` + +## System Service Installation + +Install your MCP server as a system service for automatic startup: + +```bash +# Install as systemd service (Linux) or launchd service (macOS) +frontmcp service install my-server + +# Uninstall service +frontmcp service uninstall my-server +``` + ## Common Patterns | Pattern | Correct | Incorrect | Why | diff --git a/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md b/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md index 2702fd665..85cc7bbb9 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md @@ -1,3 +1,8 @@ +--- +name: build-for-sdk +description: Build a FrontMCP server as an embeddable library with create() and connect() APIs +--- + # Building as an SDK Library Build your FrontMCP server as an embeddable library that runs without an HTTP server. Use `create()` for flat-config setup or `connect()` for platform-specific tool formatting (OpenAI, Claude, LangChain, Vercel AI). diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md index fbe8a6980..5503af498 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-cloudflare +description: Deploy a FrontMCP server to Cloudflare Workers with KV, D1, and Durable Objects +--- + # Deploy a FrontMCP Server to Cloudflare Workers This skill guides you through deploying a FrontMCP server to Cloudflare Workers. diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md index f538eebde..d6d7bae37 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-lambda +description: Deploy a FrontMCP server to AWS Lambda with API Gateway using SAM or CDK +--- + # Deploy a FrontMCP Server to AWS Lambda This skill walks you through deploying a FrontMCP server to AWS Lambda with API Gateway using SAM or CDK. diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md index 467edbd7b..8ed6eed91 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-node-dockerfile +description: Multi-stage Dockerfile for building and running a FrontMCP server in production +--- + # ---- Build Stage ---- FROM node:24-alpine AS builder diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md index 72e32ce27..31235bb9f 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-node +description: Deploy a FrontMCP server as a standalone Node.js app with Docker and process managers +--- + # Deploy a FrontMCP Server to Node.js This skill walks you through deploying a FrontMCP server as a standalone Node.js application, optionally containerized with Docker for production use. diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md index b9c41c8a0..b99d65389 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-vercel-config +description: Reference vercel.json configuration for deploying a FrontMCP server to Vercel +--- + { "$schema": "https://openapi.vercel.sh/vercel.json", "framework": null, diff --git a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md index 7c5ab19ce..f69c66690 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md @@ -1,3 +1,8 @@ +--- +name: deploy-to-vercel +description: Deploy a FrontMCP server to Vercel serverless functions with Vercel KV storage +--- + # Deploy a FrontMCP Server to Vercel This skill guides you through deploying a FrontMCP server to Vercel serverless functions with Vercel KV for persistent storage. diff --git a/libs/skills/catalog/frontmcp-development/SKILL.md b/libs/skills/catalog/frontmcp-development/SKILL.md index de85d196b..4f4fc7344 100644 --- a/libs/skills/catalog/frontmcp-development/SKILL.md +++ b/libs/skills/catalog/frontmcp-development/SKILL.md @@ -40,20 +40,25 @@ Entry point for building MCP server components. This skill helps you find the ri ## Scenario Routing Table -| Scenario | Skill | Description | -| -------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------- | -| Expose an executable action that AI clients can call | `create-tool` | Class-based or function-style tools with Zod input/output validation | -| Expose read-only data via a URI | `create-resource` | Static resources or URI template resources for dynamic data | -| Create a reusable conversation template or system prompt | `create-prompt` | Prompt entries with arguments and multi-turn message sequences | -| Build an autonomous AI loop that orchestrates tools | `create-agent` | Agent entries with LLM config, inner tools, and swarm handoff | -| Register shared services or configuration via DI | `create-provider` | Dependency injection tokens, lifecycle hooks, factory providers | -| Run a background task with progress and retries | `create-job` | Job entries with attempt tracking, retry config, and progress | -| Chain multiple jobs into a sequential pipeline | `create-workflow` | Workflow entries that compose jobs with data passing | -| Write instruction-only AI guidance (no code execution) | `create-skill` | Skill entries with markdown instructions from files, strings, or URLs | -| Write AI guidance that also orchestrates tools | `create-skill-with-tools` | Skill entries that combine instructions with registered tools | -| Look up any decorator signature or option | `decorators-guide` | Complete reference for @Tool, @Resource, @Prompt, @Agent, @App, @FrontMcp, and more | -| Integrate an external API via OpenAPI spec | `official-adapters` | OpenapiAdapter with auth, polling, inline specs, and multiple API composition | -| Use official plugins (caching, remember, feature flags) | `official-plugins` | Built-in plugins for caching, session memory, approval, and feature flags (dashboard is beta) | +| Scenario | Skill | Description | +| -------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------- | +| Expose an executable action that AI clients can call | `create-tool` | Class-based or function-style tools with Zod input/output validation | +| Expose read-only data via a URI | `create-resource` | Static resources or URI template resources for dynamic data | +| Create a reusable conversation template or system prompt | `create-prompt` | Prompt entries with arguments and multi-turn message sequences | +| Build an autonomous AI loop that orchestrates tools | `create-agent` | Agent entries with LLM config, inner tools, and swarm handoff | +| Register shared services or configuration via DI | `create-provider` | Dependency injection tokens, lifecycle hooks, factory providers | +| Run a background task with progress and retries | `create-job` | Job entries with attempt tracking, retry config, and progress | +| Chain multiple jobs into a sequential pipeline | `create-workflow` | Workflow entries that compose jobs with data passing | +| Write instruction-only AI guidance (no code execution) | `create-skill` | Skill entries with markdown instructions from files, strings, or URLs | +| Write AI guidance that also orchestrates tools | `create-skill-with-tools` | Skill entries that combine instructions with registered tools | +| Look up any decorator signature or option | `decorators-guide` | Complete reference for @Tool, @Resource, @Prompt, @Agent, @App, @FrontMcp, and more | +| Integrate an external API via OpenAPI spec | `official-adapters` | OpenapiAdapter with auth, polling, inline specs, and multiple API composition | +| Use official plugins (caching, remember, feature flags) | `official-plugins` | Built-in plugins for caching, session memory, approval, and feature flags (dashboard is beta) | +| Connect to an external data source via a custom adapter | `create-adapter` | Create custom adapters for external data sources | +| Configure LLM settings for an agent component | `create-agent-llm-config` | Configure LLM settings for agent components | +| Add will/did/around lifecycle hooks to a plugin | `create-plugin-hooks` | Add lifecycle hooks to plugins (will/did/around) | +| Annotate tools with client hints for AI clients | `create-tool-annotations` | Add MCP tool annotations for client hints | +| Define typed output schemas for tool responses | `create-tool-output-schema-types` | Define typed output schemas for tools | ## Recommended Reading Order diff --git a/libs/skills/catalog/frontmcp-development/references/create-adapter.md b/libs/skills/catalog/frontmcp-development/references/create-adapter.md index 5ed180f08..40e654b68 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-adapter.md +++ b/libs/skills/catalog/frontmcp-development/references/create-adapter.md @@ -1,3 +1,8 @@ +--- +name: create-adapter +description: Build adapters that generate MCP tools and resources from external sources automatically +--- + # Creating Custom Adapters Build adapters that automatically generate MCP tools, resources, and prompts from external sources — databases, GraphQL schemas, proprietary APIs, or any definition format. diff --git a/libs/skills/catalog/frontmcp-development/references/create-agent-llm-config.md b/libs/skills/catalog/frontmcp-development/references/create-agent-llm-config.md index 0c777c429..620d46a70 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-agent-llm-config.md +++ b/libs/skills/catalog/frontmcp-development/references/create-agent-llm-config.md @@ -1,3 +1,8 @@ +--- +name: create-agent-llm-config +description: Reference for supported LLM provider configurations including Anthropic and OpenAI +--- + # Agent LLM Configuration Reference ## Supported Providers diff --git a/libs/skills/catalog/frontmcp-development/references/create-agent.md b/libs/skills/catalog/frontmcp-development/references/create-agent.md index fe54e3c33..49cb935c7 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-agent.md +++ b/libs/skills/catalog/frontmcp-development/references/create-agent.md @@ -1,3 +1,8 @@ +--- +name: create-agent +description: Create autonomous AI agents that use LLM reasoning to plan and invoke inner tools +--- + # Creating an Autonomous Agent Agents are autonomous AI entities that use an LLM to reason, plan, and invoke inner tools to accomplish goals. In FrontMCP, agents are TypeScript classes that extend `AgentContext`, decorated with `@Agent`, and registered on a `@FrontMcp` server or inside an `@App`. diff --git a/libs/skills/catalog/frontmcp-development/references/create-job.md b/libs/skills/catalog/frontmcp-development/references/create-job.md index ab7aeef7d..c3562c08f 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-job.md +++ b/libs/skills/catalog/frontmcp-development/references/create-job.md @@ -1,3 +1,8 @@ +--- +name: create-job +description: Create long-running background jobs with retry policies, progress tracking, and permissions +--- + # Creating Jobs Jobs are long-running background tasks with built-in retry policies, progress tracking, and permission controls. Unlike tools (which execute synchronously within a request), jobs run asynchronously and persist their state across retries and restarts. diff --git a/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md b/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md index b14d6ecf7..c1ae97aa1 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md +++ b/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md @@ -1,3 +1,8 @@ +--- +name: create-plugin-hooks +description: Intercept and extend FrontMCP flows using before, after, around, and stage hook decorators +--- + # Creating Plugins with Flow Lifecycle Hooks Plugins intercept and extend FrontMCP flows using lifecycle hook decorators. Every flow (tool calls, resource reads, prompt gets, etc.) is composed of **stages**, and hooks let you run logic before, after, around, or instead of any stage. @@ -58,6 +63,42 @@ const { Stage, Will, Did, Around } = FlowHooksOf('tools:call-tool'); | `http:request` | HTTP request handling | | `agents:call-agent` | Agent invocation | +## Server Lifecycle Hooks + +In addition to flow-based hooks, plugins can register callbacks for server lifecycle events. These are not decorator-based — they use the `scope.onServerStarted()` API. + +### `onServerStarted()` + +Runs after the HTTP server is fully initialized and listening. Use for warming caches, starting background indexing, or logging readiness. + +```typescript +import { Plugin, DynamicPlugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'cache-warmer', + description: 'Warms caches when the server starts', + providers: [CacheService], +}) +class CacheWarmerPlugin extends DynamicPlugin<{}> { + constructor(scope: ScopeEntry) { + super(); + // Register lifecycle callback + scope.onServerStarted(async () => { + const cache = this.get(CacheService); + await cache.warmAll(); + console.log('Cache warmed successfully'); + }); + } +} +``` + +**Signature:** `scope.onServerStarted(callback: () => void | Promise): void` + +- Callbacks are stored and executed after `server.start()` completes +- Supports both sync and async callbacks +- Multiple callbacks are executed in registration order with `await` +- Typically used in plugin constructors or provider `onInit()` methods + ## Pre-Built Hook Type Exports For convenience, FrontMCP exports typed aliases so you do not need to call `FlowHooksOf` directly: diff --git a/libs/skills/catalog/frontmcp-development/references/create-plugin.md b/libs/skills/catalog/frontmcp-development/references/create-plugin.md index 4756bd93b..93ccad0a2 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-plugin.md +++ b/libs/skills/catalog/frontmcp-development/references/create-plugin.md @@ -1,3 +1,8 @@ +--- +name: create-plugin +description: Build plugins with providers, context extensions, lifecycle hooks, and contributed tools +--- + # Create a FrontMCP Plugin This skill covers building custom plugins for FrontMCP and using all 6 official plugins. Plugins are modular units that extend server behavior through providers, context extensions, lifecycle hooks, and contributed tools/resources/prompts. @@ -73,6 +78,47 @@ abstract class DynamicPlugin { + static override dynamicProviders(opts: { prefix: string }): ProviderType[] { + return [{ provide: GreeterService, useFactory: () => new GreeterService() }]; + } +} +``` + +```typescript +// server.ts +import { FrontMcp } from '@frontmcp/sdk'; +import GreeterPlugin from './plugins/my-greeter.plugin'; + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + plugins: [GreeterPlugin.init({ prefix: 'Hi' })], +}) +class MyServer {} +``` + ## Step 1: Create a Simple Plugin The minimal plugin only needs a name: @@ -260,11 +306,32 @@ Register with `init()`: class MyServer {} ``` -## Step 5: Extend Tool Metadata +## Step 5: Extend Metadata and Execution Context + +FrontMCP provides two extension mechanisms for plugins: **metadata augmentation** (add fields to decorators) and **context extensions** (add properties to `this` in tools/resources/prompts). -Plugins can add fields to the `@Tool` decorator via global augmentation: +### All Extensible Metadata Interfaces + +Plugins can extend these `declare global` interfaces to add custom fields to any decorator: + +| Interface | Decorator | Example Field | +| ---------------------------------------- | -------------------------- | ---------------------------- | +| `ExtendFrontMcpToolMetadata` | `@Tool({...})` | `audit: { enabled: true }` | +| `ExtendFrontMcpAgentMetadata` | `@Agent({...})` | Inherits from ToolMetadata | +| `ExtendFrontMcpResourceMetadata` | `@Resource({...})` | `cache: { ttl: 3600 }` | +| `ExtendFrontMcpResourceTemplateMetadata` | `@ResourceTemplate({...})` | `rateLimit: { max: 100 }` | +| `ExtendFrontMcpPromptMetadata` | `@Prompt({...})` | `category: 'onboarding'` | +| `ExtendFrontMcpJobMetadata` | `@Job({...})` | `priority: 'high'` | +| `ExtendFrontMcpWorkflowMetadata` | `@Workflow({...})` | `retryPolicy: 'exponential'` | +| `ExtendFrontMcpSkillMetadata` | `@Skill({...})` | `complexity: 'advanced'` | +| `ExtendFrontMcpLoggerMetadata` | Logger transports | `destination: 'sentry'` | + +### Metadata Extension Pattern + +Add custom fields to any decorator via `declare global`: ```typescript +// my-plugin.types.ts declare global { interface ExtendFrontMcpToolMetadata { audit?: { @@ -275,7 +342,7 @@ declare global { } ``` -Tools then use it: +Tools then use the custom field directly in the decorator: ```typescript @Tool({ @@ -287,12 +354,106 @@ class DeleteUserTool extends ToolContext { } ``` +The same pattern works for any of the 9 interfaces above — replace `ExtendFrontMcpToolMetadata` with the target interface. + +### Context Extension Pattern + +Add properties like `this.myService` to execution contexts. This requires both TypeScript augmentation and runtime registration. + +**Part A: TypeScript type declaration** (in a separate `.context-extension.ts` file): + +```typescript +// my-plugin.context-extension.ts +import type { MyService } from './providers/my-service.provider'; + +declare module '@frontmcp/sdk' { + interface ExecutionContextBase { + readonly myService: MyService; + } + // PromptContext has a separate prototype chain — augment it too + interface PromptContext { + readonly myService: MyService; + } +} +``` + +**Part B: Runtime registration** (in the `@Plugin` metadata): + +```typescript +@Plugin({ + name: 'my-plugin', + providers: [MyServiceProvider], + contextExtensions: [ + { + property: 'myService', + token: MyServiceToken, + errorMessage: 'MyPlugin is not installed. Add it to your app plugins.', + }, + ], +}) +export class MyPlugin extends DynamicPlugin { + /* ... */ +} +``` + +The SDK installs lazy getters on both `ExecutionContextBase.prototype` and `PromptContext.prototype` that resolve the DI token on first access. + +### ContextExtension Interface + +Each entry in the `contextExtensions` array has these fields: + +| Field | Type | Required | Description | +| -------------- | ---------------- | -------- | --------------------------------------------- | +| `property` | `string` | Yes | Property name accessible as `this.{property}` | +| `token` | `Token` | Yes | DI token to resolve when property is accessed | +| `errorMessage` | `string` | No | Custom error when plugin is not installed | + +### Side-Effect Import + +The TypeScript augmentation file must be imported somewhere in your plugin's barrel export so the type declarations take effect: + +```typescript +// index.ts +import './my-plugin.context-extension'; // side-effect import for type augmentation +export { MyPlugin } from './my-plugin.plugin'; +export { MyServiceToken } from './my-plugin.symbols'; +``` + --- ## Official Plugins For official plugin installation, configuration, and examples, see the **official-plugins** skill. FrontMCP provides 6 official plugins: CodeCall, Remember, Approval, Cache, Feature Flags, and Dashboard. Install individually or via `@frontmcp/plugins` (meta-package). +## Recommended Folder Structure + +```text +plugins/ + my-plugin/ + index.ts # Barrel exports: plugin, tokens, types, side-effect import + my-plugin.plugin.ts # Plugin class extending DynamicPlugin + my-plugin.types.ts # Options Zod schema, TypeScript types, interfaces + my-plugin.symbols.ts # DI tokens: export const MY_TOKEN: Token = Symbol('...') + my-plugin.context-extension.ts # Module augmentation (declare module '@frontmcp/sdk') + providers/ + index.ts # Barrel for providers + my-service.provider.ts # @Provider class with business logic + my-store-memory.provider.ts # In-memory store implementation + my-store-redis.provider.ts # Redis store implementation (optional) + tools/ # Optional — only if plugin provides tools + index.ts + my-action.tool.ts # @Tool class registered via @Plugin({ tools: [...] }) + __tests__/ + my-plugin.spec.ts # Plugin tests +``` + +**Key files explained:** + +- `index.ts` — Must import the context extension file as a side effect: `import './my-plugin.context-extension'` +- `symbols.ts` — All DI tokens in one place. Other files import from here, not from the plugin class +- `context-extension.ts` — `declare module '@frontmcp/sdk' { interface ExecutionContextBase { readonly myProp: T } }` +- `plugin.ts` — The `@Plugin()` decorated class. Lists `providers`, `exports`, `contextExtensions`, `tools` + ## Common Patterns | Pattern | Correct | Incorrect | Why | @@ -316,7 +477,9 @@ For official plugin installation, configuration, and examples, see the **officia - [ ] Module augmentation file exists with `declare module '@frontmcp/sdk'` block - [ ] Augmented properties are `readonly` on `ExecutionContextBase` -- [ ] Augmentation file is imported (side-effect import) in the plugin module +- [ ] `PromptContext` is augmented alongside `ExecutionContextBase` for context extensions +- [ ] `declare global` block exists for each metadata extension interface used +- [ ] Augmentation file is imported (side-effect import) in the plugin barrel export ### Runtime @@ -327,13 +490,15 @@ For official plugin installation, configuration, and examples, see the **officia ## Troubleshooting -| Problem | Cause | Solution | -| ------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- | -| `this.auditLog` has type `any` or is unrecognized | Module augmentation file not imported | Add side-effect import: `import './audit-log.context-extension'` in plugin file | -| Circular dependency error at startup | Calling `installExtension()` at module top level | Remove manual installation; use `contextExtensions` metadata array instead | -| Provider not found in tool context | Provider not listed in plugin `exports` | Add the provider to both `providers` and `exports` arrays | -| Hooks fire for unrelated apps in gateway | Plugin `scope` set to `'server'` | Change to `scope: 'app'` (default) unless server-wide behavior is intended | -| `DynamicPlugin.init()` options ignored | Overriding constructor without calling `super()` | Ensure constructor calls `super()` and merges defaults properly | +| Problem | Cause | Solution | +| ---------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `this.auditLog` has type `any` or is unrecognized | Module augmentation file not imported | Add side-effect import: `import './audit-log.context-extension'` in plugin file | +| Circular dependency error at startup | Calling `installExtension()` at module top level | Remove manual installation; use `contextExtensions` metadata array instead | +| Provider not found in tool context | Provider not listed in plugin `exports` | Add the provider to both `providers` and `exports` arrays | +| Hooks fire for unrelated apps in gateway | Plugin `scope` set to `'server'` | Change to `scope: 'app'` (default) unless server-wide behavior is intended | +| `DynamicPlugin.init()` options ignored | Overriding constructor without calling `super()` | Ensure constructor calls `super()` and merges defaults properly | +| `ProviderNotRegisteredError` for context extension | Token in `contextExtensions` not in `providers` | Ensure the token used in `contextExtensions[].token` is registered in the plugin's `providers` array. Use `{ provide: MyToken, useClass: MyService }` or list the class directly. If using `dynamicProviders()`, return the provider there | +| Provider works in tools but not in context extension | Using class reference instead of Symbol token | Create a typed `Token = Symbol('name')` in `symbols.ts`, use it in both `providers` and `contextExtensions`. Direct class references can fail if not constructable without dependencies | ## Reference diff --git a/libs/skills/catalog/frontmcp-development/references/create-prompt.md b/libs/skills/catalog/frontmcp-development/references/create-prompt.md index e219e6306..5b28c9e5e 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-prompt.md +++ b/libs/skills/catalog/frontmcp-development/references/create-prompt.md @@ -1,3 +1,8 @@ +--- +name: create-prompt +description: Define reusable AI interaction patterns that produce structured message sequences +--- + # Creating MCP Prompts Prompts define reusable AI interaction patterns in the MCP protocol. They produce structured message sequences that clients use to guide LLM conversations. In FrontMCP, prompts are classes extending `PromptContext`, decorated with `@Prompt`, that return `GetPromptResult` objects. @@ -64,8 +69,10 @@ class CodeReviewPrompt extends PromptContext { The `@Prompt` decorator accepts: - `name` (required) -- unique prompt name +- `title` (optional) -- human-readable display title for UIs (if omitted, `name` is used) - `description` (optional) -- human-readable description - `arguments` (optional) -- array of `PromptArgument` objects +- `icons` (optional) -- array of Icon objects for UI representation (per MCP spec) ### PromptArgument Structure diff --git a/libs/skills/catalog/frontmcp-development/references/create-provider.md b/libs/skills/catalog/frontmcp-development/references/create-provider.md index c8b5ec7bb..72d6ae84f 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-provider.md +++ b/libs/skills/catalog/frontmcp-development/references/create-provider.md @@ -1,3 +1,8 @@ +--- +name: create-provider +description: Create singleton DI providers for database pools, API clients, and shared services +--- + # Creating Providers (Dependency Injection) Providers are singleton services — database pools, API clients, config objects — that tools, resources, prompts, and agents can access via `this.get(token)`. diff --git a/libs/skills/catalog/frontmcp-development/references/create-resource.md b/libs/skills/catalog/frontmcp-development/references/create-resource.md index e84e0e588..fdbdbc525 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-resource.md +++ b/libs/skills/catalog/frontmcp-development/references/create-resource.md @@ -1,3 +1,8 @@ +--- +name: create-resource +description: Expose data to AI clients via URI-based static resources and parameterized templates +--- + # Creating MCP Resources Resources expose data to AI clients through URI-based access following the MCP protocol. FrontMCP supports two kinds: **static resources** with fixed URIs (`@Resource`) and **resource templates** with parameterized URI patterns (`@ResourceTemplate`). @@ -31,9 +36,11 @@ Resources expose data to AI clients through URI-based access following the MCP p The `@Resource` decorator accepts: - `name` (required) -- unique resource name +- `title` (optional) -- human-readable display title for UIs (if omitted, `name` is used) - `uri` (required) -- static URI with a valid scheme per RFC 3986 - `description` (optional) -- human-readable description - `mimeType` (optional) -- MIME type of the resource content +- `icons` (optional) -- array of Icon objects for UI representation (per MCP spec) ### Class-Based Pattern @@ -138,9 +145,11 @@ Supported return shapes: The `@ResourceTemplate` decorator accepts: - `name` (required) -- unique resource template name +- `title` (optional) -- human-readable display title for UIs (if omitted, `name` is used) - `uriTemplate` (required) -- URI pattern with `{paramName}` placeholders (RFC 6570 style) - `description` (optional) -- human-readable description - `mimeType` (optional) -- MIME type of the resource content +- `icons` (optional) -- array of Icon objects for UI representation (per MCP spec) ### Class-Based Pattern @@ -426,6 +435,82 @@ nx generate @frontmcp/nx:resource This creates the resource file, spec file, and updates barrel exports. +## Resource Argument Autocompletion + +Resource templates with parameterized URIs can provide autocompletion for their arguments. This is useful when template parameters represent dynamic values that can be searched or enumerated, such as user IDs, product names, or project slugs. + +### When to Use + +- Template parameters reference entities that exist in a database or external service (user IDs, product names, etc.) +- Clients benefit from discovering valid parameter values without prior knowledge +- The parameter space is searchable or enumerable given a partial input string + +### Types + +The autocompletion API uses two types from `@frontmcp/sdk`: + +```typescript +interface ResourceCompletionResult { + values: string[]; + total?: number; + hasMore?: boolean; +} + +type ResourceArgumentCompleter = (partial: string) => Promise | ResourceCompletionResult; +``` + +- `values` -- the list of matching completions for the partial input +- `total` -- optional total number of matches (useful when `values` is a truncated subset) +- `hasMore` -- optional flag indicating additional matches exist beyond what was returned + +### How to Implement + +Override the `getArgumentCompleter(argName)` method in your `ResourceContext` subclass. Return a completer function for argument names you support, or `null` for unknown arguments. + +```typescript +getArgumentCompleter(argName: string): ResourceArgumentCompleter | null { + if (argName === 'myParam') { + return async (partial) => { + // Search or filter based on partial input + const matches = await findMatches(partial); + return { values: matches, total: matches.length }; + }; + } + return null; +} +``` + +### Complete Example + +A user profile template resource that autocompletes user IDs by searching a user service: + +```typescript +@ResourceTemplate({ + name: 'user-profile', + description: 'User profile by ID', + 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, email: user.email }; + } + + 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), total: users.length }; + }; + } + return null; + } +} +``` + +When a client requests completions for the `userId` parameter with a partial string like `"al"`, the completer queries the user service and returns matching IDs. + ## Common Patterns | Pattern | Correct | Incorrect | Why | @@ -453,6 +538,11 @@ This creates the resource file, spec file, and updates barrel exports. - [ ] Binary resources return valid base64 in the `blob` field - [ ] DI dependencies resolve correctly via `this.get()` +### Autocompletion + +- [ ] Template resources with dynamic params implement `getArgumentCompleter()` +- [ ] Completer returns `{ values, total?, hasMore? }` matching the partial input + ## Troubleshooting | Problem | Cause | Solution | diff --git a/libs/skills/catalog/frontmcp-development/references/create-skill-with-tools.md b/libs/skills/catalog/frontmcp-development/references/create-skill-with-tools.md index b32935e85..25099a2f6 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-skill-with-tools.md +++ b/libs/skills/catalog/frontmcp-development/references/create-skill-with-tools.md @@ -1,3 +1,8 @@ +--- +name: create-skill-with-tools +description: Create skills that combine structured instructions with MCP tool references for orchestration +--- + # Creating a Skill with Tools Skills are knowledge and workflow guides that help LLMs accomplish multi-step tasks using available MCP tools. Unlike tools (which execute actions directly) or agents (which run autonomous LLM loops), skills provide structured instructions, tool references, and context that the AI client uses to orchestrate tool calls on its own. @@ -341,6 +346,44 @@ Use `hideFromDiscovery: true` to register a skill that is not listed in discover class AdminMaintenanceSkill extends SkillContext {} ``` +## Agent Skills Spec Fields + +Skills support additional metadata fields from the Anthropic Agent Skills specification: + +```typescript +@Skill({ + name: 'deploy-to-prod', + description: 'Production deployment workflow', + instructions: { file: './deploy-prod.md' }, + tools: [BuildTool, DeployTool, HealthCheckTool], + priority: 10, // Higher = earlier in search results + license: 'MIT', // License identifier + compatibility: 'Node.js 24+, Docker', // Environment requirements (max 500 chars) + allowedTools: 'Read Edit Bash(git status)', // Pre-approved tools (space-delimited) + specMetadata: { + // Arbitrary key-value metadata + author: 'platform-team', + version: '2.0.0', + }, + resources: { + // Bundled resource directories + scripts: './scripts', // Helper scripts + references: './references', // Reference documents + assets: './assets', // Static assets + }, +}) +class DeployToProdSkill extends SkillContext {} +``` + +| Field | Description | +| --------------- | ---------------------------------------------------------------- | +| `priority` | Search ranking weight; higher = earlier (default: `0`) | +| `license` | License identifier (e.g., `'MIT'`, `'Apache-2.0'`) | +| `compatibility` | Environment requirements (max 500 chars) | +| `allowedTools` | Space-delimited pre-approved tool names for the skill | +| `specMetadata` | Arbitrary `Record` map (Agent Skills `metadata`) | +| `resources` | Bundled dirs: `{ scripts?, references?, assets? }` paths | + ## Function-Style Builder For skills that do not need a class, use the `skill()` function builder: @@ -575,15 +618,81 @@ class AuditApp {} class AuditServer {} ``` +## CodeCall Compatibility + +When the `CodeCallPlugin` is active in `codecall_only` mode, all tools registered on the server are hidden from `list_tools`. The AI client only sees the three CodeCall meta-tools (`codecall:search`, `codecall:describe`, `codecall:execute`). This means skill instructions that reference tool names directly (e.g., "Use the `build_project` tool") become misleading -- the AI cannot call those tools because they do not appear in the tool listing. + +### When This Matters + +This is only relevant when the server initializes CodeCall in `codecall_only` mode: + +```typescript +CodeCallPlugin.init({ mode: 'codecall_only' }); +``` + +With `codecall_opt_in` or `metadata_driven` modes, tools remain visible in `list_tools` alongside the CodeCall meta-tools. In those modes, standard tool-referencing instructions continue to work without changes. + +### Writing Dual-Mode Instructions + +Write skill instructions that work regardless of whether CodeCall is active. Instead of referencing tool names as direct calls, instruct the AI to use the search-describe-execute pattern: + +```markdown +## Step 1: Find Available Tools + +Search for tools related to your task using codecall:search. +Query: ["build project", "run tests", "deploy"] + +## Step 2: Describe Tool Interfaces + +Once you find matching tools, use codecall:describe to understand their input schemas. + +## Step 3: Execute + +Use codecall:execute with an AgentScript that calls the tools: +const build = await callTool('build_project', { target: 'production' }); +const tests = await callTool('run_tests', { suite: 'e2e' }); +``` + +### Supporting Both Direct and CodeCall Workflows + +If you want the skill to work with and without CodeCall, list the tool names in the `tools` array (so they are associated with the skill in metadata) AND include instructions for both direct calls and the CodeCall workflow. This way: + +- In standard mode, the AI sees the tools in `list_tools` and can call them directly using the tool names from the `tools` array. +- In `codecall_only` mode, the AI follows the search-describe-execute instructions to discover and invoke the same tools through CodeCall. + +```typescript +@Skill({ + name: 'deploy-service', + description: 'Deploy a service through the pipeline', + instructions: `# Deploy Service + +## Finding the Tools +If tools are not directly visible, search for them: +Use codecall:search with query ["build project", "run tests", "deploy to environment"]. +Then use codecall:describe on each result to confirm the input schema. + +## Step 1: Build +Call build_project with the service name and target environment. +If using CodeCall: codecall:execute with callTool('build_project', { ... }). + +## Step 2: Test +Call run_tests with the test suite name. +If using CodeCall: codecall:execute with callTool('run_tests', { ... }).`, + tools: [BuildProjectTool, RunTestsTool, DeployToEnvTool], +}) +class DeployServiceSkill extends SkillContext {} +``` + ## Common Patterns -| Pattern | Correct | Incorrect | Why | -| ------------------ | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -| Tool references | `tools: [BuildTool, 'run_tests', { name: 'deploy', purpose: '...', required: true }]` | `tools: [{ class: BuildTool }]` (object with `class` key) | The `tools` array accepts class refs, strings, or `{ name, purpose, required }` objects only | -| Tool validation | `toolValidation: 'strict'` for production skills | Omitting `toolValidation` for critical workflows | Default is `'warn'`; production skills should fail fast on missing tools with `'strict'` | -| Instruction source | `instructions: { file: './skills/deploy.md' }` for long content | Inlining hundreds of lines in the decorator string | File-based instructions keep decorator metadata readable and instructions maintainable | -| Skill visibility | `visibility: 'both'` (default) for public skills | Setting `visibility: 'mcp'` when HTTP discovery is also needed | Skills with `'mcp'` visibility are hidden from `/llm.txt` and `/skills` HTTP endpoints | -| Parameter types | `parameters: [{ name: 'env', type: 'string', required: true }]` | `parameters: { env: 'string' }` (plain object shape) | Parameters must be an array of `{ name, description, type, required?, default? }` objects | +| Pattern | Correct | Incorrect | Why | +| ---------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Tool references | `tools: [BuildTool, 'run_tests', { name: 'deploy', purpose: '...', required: true }]` | `tools: [{ class: BuildTool }]` (object with `class` key) | The `tools` array accepts class refs, strings, or `{ name, purpose, required }` objects only | +| Tool validation | `toolValidation: 'strict'` for production skills | Omitting `toolValidation` for critical workflows | Default is `'warn'`; production skills should fail fast on missing tools with `'strict'` | +| Instruction source | `instructions: { file: './skills/deploy.md' }` for long content | Inlining hundreds of lines in the decorator string | File-based instructions keep decorator metadata readable and instructions maintainable | +| Skill visibility | `visibility: 'both'` (default) for public skills | Setting `visibility: 'mcp'` when HTTP discovery is also needed | Skills with `'mcp'` visibility are hidden from `/llm.txt` and `/skills` HTTP endpoints | +| Parameter types | `parameters: [{ name: 'env', type: 'string', required: true }]` | `parameters: { env: 'string' }` (plain object shape) | Parameters must be an array of `{ name, description, type, required?, default? }` objects | +| CodeCall compatibility | List tools AND include codecall:search/execute instructions | Only listing tools by name | When CodeCall hides tools, AI can't find them without search instructions | ## Verification Checklist diff --git a/libs/skills/catalog/frontmcp-development/references/create-skill.md b/libs/skills/catalog/frontmcp-development/references/create-skill.md index 82532defe..b1dcc9e5c 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-skill.md +++ b/libs/skills/catalog/frontmcp-development/references/create-skill.md @@ -1,3 +1,8 @@ +--- +name: create-skill +description: Create instruction-only skills that package knowledge and workflow guides for AI clients +--- + # Creating Instruction-Only Skills Skills are knowledge and workflow packages that teach AI clients how to accomplish tasks. Unlike tools (which execute actions) or agents (which run autonomous LLM loops), a skill provides structured instructions that the AI follows on its own. An instruction-only skill contains no tool references -- it is purely a guide. @@ -30,16 +35,23 @@ Create a class extending `SkillContext` and decorate it with `@Skill`. The decor ### SkillMetadata Fields -| Field | Type | Required | Description | -| ------------------- | ----------------------------------------------- | -------- | ----------------------------------------------------------- | -| `name` | `string` | Yes | Unique skill name in kebab-case | -| `description` | `string` | Yes | Short description of what the skill teaches | -| `instructions` | `string \| { file: string } \| { url: string }` | Yes | The skill content -- see instruction sources below | -| `parameters` | `SkillParameter[]` | No | Customization parameters for the skill | -| `examples` | `SkillExample[]` | No | Usage scenarios and expected outcomes | -| `tags` | `string[]` | No | Categorization tags for discovery | -| `visibility` | `'mcp' \| 'http' \| 'both'` | No | Where the skill is discoverable (default: `'both'`) | -| `hideFromDiscovery` | `boolean` | No | Register but hide from listing endpoints (default: `false`) | +| Field | Type | Required | Description | +| ------------------- | ----------------------------------------------- | -------- | -------------------------------------------------------------- | +| `name` | `string` | Yes | Unique skill name in kebab-case (max 64 chars) | +| `description` | `string` | Yes | Short description (max 1024 chars, no HTML/XML) | +| `instructions` | `string \| { file: string } \| { url: string }` | Yes | The skill content -- see instruction sources below | +| `parameters` | `SkillParameter[]` | No | Customization parameters for the skill | +| `examples` | `SkillExample[]` | No | Usage scenarios and expected outcomes | +| `tags` | `string[]` | No | Categorization tags for discovery | +| `visibility` | `'mcp' \| 'http' \| 'both'` | No | Where the skill is discoverable (default: `'both'`) | +| `hideFromDiscovery` | `boolean` | No | Register but hide from listing endpoints (default: `false`) | +| `priority` | `number` | No | Search ranking weight; higher = earlier (default: `0`) | +| `toolValidation` | `'strict' \| 'warn' \| 'ignore'` | No | Behavior when referenced tools are missing (default: `'warn'`) | +| `license` | `string` | No | License identifier per Agent Skills spec (e.g., `'MIT'`) | +| `compatibility` | `string` | No | Environment requirements (max 500 chars) | +| `specMetadata` | `Record` | No | Arbitrary key-value map (Agent Skills spec `metadata` field) | +| `allowedTools` | `string` | No | Space-delimited pre-approved tool names (Agent Skills spec) | +| `resources` | `SkillResources` | No | Bundled dirs: `{ scripts?, references?, assets? }` | ### Basic Example diff --git a/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md b/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md index 8d45e4c46..8a536e404 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md +++ b/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md @@ -1,3 +1,8 @@ +--- +name: create-tool-annotations +description: Reference for MCP tool annotation hints like readOnly, destructive, and idempotent +--- + # Tool Annotations Reference Annotations provide hints to MCP clients about tool behavior: diff --git a/libs/skills/catalog/frontmcp-development/references/create-tool-output-schema-types.md b/libs/skills/catalog/frontmcp-development/references/create-tool-output-schema-types.md index 87d0d64b7..d10332907 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-tool-output-schema-types.md +++ b/libs/skills/catalog/frontmcp-development/references/create-tool-output-schema-types.md @@ -1,3 +1,8 @@ +--- +name: create-tool-output-schema-types +description: Reference for all supported outputSchema types including Zod shapes and JSON Schema +--- + # Output Schema Types Reference All supported `outputSchema` types for `@Tool`: diff --git a/libs/skills/catalog/frontmcp-development/references/create-tool.md b/libs/skills/catalog/frontmcp-development/references/create-tool.md index 8a6d33571..61e45cf0e 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-tool.md +++ b/libs/skills/catalog/frontmcp-development/references/create-tool.md @@ -1,3 +1,8 @@ +--- +name: create-tool +description: Build MCP tools with Zod input/output validation and dependency injection +--- + # Creating an MCP Tool Tools are the primary way to expose executable actions to AI clients in the MCP protocol. In FrontMCP, tools are TypeScript classes that extend `ToolContext`, decorated with `@Tool`, and registered on a `@FrontMcp` server or inside an `@App`. @@ -67,7 +72,19 @@ class GreetUserTool extends ToolContext { - `this.output` -- the output (available after execute) - `this.metadata` -- tool metadata from the decorator - `this.scope` -- the current scope instance -- `this.context` -- the execution context +- `this.context` -- the execution context (see below) + +**`this.context` properties (FrontMcpContext):** + +| Property | Type | Description | +| -------------- | ------------------- | ----------------------------------- | +| `requestId` | `string` | Unique ID for this request | +| `sessionId` | `string` | Session identifier | +| `scopeId` | `string` | Scope identifier | +| `authInfo` | `Partial` | Authentication info for the request | +| `traceContext` | `TraceContext` | Distributed tracing context | +| `timestamp` | `number` | Request timestamp | +| `metadata` | `RequestMetadata` | Request headers, client IP, etc. | ## Input Schema: Zod Raw Shapes @@ -411,6 +428,102 @@ class ExpensiveOperationTool extends ToolContext { } ``` +## Auth Providers + +Declare which auth providers a tool requires. Credentials are loaded before tool execution. + +```typescript +// String shorthand — single provider +@Tool({ + name: 'create_issue', + description: 'Create a GitHub issue', + inputSchema: { title: z.string(), body: z.string() }, + authProviders: ['github'], +}) +class CreateIssueTool extends ToolContext { + /* ... */ +} + +// Full mapping — with scopes and required flag +@Tool({ + name: 'deploy_app', + description: 'Deploy to cloud', + inputSchema: { env: z.string() }, + authProviders: [ + { name: 'github', required: true, scopes: ['repo', 'workflow'] }, + { name: 'aws', required: false, alias: 'cloud' }, + ], +}) +class DeployAppTool extends ToolContext { + /* ... */ +} +``` + +Auth provider mapping fields: + +- `name` — Provider name (must match a registered `@AuthProvider`) +- `required?` — Whether credential is required (default: `true`) +- `scopes?` — Required OAuth scopes +- `alias?` — Alias for injection when using multiple providers + +## Elicitation (Interactive Input) + +Tools can request interactive input from users mid-execution using `this.elicit()`. Requires `elicitation` to be enabled at server level. + +```typescript +@Tool({ + name: 'confirm_delete', + description: 'Delete a resource after user confirmation', + inputSchema: { resourceId: z.string() }, +}) +class ConfirmDeleteTool extends ToolContext { + async execute(input: { resourceId: string }) { + const result = await this.elicit('Are you sure you want to delete this resource?', { + confirm: z.boolean().describe('Confirm deletion'), + reason: z.string().optional().describe('Reason for deletion'), + }); + + if (result.action === 'accept' && result.data.confirm) { + await this.get(ResourceService).delete(input.resourceId); + return 'Resource deleted'; + } + return 'Deletion cancelled'; + } +} +``` + +> **Note:** Elicitation must be enabled at server level: `@FrontMcp({ elicitation: { enabled: true } })`. See `configure-elicitation` for full configuration options. + +## Tool Examples + +Provide usage examples for documentation and discovery: + +```typescript +@Tool({ + name: 'convert_currency', + description: 'Convert between currencies', + inputSchema: { + amount: z.number(), + from: z.string(), + to: z.string(), + }, + examples: [ + { + description: 'Convert USD to EUR', + input: { amount: 100, from: 'USD', to: 'EUR' }, + output: { converted: 85.5, rate: 0.855 }, + }, + { + description: 'Convert with large amount', + input: { amount: 1_000_000, from: 'GBP', to: 'JPY' }, + }, + ], +}) +class ConvertCurrencyTool extends ToolContext { + /* ... */ +} +``` + ## Common Patterns | Pattern | Correct | Incorrect | Why | diff --git a/libs/skills/catalog/frontmcp-development/references/create-workflow.md b/libs/skills/catalog/frontmcp-development/references/create-workflow.md index 001c521a6..926a098af 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-workflow.md +++ b/libs/skills/catalog/frontmcp-development/references/create-workflow.md @@ -1,3 +1,8 @@ +--- +name: create-workflow +description: Connect multiple jobs into managed DAG pipelines with dependencies, conditions, and triggers +--- + # Creating Workflows Workflows connect multiple jobs into managed execution pipelines with step dependencies, conditions, and triggers. A workflow defines a directed acyclic graph (DAG) of steps where each step runs a named job, and the framework handles ordering, parallelism, error propagation, and trigger management. diff --git a/libs/skills/catalog/frontmcp-development/references/decorators-guide.md b/libs/skills/catalog/frontmcp-development/references/decorators-guide.md index 9586d0e1b..6762d9a57 100644 --- a/libs/skills/catalog/frontmcp-development/references/decorators-guide.md +++ b/libs/skills/catalog/frontmcp-development/references/decorators-guide.md @@ -1,3 +1,8 @@ +--- +name: decorators-guide +description: Complete reference for the hierarchical decorator system from @FrontMcp to @Tool +--- + # FrontMCP Decorators - Complete Reference ## Architecture Overview @@ -56,28 +61,34 @@ FrontMCP uses a hierarchical decorator system. The nesting order is: **Key fields:** -| Field | Description | -| --------------- | ------------------------------------------------------------------------------- | -| `info` | Server name, version, and description | -| `apps` | Array of `@App` classes to mount | -| `redis?` | Redis connection options | -| `plugins?` | Global plugins | -| `providers?` | Global DI providers | -| `tools?` | Standalone tools (outside apps) | -| `resources?` | Standalone resources | -| `skills?` | Standalone skills | -| `skillsConfig?` | Skills feature configuration (enabled, cache, auth) | -| `transport?` | Transport preset ('modern', 'legacy', 'stateless-api', 'full') or config object | -| `auth?` | Authentication mode and OAuth configuration (AuthOptionsInput) | -| `http?` | HTTP server options (port, host, cors) | -| `logging?` | Logging configuration | -| `elicitation?` | Elicitation store config | -| `sqlite?` | SQLite storage config | -| `pubsub?` | Pub/sub configuration | -| `jobs?` | Job scheduler config | -| `throttle?` | Rate limiting config | -| `pagination?` | Pagination defaults | -| `ui?` | UI configuration | +| Field | Description | +| --------------- | -------------------------------------------------------------------------------- | +| `info` | Server name, version, and description | +| `apps` | Array of `@App` classes to mount | +| `serve?` | Auto-start HTTP server (default: `true`). Set `false` for programmatic usage | +| `splitByApp?` | If `true`, each app gets its own scope and basePath. Default: `false` | +| `redis?` | Redis / Vercel KV connection for sessions, transport persistence, auth tokens | +| `plugins?` | Global plugins (instantiated per scope) | +| `providers?` | Global DI providers available to all apps | +| `tools?` | Standalone tools (outside apps, merged with app tools) | +| `resources?` | Standalone resources (merged with app resources) | +| `skills?` | Standalone skills (merged with app skills) | +| `skillsConfig?` | Skills HTTP endpoints (`/llm.txt`, `/skills`) and MCP tool config | +| `transport?` | Transport preset (`'modern'`, `'legacy'`, `'stateless-api'`, `'full'`) or object | +| `auth?` | Authentication mode: `'public'`, `'transparent'`, `'local'`, `'remote'` | +| `http?` | HTTP server options (port, host, cors, socketPath) | +| `logging?` | Logging configuration (transports and levels) | +| `elicitation?` | Enable interactive user input during tool execution | +| `sqlite?` | SQLite storage for local deployments (sessions, events) | +| `pubsub?` | Redis pub/sub for resource subscriptions (falls back to `redis` config) | +| `jobs?` | Background jobs/workflows system (`{ enabled, store? }`) | +| `throttle?` | Server-level guard config (see note below) | +| `pagination?` | List operation pagination (`tools/list` endpoint) | +| `ui?` | UI rendering config (CDN overrides for widget imports) | +| `extApps?` | Widget-to-host MCP Apps communication (host capabilities, session validation) | +| `loader?` | Default npm/ESM package loader for `App.esm()` / `App.remote()` apps | + +> **Throttle vs per-tool guards:** Server-level `throttle` is a `GuardConfig` object with `global`, `defaultRateLimit`, `defaultConcurrency`, `defaultTimeout` sub-fields that set server-wide defaults. Tool-level `rateLimit`, `concurrency`, `timeout` fields (on `@Tool`) override these defaults per tool. ```typescript import { FrontMcp } from '@frontmcp/sdk'; @@ -103,21 +114,23 @@ class MyServer {} **Key fields:** -| Field | Description | -| ------------- | ----------------------------------------------------- | -| `name` | Application name | -| `tools?` | Array of tool classes or function-built tools | -| `resources?` | Array of resource classes or function-built resources | -| `prompts?` | Array of prompt classes or function-built prompts | -| `agents?` | Array of agent classes | -| `skills?` | Array of skill definitions | -| `plugins?` | App-scoped plugins | -| `providers?` | App-scoped DI providers | -| `adapters?` | External source adapters | -| `auth?` | Auth configuration | -| `standalone?` | Whether the app runs independently | -| `jobs?` | Job definitions | -| `workflows?` | Workflow definitions | +| Field | Description | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Application name (unique within server) | +| `description?` | Human-readable description for docs and UIs | +| `tools?` | Array of tool classes or function-built tools | +| `resources?` | Array of resource classes or function-built resources | +| `prompts?` | Array of prompt classes or function-built prompts | +| `agents?` | Array of agent classes (each exposed as `use-agent:` tool) | +| `skills?` | Array of skill definitions | +| `plugins?` | App-scoped plugins | +| `providers?` | App-scoped DI providers | +| `authProviders?` | Named auth providers (e.g., GitHub, Google OAuth) separate from `auth` | +| `adapters?` | External source adapters (e.g., OpenAPI) | +| `auth?` | App-level auth config (overrides server default) | +| `standalone?` | `boolean \| 'includeInParent'` — `true`: isolated scope, excluded. `'includeInParent'`: isolated scope but tools exposed in parent | +| `jobs?` | Background job definitions | +| `workflows?` | Multi-step workflow definitions | ```typescript import { App } from '@frontmcp/sdk'; @@ -142,19 +155,21 @@ class AnalyticsApp {} **Key fields:** -| Field | Description | -| -------------------- | ---------------------------------------------------------- | -| `name` | Tool name (used in MCP protocol) | -| `description` | Human-readable description for the LLM | -| `inputSchema` | Zod raw shape defining input parameters | -| `outputSchema?` | Zod schema for output validation | -| `annotations?` | MCP tool annotations (readOnlyHint, destructiveHint, etc.) | -| `tags?` | Categorization tags | -| `hideFromDiscovery?` | Hide from tool listing | -| `concurrency?` | Max concurrent executions | -| `rateLimit?` | Rate limiting configuration | -| `timeout?` | Execution timeout in ms | -| `ui?` | UI rendering hints | +| Field | Description | +| -------------------- | -------------------------------------------------------------------- | +| `name` | Tool name (used in MCP protocol, snake_case) | +| `description` | Human-readable description for the LLM | +| `inputSchema` | Zod raw shape defining input parameters | +| `outputSchema?` | Output type: Zod schema, `'string'`, `'image'`, `'audio'`, etc. | +| `annotations?` | MCP tool annotations (`readOnlyHint`, `destructiveHint`, etc.) | +| `tags?` | Categorization tags for filtering | +| `hideFromDiscovery?` | Hide from `tools/list` (still callable directly) | +| `examples?` | Usage examples: `[{ description, input, output? }]` | +| `authProviders?` | Per-tool auth providers: `['GitHub']` or `[{ name, scopes, alias }]` | +| `rateLimit?` | Rate limiting: `{ maxRequests, windowMs, partitionBy }` | +| `concurrency?` | Concurrency control: `{ maxConcurrent }` | +| `timeout?` | Execution timeout: `{ executeMs }` | +| `ui?` | UI widget configuration for tool rendering | ```typescript import { Tool, ToolContext } from '@frontmcp/sdk'; @@ -188,9 +203,11 @@ class SearchUsersTool extends ToolContext { | Field | Description | | -------------- | ------------------------------------------------------------------- | -| `name` | Prompt name | +| `name` | Prompt name (used in MCP protocol) | +| `title?` | Human-readable display title for UIs | | `description?` | What this prompt does | | `arguments?` | Array of argument definitions (`{ name, description?, required? }`) | +| `icons?` | Array of Icon objects for UI representation (per MCP spec) | ```typescript import { Prompt, PromptContext } from '@frontmcp/sdk'; @@ -232,10 +249,12 @@ class CodeReviewPrompt extends PromptContext { | Field | Description | | -------------- | -------------------------------------------- | -| `name` | Resource name | +| `name` | Resource name (used in MCP protocol) | +| `title?` | Human-readable display title for UIs | | `uri` | Fixed URI (e.g., `config://app/settings`) | | `description?` | What this resource provides | | `mimeType?` | Content MIME type (e.g., `application/json`) | +| `icons?` | Array of Icon objects for UI representation | ```typescript import { Resource, ResourceContext } from '@frontmcp/sdk'; @@ -267,9 +286,11 @@ class AppConfigResource extends ResourceContext { | Field | Description | | -------------- | --------------------------------------------------------------- | | `name` | Resource template name | +| `title?` | Human-readable display title for UIs | | `uriTemplate` | URI template with parameters (e.g., `users://{userId}/profile`) | | `description?` | What this resource provides | | `mimeType?` | Content MIME type | +| `icons?` | Array of Icon objects for UI representation | ```typescript import { ResourceTemplate, ResourceContext } from '@frontmcp/sdk'; @@ -340,16 +361,24 @@ class ResearchAgent extends AgentContext { **Key fields:** -| Field | Description | -| ----------------- | ------------------------------------------------------ | -| `name` | Skill name | -| `description` | What this skill enables | -| `instructions` | Detailed instructions the LLM should follow | -| `tools?` | Tools bundled with this skill | -| `parameters?` | Configurable parameters | -| `examples?` | Usage examples | -| `visibility?` | Where skill is visible: `'mcp'`, `'http'`, or `'both'` | -| `toolValidation?` | Validation rules for tool usage | +| Field | Description | +| -------------------- | ------------------------------------------------------------------------------ | +| `name` | Skill name (kebab-case, max 64 chars) | +| `description` | What this skill enables (max 1024 chars, no HTML/XML) | +| `instructions` | Inline string, `{ file: '...' }`, or `{ url: '...' }` | +| `tools?` | Tool classes, names, or `{ tool/name, purpose?, required? }` refs | +| `parameters?` | Input parameters: `[{ name, description?, type?, default? }]` | +| `examples?` | Usage examples: `[{ scenario, parameters?, expectedOutcome? }]` | +| `visibility?` | Discovery scope: `'mcp'`, `'http'`, or `'both'` (default: `'both'`) | +| `toolValidation?` | `'strict'` \| `'warn'` \| `'ignore'` for missing tool refs (default: `'warn'`) | +| `priority?` | Search ranking weight (higher = earlier). Default: `0` | +| `hideFromDiscovery?` | Hide from search results; still loadable by ID | +| `tags?` | Tags for categorization and search | +| `license?` | License identifier (per Agent Skills spec, e.g., `'MIT'`) | +| `compatibility?` | Environment requirements (max 500 chars, e.g., `'Node.js 18+'`) | +| `specMetadata?` | Arbitrary key-value map (Agent Skills spec `metadata` field) | +| `allowedTools?` | Space-delimited pre-approved tool names (Agent Skills spec) | +| `resources?` | Bundled dirs: `{ scripts?, references?, assets? }` (Agent Skills spec) | ```typescript import { Skill } from '@frontmcp/sdk'; @@ -493,23 +522,35 @@ class ApprovalFlow {} **Key fields:** -| Field | Description | -| ------------- | --------------------------------------------------------- | -| `name` | Job name | -| `description` | What the job does | -| `schedule?` | Cron expression (e.g., `'0 */6 * * *'` for every 6 hours) | +| Field | Description | +| -------------------- | ------------------------------------------------------------- | +| `name` | Job name | +| `description` | What the job does | +| `inputSchema` | Zod schema for job input parameters | +| `outputSchema` | Zod schema for job output | +| `retry?` | `{ maxAttempts, backoffMs, backoffMultiplier, maxBackoffMs }` | +| `timeout?` | Execution timeout in ms | +| `tags?` | Categorization tags | +| `labels?` | Key-value labels (e.g., `{ env: 'prod' }`) | +| `hideFromDiscovery?` | Hide from job listing | +| `permissions?` | Access control: `[{ action: 'execute', roles: ['admin'] }]` | ```typescript import { Job, JobContext } from '@frontmcp/sdk'; +import { z } from 'zod'; @Job({ name: 'sync_data', description: 'Synchronize data from external sources', - schedule: '0 */6 * * *', + inputSchema: z.object({ source: z.string().describe('Data source to sync') }), + outputSchema: z.object({ synced: z.number() }), + retry: { maxAttempts: 3, backoffMs: 1000, backoffMultiplier: 2, maxBackoffMs: 60_000 }, + timeout: 300_000, }) class SyncDataJob extends JobContext { - async execute() { - await this.get(SyncService).runFullSync(); + async execute(input: { source: string }) { + const count = await this.get(SyncService).runFullSync(input.source); + return { synced: count }; } } ``` @@ -524,11 +565,34 @@ class SyncDataJob extends JobContext { **Key fields:** -| Field | Description | -| ------------- | ----------------------------------- | -| `name` | Workflow name | -| `description` | What this workflow accomplishes | -| `steps` | Array of step definitions (ordered) | +| Field | Description | +| -------------------- | ------------------------------------------------------------------ | +| `name` | Workflow name | +| `description` | What this workflow accomplishes | +| `steps` | Array of step definitions (see step fields below) | +| `trigger?` | `'manual'` \| `'webhook'` \| `'event'` | +| `webhook?` | `{ path, secret, methods }` — required when trigger is `'webhook'` | +| `timeout?` | Overall workflow timeout in ms | +| `maxConcurrency?` | Maximum parallel step concurrency (default: 5) | +| `tags?` | Categorization tags | +| `labels?` | Key-value labels (e.g., `{ env: 'prod' }`) | +| `hideFromDiscovery?` | Hide from workflow listing | +| `permissions?` | Access control: `[{ action: 'execute', roles: ['admin'] }]` | +| `inputSchema?` | Zod schema for workflow input parameters | +| `outputSchema?` | Zod schema for workflow output | + +**Step fields:** + +| Step Field | Description | +| ------------------ | --------------------------------------------------------------- | +| `id` | Unique step identifier | +| `jobName` | Name of the `@Job` to execute | +| `input?` | Static object or `(steps) => object` function for dynamic input | +| `dependsOn?` | Array of step IDs that must complete first | +| `condition?` | `(steps) => boolean` — skip step if returns false | +| `continueOnError?` | Continue workflow if this step fails (default: `false`) | +| `timeout?` | Per-step timeout in ms | +| `retry?` | Per-step retry config (same shape as `@Job.retry`) | ```typescript import { Workflow } from '@frontmcp/sdk'; @@ -536,10 +600,13 @@ import { Workflow } from '@frontmcp/sdk'; @Workflow({ name: 'deploy_pipeline', description: 'Full deployment pipeline', + trigger: 'webhook', + webhookConfig: { path: '/hooks/deploy', secret: process.env.WEBHOOK_SECRET!, methods: ['POST'] }, + timeout: 600_000, steps: [ - { name: 'build', job: BuildJob }, - { name: 'test', job: TestJob }, - { name: 'deploy', job: DeployJob }, + { id: 'build', jobName: 'build_app', input: { env: 'production' } }, + { id: 'test', jobName: 'run_tests', dependsOn: ['build'] }, + { id: 'deploy', jobName: 'deploy_app', dependsOn: ['test'], condition: (steps) => steps.test.success }, ], }) class DeployPipeline {} diff --git a/libs/skills/catalog/frontmcp-development/references/official-adapters.md b/libs/skills/catalog/frontmcp-development/references/official-adapters.md index 04d9bb856..4f103f3cc 100644 --- a/libs/skills/catalog/frontmcp-development/references/official-adapters.md +++ b/libs/skills/catalog/frontmcp-development/references/official-adapters.md @@ -1,3 +1,8 @@ +--- +name: official-adapters +description: Convert OpenAPI specs and external definitions into MCP tools automatically +--- + # Official Adapters Adapters convert external definitions (OpenAPI specs, Lambda functions, etc.) into MCP tools, resources, and prompts automatically. diff --git a/libs/skills/catalog/frontmcp-development/references/official-plugins.md b/libs/skills/catalog/frontmcp-development/references/official-plugins.md index c309af683..09ff58378 100644 --- a/libs/skills/catalog/frontmcp-development/references/official-plugins.md +++ b/libs/skills/catalog/frontmcp-development/references/official-plugins.md @@ -1,3 +1,8 @@ +--- +name: official-plugins +description: Guide to the 6 official plugins for discovery, memory, auth, caching, flags, and monitoring +--- + # Official FrontMCP Plugins FrontMCP ships 6 official plugins that extend server behavior with cross-cutting concerns: semantic tool discovery, session memory, authorization workflows, result caching, feature gating, and visual monitoring. Install individually or via `@frontmcp/plugins` (meta-package re-exporting cache, codecall, and remember). diff --git a/libs/skills/catalog/frontmcp-extensibility/SKILL.md b/libs/skills/catalog/frontmcp-extensibility/SKILL.md new file mode 100644 index 000000000..a84b9483b --- /dev/null +++ b/libs/skills/catalog/frontmcp-extensibility/SKILL.md @@ -0,0 +1,103 @@ +--- +name: frontmcp-extensibility +description: 'Extend FrontMCP servers with external npm packages and libraries. Covers VectoriaDB for semantic search, and patterns for integrating third-party services into providers and tools. Use when adding search, ML, database, or external API capabilities beyond the core SDK.' +tags: [extensibility, vectoriadb, search, integration, npm, provider, external-services] +category: extensibility +targets: [all] +bundle: [full] +priority: 10 +visibility: both +license: Apache-2.0 +metadata: + docs: https://docs.agentfront.dev/frontmcp/extensibility/overview +--- + +# FrontMCP Extensibility + +Patterns and examples for extending FrontMCP servers with external npm packages. The core SDK handles MCP protocol, DI, and lifecycle — this skill shows how to integrate third-party libraries as providers and tools. + +## When to Use This Skill + +### Must Use + +- Adding semantic search or similarity matching to your server (VectoriaDB) +- Integrating an external npm package as a FrontMCP provider +- Building tools that wrap third-party services (databases, APIs, ML models) + +### Recommended + +- Looking for patterns to structure external service integrations +- Deciding between provider-based vs direct integration for a library +- Adding capabilities like applescript automation, VM execution, or data processing + +### Skip When + +- You need to build core MCP components (see `frontmcp-development`) +- You need to configure auth, transport, or CORS (see `frontmcp-config`) +- You need to write a plugin with hooks and context extensions (see `create-plugin`) + +> **Decision:** Use this skill when integrating external libraries into your FrontMCP server as providers or tools. + +## Scenario Routing Table + +| Scenario | Reference | Description | +| --------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------- | +| Add in-memory semantic search with VectoriaDB | `references/vectoriadb.md` | TF-IDF indexing, field weighting, provider+tool pattern | +| Load an app from an npm package | `multi-app-composition` (in frontmcp-setup) | `App.esm('@scope/pkg@^1.0.0', 'AppName')` pattern | +| Connect to a remote MCP server | `multi-app-composition` (in frontmcp-setup) | `App.remote('https://...', 'ns')` pattern | +| Build a reusable plugin with hooks | `create-plugin-hooks` (in frontmcp-development) | `DynamicPlugin`, context extensions, lifecycle hooks | +| Build a custom adapter for an external source | `create-adapter` (in frontmcp-development) | `DynamicAdapter` for OpenAPI, GraphQL, or custom sources | +| Auto-generate tools from an OpenAPI spec | `official-adapters` (in frontmcp-development) | `OpenapiAdapter` with filtering, auth, and transforms | + +## Integration Pattern + +The standard pattern for integrating any external library: + +1. **Create a provider** — wraps the library as a singleton or scoped service +2. **Register the provider** — add to `@App({ providers: [...] })` or `@FrontMcp({ providers: [...] })` +3. **Create tools** — expose the provider's capabilities as MCP tools via `this.get(TOKEN)` +4. **Optionally create resources** — expose data as MCP resources with autocompletion + +```typescript +// 1. Provider wraps the library +@Provider({ name: 'my-search', provide: SearchToken, scope: ProviderScope.GLOBAL }) +export class SearchProvider { + private client: ExternalLibrary; + constructor() { + this.client = new ExternalLibrary({ + /* config */ + }); + } + async search(query: string) { + return this.client.query(query); + } +} + +// 2. Tool exposes it +@Tool({ name: 'search', inputSchema: { query: z.string() } }) +export default class SearchTool extends ToolContext { + async execute(input: { query: string }) { + return this.get(SearchToken).search(input.query); + } +} +``` + +## Available Integrations + +| Library | Purpose | Reference | +| -------------- | -------------------------------- | -------------------------- | +| **VectoriaDB** | In-memory TF-IDF semantic search | `references/vectoriadb.md` | + +More integrations can be added as references (e.g., enclave-vm, applescript, database clients). + +## Verification Checklist + +- [ ] External library is in `dependencies` (not `devDependencies`) +- [ ] Provider wraps the library with proper initialization and cleanup +- [ ] Provider is registered in `@App` or `@FrontMcp` with a typed DI token +- [ ] Tools use `this.get(TOKEN)` to access the provider (not direct imports) +- [ ] Error handling wraps library-specific errors into MCP error classes + +## Reference + +- Related skills: `create-provider`, `create-tool`, `frontmcp-development` diff --git a/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md b/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md new file mode 100644 index 000000000..1227ef65d --- /dev/null +++ b/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md @@ -0,0 +1,289 @@ +--- +name: vectoriadb +description: Use VectoriaDB for in-memory vector search with ML-based or TF-IDF engines in FrontMCP servers +--- + +# VectoriaDB Integration + +Use VectoriaDB for in-memory vector search in FrontMCP servers. Two engines are available: + +- **VectoriaDB** — ML-based semantic search using transformer models. Best for understanding meaning ("find users" matches "list accounts"). +- **TFIDFVectoria** — Zero-dependency keyword search using TF-IDF scoring. Best for exact/fuzzy keyword matching with no model downloads. + +Both are included in the `vectoriadb` package (already a FrontMCP dependency). + +## When to Use + +| Engine | Use When | Dependencies | Init | +| --------------- | ----------------------------------------------------------------- | --------------- | ------------------------------------ | +| `TFIDFVectoria` | Keyword matching, zero deps, no network, small corpus (<10K docs) | None | Synchronous | +| `VectoriaDB` | Semantic understanding, similarity matching, large corpus | transformers.js | Async (downloads model on first run) | + +## TFIDFVectoria — Lightweight Keyword Search + +Zero dependencies, synchronous initialization. Good for tool discovery, FAQ matching, and simple search features. + +### Basic Usage + +```typescript +import { TFIDFVectoria } from 'vectoriadb'; + +const db = new TFIDFVectoria({ + defaultSimilarityThreshold: 0.0, + defaultTopK: 10, +}); + +// Add documents (id, text) +db.addDocument('users-list', 'List all users with pagination and filtering'); +db.addDocument('users-create', 'Create a new user account with email and password'); +db.addDocument('orders-list', 'List orders for a customer with date range filters'); + +// Build the index (required after adding documents) +db.buildIndex(); + +// Search +const results = db.search('find users', 5); +// results: [{ id: 'users-list', score: 0.82 }, { id: 'users-create', score: 0.65 }] +``` + +### With Field Weights + +Weight different fields to control scoring influence: + +```typescript +const db = new TFIDFVectoria({ + fields: { + name: { weight: 3 }, // Name matches are 3x more important + description: { weight: 2 }, // Description matches are 2x + tags: { weight: 1 }, // Tags are baseline + }, +}); + +db.addDocument('weather-tool', { + name: 'get_weather', + description: 'Fetch current weather conditions for a city', + tags: 'weather forecast temperature', +}); + +db.buildIndex(); +const results = db.search('temperature forecast', 5); +``` + +### FrontMCP Provider Pattern + +```typescript +import { Provider, ProviderScope } from '@frontmcp/sdk'; +import { TFIDFVectoria } from 'vectoriadb'; + +export const FAQSearch = Symbol('FAQSearch'); + +@Provider({ name: 'faq-search', provide: FAQSearch, scope: ProviderScope.GLOBAL }) +export class FAQSearchProvider { + private db = new TFIDFVectoria({ + fields: { + question: { weight: 3 }, + answer: { weight: 1 }, + tags: { weight: 2 }, + }, + }); + + async initialize(faqs: Array<{ id: string; question: string; answer: string; tags: string }>) { + for (const faq of faqs) { + this.db.addDocument(faq.id, { + question: faq.question, + answer: faq.answer, + tags: faq.tags, + }); + } + this.db.buildIndex(); + } + + search(query: string, limit = 5) { + return this.db.search(query, limit); + } +} +``` + +## VectoriaDB — Semantic ML Search + +Uses transformer models for true semantic understanding. "find users" matches "list accounts" even without shared keywords. + +### Basic Usage + +```typescript +import { VectoriaDB, DocumentMetadata } from 'vectoriadb'; + +interface ProductDoc extends DocumentMetadata { + name: string; + category: string; + price: number; +} + +const db = new VectoriaDB({ + modelName: 'Xenova/all-MiniLM-L6-v2', // Default model + cacheDir: './.cache/transformers', // Model cache + defaultSimilarityThreshold: 0.4, + defaultTopK: 10, + useHNSW: true, // Enable HNSW for fast search on large datasets +}); + +// Must initialize before use (downloads model on first run) +await db.initialize(); + +// Add documents +await db.add('prod-1', 'Wireless noise-canceling headphones with 30h battery', { + id: 'prod-1', + name: 'QuietComfort Ultra', + category: 'audio', + price: 349, +}); + +// Semantic search — understands meaning, not just keywords +const results = await db.search('something to block office noise', { + topK: 5, + threshold: 0.4, +}); +// results[0].metadata.name === 'QuietComfort Ultra' (semantic match!) +``` + +### Batch Operations + +```typescript +await db.addMany([ + { + id: 'doc-1', + text: 'First document content', + metadata: { + /* ... */ + }, + }, + { + id: 'doc-2', + text: 'Second document content', + metadata: { + /* ... */ + }, + }, +]); +``` + +### Filtered Search + +```typescript +const results = await db.search('wireless audio', { + topK: 5, + filter: (meta) => meta.category === 'audio' && meta.price < 300, +}); +``` + +### Persistence with Storage Adapters + +```typescript +import { VectoriaDB, FileStorageAdapter } from 'vectoriadb'; + +const db = new VectoriaDB({ + storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/vectors' }), +}); + +await db.initialize(); +// After adding documents, persist to disk +await db.saveToStorage(); +// On next startup, restores without re-embedding +await db.loadFromStorage(); +``` + +### FrontMCP Provider Pattern + +```typescript +import { Provider, ProviderScope } from '@frontmcp/sdk'; +import { VectoriaDB, FileStorageAdapter } from 'vectoriadb'; +import type { DocumentMetadata } from 'vectoriadb'; + +export const KnowledgeBase = Symbol('KnowledgeBase'); + +interface Article extends DocumentMetadata { + title: string; + category: string; +} + +@Provider({ name: 'knowledge-base', provide: KnowledgeBase, scope: ProviderScope.GLOBAL }) +export class KnowledgeBaseProvider { + private db: VectoriaDB
; + private ready: Promise; + + constructor() { + this.db = new VectoriaDB
({ + useHNSW: true, + storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/kb-vectors' }), + }); + this.ready = this.db.initialize(); + } + + async search(query: string, options?: { category?: string; limit?: number }) { + await this.ready; + return this.db.search(query, { + topK: options?.limit ?? 10, + filter: options?.category ? (m) => m.category === options.category : undefined, + }); + } + + async index(id: string, text: string, metadata: Article) { + await this.ready; + if (this.db.has(id)) { + await this.db.update(id, { text, metadata }); + } else { + await this.db.add(id, text, metadata); + } + await this.db.saveToStorage(); + } +} +``` + +## Configuration Reference + +### VectoriaDB Options + +| Option | Type | Default | Description | +| ---------------------------- | -------------- | --------------------------- | ---------------------------------------- | +| `modelName` | string | `'Xenova/all-MiniLM-L6-v2'` | Transformer model for embeddings | +| `cacheDir` | string | `'./.cache/transformers'` | Model download cache directory | +| `defaultSimilarityThreshold` | number | `0.3` | Minimum similarity score (0-1) | +| `defaultTopK` | number | `10` | Default results limit | +| `useHNSW` | boolean | `false` | Enable HNSW index for O(log n) search | +| `maxDocuments` | number | `100000` | Max documents (DoS protection) | +| `storageAdapter` | StorageAdapter | None | Persistence adapter (FileStorageAdapter) | + +### TFIDFVectoria Options + +| Option | Type | Default | Description | +| ---------------------------- | -------------------------- | ------- | ------------------------ | +| `defaultSimilarityThreshold` | number | `0.0` | Minimum similarity score | +| `defaultTopK` | number | `10` | Default results limit | +| `fields` | Record | None | Field-weighted indexing | + +## Choosing Between Engines + +| Criterion | TFIDFVectoria | VectoriaDB | +| ------------------ | ------------------------------ | ----------------------------------------- | +| **Dependencies** | Zero | transformers.js (~50MB model) | +| **Initialization** | Synchronous, instant | Async, first-run model download | +| **Search quality** | Keyword-based (exact/fuzzy) | Semantic (understands meaning) | +| **Best for** | Tool discovery, FAQ, <10K docs | Knowledge base, recommendations, any size | +| **Reindex needed** | Yes (`buildIndex()` after add) | No (auto-indexed on add) | +| **Persistence** | Not built-in | FileStorageAdapter | + +## Verification Checklist + +- [ ] Correct engine chosen based on requirements (TFIDFVectoria vs VectoriaDB) +- [ ] Provider wraps the database with proper initialization +- [ ] `buildIndex()` called after adding documents (TFIDFVectoria only) +- [ ] `await db.initialize()` called before any operations (VectoriaDB only) +- [ ] Field weights configured based on domain relevance +- [ ] Storage adapter configured if persistence is needed +- [ ] Search tool injects provider via `this.get(TOKEN)` + +## Reference + +- [VectoriaDB Documentation](https://docs.agentfront.dev/vectoriadb/get-started/welcome) +- [TFIDFVectoria API](https://docs.agentfront.dev/vectoriadb/api-reference/tfidf-vectoria/constructor) +- Related skills: `create-provider`, `create-tool`, `frontmcp-development` diff --git a/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md b/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md index eabf806cf..985a65258 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md @@ -1,3 +1,8 @@ +--- +name: example-knowledge-base +description: Multi-app knowledge base with vector storage, semantic search, AI research agent, and audit plugin +--- + # Example: Knowledge Base (Advanced) > Skills used: setup-project, multi-app-composition, create-tool, create-resource, create-provider, create-agent, create-plugin, configure-auth, deploy-to-vercel diff --git a/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md b/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md index c39fd9b9f..15dc0d0eb 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md @@ -1,3 +1,8 @@ +--- +name: example-task-manager +description: Authenticated task management server with CRUD tools, Redis storage, OAuth, and Vercel deployment +--- + # Example: Task Manager (Intermediate) > Skills used: setup-project, create-tool, create-provider, configure-auth, configure-session, setup-redis, setup-testing, deploy-to-vercel diff --git a/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md b/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md index 04828b3c3..96fc8b998 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md @@ -1,3 +1,8 @@ +--- +name: example-weather-api +description: Beginner MCP server with a weather lookup tool, static resource, Zod schemas, and E2E tests +--- + # Example: Weather API (Beginner) > Skills used: setup-project, create-tool, create-resource, setup-testing, deploy-to-node diff --git a/libs/skills/catalog/frontmcp-production-readiness/SKILL.md b/libs/skills/catalog/frontmcp-production-readiness/SKILL.md index a9e9ecb25..0c41371ce 100644 --- a/libs/skills/catalog/frontmcp-production-readiness/SKILL.md +++ b/libs/skills/catalog/frontmcp-production-readiness/SKILL.md @@ -14,7 +14,7 @@ metadata: # FrontMCP Production Readiness Audit -Comprehensive audit skill for preparing FrontMCP servers for production deployment. Reviews security, performance, reliability, observability, and deployment configuration. +Router for production readiness checklists. Start with the common checklist (security, performance, reliability, observability), then follow the target-specific checklist for your deployment environment. ## When to Use This Skill @@ -23,231 +23,76 @@ Comprehensive audit skill for preparing FrontMCP servers for production deployme - Before deploying a FrontMCP server to production for the first time - After major feature additions or architectural changes - During security reviews or compliance audits -- When troubleshooting production issues (performance, crashes, security incidents) ### Recommended - As part of PR reviews for infrastructure-touching changes - Quarterly health checks on production deployments -- When onboarding new team members to understand production requirements +- When switching deployment targets ### Skip When -- Building a prototype or proof-of-concept (focus on functionality first) +- Building a prototype or proof-of-concept - Running in development/local mode only -- The server has no external exposure (purely internal MCP client) -> **Decision:** Use this skill when preparing for or auditing a production deployment. Reference `security-checklist` or `performance-checklist` for deep dives into specific areas. +> **Decision:** Use this skill when preparing for production. Start with `common-checklist`, then pick your deployment target. -## Scenario Routing Table - -| Scenario | Section / Reference | Description | -| ------------------------------------ | --------------------------- | ---------------------------------------------------------------- | -| Full production audit before go-live | All sections below | Walk through every checklist | -| Security-focused audit | `security-checklist` | Auth, CORS, input validation, secrets, rate limiting | -| Performance optimization | `performance-checklist` | Caching, connection pooling, response optimization, memory leaks | -| Reliability and error handling | Reliability section below | Error handling, graceful shutdown, health checks, retries | -| Observability setup | Observability section below | Structured logging, metrics, error tracking | -| Deployment configuration review | Deployment section below | Docker, env vars, CI/CD, scaling | - -## Security Checklist - -### Authentication & Authorization - -- [ ] JWT_SECRET is set to a strong random value (not the default) -- [ ] Authentication is enabled (`auth` config in `@FrontMcp` or `@frontmcp/auth`) -- [ ] API keys/tokens are loaded from environment variables, never hardcoded -- [ ] Session storage uses Redis (not in-memory) for multi-instance deployments -- [ ] Session TTL is configured appropriately (not infinite) -- [ ] Tool-level authorization is enforced where needed (ApprovalPlugin or custom) -- [ ] OAuth redirect URIs are restricted to known domains - -### CORS Configuration - -- [ ] CORS is NOT permissive (don't allow all origins in production) -- [ ] Specific allowed origins are listed: `cors: { origin: ['https://your-app.com'] }` -- [ ] Credentials mode is only enabled if cookies/sessions are needed -- [ ] Preflight cache (`maxAge`) is set to reduce OPTIONS requests - -### Input Validation - -- [ ] All tool inputs use Zod schemas (never trust raw input) -- [ ] All tool outputs use `outputSchema` to prevent data leaks -- [ ] Path parameters and query params are validated -- [ ] File paths are sanitized to prevent directory traversal -- [ ] SQL queries use parameterized statements (never string interpolation) - -### Secrets Management - -- [ ] No secrets in source code or git history -- [ ] `.env` files are in `.gitignore` -- [ ] Production secrets are managed via secret manager (AWS SSM, Vault, etc.) -- [ ] API keys have minimum required permissions -- [ ] Secrets are rotated on a schedule - -### Rate Limiting & Abuse Prevention - -- [ ] Rate limiting is configured for public-facing endpoints -- [ ] Per-client/per-IP limits are set -- [ ] Throttle configuration uses `@FrontMcp({ throttle: {...} })` -- [ ] Large payload limits are set to prevent memory exhaustion - -### Dependency Security - -- [ ] `npm audit` or `yarn audit` shows no high/critical vulnerabilities -- [ ] Dependencies are pinned or use tilde ranges (not `*` or `latest`) -- [ ] No unused dependencies in package.json - -## Performance Checklist - -### Caching - -- [ ] CachePlugin is configured for read-heavy tools -- [ ] Cache TTL is tuned per tool (not one-size-fits-all) -- [ ] Cache uses Redis in production (not in-memory) -- [ ] Cache bypass header is configured for debugging -- [ ] Stale cache invalidation strategy is defined - -### Connection Management - -- [ ] Redis connection pooling is configured (not one connection per request) -- [ ] Database connections use connection pools -- [ ] HTTP client connections use keep-alive -- [ ] Connection timeouts are set (don't hang indefinitely) - -### Response Optimization - -- [ ] Large responses are paginated or streamed -- [ ] Tools return only necessary data (no over-fetching) -- [ ] OpenAPI adapter responses are not unnecessarily large -- [ ] Binary data uses proper encoding (base64 only when necessary) - -### Memory Management +## Step 1: Detect Deployment Target -- [ ] No memory leaks from event listeners or unclosed connections -- [ ] Large data processing uses streams instead of buffering -- [ ] Provider lifecycle `dispose()` is implemented for cleanup -- [ ] Session storage has TTL to prevent unbounded growth +Check the project to determine the deployment target: -### Startup Performance +1. Look at `package.json` scripts for `frontmcp build --target ` +2. Check for target-specific files: `ci/Dockerfile` (node), `vercel.json` (vercel), `wrangler.toml` (cloudflare), `ci/template.yaml` (lambda) +3. Check if the build target is `cli` or `browser` in the build config +4. If unclear, ask the user which environment they're deploying to -- [ ] Server startup time is acceptable (< 5s for most apps) -- [ ] Lazy-load expensive dependencies (ML models, large configs) -- [ ] OpenAPI spec fetching uses caching and doesn't block startup +## Step 2: Run Common Checklist -## Reliability Checklist +Always start with the common checklist — it covers security, performance, reliability, and observability that apply to every target. -### Error Handling +## Step 3: Run Target-Specific Checklist -- [ ] All tools use `this.fail()` with specific MCP error classes -- [ ] Unknown errors are caught and wrapped (never expose stack traces) -- [ ] Error responses include MCP error codes for client handling -- [ ] Async errors are properly caught (no unhandled promise rejections) +After the common checklist, run the checklist for your deployment target. -### Graceful Shutdown - -- [ ] SIGTERM handler is configured for clean shutdown -- [ ] In-flight requests complete before process exit -- [ ] Redis/database connections are closed on shutdown -- [ ] Health check returns unhealthy during shutdown drain - -### Health Checks - -- [ ] `/health` endpoint is implemented and monitored -- [ ] Health check verifies downstream dependencies (Redis, databases) -- [ ] Readiness probe is separate from liveness probe (K8s) -- [ ] Health check doesn't perform expensive operations - -### Retry & Circuit Breaking - -- [ ] External API calls have retry logic with exponential backoff -- [ ] Circuit breaker pattern for unreliable downstream services -- [ ] Timeouts are set for all external calls -- [ ] Job retries have maximum attempt limits - -## Observability Checklist - -### Structured Logging - -- [ ] Logs use structured format (JSON in production) -- [ ] Log levels are appropriate (info for normal, error for failures) -- [ ] Sensitive data is redacted from logs (tokens, passwords, PII) -- [ ] Request/response logging includes correlation IDs -- [ ] Log volume is manageable (not logging every request body) - -### Metrics - -- [ ] Request count and latency metrics are exposed -- [ ] Error rate metrics are tracked -- [ ] Tool execution duration is measured -- [ ] Resource utilization (memory, CPU) is monitored - -### Error Tracking - -- [ ] Unhandled errors are captured and reported -- [ ] Error tracking service is integrated (Sentry, Datadog, etc.) -- [ ] Error alerts are configured for critical failures -- [ ] Error context includes tool name, input summary, and user context - -## Deployment Checklist - -### Docker & Containers - -- [ ] Dockerfile uses multi-stage build (separate build and runtime stages) -- [ ] Base image is minimal (node:slim, not full node image) -- [ ] Non-root user is configured in the container -- [ ] `.dockerignore` excludes dev files, node_modules, .git -- [ ] Container health check is defined -- [ ] Resource limits (memory, CPU) are set in deployment config - -### Environment Configuration - -- [ ] `NODE_ENV=production` is set -- [ ] All required env vars are documented in `.env.example` -- [ ] Env vars are validated at startup (fail fast on missing config) -- [ ] Port binding uses `process.env.PORT` for platform compatibility -- [ ] No dev dependencies are installed in production (`npm install --production`) - -### CI/CD Pipeline - -- [ ] Tests run on every PR (unit + E2E) -- [ ] Build step produces optimized output (`frontmcp build`) -- [ ] Docker image is built and pushed automatically -- [ ] Deployment is automated with rollback capability -- [ ] Database migrations run as separate step (not in server startup) - -### Scaling - -- [ ] Server is stateless (session state in Redis, not memory) -- [ ] Multiple instances can run behind a load balancer -- [ ] WebSocket/SSE connections are handled by sticky sessions or Redis pub/sub -- [ ] Auto-scaling is configured based on CPU/memory/request metrics - -## Common Anti-Patterns +## Scenario Routing Table -| Anti-Pattern | Why It's Bad | Fix | -| ------------------------- | -------------------------------------------- | ------------------------------------------- | -| Default JWT_SECRET | Anyone can forge tokens | Set a strong random secret | -| In-memory session store | Lost on restart, not shared across instances | Use Redis | -| `cors: { origin: '*' }` | Any website can call your server | Restrict to known origins | -| No output schema on tools | May leak internal data | Always define `outputSchema` | -| Synchronous file I/O | Blocks event loop | Use async operations from `@frontmcp/utils` | -| Hardcoded secrets | Committed to git, visible in source | Use environment variables | -| No health check | Can't detect unhealthy instances | Implement `/health` endpoint | -| Unbounded caching | Memory grows forever | Set TTL on all caches | +| Scenario | Reference | Description | +| -------------------------------------------------------- | -------------------------------------- | --------------------------------------------------- | +| Common security, performance, reliability, observability | `references/common-checklist.md` | Applies to ALL targets — run this first | +| Standalone Node.js server with Docker | `references/production-node-server.md` | Docker, health checks, Redis, scaling, CI/CD | +| Node.js SDK / direct client (npm package) | `references/production-node-sdk.md` | create()/connect() API, disposal, npm publishing | +| Vercel serverless / edge | `references/production-vercel.md` | Vercel config, edge runtime, cold starts, Vercel KV | +| Cloudflare Workers | `references/production-cloudflare.md` | Wrangler, Workers runtime, KV, Durable Objects | +| AWS Lambda | `references/production-lambda.md` | SAM template, cold starts, DynamoDB, API Gateway | +| CLI daemon (local MCP server) | `references/production-cli-daemon.md` | Process manager, socket files, service registration | +| CLI binary (one-shot execution) | `references/production-cli-binary.md` | Fast startup, stdio transport, exit codes, npm bin | +| Browser SDK | `references/production-browser.md` | Bundle size, browser APIs, CSP, CDN distribution | + +## Quick Reference: Target Detection + +| File / Signal Found | Target | +| ----------------------------------------------------- | ----------------------------------------------- | +| `ci/Dockerfile` or `ci/docker-compose.yml` | Standalone server → `production-node-server.md` | +| `serve: false` or `create()` API usage | SDK / direct client → `production-node-sdk.md` | +| `vercel.json` | Vercel → `production-vercel.md` | +| `wrangler.toml` | Cloudflare → `production-cloudflare.md` | +| `ci/template.yaml` | Lambda → `production-lambda.md` | +| `frontmcp start` / `socket` / `service install` usage | CLI daemon → `production-cli-daemon.md` | +| `build --target cli` + `bin` in package.json | CLI binary → `production-cli-binary.md` | +| `build --target browser` in scripts | Browser → `production-browser.md` | ## Verification Checklist -After completing this audit: +After completing both common and target-specific checklists: 1. Run `frontmcp doctor` to check project configuration 2. Run `frontmcp test` to ensure all tests pass 3. Run `frontmcp build` to verify production build succeeds 4. Deploy to staging and run E2E tests against it 5. Review logs for any warnings or errors during startup -6. Load test with expected production traffic patterns +6. Update README for the deployment target (see `frontmcp-setup` → `references/readme-guide.md`) ## Reference - [FrontMCP Production Guide](https://docs.agentfront.dev/frontmcp/production) -- Related skills: `frontmcp-config`, `frontmcp-deployment`, `frontmcp-testing` +- Related skills: `frontmcp-config`, `frontmcp-deployment`, `frontmcp-testing`, `frontmcp-setup` diff --git a/libs/skills/catalog/frontmcp-production-readiness/references/common-checklist.md b/libs/skills/catalog/frontmcp-production-readiness/references/common-checklist.md new file mode 100644 index 000000000..aa490f04f --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/references/common-checklist.md @@ -0,0 +1,156 @@ +--- +name: common-checklist +description: Security, performance, reliability, and observability checks for all deployment targets +--- + +# Common Production Readiness Checklist + +These checks apply to ALL deployment targets. Run them first, then proceed to your target-specific checklist. + +## Security + +### Authentication & Authorization + +- [ ] JWT_SECRET is set to a strong random value (not the default) +- [ ] Authentication is enabled (`auth` config in `@FrontMcp` or `@frontmcp/auth`) +- [ ] API keys/tokens are loaded from environment variables, never hardcoded +- [ ] Session storage uses Redis or platform-native store (not in-memory) for multi-instance +- [ ] Session TTL is configured appropriately (not infinite) +- [ ] Tool-level authorization is enforced where needed (ApprovalPlugin or custom) +- [ ] OAuth redirect URIs are restricted to known domains + +### CORS Configuration + +- [ ] CORS is NOT permissive (don't allow all origins in production) +- [ ] Specific allowed origins are listed: `cors: { origin: ['https://your-app.com'] }` +- [ ] Credentials mode is only enabled if cookies/sessions are needed +- [ ] Preflight cache (`maxAge`) is set to reduce OPTIONS requests + +### Input Validation + +- [ ] All tool inputs use Zod schemas (never trust raw input) +- [ ] All tool outputs use `outputSchema` to prevent data leaks +- [ ] Path parameters and query params are validated +- [ ] File paths are sanitized to prevent directory traversal +- [ ] SQL queries use parameterized statements (never string interpolation) + +### Secrets Management + +- [ ] No secrets in source code or git history +- [ ] `.env` files are in `.gitignore` +- [ ] Production secrets are managed via secret manager (AWS SSM, Vault, etc.) +- [ ] API keys have minimum required permissions +- [ ] Secrets are rotated on a schedule + +### Rate Limiting + +- [ ] Rate limiting is configured for public-facing endpoints +- [ ] Per-client/per-IP limits are set +- [ ] Throttle configuration uses `@FrontMcp({ throttle: {...} })` +- [ ] Large payload limits are set to prevent memory exhaustion + +### Dependencies + +- [ ] `npm audit` shows no high/critical vulnerabilities +- [ ] Dependencies are pinned or use tilde ranges (not `*` or `latest`) +- [ ] No unused dependencies in package.json + +## Performance + +### Caching + +- [ ] CachePlugin is configured for read-heavy tools +- [ ] Cache TTL is tuned per tool (not one-size-fits-all) +- [ ] Stale cache invalidation strategy is defined + +### Response Optimization + +- [ ] Large responses are paginated or streamed +- [ ] Tools return only necessary data (no over-fetching) +- [ ] Binary data uses proper encoding (base64 only when necessary) + +### Memory Management + +- [ ] No memory leaks from event listeners or unclosed connections +- [ ] Large data processing uses streams instead of buffering +- [ ] Provider lifecycle `dispose()` is implemented for cleanup +- [ ] Session storage has TTL to prevent unbounded growth + +## Reliability + +### Error Handling + +- [ ] All tools use `this.fail()` with specific MCP error classes +- [ ] Unknown errors are caught and wrapped (never expose stack traces) +- [ ] Error responses include MCP error codes for client handling +- [ ] Async errors are properly caught (no unhandled promise rejections) + +### Retry & Circuit Breaking + +- [ ] External API calls have retry logic with exponential backoff +- [ ] Circuit breaker pattern for unreliable downstream services +- [ ] Timeouts are set for all external calls +- [ ] Job retries have maximum attempt limits + +## Observability + +### Logging + +- [ ] Logs use structured format (JSON in production) +- [ ] Log levels are appropriate (info for normal, error for failures) +- [ ] Sensitive data is redacted from logs (tokens, passwords, PII) +- [ ] Request/response logging includes correlation IDs + +### Monitoring + +- [ ] Request count and latency metrics are exposed +- [ ] Error rate metrics are tracked +- [ ] Tool execution duration is measured +- [ ] Error tracking service is integrated (Sentry, Datadog, etc.) + +## Jobs & Workflows (if enabled) + +- [ ] Jobs Redis store is configured for production (`jobs: { enabled: true, store: { redis } }`) +- [ ] Job retry config has reasonable `maxAttempts` and `maxBackoffMs` +- [ ] Workflow timeout is set to prevent runaway workflows +- [ ] Job execution times are monitored (long-running jobs need alerting) +- [ ] Workflow step `continueOnError` is only used for non-critical steps + +## Skills HTTP Endpoints (if enabled) + +- [ ] Skills HTTP auth is configured (`skillsConfig.auth: 'api-key'` or `'bearer'`) +- [ ] Skills caching is enabled for production (`skillsConfig.cache: { enabled: true }`) +- [ ] Cache TTL is tuned for skill instruction freshness requirements +- [ ] `/llm.txt` and `/skills` endpoints are tested for correct responses + +## ExtApps / Widgets (if enabled) + +- [ ] Host capabilities are reviewed — only enable what widgets need +- [ ] `serverToolProxy` is disabled if widgets should not call MCP tools +- [ ] Widget session validation is active (default with HTTP transport) +- [ ] CSP headers are configured for hosted widget origins + +## SQLite (if used) + +- [ ] WAL mode is enabled for concurrent read/write performance +- [ ] Database file path is writable and persistent (not ephemeral storage) +- [ ] Backup strategy is defined (periodic file copy or WAL checkpoint) +- [ ] Database size is monitored to prevent disk exhaustion + +## Documentation + +- [ ] README.md is up-to-date for the deployment target (see `frontmcp-setup` → `references/readme-guide.md`) +- [ ] API documentation covers all tools and resources +- [ ] Environment variables are documented in `.env.example` + +## Common Anti-Patterns + +| Anti-Pattern | Fix | +| ------------------------- | ------------------------------------------- | +| Default JWT_SECRET | Set a strong random secret | +| In-memory session store | Use Redis or platform-native storage | +| `cors: { origin: '*' }` | Restrict to known origins | +| No output schema on tools | Always define `outputSchema` | +| Synchronous file I/O | Use async operations from `@frontmcp/utils` | +| Hardcoded secrets | Use environment variables | +| Unbounded caching | Set TTL on all caches | diff --git a/libs/skills/catalog/frontmcp-production-readiness/references/production-browser.md b/libs/skills/catalog/frontmcp-production-readiness/references/production-browser.md new file mode 100644 index 000000000..7da406df3 --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/references/production-browser.md @@ -0,0 +1,46 @@ +--- +name: production-browser +description: Checklist for publishing FrontMCP as a browser-compatible SDK bundle +--- + +# Production Readiness: Browser SDK + +Target-specific checklist for publishing FrontMCP as a browser-compatible SDK. + +> Run the `common-checklist` first, then use this checklist for browser-specific items. + +## Build + +- [ ] `frontmcp build --target browser` produces a correct ESM/UMD bundle +- [ ] Bundle size is acceptable (check with `npx bundlesize` or similar) +- [ ] Tree-shaking works (no unnecessary code in final bundle) +- [ ] Source maps are generated for debugging (but not shipped to production CDN) + +## Browser Compatibility + +- [ ] No Node.js-only APIs (`fs`, `path`, `child_process`, `net`, `crypto`) +- [ ] All crypto uses `@frontmcp/utils` (wraps Web Crypto API) +- [ ] All file operations removed or polyfilled +- [ ] Fetch API used instead of Node http/https modules +- [ ] Works in major browsers (Chrome, Firefox, Safari, Edge) + +## Security + +- [ ] No secrets bundled in the client-side code +- [ ] API keys are NOT in the browser bundle (use server-side proxy) +- [ ] CORS is configured on the server to accept browser origins +- [ ] Content Security Policy (CSP) headers are compatible + +## Distribution + +- [ ] Package exports both ESM and CJS: `"module"` and `"main"` in package.json +- [ ] `"browser"` field in package.json points to the browser build +- [ ] TypeScript declarations (`.d.ts`) are included +- [ ] CDN-friendly: works via `