From 8c15da55c2db066ff58c8aa94219da0e1a1cbf4b Mon Sep 17 00:00:00 2001 From: "agentfront[bot]" Date: Tue, 31 Mar 2026 03:29:55 +0000 Subject: [PATCH] Cherry-pick: feat: add examples directory support and update metadata schema in skill files Cherry-picked from #323 (merged to release/1.0.x) Original commit: 6b993682d5eafb1ff15e83d9c1310f7673448097 Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com> --- .gitignore | 1 + .../contexts/resource-context.mdx | 45 + libs/cli/src/commands/skills/read.ts | 72 +- libs/cli/src/commands/skills/register.ts | 16 +- .../src/common/interfaces/skill.interface.ts | 22 + .../sdk/src/common/metadata/skill.metadata.ts | 3 + .../__tests__/read-skill-content.spec.ts | 253 ++ libs/sdk/src/skill/skill-directory-loader.ts | 7 +- libs/sdk/src/skill/skill.instance.ts | 24 +- libs/sdk/src/skill/skill.utils.ts | 106 +- libs/sdk/src/skill/tools/index.ts | 5 +- .../skill/tools/read-skill-content.tool.ts | 247 ++ libs/skills/README.md | 31 +- .../__tests__/skills-validation.spec.ts | 426 ++- libs/skills/catalog/TEMPLATE.md | 55 + libs/skills/catalog/frontmcp-config/SKILL.md | 4 +- .../local-self-signed-tokens.md | 77 + .../remote-enterprise-oauth.md | 73 + .../transparent-jwt-validation.md | 64 + .../examples/configure-auth/multi-app-auth.md | 87 + .../configure-auth/public-mode-setup.md | 63 + .../configure-auth/remote-oauth-with-vault.md | 76 + .../basic-confirmation-gate.md | 83 + .../distributed-elicitation-redis.md | 87 + .../configure-http/cors-restricted-origins.md | 52 + .../entry-path-reverse-proxy.md | 72 + .../configure-http/unix-socket-local.md | 64 + .../multi-server-key-prefix.md | 68 + .../configure-session/redis-session-store.md | 52 + .../configure-session/vercel-kv-session.md | 52 + .../full-guard-config.md | 99 + .../minimal-guard-config.md | 55 + .../distributed-redis-throttle.md | 94 + .../configure-throttle/per-tool-rate-limit.md | 92 + .../server-level-rate-limit.md | 83 + .../legacy-preset-nodejs.md | 65 + .../stateless-api-serverless.md | 69 + .../custom-protocol-flags.md | 74 + .../distributed-sessions-redis.md | 86 + .../stateless-serverless.md | 69 + .../references/configure-auth-modes.md | 10 + .../references/configure-auth.md | 10 + .../references/configure-elicitation.md | 9 + .../references/configure-http.md | 10 + .../references/configure-session.md | 10 + .../configure-throttle-guard-config.md | 9 + .../references/configure-throttle.md | 10 + .../configure-transport-protocol-presets.md | 9 + .../references/configure-transport.md | 10 + .../frontmcp-config/references/setup-redis.md | 5 + .../references/setup-sqlite.md | 5 + .../catalog/frontmcp-deployment/SKILL.md | 4 +- .../browser-build-with-custom-entry.md | 43 + .../browser-crypto-and-storage.md | 85 + .../build-for-browser/react-provider-setup.md | 61 + .../build-for-cli/cli-binary-build.md | 66 + .../build-for-cli/unix-socket-daemon.md | 76 + .../examples/build-for-sdk/connect-openai.md | 78 + .../build-for-sdk/create-flat-config.md | 85 + .../build-for-sdk/multi-platform-connect.md | 104 + .../basic-worker-deploy.md | 82 + .../worker-custom-domain.md | 97 + .../worker-with-kv-storage.md | 92 + .../deploy-to-lambda/cdk-deployment.md | 92 + .../lambda-handler-with-cors.md | 113 + .../deploy-to-lambda/sam-template-basic.md | 100 + .../basic-multistage-dockerfile.md | 63 + .../secure-nonroot-dockerfile.md | 89 + .../docker-compose-with-redis.md | 101 + .../examples/deploy-to-node/pm2-with-nginx.md | 79 + .../deploy-to-node/resource-limits.md | 92 + .../minimal-vercel-config.md | 49 + .../vercel-config-with-security-headers.md | 92 + .../vercel-mcp-endpoint-test.md | 69 + .../deploy-to-vercel/vercel-with-kv.md | 82 + .../vercel-with-skills-cache.md | 90 + .../references/build-for-browser.md | 10 + .../references/build-for-cli.md | 9 + .../references/build-for-sdk.md | 10 + .../references/deploy-to-cloudflare.md | 10 + .../references/deploy-to-lambda.md | 10 + .../references/deploy-to-node-dockerfile.md | 9 + .../references/deploy-to-node.md | 10 + .../references/deploy-to-vercel-config.md | 9 + .../references/deploy-to-vercel.md | 10 + .../catalog/frontmcp-development/SKILL.md | 4 +- .../create-adapter/basic-api-adapter.md | 92 + .../create-adapter/namespaced-adapter.md | 124 + .../anthropic-config.md | 81 + .../create-agent-llm-config/openai-config.md | 80 + .../create-agent/basic-agent-with-tools.md | 121 + .../create-agent/custom-multi-pass-agent.md | 95 + .../create-agent/nested-agents-with-swarm.md | 111 + .../examples/create-job/basic-report-job.md | 87 + .../create-job/job-with-permissions.md | 117 + .../examples/create-job/job-with-retry.md | 88 + .../basic-logging-plugin.md | 69 + .../caching-with-around.md | 80 + .../tool-level-hooks-and-stage-replacement.md | 100 + .../basic-plugin-with-provider.md | 69 + .../configurable-dynamic-plugin.md | 178 ++ .../plugin-with-context-extension.md | 107 + .../examples/create-prompt/basic-prompt.md | 72 + .../create-prompt/dynamic-rag-prompt.md | 92 + .../create-prompt/multi-turn-debug-session.md | 86 + .../basic-database-provider.md | 113 + .../config-and-api-providers.md | 107 + .../create-resource/basic-static-resource.md | 72 + .../binary-and-multi-content.md | 111 + .../create-resource/parameterized-template.md | 84 + .../basic-tool-orchestration.md | 76 + .../directory-skill-with-tools.md | 149 + .../incident-response-skill.md | 92 + .../create-skill/basic-inline-skill.md | 96 + .../create-skill/directory-based-skill.md | 115 + .../create-skill/parameterized-skill.md | 96 + .../destructive-delete-tool.md | 94 + .../readonly-query-tool.md | 60 + .../primitive-and-media-outputs.md | 104 + .../zod-raw-shape-output.md | 63 + .../zod-schema-advanced-output.md | 103 + .../examples/create-tool/basic-class-tool.md | 62 + .../create-tool/tool-with-di-and-errors.md | 84 + .../tool-with-rate-limiting-and-progress.md | 93 + .../create-workflow/basic-deploy-pipeline.md | 91 + .../parallel-validation-pipeline.md | 90 + .../webhook-triggered-workflow.md | 136 + .../agent-skill-job-workflow.md | 145 + .../basic-server-with-app-and-tools.md | 124 + .../multi-app-with-plugins-and-providers.md | 149 + .../authenticated-adapter-with-polling.md | 84 + .../basic-openapi-adapter.md | 54 + .../multi-api-hub-with-inline-spec.md | 130 + .../cache-and-feature-flags.md | 117 + .../production-multi-plugin-setup.md | 147 + .../remember-plugin-session-memory.md | 104 + .../references/create-adapter.md | 9 + .../references/create-agent-llm-config.md | 9 + .../references/create-agent.md | 10 + .../references/create-job.md | 10 + .../references/create-plugin-hooks.md | 10 + .../references/create-plugin.md | 10 + .../references/create-prompt.md | 10 + .../references/create-provider.md | 9 + .../references/create-resource.md | 67 +- .../references/create-skill-with-tools.md | 10 + .../references/create-skill.md | 10 + .../references/create-tool-annotations.md | 9 + .../create-tool-output-schema-types.md | 10 + .../references/create-tool.md | 10 + .../references/create-workflow.md | 10 + .../references/decorators-guide.md | 10 + .../references/official-adapters.md | 10 + .../references/official-plugins.md | 10 + .../catalog/frontmcp-extensibility/SKILL.md | 2 +- .../vectoriadb/product-catalog-search.md | 175 ++ .../semantic-search-with-persistence.md | 138 + .../vectoriadb/tfidf-keyword-search.md | 103 + .../references/vectoriadb.md | 10 + libs/skills/catalog/frontmcp-guides/SKILL.md | 4 +- .../agent-and-plugin.md | 160 ++ .../multi-app-composition.md | 92 + .../vector-search-and-resources.md | 135 + .../auth-and-crud-tools.md | 135 + .../authenticated-e2e-tests.md | 148 + .../redis-provider-with-di.md | 129 + .../server-and-app-setup.md | 75 + .../example-weather-api/unit-and-e2e-tests.md | 142 + .../weather-tool-with-schemas.md | 74 + .../references/example-knowledge-base.md | 10 + .../references/example-task-manager.md | 10 + .../references/example-weather-api.md | 10 + .../frontmcp-production-readiness/SKILL.md | 4 +- .../caching-and-performance.md | 102 + .../common-checklist/observability-setup.md | 104 + .../common-checklist/security-hardening.md | 95 + .../browser-bundle-config.md | 93 + .../cross-platform-crypto.md | 116 + .../security-and-performance.md | 128 + .../binary-build-config.md | 109 + .../stdio-transport-error-handling.md | 132 + .../daemon-socket-config.md | 82 + .../graceful-shutdown-cleanup.md | 107 + .../security-and-permissions.md | 119 + .../durable-objects-state.md | 124 + .../workers-runtime-constraints.md | 103 + .../production-cloudflare/wrangler-config.md | 89 + .../cold-start-connection-reuse.md | 122 + .../production-lambda/sam-template.md | 107 + .../scaling-and-monitoring.md | 138 + .../basic-sdk-lifecycle.md | 85 + .../multi-instance-cleanup.md | 110 + .../package-json-config.md | 107 + .../docker-multi-stage.md | 103 + .../graceful-shutdown.md | 87 + .../redis-session-scaling.md | 97 + .../cold-start-optimization.md | 104 + .../stateless-serverless-design.md | 91 + .../production-vercel/vercel-edge-config.md | 78 + .../references/common-checklist.md | 10 + .../references/production-browser.md | 10 + .../references/production-cli-binary.md | 9 + .../references/production-cli-daemon.md | 10 + .../references/production-cloudflare.md | 10 + .../references/production-lambda.md | 10 + .../references/production-node-sdk.md | 10 + .../references/production-node-server.md | 10 + .../references/production-vercel.md | 10 + .../bundle-presets-scaffolding.md | 61 + .../install-and-search-skills.md | 83 + .../local-apps-with-shared-tools.md | 87 + .../per-app-auth-and-isolation.md | 88 + .../remote-and-esm-apps.md | 81 + .../nx-workflow/build-test-affected.md | 77 + .../nx-workflow/multi-server-deployment.md | 93 + .../nx-workflow/scaffold-and-generate.md | 62 + .../nx-generator-scaffolding.md | 73 + .../nx-workspace-with-apps.md | 85 + .../shared-library-usage.md | 89 + .../dev-workflow-commands.md | 64 + .../feature-folder-organization.md | 111 + .../minimal-standalone-layout.md | 73 + .../readme-guide/node-server-readme.md | 89 + .../readme-guide/vercel-deployment-readme.md | 90 + .../setup-project/basic-node-server.md | 99 + .../setup-project/cli-scaffold-with-flags.md | 77 + .../setup-project/vercel-serverless-server.md | 89 + .../setup-redis/docker-redis-local-dev.md | 88 + .../hybrid-vercel-kv-with-pubsub.md | 78 + .../setup-redis/vercel-kv-serverless.md | 78 + .../setup-sqlite/basic-sqlite-setup.md | 75 + .../setup-sqlite/encrypted-sqlite-storage.md | 55 + .../setup-sqlite/unix-socket-daemon.md | 70 + .../references/frontmcp-skills-usage.md | 9 + .../references/multi-app-composition.md | 10 + .../frontmcp-setup/references/nx-workflow.md | 10 + .../references/project-structure-nx.md | 10 + .../project-structure-standalone.md | 10 + .../frontmcp-setup/references/readme-guide.md | 9 + .../references/setup-project.md | 10 + .../frontmcp-setup/references/setup-redis.md | 10 + .../frontmcp-setup/references/setup-sqlite.md | 10 + .../setup-testing/fixture-based-e2e-test.md | 70 + .../jest-config-with-coverage.md | 59 + .../unit-test-tool-resource-prompt.md | 115 + .../examples/test-auth/oauth-flow-test.md | 78 + .../test-auth/role-based-access-test.md | 88 + .../examples/test-auth/token-factory-test.md | 71 + .../browser-bundle-validation.md | 58 + .../playwright-browser-test.md | 69 + .../test-cli-binary/binary-startup-test.md | 77 + .../test-cli-binary/js-bundle-import-test.md | 56 + .../test-direct-client/basic-create-test.md | 74 + .../openai-claude-format-test.md | 79 + .../test-e2e-handler/basic-e2e-test.md | 67 + .../manual-client-with-transport.md | 72 + .../tool-call-and-error-e2e.md | 73 + .../test-tool-unit/basic-tool-test.md | 69 + .../test-tool-unit/schema-validation-test.md | 82 + .../tool-error-handling-test.md | 92 + .../references/setup-testing.md | 10 + .../frontmcp-testing/references/test-auth.md | 10 + .../references/test-browser-build.md | 9 + .../references/test-cli-binary.md | 9 + .../references/test-direct-client.md | 9 + .../references/test-e2e-handler.md | 10 + .../references/test-tool-unit.md | 10 + libs/skills/catalog/skills-manifest.json | 2450 ++++++++++++++++- libs/skills/project.json | 2 +- libs/skills/scripts/generate-manifest.mjs | 131 +- libs/skills/src/manifest.ts | 26 + 271 files changed, 20819 insertions(+), 158 deletions(-) create mode 100644 libs/sdk/src/skill/__tests__/read-skill-content.spec.ts create mode 100644 libs/sdk/src/skill/tools/read-skill-content.tool.ts create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/transparent-jwt-validation.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth/multi-app-auth.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth/public-mode-setup.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-elicitation/basic-confirmation-gate.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-elicitation/distributed-elicitation-redis.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-http/unix-socket-local.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-session/multi-server-key-prefix.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-session/redis-session-store.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/full-guard-config.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/minimal-guard-config.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-throttle/per-tool-rate-limit.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-throttle/server-level-rate-limit.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/legacy-preset-nodejs.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md create mode 100644 libs/skills/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md create mode 100644 libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-adapter/basic-api-adapter.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-adapter/namespaced-adapter.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/anthropic-config.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/openai-config.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-agent/basic-agent-with-tools.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-job/basic-report-job.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-job/job-with-permissions.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-job/job-with-retry.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/basic-logging-plugin.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/caching-with-around.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin/basic-plugin-with-provider.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin/configurable-dynamic-plugin.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-plugin/plugin-with-context-extension.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-prompt/basic-prompt.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-prompt/dynamic-rag-prompt.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-prompt/multi-turn-debug-session.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-resource/basic-static-resource.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-resource/binary-and-multi-content.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-resource/parameterized-template.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/basic-tool-orchestration.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/directory-skill-with-tools.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/incident-response-skill.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill/basic-inline-skill.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill/directory-based-skill.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-skill/parameterized-skill.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-workflow/basic-deploy-pipeline.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-workflow/parallel-validation-pipeline.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-adapters/authenticated-adapter-with-polling.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-adapters/basic-openapi-adapter.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-adapters/multi-api-hub-with-inline-spec.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md create mode 100644 libs/skills/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md create mode 100644 libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md create mode 100644 libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md create mode 100644 libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/multi-app-composition.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md create mode 100644 libs/skills/catalog/frontmcp-guides/examples/example-weather-api/weather-tool-with-schemas.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/browser-bundle-config.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/cross-platform-crypto.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/security-and-performance.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cli-binary/binary-build-config.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cli-binary/stdio-transport-error-handling.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cli-daemon/daemon-socket-config.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cli-daemon/graceful-shutdown-cleanup.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cli-daemon/security-and-permissions.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cloudflare/durable-objects-state.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cloudflare/workers-runtime-constraints.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-cloudflare/wrangler-config.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-lambda/cold-start-connection-reuse.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-lambda/sam-template.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-lambda/scaling-and-monitoring.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-sdk/basic-sdk-lifecycle.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-sdk/multi-instance-cleanup.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-server/docker-multi-stage.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-server/graceful-shutdown.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-node-server/redis-session-scaling.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-vercel/cold-start-optimization.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-vercel/stateless-serverless-design.md create mode 100644 libs/skills/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/frontmcp-skills-usage/bundle-presets-scaffolding.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/frontmcp-skills-usage/install-and-search-skills.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/multi-app-composition/local-apps-with-shared-tools.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/multi-app-composition/remote-and-esm-apps.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-nx/nx-workspace-with-apps.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-nx/shared-library-usage.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-standalone/dev-workflow-commands.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-standalone/feature-folder-organization.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/project-structure-standalone/minimal-standalone-layout.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/readme-guide/node-server-readme.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/readme-guide/vercel-deployment-readme.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-project/basic-node-server.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-project/cli-scaffold-with-flags.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-project/vercel-serverless-server.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-redis/docker-redis-local-dev.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-redis/hybrid-vercel-kv-with-pubsub.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-redis/vercel-kv-serverless.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-sqlite/basic-sqlite-setup.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-sqlite/encrypted-sqlite-storage.md create mode 100644 libs/skills/catalog/frontmcp-setup/examples/setup-sqlite/unix-socket-daemon.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/setup-testing/fixture-based-e2e-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/setup-testing/jest-config-with-coverage.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/setup-testing/unit-test-tool-resource-prompt.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-auth/oauth-flow-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-auth/role-based-access-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-auth/token-factory-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-browser-build/browser-bundle-validation.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-browser-build/playwright-browser-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-cli-binary/binary-startup-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-cli-binary/js-bundle-import-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-direct-client/basic-create-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-direct-client/openai-claude-format-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-e2e-handler/basic-e2e-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-e2e-handler/manual-client-with-transport.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-e2e-handler/tool-call-and-error-e2e.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-tool-unit/basic-tool-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-tool-unit/schema-validation-test.md create mode 100644 libs/skills/catalog/frontmcp-testing/examples/test-tool-unit/tool-error-handling-test.md diff --git a/.gitignore b/.gitignore index 320dac513..8a083cf30 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ out-tsc .junie .env +.env.local # dependencies node_modules diff --git a/docs/frontmcp/sdk-reference/contexts/resource-context.mdx b/docs/frontmcp/sdk-reference/contexts/resource-context.mdx index eff5178c5..39c2b4894 100644 --- a/docs/frontmcp/sdk-reference/contexts/resource-context.mdx +++ b/docs/frontmcp/sdk-reference/contexts/resource-context.mdx @@ -309,6 +309,51 @@ class DocsApp {} export default class DocServer {} ``` +## Argument Autocompletion + +Resource templates can provide autocompletion for their URI parameters. There are two approaches, both with full DI access via `this.get()`: + +### Convention-Based (Preferred) + +Define a method named `${argName}Completer` on your resource class. The framework discovers it automatically. + +```typescript +@ResourceTemplate({ + name: 'user-profile', + uriTemplate: 'users://{userId}/profile', + mimeType: 'application/json', +}) +class UserProfileResource extends ResourceContext<{ userId: string }> { + async execute(uri: string, params: { userId: string }) { + const user = await this.get(UserService).findById(params.userId); + return { id: user.id, name: user.name }; + } + + async userIdCompleter(partial: string) { + const users = await this.get(UserService).search(partial); + return { values: users.map(u => u.id), total: users.length }; + } +} +``` + +### Override-Based + +Override `getArgumentCompleter(argName)` for dynamic dispatch across multiple parameters. + +```typescript +getArgumentCompleter(argName: string): ResourceArgumentCompleter | null { + if (argName === 'userId') { + return async (partial) => { + const users = await this.get(UserService).search(partial); + return { values: users.map(u => u.id) }; + }; + } + return null; +} +``` + +Convention-based completers take priority when both are present. + ## Related diff --git a/libs/cli/src/commands/skills/read.ts b/libs/cli/src/commands/skills/read.ts index b46b77b4c..5c27209fd 100644 --- a/libs/cli/src/commands/skills/read.ts +++ b/libs/cli/src/commands/skills/read.ts @@ -16,7 +16,7 @@ function stripFrontmatter(content: string): string { export async function readSkill( nameOrPath: string, - options: { reference?: string; listRefs?: boolean }, + options: { reference?: string; listRefs?: boolean; listExamples?: boolean; examplesForRef?: string }, ): Promise { // Support colon syntax: "skillName:path/to/file.ext" let skillName = nameOrPath; @@ -61,6 +61,68 @@ export async function readSkill( return; } + // Mode 1b: List examples + const refFilter = options.examplesForRef?.trim(); + if (options.listExamples || options.examplesForRef !== undefined) { + if (options.examplesForRef !== undefined && !refFilter) { + console.error(c('red', 'Reference name for --examples cannot be empty.')); + process.exit(1); + } + const refs = entry.references ?? []; + + // Collect all examples, optionally filtered by reference + const allExamples: Array<{ ref: string; name: string; level: string; description: string }> = []; + for (const ref of refs) { + if (refFilter && ref.name !== refFilter) continue; + if (!ref.examples || ref.examples.length === 0) continue; + for (const ex of ref.examples) { + allExamples.push({ ref: ref.name, name: ex.name, level: ex.level, description: ex.description }); + } + } + + if (refFilter && !refs.some((r) => r.name === refFilter)) { + console.error(c('red', `Reference "${refFilter}" not found in skill "${skillName}".`)); + console.log(c('gray', `Use 'frontmcp skills read ${skillName} --refs' to list available references.`)); + process.exit(1); + } + + if (allExamples.length === 0) { + const scope = refFilter ? `reference "${refFilter}"` : `skill "${skillName}"`; + console.log(c('yellow', `No examples found for ${scope}.`)); + return; + } + + const title = refFilter ? `Examples for ${skillName} > ${refFilter}` : `Examples for ${skillName}`; + console.log(c('bold', `\n ${title}:\n`)); + + let currentRef = ''; + for (const ex of allExamples) { + if (ex.ref !== currentRef) { + currentRef = ex.ref; + console.log(` ${c('cyan', currentRef)}`); + } + const levelTag = + ex.level === 'advanced' + ? c('red', ex.level) + : ex.level === 'intermediate' + ? c('yellow', ex.level) + : c('green', ex.level); + console.log(` ${c('green', ex.name)} ${c('gray', `[${levelTag}]`)}`); + if (ex.description) { + console.log(` ${c('gray', ex.description)}`); + } + } + console.log(''); + console.log( + c( + 'gray', + ` ${allExamples.length} example(s). Read with: frontmcp skills read ${skillName}:examples//.md`, + ), + ); + console.log(''); + return; + } + // Mode 2: Read a specific file (reference or any file in skill dir) if (filePath) { // Try exact path first, then references/.md fallback @@ -122,6 +184,10 @@ export async function readSkill( console.log(c('gray', ` Has resources: ${entry.hasResources}`)); if (entry.references && entry.references.length > 0) { console.log(c('gray', ` References: ${entry.references.length} (use --refs to list)`)); + const exampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0); + if (exampleCount > 0) { + console.log(c('gray', ` Examples: ${exampleCount} (use --examples to list)`)); + } } console.log(''); console.log(c('gray', ' ─────────────────────────────────────')); @@ -134,5 +200,9 @@ export async function readSkill( console.log(c('gray', ` Install: frontmcp skills install ${skillName} --provider claude`)); if (entry.references && entry.references.length > 0) { console.log(c('gray', ` References: frontmcp skills read ${skillName} --refs`)); + const footerExampleCount = entry.references.reduce((sum, r) => sum + (r.examples?.length ?? 0), 0); + if (footerExampleCount > 0) { + console.log(c('gray', ` Examples: frontmcp skills read ${skillName} --examples`)); + } } } diff --git a/libs/cli/src/commands/skills/register.ts b/libs/cli/src/commands/skills/register.ts index fe059d31d..1e1b201ef 100644 --- a/libs/cli/src/commands/skills/register.ts +++ b/libs/cli/src/commands/skills/register.ts @@ -68,8 +68,16 @@ export function registerSkillsCommands(program: Command): void { .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 }); - }); + .option('--examples [reference]', 'List examples for the skill, optionally filtered by reference name') + .action( + async (name: string, reference: string | undefined, options: { refs?: boolean; examples?: boolean | string }) => { + const { readSkill } = await import('./read.js'); + await readSkill(name, { + reference, + listRefs: options.refs, + listExamples: options.examples === true ? true : undefined, + examplesForRef: typeof options.examples === 'string' ? options.examples : undefined, + }); + }, + ); } diff --git a/libs/sdk/src/common/interfaces/skill.interface.ts b/libs/sdk/src/common/interfaces/skill.interface.ts index 3281e63e8..3fc17c18f 100644 --- a/libs/sdk/src/common/interfaces/skill.interface.ts +++ b/libs/sdk/src/common/interfaces/skill.interface.ts @@ -22,6 +22,22 @@ export interface SkillReferenceInfo { filename: string; } +/** + * Metadata for a resolved example file within a skill's examples/ directory. + */ +export interface SkillExampleInfo { + /** Example name (filename without .md) */ + name: string; + /** Short description from frontmatter */ + description: string; + /** Parent reference name (examples are grouped by reference) */ + reference: string; + /** Complexity level */ + level: string; + /** Filename relative to the examples directory */ + filename: string; +} + /** * Full content returned when loading a skill. * Contains all information needed for an LLM to execute the skill. @@ -97,6 +113,12 @@ export interface SkillContent { * Each entry contains name, description, and filename for the reference. */ resolvedReferences?: SkillReferenceInfo[]; + + /** + * Resolved example metadata from the skill's examples/ directory. + * Examples are grouped by reference and contain name, description, level, and filename. + */ + resolvedExamples?: SkillExampleInfo[]; } /** diff --git a/libs/sdk/src/common/metadata/skill.metadata.ts b/libs/sdk/src/common/metadata/skill.metadata.ts index 88f0afa15..6bef4b1f0 100644 --- a/libs/sdk/src/common/metadata/skill.metadata.ts +++ b/libs/sdk/src/common/metadata/skill.metadata.ts @@ -28,6 +28,8 @@ export interface SkillResources { references?: string; /** Path to assets directory */ assets?: string; + /** Path to examples directory */ + examples?: string; } /** @@ -411,6 +413,7 @@ const skillResourcesSchema = z.object({ scripts: z.string().optional(), references: z.string().optional(), assets: z.string().optional(), + examples: z.string().optional(), }); export const skillMetadataSchema = z diff --git a/libs/sdk/src/skill/__tests__/read-skill-content.spec.ts b/libs/sdk/src/skill/__tests__/read-skill-content.spec.ts new file mode 100644 index 000000000..0a3c53936 --- /dev/null +++ b/libs/sdk/src/skill/__tests__/read-skill-content.spec.ts @@ -0,0 +1,253 @@ +/** + * ReadSkillContent Tool Tests + * + * Tests for the readSkillContent MCP tool which reads individual + * reference and example files from loaded skills. + */ + +import 'reflect-metadata'; +import { SkillInstance, createSkillInstance } from '../skill.instance'; +import { SkillKind, SkillRecord, SkillMetadata, EntryOwnerRef } from '../../common'; +import type { SkillContent, SkillReferenceInfo, SkillExampleInfo } from '../../common/interfaces'; +import ProviderRegistry from '../../provider/provider.registry'; +import { Scope } from '../../scope'; + +// Mock file operations +jest.mock('@frontmcp/utils', () => ({ + ...jest.requireActual('@frontmcp/utils'), + readFile: jest.fn(), + fileExists: jest.fn(), + readdir: jest.fn(), + stat: jest.fn(), +})); + +import { readFile } from '@frontmcp/utils'; +const mockReadFile = readFile as jest.MockedFunction; + +// Mock loadInstructions to avoid file I/O during instance creation +jest.mock('../skill.utils', () => ({ + ...jest.requireActual('../skill.utils'), + loadInstructions: jest.fn().mockResolvedValue('mock instructions'), + resolveReferences: jest.fn(), + resolveExamples: jest.fn(), +})); + +// Helper to create mock ProviderRegistry +const createMockProviderRegistry = (): ProviderRegistry => { + const mockScope = { + logger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }, + } as unknown as Scope; + + return { + getActiveScope: () => mockScope, + } as unknown as ProviderRegistry; +}; + +const createMockOwner = (id = 'test-app'): EntryOwnerRef => ({ + kind: 'app', + id, + ref: Symbol('test-app-token'), +}); + +describe('ReadSkillContent - file resolution logic', () => { + let instance: SkillInstance; + + beforeEach(() => { + jest.clearAllMocks(); + + const metadata: SkillMetadata = { + name: 'test-skill', + description: 'A test skill', + instructions: 'inline instructions', + resources: { + references: 'references', + examples: 'examples', + }, + }; + + const record: SkillRecord = { + kind: SkillKind.FILE, + provide: Symbol('test-skill'), + metadata, + filePath: '/skills/test-skill/SKILL.md', + }; + + instance = createSkillInstance(record, createMockProviderRegistry(), createMockOwner()); + }); + + describe('getBaseDir()', () => { + it('should return the directory of the skill file for FILE records', () => { + expect(instance.getBaseDir()).toBe('/skills/test-skill'); + }); + + it('should return callerDir for VALUE records', () => { + const metadata: SkillMetadata = { + name: 'value-skill', + description: 'test', + instructions: 'inline', + resources: { references: 'refs' }, + }; + + const record: SkillRecord = { + kind: SkillKind.VALUE, + provide: Symbol('value-skill'), + metadata, + callerDir: '/some/caller/dir', + }; + + const valueInstance = createSkillInstance(record, createMockProviderRegistry(), createMockOwner()); + expect(valueInstance.getBaseDir()).toBe('/some/caller/dir'); + }); + + it('should return undefined for CLASS_TOKEN records without callerDir', () => { + const metadata: SkillMetadata = { + name: 'class-skill', + description: 'test', + instructions: 'inline', + }; + + const record: SkillRecord = { + kind: SkillKind.CLASS_TOKEN, + provide: class {} as any, + metadata, + }; + + const classInstance = createSkillInstance(record, createMockProviderRegistry(), createMockOwner()); + expect(classInstance.getBaseDir()).toBeUndefined(); + }); + }); + + describe('getResources()', () => { + it('should return the resources from metadata', () => { + const resources = instance.getResources(); + expect(resources).toEqual({ + references: 'references', + examples: 'examples', + }); + }); + }); +}); + +describe('ReadSkillContent - reference and example lookup', () => { + const mockReferences: SkillReferenceInfo[] = [ + { name: 'deploy-to-vercel', description: 'Deploy to Vercel', filename: 'deploy-to-vercel.md' }, + { name: 'deploy-to-node', description: 'Deploy to Node.js', filename: 'deploy-to-node.md' }, + ]; + + const mockExamples: SkillExampleInfo[] = [ + { + name: 'vercel-with-kv', + description: 'Vercel with KV storage', + reference: 'deploy-to-vercel', + level: 'basic', + filename: 'deploy-to-vercel/vercel-with-kv.md', + }, + { + name: 'vercel-with-skills-cache', + description: 'Vercel with skills cache', + reference: 'deploy-to-vercel', + level: 'intermediate', + filename: 'deploy-to-vercel/vercel-with-skills-cache.md', + }, + ]; + + it('should find a reference by name', () => { + const found = mockReferences.find((r) => r.name === 'deploy-to-vercel'); + expect(found).toBeDefined(); + expect(found?.filename).toBe('deploy-to-vercel.md'); + }); + + it('should find an example by name', () => { + const found = mockExamples.find((e) => e.name === 'vercel-with-kv'); + expect(found).toBeDefined(); + expect(found?.reference).toBe('deploy-to-vercel'); + expect(found?.level).toBe('basic'); + expect(found?.filename).toBe('deploy-to-vercel/vercel-with-kv.md'); + }); + + it('should return undefined for non-existent reference', () => { + const found = mockReferences.find((r) => r.name === 'nonexistent'); + expect(found).toBeUndefined(); + }); + + it('should list available names when not found', () => { + const availableNames = mockReferences.map((r) => r.name); + expect(availableNames).toEqual(['deploy-to-vercel', 'deploy-to-node']); + }); + + it('should list available example names when not found', () => { + const availableNames = mockExamples.map((e) => e.name); + expect(availableNames).toEqual(['vercel-with-kv', 'vercel-with-skills-cache']); + }); +}); + +describe('ReadSkillContent - file reading and frontmatter parsing', () => { + it('should strip frontmatter and return body for reference files', async () => { + const fileContent = [ + '---', + 'name: deploy-to-vercel', + 'description: Deploy to Vercel serverless', + '---', + '', + '# Deploy to Vercel', + '', + 'Step 1: Configure vercel.json', + ].join('\n'); + + mockReadFile.mockResolvedValueOnce(fileContent); + + const result = await mockReadFile('/skills/test-skill/references/deploy-to-vercel.md'); + expect(result).toBe(fileContent); + + // Verify frontmatter can be parsed + const { parseSkillMdFrontmatter } = require('../skill-md-parser'); + const { frontmatter, body } = parseSkillMdFrontmatter(fileContent); + expect(frontmatter['name']).toBe('deploy-to-vercel'); + expect(frontmatter['description']).toBe('Deploy to Vercel serverless'); + expect(body).toContain('# Deploy to Vercel'); + expect(body).not.toContain('---'); + }); + + it('should strip frontmatter and return body for example files', async () => { + const fileContent = [ + '---', + 'name: vercel-with-kv', + 'reference: deploy-to-vercel', + 'level: basic', + 'description: Deploy with Vercel KV storage', + '---', + '', + '# Vercel with KV', + '', + 'This example shows how to deploy with Vercel KV.', + '', + '## Code', + '', + '```typescript', + "import { FrontMcp } from '@frontmcp/sdk';", + '```', + ].join('\n'); + + mockReadFile.mockResolvedValueOnce(fileContent); + + const result = await mockReadFile('/skills/test-skill/examples/deploy-to-vercel/vercel-with-kv.md'); + expect(result).toBe(fileContent); + + const { parseSkillMdFrontmatter } = require('../skill-md-parser'); + const { frontmatter, body } = parseSkillMdFrontmatter(fileContent); + expect(frontmatter['name']).toBe('vercel-with-kv'); + expect(frontmatter['reference']).toBe('deploy-to-vercel'); + expect(frontmatter['level']).toBe('basic'); + expect(body).toContain('# Vercel with KV'); + expect(body).toContain("import { FrontMcp } from '@frontmcp/sdk'"); + }); + + it('should handle files without frontmatter', async () => { + const fileContent = '# No Frontmatter\n\nJust content.'; + + const { parseSkillMdFrontmatter } = require('../skill-md-parser'); + const { frontmatter, body } = parseSkillMdFrontmatter(fileContent); + expect(frontmatter).toEqual({}); + expect(body).toBe(fileContent); + }); +}); diff --git a/libs/sdk/src/skill/skill-directory-loader.ts b/libs/sdk/src/skill/skill-directory-loader.ts index dc8d8852b..793604338 100644 --- a/libs/sdk/src/skill/skill-directory-loader.ts +++ b/libs/sdk/src/skill/skill-directory-loader.ts @@ -42,21 +42,24 @@ export async function scanSkillResources(dirPath: string): Promise { const scriptsPath = joinPath(dirPath, 'scripts'); const referencesPath = joinPath(dirPath, 'references'); const assetsPath = joinPath(dirPath, 'assets'); + const examplesPath = joinPath(dirPath, 'examples'); const checks = await Promise.all([ checkDirectory(scriptsPath), checkDirectory(referencesPath), checkDirectory(assetsPath), + checkDirectory(examplesPath), fileExists(joinPath(dirPath, 'SKILL.md')), ]); if (checks[0]) resources.scripts = scriptsPath; if (checks[1]) resources.references = referencesPath; if (checks[2]) resources.assets = assetsPath; + if (checks[3]) resources.examples = examplesPath; return { resources, - hasSkillMd: checks[3], + hasSkillMd: checks[4], }; } @@ -108,7 +111,7 @@ export async function loadSkillDirectory(dirPath: string, logger?: FrontMcpLogge // Scan for resource directories const { resources } = await scanSkillResources(dirPath); - const hasResources = resources.scripts || resources.references || resources.assets; + const hasResources = resources.scripts || resources.references || resources.assets || resources.examples; if (hasResources) { partialMetadata.resources = resources; } diff --git a/libs/sdk/src/skill/skill.instance.ts b/libs/sdk/src/skill/skill.instance.ts index 4b48c8f60..4a391a594 100644 --- a/libs/sdk/src/skill/skill.instance.ts +++ b/libs/sdk/src/skill/skill.instance.ts @@ -1,11 +1,11 @@ // file: libs/sdk/src/skill/skill.instance.ts import { EntryOwnerRef, SkillEntry, SkillKind, SkillRecord, SkillToolRef, normalizeToolRef } from '../common'; -import { SkillContent, SkillReferenceInfo } from '../common/interfaces'; +import { SkillContent, SkillReferenceInfo, SkillExampleInfo } from '../common/interfaces'; import { SkillVisibility } from '../common/metadata/skill.metadata'; import ProviderRegistry from '../provider/provider.registry'; import { ScopeEntry } from '../common'; -import { loadInstructions, buildSkillContent, resolveReferences } from './skill.utils'; +import { loadInstructions, buildSkillContent, resolveReferences, resolveExamples } from './skill.utils'; import { dirname, pathResolve } from '@frontmcp/utils'; /** @@ -124,7 +124,7 @@ export class SkillInstance extends SkillEntry { /** * Resolve the base directory for this skill (for file/reference resolution). */ - private getBaseDir(): string | undefined { + getBaseDir(): string | undefined { if (this.record.kind === SkillKind.FILE) { return dirname(this.record.filePath) || undefined; } @@ -140,19 +140,33 @@ export class SkillInstance extends SkillEntry { } const instructions = await this.loadInstructions(); + const baseDir = this.getBaseDir(); // 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); + // Resolve examples from the examples/ directory if it exists + const examplesPath = this.metadata.resources?.examples; + let resolvedExs: SkillExampleInfo[] | undefined; + if (examplesPath) { + const exDir = examplesPath.startsWith('/') + ? examplesPath + : baseDir + ? pathResolve(baseDir, examplesPath) + : undefined; + if (exDir) { + resolvedExs = await resolveExamples(exDir); + } + } + + const baseContent = buildSkillContent(this.metadata, instructions, resolvedRefs, resolvedExs); // 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 537acc3ea..8cb84e162 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, SkillReferenceInfo } from '../common/interfaces'; -import { readFile, readdir, fileExists } from '@frontmcp/utils'; +import { SkillContent, SkillReferenceInfo, SkillExampleInfo } from '../common/interfaces'; +import { readFile, readdir, fileExists, stat } from '@frontmcp/utils'; import { InvalidSkillError, SkillInstructionFetchError, InvalidInstructionSourceError } from '../errors'; import { stripFrontmatter } from './skill-md-parser'; @@ -171,12 +171,16 @@ export function buildSkillContent( metadata: SkillMetadata, instructions: string, resolvedReferences?: SkillReferenceInfo[], + resolvedExamples?: SkillExampleInfo[], ): SkillContent { // Append references routing table to instructions if references exist let finalInstructions = instructions; if (resolvedReferences && resolvedReferences.length > 0) { finalInstructions += buildReferencesTable(resolvedReferences); } + if (resolvedExamples && resolvedExamples.length > 0) { + finalInstructions += buildExamplesTable(resolvedExamples); + } return { id: metadata.id ?? metadata.name, @@ -192,6 +196,7 @@ export function buildSkillContent( allowedTools: metadata.allowedTools, resources: metadata.resources, resolvedReferences, + resolvedExamples, }; } @@ -209,6 +214,103 @@ function buildReferencesTable(refs: SkillReferenceInfo[]): string { return lines.join('\n'); } +/** + * Build a markdown examples table to append to skill instructions. + * Groups examples by their parent reference. + */ +function buildExamplesTable(examples: SkillExampleInfo[]): string { + const lines: string[] = [ + '', + '', + '## Examples', + '', + '| Example | Reference | Level | Description |', + '| ------- | --------- | ----- | ----------- |', + ]; + + for (const ex of examples) { + lines.push(`| \`${ex.name}\` | \`${ex.reference}\` | ${ex.level} | ${ex.description} |`); + } + + return lines.join('\n'); +} + +/** + * Scan an examples/ directory for .md files organized by reference subdirectories. + * Parses YAML frontmatter for name, description, reference, and level. + * + * @param examplesDir - Absolute path to the examples/ directory + * @returns Array of resolved example info, or undefined if no examples + */ +export async function resolveExamples(examplesDir: string): Promise { + if (!(await fileExists(examplesDir))) return undefined; + + let refDirs: string[]; + try { + refDirs = (await readdir(examplesDir)).sort(); + } catch { + return undefined; + } + + const examples: SkillExampleInfo[] = []; + + for (const refDir of refDirs) { + const refPath = `${examplesDir}/${refDir}`; + try { + const s = await stat(refPath); + if (!s.isDirectory()) continue; + } catch { + continue; + } + + let files: string[]; + try { + files = (await readdir(refPath)).filter((f: string) => f.endsWith('.md')).sort(); + } catch { + continue; + } + + for (const file of files) { + const content = await readFile(`${refPath}/${file}`, 'utf-8'); + const filenameWithoutExt = file.replace(/\.md$/, ''); + + let name = filenameWithoutExt; + let description = ''; + let reference = refDir; + let level = 'basic'; + + // Parse 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; + if (key === 'reference' && val) reference = val; + if (key === 'level' && val) level = val; + } + } + + // Fallback description from first paragraph + if (!description) { + const body = fmMatch ? content.substring(content.indexOf('---', 3) + 3).trim() : content.trim(); + description = extractFirstParagraph(body); + } + + examples.push({ name, description, reference, level, filename: `${refDir}/${file}` }); + } + } + + return examples.length > 0 ? examples : undefined; +} + /** * Scan a references/ directory for .md files and extract metadata. * Parses YAML frontmatter for name/description, falls back to heading/paragraph. diff --git a/libs/sdk/src/skill/tools/index.ts b/libs/sdk/src/skill/tools/index.ts index 63d9e0f3d..5c9253986 100644 --- a/libs/sdk/src/skill/tools/index.ts +++ b/libs/sdk/src/skill/tools/index.ts @@ -11,13 +11,14 @@ import { SearchSkillsTool } from './search-skills.tool'; import { LoadSkillsTool } from './load-skills.tool'; +import { ReadSkillContentTool } from './read-skill-content.tool'; -export { SearchSkillsTool, LoadSkillsTool }; +export { SearchSkillsTool, LoadSkillsTool, ReadSkillContentTool }; /** * Get all skill-related tools. * Used by the SDK to register skill tools when skills are available. */ export function getSkillTools() { - return [SearchSkillsTool, LoadSkillsTool]; + return [SearchSkillsTool, LoadSkillsTool, ReadSkillContentTool]; } diff --git a/libs/sdk/src/skill/tools/read-skill-content.tool.ts b/libs/sdk/src/skill/tools/read-skill-content.tool.ts new file mode 100644 index 000000000..f86a9c523 --- /dev/null +++ b/libs/sdk/src/skill/tools/read-skill-content.tool.ts @@ -0,0 +1,247 @@ +// file: libs/sdk/src/skill/tools/read-skill-content.tool.ts + +import { z } from 'zod'; +import { Tool, ToolContext } from '../../common'; +import { SkillInstance } from '../skill.instance'; +import { readFile, pathResolve } from '@frontmcp/utils'; +import { parseSkillMdFrontmatter } from '../skill-md-parser'; + +/** + * Input schema for readSkillContent tool. + */ +const inputSchema = { + skillId: z.string().min(1).describe('ID or name of the skill (as returned by searchSkills or loadSkills)'), + type: z.enum(['reference', 'example']).describe('Type of content to read: reference or example'), + name: z + .string() + .min(1) + .describe('Name of the reference or example to read (as shown in the routing table from loadSkills)'), +}; + +/** + * Output schema for readSkillContent tool. + */ +const outputSchema = { + skillId: z.string(), + skillName: z.string(), + type: z.enum(['reference', 'example']), + name: z.string(), + description: z.string(), + content: z.string().describe('Markdown body with frontmatter stripped'), + frontmatter: z.record(z.string(), z.unknown()).optional().describe('Parsed YAML frontmatter fields from the file'), + reference: z.string().optional().describe('Parent reference name (examples only)'), + level: z.string().optional().describe('Complexity level: basic, intermediate, or advanced (examples only)'), + available: z + .array(z.string()) + .optional() + .describe('Available names for the requested type (included when the requested name is not found)'), +}; + +type Input = z.infer>; +type Output = z.infer>; + +/** + * Tool for reading individual reference or example files from a loaded skill. + * + * After loading a skill with `loadSkills`, the instructions include a routing table + * listing available references and examples. Use this tool to read the full content + * of a specific reference or example file. + * + * @example + * ```typescript + * // Read a reference + * const ref = await readSkillContent({ + * skillId: 'frontmcp-deployment', + * type: 'reference', + * name: 'deploy-to-vercel', + * }); + * + * // Read an example + * const ex = await readSkillContent({ + * skillId: 'frontmcp-deployment', + * type: 'example', + * name: 'vercel-with-kv', + * }); + * ``` + */ +@Tool({ + name: 'readSkillContent', + description: + 'Read the full content of a specific reference or example from a skill. ' + + 'After loading a skill with loadSkills, the instructions include routing tables ' + + 'listing available references and examples by name. Use this tool to read ' + + 'the complete content of any listed item.\n\n' + + '**When to use:**\n' + + '- After loadSkills shows a reference you need to read for detailed guidance\n' + + '- When you need a worked example for a specific scenario\n' + + '- When the routing table lists a topic you want to explore further\n\n' + + '**Example flow:**\n' + + '1. loadSkills({ skillIds: ["frontmcp-deployment"] }) — see routing table\n' + + '2. readSkillContent({ skillId: "frontmcp-deployment", type: "reference", name: "deploy-to-vercel" })\n' + + '3. readSkillContent({ skillId: "frontmcp-deployment", type: "example", name: "vercel-with-kv" })', + inputSchema, + outputSchema, + tags: ['skills', 'references', 'examples', 'content'], + annotations: { + title: 'Read Skill Content', + readOnlyHint: true, + }, +}) +export class ReadSkillContentTool extends ToolContext { + async execute(input: Input): Promise { + const skillRegistry = this.scope.skills; + + if (!skillRegistry) { + this.fail(new Error('Skills are not available in this scope')); + } + + // Load skill content (uses cache if already loaded) + const loadResult = await skillRegistry.loadSkill(input.skillId); + if (!loadResult) { + this.fail(new Error(`Skill "${input.skillId}" not found. Use searchSkills to discover available skills.`)); + } + + const { skill } = loadResult; + + // Find the entry to get the SkillInstance for path resolution + const entry = skillRegistry.findByName(input.skillId); + if (!entry) { + this.fail(new Error(`Skill "${input.skillId}" entry not found.`)); + } + + if (input.type === 'reference') { + return this.readReference(input, skill, entry as SkillInstance); + } + return this.readExample(input, skill, entry as SkillInstance); + } + + private async readReference( + input: Input, + skill: { resolvedReferences?: Array<{ name: string; description: string; filename: string }> } & { + id: string; + name: string; + }, + instance: SkillInstance, + ): Promise { + const refs = skill.resolvedReferences ?? []; + const refEntry = refs.find((r) => r.name === input.name); + + if (!refEntry) { + const availableNames = refs.map((r) => r.name); + return { + skillId: skill.id, + skillName: skill.name, + type: 'reference', + name: input.name, + description: `Reference "${input.name}" not found.`, + content: + availableNames.length > 0 + ? `Reference "${input.name}" not found. Available references: ${availableNames.join(', ')}` + : `Skill "${skill.name}" has no references.`, + available: availableNames, + }; + } + + const content = await this.readFileContent(instance, 'references', refEntry.filename); + const { frontmatter, body } = parseSkillMdFrontmatter(content); + + return { + skillId: skill.id, + skillName: skill.name, + type: 'reference', + name: refEntry.name, + description: refEntry.description, + content: body, + frontmatter: flattenFrontmatter(frontmatter), + }; + } + + private async readExample( + input: Input, + skill: { + resolvedExamples?: Array<{ + name: string; + description: string; + reference: string; + level: string; + filename: string; + }>; + } & { + id: string; + name: string; + }, + instance: SkillInstance, + ): Promise { + const examples = skill.resolvedExamples ?? []; + const exEntry = examples.find((e) => e.name === input.name); + + if (!exEntry) { + const availableNames = examples.map((e) => e.name); + return { + skillId: skill.id, + skillName: skill.name, + type: 'example', + name: input.name, + description: `Example "${input.name}" not found.`, + content: + availableNames.length > 0 + ? `Example "${input.name}" not found. Available examples: ${availableNames.join(', ')}` + : `Skill "${skill.name}" has no examples.`, + available: availableNames, + }; + } + + const content = await this.readFileContent(instance, 'examples', exEntry.filename); + const { frontmatter, body } = parseSkillMdFrontmatter(content); + + return { + skillId: skill.id, + skillName: skill.name, + type: 'example', + name: exEntry.name, + description: exEntry.description, + content: body, + frontmatter: flattenFrontmatter(frontmatter), + reference: exEntry.reference, + level: exEntry.level, + }; + } + + private async readFileContent( + instance: SkillInstance, + resourceType: 'references' | 'examples', + filename: string, + ): Promise { + const baseDir = instance.getBaseDir(); + const resources = instance.getResources(); + const resourcePath = resources?.[resourceType]; + + if (!baseDir || !resourcePath) { + this.fail(new Error(`Skill does not have a ${resourceType} directory configured.`)); + } + + const resourceDir = resourcePath.startsWith('/') ? resourcePath : pathResolve(baseDir, resourcePath); + const filePath = pathResolve(resourceDir, filename); + + try { + return await readFile(filePath, 'utf-8'); + } catch { + this.fail( + new Error(`Failed to read ${resourceType} file "${filename}". The file may have been moved or deleted.`), + ); + } + } +} + +/** + * Flatten frontmatter values to string representation for the output schema. + */ +function flattenFrontmatter(fm: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(fm)) { + if (value !== undefined && value !== null) { + result[key] = value; + } + } + return result; +} diff --git a/libs/skills/README.md b/libs/skills/README.md index 0bda14540..94367e8e5 100644 --- a/libs/skills/README.md +++ b/libs/skills/README.md @@ -4,7 +4,7 @@ Curated skills catalog for FrontMCP projects. Skills are SKILL.md-based instruct ## Structure -The catalog uses a **router skill model** — 6 domain-scoped router skills, each containing a SKILL.md with a routing table and a `references/` directory with detailed reference files. +The catalog uses a **router skill model** — domain-scoped router skills, each containing a SKILL.md with a routing table, a `references/` directory with detailed topic guides, and optional `examples/` directories for standalone example files. ```text catalog/ @@ -29,6 +29,8 @@ frontmcp-development/ └── ... ``` +Example files live under `examples//` and are the canonical source of example metadata used by `skills-manifest.json` and the reference `## Examples` tables. + ## SKILL.md Frontmatter ```yaml @@ -69,6 +71,32 @@ Step-by-step markdown instructions here... 3. Add a routing entry in the router's `SKILL.md` routing table 4. Run `nx test skills` to validate +## Adding Example Files + +Each reference may have a matching `examples//` directory with standalone example files. + +```yaml +--- +name: example-name +reference: parent-reference-name +level: basic +description: One sentence describing the exact scenario this example covers. +tags: [keyword1, keyword2, keyword3] +features: + - Concrete API or pattern this example demonstrates + - Another concrete behavior shown in the code +--- +``` + +Rules: + +- `name` must match the filename without `.md` +- `reference` must match the parent examples directory +- `description` is the canonical one-line summary reused by the manifest and reference table +- `tags` are searchable keywords for the example +- `features` are the concrete APIs or patterns demonstrated by the code +- The first paragraph under the H1 should semantically match `description` + ## Manifest Entry Each router skill has a corresponding entry in `skills-manifest.json`: @@ -125,3 +153,4 @@ Tests verify: - Names match between manifest and frontmatter - `hasResources` flags are accurate - Targets, categories, and bundles use valid values +- Example metadata stays in sync across example files, reference tables, and the manifest diff --git a/libs/skills/__tests__/skills-validation.spec.ts b/libs/skills/__tests__/skills-validation.spec.ts index 154277233..0d93f3e84 100644 --- a/libs/skills/__tests__/skills-validation.spec.ts +++ b/libs/skills/__tests__/skills-validation.spec.ts @@ -12,7 +12,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { parseSkillMdFrontmatter, skillMdFrontmatterToMetadata } from '../../sdk/src/skill/skill-md-parser'; import type { SkillManifest, SkillCatalogEntry } from '../src/manifest'; -import { VALID_TARGETS, VALID_CATEGORIES, VALID_BUNDLES } from '../src/manifest'; +import { VALID_TARGETS, VALID_CATEGORIES, VALID_BUNDLES, VALID_EXAMPLE_LEVELS } from '../src/manifest'; const CATALOG_DIR = path.resolve(__dirname, '..', 'catalog'); const MANIFEST_PATH = path.join(CATALOG_DIR, 'skills-manifest.json'); @@ -51,6 +51,158 @@ function findAllSkillDirs(): string[] { return dirs; } +function readMarkdownBody(content: string): string { + return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ''); +} + +function extractFirstParagraph(content: string): string { + const body = readMarkdownBody(content); + const lines = body.split(/\r?\n/); + let sawHeading = false; + const paragraph: string[] = []; + + for (const line of lines) { + const trimmed = line.trim(); + if (!sawHeading && trimmed.startsWith('#')) { + sawHeading = true; + continue; + } + if (!sawHeading) continue; + if (!trimmed) { + if (paragraph.length > 0) break; + continue; + } + if (trimmed.startsWith('##')) { + if (paragraph.length > 0) break; + continue; + } + paragraph.push(trimmed); + } + + return paragraph.join(' '); +} + +function extractSectionBullets(content: string, heading: string): string[] { + const body = readMarkdownBody(content); + const lines = body.split(/\r?\n/); + const bullets: string[] = []; + let inSection = false; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === `## ${heading}`) { + inSection = true; + continue; + } + if (!inSection) continue; + if (trimmed.startsWith('## ')) break; + if (trimmed.startsWith('- ')) { + bullets.push(trimmed.slice(2).trim()); + } + } + + return bullets; +} + +function humanizeExampleLevel(level: string): string { + return level.charAt(0).toUpperCase() + level.slice(1); +} + +function parseExamplesTableRows( + content: string, +): Array<{ name: string; level: string; description: string; href?: string }> { + const lines = content.split(/\r?\n/); + const rows: Array<{ name: string; level: string; description: string; href?: string }> = []; + let inTable = false; + + for (const line of lines) { + const normalizedCells = line + .split('|') + .map((c) => c.trim()) + .filter(Boolean); + if ( + normalizedCells.length === 3 && + normalizedCells[0] === 'Example' && + normalizedCells[1] === 'Level' && + normalizedCells[2] === 'Description' + ) { + inTable = true; + continue; + } + if (!inTable) continue; + if (line.startsWith('| ---')) continue; + if (!line.startsWith('|')) break; + + const cells = line.split('|').map((cell) => cell.trim()); + if (cells.length < 5) continue; + + const exampleCell = cells[1]; + const level = cells[2]; + const description = cells[3]; + const nameMatch = exampleCell.match(/\[`([^`]+)`\]/); + const hrefMatch = exampleCell.match(/\]\(([^)]+)\)/); + + rows.push({ + name: nameMatch ? nameMatch[1] : exampleCell, + level, + description, + href: hrefMatch ? hrefMatch[1] : undefined, + }); + } + + return rows; +} + +function getAllReferenceFiles(): { skill: string; file: string; fullPath: string }[] { + const results: { skill: string; file: string; fullPath: string }[] = []; + const entries = fs.readdirSync(CATALOG_DIR).filter((f) => { + const full = path.join(CATALOG_DIR, f); + return fs.statSync(full).isDirectory() && f !== 'node_modules'; + }); + for (const entry of entries) { + const refsDir = path.join(CATALOG_DIR, entry, 'references'); + if (fs.existsSync(refsDir)) { + const files = fs.readdirSync(refsDir).filter((f) => f.endsWith('.md')); + for (const file of files) { + results.push({ skill: entry, file, fullPath: path.join(refsDir, file) }); + } + } + const skillMd = path.join(CATALOG_DIR, entry, 'SKILL.md'); + if (fs.existsSync(skillMd)) { + results.push({ skill: entry, file: 'SKILL.md', fullPath: skillMd }); + } + } + return results; +} + +function getAllExampleFiles(): { skill: string; reference: string; file: string; fullPath: string }[] { + const results: { skill: string; reference: string; file: string; fullPath: string }[] = []; + const entries = fs.readdirSync(CATALOG_DIR).filter((f) => { + const full = path.join(CATALOG_DIR, f); + return fs.statSync(full).isDirectory() && f !== 'node_modules'; + }); + for (const entry of entries) { + const examplesDir = path.join(CATALOG_DIR, entry, 'examples'); + if (!fs.existsSync(examplesDir)) continue; + const refDirs = fs.readdirSync(examplesDir).filter((f) => { + return fs.statSync(path.join(examplesDir, f)).isDirectory(); + }); + for (const refDir of refDirs) { + const refPath = path.join(examplesDir, refDir); + const files = fs.readdirSync(refPath).filter((f) => f.endsWith('.md')); + for (const file of files) { + results.push({ + skill: entry, + reference: refDir, + file, + fullPath: path.join(refPath, file), + }); + } + } + } + return results; +} + describe('skills catalog validation', () => { let manifest: SkillManifest; let skillDirs: string[]; @@ -277,35 +429,18 @@ describe('skills catalog validation', () => { }); describe('semantic content validation', () => { - /** - * Collects all .md files under references/ for all catalog skills. - */ - function getAllReferenceFiles(): { skill: string; file: string; fullPath: string }[] { - const results: { skill: string; file: string; fullPath: string }[] = []; - const entries = fs.readdirSync(CATALOG_DIR).filter((f) => { - const full = path.join(CATALOG_DIR, f); - return fs.statSync(full).isDirectory() && f !== 'node_modules'; - }); - for (const entry of entries) { - const refsDir = path.join(CATALOG_DIR, entry, 'references'); - if (fs.existsSync(refsDir)) { - const files = fs.readdirSync(refsDir).filter((f) => f.endsWith('.md')); - for (const file of files) { - results.push({ skill: entry, file, fullPath: path.join(refsDir, file) }); - } - } - // Also include the SKILL.md itself - const skillMd = path.join(CATALOG_DIR, entry, 'SKILL.md'); - if (fs.existsSync(skillMd)) { - results.push({ skill: entry, file: 'SKILL.md', fullPath: skillMd }); - } - } - return results; - } + const documentationFiles = [ + ...getAllReferenceFiles(), + ...getAllExampleFiles().map(({ skill, file, fullPath, reference }) => ({ + skill, + file: `examples/${reference}/${file}`, + fullPath, + })), + ]; it('should not use invalid LLM "adapter" field in code examples', () => { const violations: string[] = []; - for (const { skill, file, fullPath } of getAllReferenceFiles()) { + for (const { skill, file, fullPath } of documentationFiles) { const content = fs.readFileSync(fullPath, 'utf-8'); // Match adapter: 'anthropic' or adapter: 'openai' in code blocks const adapterMatches = content.match(/adapter:\s*['"](?:anthropic|openai)['"]/g); @@ -318,7 +453,7 @@ describe('skills catalog validation', () => { it('should not use auth string shorthand in decorator context', () => { const violations: string[] = []; - for (const { skill, file, fullPath } of getAllReferenceFiles()) { + for (const { skill, file, fullPath } of documentationFiles) { const content = fs.readFileSync(fullPath, 'utf-8'); // Match auth: 'remote', auth: 'public', auth: 'transparent' as standalone config values const authShorthand = content.match(/auth:\s*['"](?:remote|public|transparent)['"]/g); @@ -332,7 +467,7 @@ describe('skills catalog validation', () => { it('should not use "streamable-http" as a transport preset in SDK context', () => { const violations: string[] = []; const validPresets = ['modern', 'legacy', 'stateless-api', 'full']; - for (const { skill, file, fullPath } of getAllReferenceFiles()) { + for (const { skill, file, fullPath } of documentationFiles) { const content = fs.readFileSync(fullPath, 'utf-8'); // Match protocol: 'streamable-http' or transport: 'streamable-http' const matches = content.match(/(?:protocol|transport):\s*['"]streamable-http['"]/g); @@ -347,7 +482,7 @@ describe('skills catalog validation', () => { it('should not use bare @App() without metadata', () => { const violations: string[] = []; - for (const { skill, file, fullPath } of getAllReferenceFiles()) { + for (const { skill, file, fullPath } of documentationFiles) { const content = fs.readFileSync(fullPath, 'utf-8'); // Match @App() with empty parens (no arguments) const bareApp = content.match(/@App\(\s*\)/g); @@ -360,7 +495,7 @@ describe('skills catalog validation', () => { it('should not use "session:" as a top-level @FrontMcp field', () => { const violations: string[] = []; - for (const { skill, file, fullPath } of getAllReferenceFiles()) { + for (const { skill, file, fullPath } of documentationFiles) { const content = fs.readFileSync(fullPath, 'utf-8'); // Look for session: { ... } in decorator blocks (preceded by @FrontMcp) // Simple heuristic: find session: { store in code blocks @@ -373,6 +508,235 @@ describe('skills catalog validation', () => { }); }); + describe('examples validation', () => { + it('every examples/ subfolder should match a reference filename', () => { + const mismatches: string[] = []; + const entries = fs.readdirSync(CATALOG_DIR).filter((f) => { + const full = path.join(CATALOG_DIR, f); + return fs.statSync(full).isDirectory() && f !== 'node_modules'; + }); + for (const entry of entries) { + const examplesDir = path.join(CATALOG_DIR, entry, 'examples'); + if (!fs.existsSync(examplesDir)) continue; + const refsDir = path.join(CATALOG_DIR, entry, 'references'); + const refNames = fs.existsSync(refsDir) + ? fs + .readdirSync(refsDir) + .filter((f) => f.endsWith('.md')) + .map((f) => f.replace(/\.md$/, '')) + : []; + const exampleDirs = fs.readdirSync(examplesDir).filter((f) => { + return fs.statSync(path.join(examplesDir, f)).isDirectory(); + }); + for (const dir of exampleDirs) { + if (!refNames.includes(dir)) { + mismatches.push(`${entry}/examples/${dir} has no matching reference file`); + } + } + } + expect(mismatches).toEqual([]); + }); + + it('every example .md file should have valid frontmatter', () => { + const invalid: string[] = []; + for (const { skill, reference, file, fullPath } of getAllExampleFiles()) { + const content = fs.readFileSync(fullPath, 'utf-8'); + const { frontmatter } = parseSkillMdFrontmatter(content); + const expectedName = file.replace(/\.md$/, ''); + if (!frontmatter['name'] || typeof frontmatter['name'] !== 'string') { + invalid.push(`${skill}/examples/${reference}/${file}: missing or invalid "name" in frontmatter`); + } + if (frontmatter['name'] && frontmatter['name'] !== expectedName) { + invalid.push( + `${skill}/examples/${reference}/${file}: frontmatter "name" must match filename "${expectedName}"`, + ); + } + if (!frontmatter['reference'] || typeof frontmatter['reference'] !== 'string') { + invalid.push(`${skill}/examples/${reference}/${file}: missing or invalid "reference" in frontmatter`); + } + if ( + !frontmatter['level'] || + !(VALID_EXAMPLE_LEVELS as readonly string[]).includes(frontmatter['level'] as string) + ) { + invalid.push( + `${skill}/examples/${reference}/${file}: missing or invalid "level" in frontmatter (must be ${VALID_EXAMPLE_LEVELS.join(', ')})`, + ); + } + if (!frontmatter['description'] || typeof frontmatter['description'] !== 'string') { + invalid.push(`${skill}/examples/${reference}/${file}: missing or invalid "description" in frontmatter`); + } + const tags = frontmatter['tags']; + if (!Array.isArray(tags) || tags.length === 0 || tags.some((tag) => typeof tag !== 'string' || !tag.trim())) { + invalid.push(`${skill}/examples/${reference}/${file}: missing or invalid "tags" in frontmatter`); + } + const features = frontmatter['features']; + if ( + !Array.isArray(features) || + features.length === 0 || + features.some((feature) => typeof feature !== 'string' || !feature.trim()) + ) { + invalid.push(`${skill}/examples/${reference}/${file}: missing or invalid "features" in frontmatter`); + } + // reference field should match the parent directory name + if (frontmatter['reference'] && frontmatter['reference'] !== reference) { + invalid.push( + `${skill}/examples/${reference}/${file}: frontmatter "reference" is "${frontmatter['reference']}" but expected "${reference}"`, + ); + } + } + expect(invalid).toEqual([]); + }); + + it('example frontmatter should stay aligned with the example body', () => { + const mismatches: string[] = []; + for (const { skill, reference, file, fullPath } of getAllExampleFiles()) { + const content = fs.readFileSync(fullPath, 'utf-8'); + const { frontmatter } = parseSkillMdFrontmatter(content); + const description = typeof frontmatter['description'] === 'string' ? frontmatter['description'] : ''; + const features = Array.isArray(frontmatter['features']) + ? frontmatter['features'].filter((feature): feature is string => typeof feature === 'string') + : []; + const firstParagraph = extractFirstParagraph(content); + const whatThisDemonstrates = extractSectionBullets(content, 'What This Demonstrates'); + + if (description !== firstParagraph) { + mismatches.push( + `${skill}/examples/${reference}/${file}: frontmatter "description" must match the first paragraph after the H1`, + ); + } + if (JSON.stringify(features) !== JSON.stringify(whatThisDemonstrates)) { + mismatches.push( + `${skill}/examples/${reference}/${file}: frontmatter "features" must match the "What This Demonstrates" bullets`, + ); + } + } + expect(mismatches).toEqual([]); + }); + + it('manifest example entries should match example file metadata', () => { + const mismatches: string[] = []; + const manifestExampleKeys = new Set(); + for (const entry of manifest.skills) { + if (!entry.references) continue; + for (const ref of entry.references) { + const examples = ref.examples ?? []; + const exampleDir = path.join(CATALOG_DIR, entry.path, 'examples', ref.name); + for (const example of examples) { + manifestExampleKeys.add(`${entry.path}/${ref.name}/${example.name}`); + const exampleFile = path.join(exampleDir, `${example.name}.md`); + if (!fs.existsSync(exampleFile)) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}.md listed in manifest but missing on disk`); + continue; + } + if (!(VALID_EXAMPLE_LEVELS as readonly string[]).includes(example.level)) { + mismatches.push(`${entry.name}/${ref.name}/${example.name} has invalid level "${example.level}"`); + } + if (!Array.isArray(example.tags) || example.tags.length === 0) { + mismatches.push(`${entry.name}/${ref.name}/${example.name} has invalid manifest tags`); + } + if (!Array.isArray(example.features) || example.features.length === 0) { + mismatches.push(`${entry.name}/${ref.name}/${example.name} has invalid manifest features`); + } + + const { frontmatter } = parseSkillMdFrontmatter(fs.readFileSync(exampleFile, 'utf-8')); + const fileDescription = typeof frontmatter['description'] === 'string' ? frontmatter['description'] : ''; + const fileLevel = typeof frontmatter['level'] === 'string' ? frontmatter['level'] : ''; + const fileTags = Array.isArray(frontmatter['tags']) + ? frontmatter['tags'].filter((tag): tag is string => typeof tag === 'string') + : []; + const fileFeatures = Array.isArray(frontmatter['features']) + ? frontmatter['features'].filter((feature): feature is string => typeof feature === 'string') + : []; + + if (example.description !== fileDescription) { + mismatches.push( + `${entry.name}/${ref.name}/${example.name}: manifest description differs from example file`, + ); + } + if (example.level !== fileLevel) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}: manifest level differs from example file`); + } + if (JSON.stringify(example.tags) !== JSON.stringify(fileTags)) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}: manifest tags differ from example file`); + } + if (JSON.stringify(example.features) !== JSON.stringify(fileFeatures)) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}: manifest features differ from example file`); + } + } + } + } + + for (const { skill, reference, file } of getAllExampleFiles()) { + const exampleName = file.replace(/\.md$/, ''); + const key = `${skill}/${reference}/${exampleName}`; + if (!manifestExampleKeys.has(key)) { + mismatches.push(`${key}.md exists on disk but is missing from the manifest`); + } + } + + expect(mismatches).toEqual([]); + }); + + it('reference example tables should match manifest example metadata', () => { + const mismatches: string[] = []; + for (const entry of manifest.skills) { + for (const ref of entry.references ?? []) { + const referencePath = path.join(CATALOG_DIR, entry.path, 'references', `${ref.name}.md`); + if (!fs.existsSync(referencePath)) continue; + + const tableRows = parseExamplesTableRows(fs.readFileSync(referencePath, 'utf-8')); + const examples = ref.examples ?? []; + + if (tableRows.length !== examples.length) { + mismatches.push( + `${entry.name}/${ref.name}: reference table has ${tableRows.length} rows but manifest has ${examples.length} examples`, + ); + continue; + } + + for (const example of examples) { + const row = tableRows.find((tableRow) => tableRow.name === example.name); + if (!row) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}: missing from reference example table`); + continue; + } + + if (row.description !== example.description) { + mismatches.push( + `${entry.name}/${ref.name}/${example.name}: reference table description differs from manifest`, + ); + } + if (row.level !== humanizeExampleLevel(example.level)) { + mismatches.push(`${entry.name}/${ref.name}/${example.name}: reference table level differs from manifest`); + } + // Validate that the href resolves to the expected example file + if (!row.href) { + mismatches.push( + `${entry.name}/${ref.name}/${example.name}: missing href link in reference example table`, + ); + } else { + const expectedHref = `../examples/${ref.name}/${example.name}.md`; + if (row.href !== expectedHref) { + mismatches.push( + `${entry.name}/${ref.name}/${example.name}: href "${row.href}" does not match expected "${expectedHref}"`, + ); + } + // Also verify the target file exists on disk + const resolvedPath = path.resolve(path.dirname(referencePath), row.href); + if (!fs.existsSync(resolvedPath)) { + mismatches.push( + `${entry.name}/${ref.name}/${example.name}: href target "${row.href}" does not exist on disk`, + ); + } + } + } + } + } + + expect(mismatches).toEqual([]); + }); + }); + describe('new-format migration tracking', () => { function getSkillBody(dir: string): string { return fs.readFileSync(path.join(CATALOG_DIR, dir, 'SKILL.md'), 'utf-8'); diff --git a/libs/skills/catalog/TEMPLATE.md b/libs/skills/catalog/TEMPLATE.md index d5145c209..36110470a 100644 --- a/libs/skills/catalog/TEMPLATE.md +++ b/libs/skills/catalog/TEMPLATE.md @@ -88,6 +88,61 @@ Continue with subsequent steps. | -------------------- | -------------- | ------------- | | Common error message | Why it happens | How to fix it | +## Examples + +Each reference file has a corresponding `examples//` directory with standalone, copy-pasteable examples. + +### Example file structure + +````markdown +--- +name: example-name +reference: parent-reference-name +level: basic | intermediate | advanced +description: One sentence describing the exact scenario this example covers. +tags: [keyword1, keyword2, keyword3] +features: + - Concrete API or pattern this example demonstrates + - Another concrete behavior shown in the code +--- + +# Example Title + +One sentence expanding slightly on the frontmatter description. + +## Code + +\```typescript +// src/path/to/file.ts +import { ... } from '@frontmcp/sdk'; +// Complete, self-contained code +\``` + +## What This Demonstrates + +- Key pattern or API shown + +## Related + +- See `reference-name` for the full API reference +```` + +Use the example file frontmatter as the single source of truth for example metadata. Reference `## Examples` tables and `skills-manifest.json` should mirror `name`, `level`, `description`, `tags`, and `features` from the example file. + +### Linking from references + +Add a `## Examples` section at the bottom of each reference file (before `## Reference`): + +```markdown +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------ | ----- | ------------- | +| [`example-name`](../examples/reference-name/example-name.md) | Basic | What it shows | + +> See all examples in [`examples/reference-name/`](../examples/reference-name/) +``` + ## Reference - [Documentation](https://docs.agentfront.dev/frontmcp/...) diff --git a/libs/skills/catalog/frontmcp-config/SKILL.md b/libs/skills/catalog/frontmcp-config/SKILL.md index f5fe68d9a..2984d2b06 100644 --- a/libs/skills/catalog/frontmcp-config/SKILL.md +++ b/libs/skills/catalog/frontmcp-config/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/configuration/overview + docs: https://docs.agentfront.dev/frontmcp/fundamentals/overview --- # FrontMCP Configuration Router @@ -152,5 +152,5 @@ Server (@FrontMcp) ← Global defaults ## Reference -- [Configuration Overview](https://docs.agentfront.dev/frontmcp/configuration/overview) +- [FrontMCP Overview](https://docs.agentfront.dev/frontmcp/fundamentals/overview) - Related skills: `configure-transport`, `configure-http`, `configure-throttle`, `configure-elicitation`, `configure-auth`, `configure-session`, `setup-redis`, `setup-sqlite` diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md new file mode 100644 index 000000000..2597076b5 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md @@ -0,0 +1,77 @@ +--- +name: local-self-signed-tokens +reference: configure-auth-modes +level: intermediate +description: 'Configure a server that signs its own JWT tokens with consent and incremental auth enabled.' +tags: [config, auth, redis, local, auth-modes, modes] +features: + - "Using `mode: 'local'` so the server signs its own JWTs" + - 'Setting `local.issuer` and `local.audience` to control token claims' + - 'Enabling `consent` for explicit user authorization flow' + - 'Enabling `incrementalAuth` to request additional scopes progressively' + - 'Using Redis for token storage in production' +--- + +# Local Self-Signed Tokens + +Configure a server that signs its own JWT tokens with consent and incremental auth enabled. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'manage_users', + description: 'Manage user accounts', + inputSchema: { action: z.enum(['list', 'create', 'delete']), userId: z.string().optional() }, + outputSchema: { success: z.boolean(), message: z.string() }, +}) +class ManageUsersTool extends ToolContext { + async execute(input: { action: string; userId?: string }) { + return { success: true, message: `Action ${input.action} completed` }; + } +} + +@App({ + name: 'internal-api', + auth: { + mode: 'local', + local: { + issuer: 'my-internal-server', + audience: 'internal-api', + }, + tokenStorage: 'redis', + consent: { enabled: true }, + incrementalAuth: { enabled: true }, + }, + tools: [ManageUsersTool], +}) +class InternalApi {} + +@FrontMcp({ + info: { name: 'local-auth-server', version: '1.0.0' }, + apps: [InternalApi], + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'localhost', + port: 6379, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `mode: 'local'` so the server signs its own JWTs +- Setting `local.issuer` and `local.audience` to control token claims +- Enabling `consent` for explicit user authorization flow +- Enabling `incrementalAuth` to request additional scopes progressively +- Using Redis for token storage in production + +## Related + +- See `configure-auth-modes` for a comparison of all auth modes +- See `configure-session` for session storage configuration diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md new file mode 100644 index 000000000..cd3e8caad --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md @@ -0,0 +1,73 @@ +--- +name: remote-enterprise-oauth +reference: configure-auth-modes +level: advanced +description: 'Delegate authentication to an external OAuth orchestrator with Redis-backed token storage.' +tags: [config, oauth, auth, redis, remote, auth-modes] +features: + - "Using `mode: 'remote'` to delegate to an external OAuth 2.1 authorization server" + - 'Loading `clientId` and `clientSecret` from environment variables (never hardcoded)' + - 'Configuring Redis-backed token storage for production persistence' + - 'Full OAuth flow: clients are redirected to the provider and return with an authorization code' +--- + +# Remote Enterprise OAuth + +Delegate authentication to an external OAuth orchestrator with Redis-backed token storage. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'query_data', + description: 'Query enterprise data warehouse', + inputSchema: { sql: z.string() }, + outputSchema: { rows: z.array(z.record(z.string(), z.unknown())), rowCount: z.number() }, +}) +class QueryDataTool extends ToolContext { + async execute(input: { sql: string }) { + return { rows: [{ id: 1, name: 'example' }], rowCount: 1 }; + } +} + +@App({ + name: 'enterprise-api', + auth: { + mode: 'remote', + provider: 'https://auth.example.com', + clientId: process.env['OAUTH_CLIENT_ID']!, + clientSecret: process.env['OAUTH_CLIENT_SECRET'], + tokenStorage: 'redis', + }, + tools: [QueryDataTool], +}) +class EnterpriseApi {} + +@FrontMcp({ + info: { name: 'enterprise-server', version: '1.0.0' }, + apps: [EnterpriseApi], + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'redis.internal', + port: Number(process.env['REDIS_PORT'] ?? 6379), + password: process.env['REDIS_PASSWORD'], + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `mode: 'remote'` to delegate to an external OAuth 2.1 authorization server +- Loading `clientId` and `clientSecret` from environment variables (never hardcoded) +- Configuring Redis-backed token storage for production persistence +- Full OAuth flow: clients are redirected to the provider and return with an authorization code + +## Related + +- See `configure-auth-modes` for a comparison of all auth modes +- See `setup-redis` for Redis provisioning details diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/transparent-jwt-validation.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/transparent-jwt-validation.md new file mode 100644 index 000000000..1eb0f561d --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth-modes/transparent-jwt-validation.md @@ -0,0 +1,64 @@ +--- +name: transparent-jwt-validation +reference: configure-auth-modes +level: basic +description: 'Validate externally-issued JWTs without managing token lifecycle on the server.' +tags: [config, auth, transparent, auth-modes, modes, jwt] +features: + - "Using `mode: 'transparent'` to validate tokens from an external identity provider" + - 'Setting `expectedAudience` to restrict which tokens are accepted' + - 'The server fetches JWKS from `{provider}/.well-known/jwks.json` automatically' +--- + +# Transparent JWT Validation + +Validate externally-issued JWTs without managing token lifecycle on the server. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get_profile', + description: 'Get the authenticated user profile', + inputSchema: { userId: z.string() }, + outputSchema: { id: z.string(), email: z.string() }, +}) +class GetProfileTool extends ToolContext { + async execute(input: { userId: string }) { + return { id: input.userId, email: `${input.userId}@example.com` }; + } +} + +@App({ + name: 'api', + auth: { + mode: 'transparent', + provider: 'https://auth.example.com', + expectedAudience: 'my-api', + clientId: 'my-client-id', + }, + tools: [GetProfileTool], +}) +class ApiApp {} + +@FrontMcp({ + info: { name: 'transparent-server', version: '1.0.0' }, + apps: [ApiApp], +}) +class Server {} +``` + +## What This Demonstrates + +- Using `mode: 'transparent'` to validate tokens from an external identity provider +- Setting `expectedAudience` to restrict which tokens are accepted +- The server fetches JWKS from `{provider}/.well-known/jwks.json` automatically + +## Related + +- See `configure-auth-modes` for a comparison of all auth modes +- See `configure-auth` for the full authentication setup guide diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth/multi-app-auth.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth/multi-app-auth.md new file mode 100644 index 000000000..ddc437916 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth/multi-app-auth.md @@ -0,0 +1,87 @@ +--- +name: multi-app-auth +reference: configure-auth +level: advanced +description: 'Configure a single FrontMCP server with multiple apps, each using a different auth mode -- public for open endpoints and remote for admin endpoints.' +tags: [config, auth, security, multi-app, remote, multi] +features: + - 'Hosting multiple `@App` instances on a single FrontMCP server with different auth modes' + - 'Using `public` mode for open-access endpoints alongside `remote` mode for admin-only endpoints' + - 'Isolating tools per app so each security posture governs only its own tools' +--- + +# Multi-App Auth with Different Security Postures + +Configure a single FrontMCP server with multiple apps, each using a different auth mode -- public for open endpoints and remote for admin endpoints. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'public_search', + description: 'Search public records', + inputSchema: { query: z.string() }, + outputSchema: { results: z.array(z.string()) }, +}) +class PublicSearchTool extends ToolContext { + async execute(input: { query: string }) { + return { results: [`Public result: ${input.query}`] }; + } +} + +@Tool({ + name: 'admin_config', + description: 'Modify server configuration (admin only)', + inputSchema: { key: z.string(), value: z.string() }, + outputSchema: { updated: z.boolean() }, +}) +class AdminConfigTool extends ToolContext { + async execute(input: { key: string; value: string }) { + // Only authenticated admins can reach this tool + return { updated: true }; + } +} + +@App({ + name: 'public-api', + auth: { + mode: 'public', + sessionTtl: 3600, + anonymousScopes: ['read'], + }, + tools: [PublicSearchTool], +}) +class PublicApi {} + +@App({ + name: 'admin-api', + auth: { + mode: 'remote', + provider: 'https://auth.example.com', + clientId: process.env['ADMIN_OAUTH_CLIENT_ID'] ?? 'admin-client', + }, + tools: [AdminConfigTool], +}) +class AdminApi {} + +@FrontMcp({ + info: { name: 'multi-app-server', version: '1.0.0' }, + apps: [PublicApi, AdminApi], +}) +class Server {} +``` + +## What This Demonstrates + +- Hosting multiple `@App` instances on a single FrontMCP server with different auth modes +- Using `public` mode for open-access endpoints alongside `remote` mode for admin-only endpoints +- Isolating tools per app so each security posture governs only its own tools + +## Related + +- See `configure-auth` for individual auth mode configuration details +- See `configure-auth-modes` for a feature comparison table across all modes diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth/public-mode-setup.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth/public-mode-setup.md new file mode 100644 index 000000000..061a5aa6c --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth/public-mode-setup.md @@ -0,0 +1,63 @@ +--- +name: public-mode-setup +reference: configure-auth +level: basic +description: 'Set up a FrontMCP server with public (unauthenticated) access and anonymous scopes.' +tags: [config, auth, session, public, mode, setup] +features: + - "Configuring `mode: 'public'` for unauthenticated access" + - 'Setting `sessionTtl` to control anonymous session lifetime' + - 'Granting `anonymousScopes` so tools can check scope-based permissions even without auth' +--- + +# Public Auth Mode Setup + +Set up a FrontMCP server with public (unauthenticated) access and anonymous scopes. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'search', + description: 'Search public records', + inputSchema: { query: z.string() }, + outputSchema: { results: z.array(z.string()) }, +}) +class SearchTool extends ToolContext { + async execute(input: { query: string }) { + return { results: [`Result for: ${input.query}`] }; + } +} + +@App({ + name: 'public-api', + auth: { + mode: 'public', + sessionTtl: 3600, + anonymousScopes: ['read'], + }, + tools: [SearchTool], +}) +class PublicApi {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [PublicApi], +}) +class Server {} +``` + +## What This Demonstrates + +- Configuring `mode: 'public'` for unauthenticated access +- Setting `sessionTtl` to control anonymous session lifetime +- Granting `anonymousScopes` so tools can check scope-based permissions even without auth + +## Related + +- See `configure-auth` for all four auth modes +- See `configure-auth-modes` for a detailed comparison of modes diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md b/libs/skills/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md new file mode 100644 index 000000000..e8598b19f --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md @@ -0,0 +1,76 @@ +--- +name: remote-oauth-with-vault +reference: configure-auth +level: intermediate +description: 'Configure a FrontMCP server with remote OAuth 2.1 authentication and use the credential vault to call downstream APIs on behalf of the authenticated user.' +tags: [config, oauth, auth, remote, vault] +features: + - "Configuring `mode: 'remote'` for full OAuth 2.1 authorization flow" + - 'Loading `clientId` from environment variables instead of hardcoding' + - "Using `this.authProviders.headers('github')` to get pre-formatted auth headers for downstream API calls" +--- + +# Remote OAuth Mode with Credential Vault + +Configure a FrontMCP server with remote OAuth 2.1 authentication and use the credential vault to call downstream APIs on behalf of the authenticated user. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'create_github_issue', + description: 'Create a GitHub issue on behalf of the user', + inputSchema: { + repo: z.string(), + title: z.string(), + body: z.string(), + }, + outputSchema: { issueUrl: z.string() }, +}) +class CreateGithubIssueTool extends ToolContext { + async execute(input: { repo: string; title: string; body: string }) { + // Access downstream credentials via the authProviders context extension + const headers = await this.authProviders.headers('github'); + + const response = await fetch(`https://api.github.com/repos/${input.repo}/issues`, { + method: 'POST', + headers: { ...headers, 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: input.title, body: input.body }), + }); + const issue = await response.json(); + return { issueUrl: issue.html_url }; + } +} + +@App({ + name: 'dev-tools', + auth: { + mode: 'remote', + provider: 'https://auth.example.com', + clientId: process.env['OAUTH_CLIENT_ID'] ?? 'mcp-client-id', + }, + tools: [CreateGithubIssueTool], +}) +class DevToolsApp {} + +@FrontMcp({ + info: { name: 'dev-tools-server', version: '1.0.0' }, + apps: [DevToolsApp], +}) +class Server {} +``` + +## What This Demonstrates + +- Configuring `mode: 'remote'` for full OAuth 2.1 authorization flow +- Loading `clientId` from environment variables instead of hardcoding +- Using `this.authProviders.headers('github')` to get pre-formatted auth headers for downstream API calls + +## Related + +- See `configure-auth` for credential vault API (`get`, `headers`, `has`, `refresh`) +- See `configure-session` for setting up Redis-based session storage in production diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/basic-confirmation-gate.md b/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/basic-confirmation-gate.md new file mode 100644 index 000000000..57939f170 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/basic-confirmation-gate.md @@ -0,0 +1,83 @@ +--- +name: basic-confirmation-gate +reference: configure-elicitation +level: basic +description: 'Request user confirmation before executing a destructive action.' +tags: [config, elicitation, confirmation, gate] +features: + - 'Enabling elicitation with `elicitation: { enabled: true }` in the `@FrontMcp` decorator' + - 'Using `this.elicit()` to pause tool execution and request user confirmation' + - 'Handling the case where the client does not support elicitation (`!confirmation`)' + - 'Using a boolean `requestedSchema` for simple yes/no confirmations' +--- + +# Basic Confirmation Gate + +Request user confirmation before executing a destructive action. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'delete_records', + description: 'Delete records from the database', + inputSchema: { + table: z.string(), + filter: z.string(), + }, + outputSchema: { deleted: z.number() }, +}) +class DeleteRecordsTool extends ToolContext { + async execute(input: { table: string; filter: string }) { + const count = 42; // simulate counting matching records + + const confirmation = await this.elicit({ + message: `This will delete ${count} records from ${input.table}. Are you sure?`, + requestedSchema: { + type: 'object', + properties: { + confirmed: { type: 'boolean', description: 'Confirm deletion' }, + }, + required: ['confirmed'], + }, + }); + + if (!confirmation || !confirmation.confirmed) { + return { deleted: 0 }; + } + + return { deleted: count }; + } +} + +@App({ + name: 'db-tools', + tools: [DeleteRecordsTool], +}) +class DbApp {} + +@FrontMcp({ + info: { name: 'elicit-server', version: '1.0.0' }, + apps: [DbApp], + elicitation: { + enabled: true, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Enabling elicitation with `elicitation: { enabled: true }` in the `@FrontMcp` decorator +- Using `this.elicit()` to pause tool execution and request user confirmation +- Handling the case where the client does not support elicitation (`!confirmation`) +- Using a boolean `requestedSchema` for simple yes/no confirmations + +## Related + +- See `configure-elicitation` for the full elicitation configuration reference +- See `setup-redis` for distributed elicitation state diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/distributed-elicitation-redis.md b/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/distributed-elicitation-redis.md new file mode 100644 index 000000000..17b30edd5 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-elicitation/distributed-elicitation-redis.md @@ -0,0 +1,87 @@ +--- +name: distributed-elicitation-redis +reference: configure-elicitation +level: intermediate +description: 'Configure elicitation with Redis storage for multi-instance production deployments.' +tags: [config, redis, elicitation, distributed] +features: + - 'Configuring Redis-backed elicitation state for multi-instance deployments' + - 'Using a `requestedSchema` with both required and optional fields' + - 'Elicitation state is shared across server instances so the response can arrive at any replica' + - 'Loading Redis connection details from environment variables' +--- + +# Distributed Elicitation with Redis + +Configure elicitation with Redis storage for multi-instance production deployments. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'deploy_service', + description: 'Deploy a service to production', + inputSchema: { + service: z.string(), + version: z.string(), + }, + outputSchema: { deploymentId: z.string(), status: z.string() }, +}) +class DeployServiceTool extends ToolContext { + async execute(input: { service: string; version: string }) { + const confirmation = await this.elicit({ + message: `Deploy ${input.service}@${input.version} to production?`, + requestedSchema: { + type: 'object', + properties: { + confirmed: { type: 'boolean', description: 'Confirm deployment' }, + reason: { type: 'string', description: 'Deployment reason (optional)' }, + }, + required: ['confirmed'], + }, + }); + + if (!confirmation || !confirmation.confirmed) { + return { deploymentId: '', status: 'cancelled' }; + } + + return { deploymentId: 'deploy-abc123', status: 'started' }; + } +} + +@App({ + name: 'deploy-tools', + tools: [DeployServiceTool], +}) +class DeployApp {} + +@FrontMcp({ + info: { name: 'deploy-server', version: '1.0.0' }, + apps: [DeployApp], + elicitation: { + enabled: true, + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'localhost', + port: Number(process.env['REDIS_PORT'] ?? 6379), + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Configuring Redis-backed elicitation state for multi-instance deployments +- Using a `requestedSchema` with both required and optional fields +- Elicitation state is shared across server instances so the response can arrive at any replica +- Loading Redis connection details from environment variables + +## Related + +- See `configure-elicitation` for the full elicitation configuration reference +- See `configure-session` for session storage with Redis diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md b/libs/skills/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md new file mode 100644 index 000000000..859c47200 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md @@ -0,0 +1,52 @@ +--- +name: cors-restricted-origins +reference: configure-http +level: basic +description: 'Configure CORS to allow only specific frontend origins with credentials.' +tags: [config, browser, http, cors, restricted, origins] +features: + - 'Restricting CORS to explicit origins instead of the permissive default' + - 'Enabling `credentials: true` with specific origins (required -- browsers reject `*` with credentials)' + - 'Setting `maxAge` to reduce preflight request overhead' + - 'Reading port from an environment variable with a fallback' +--- + +# CORS with Restricted Origins + +Configure CORS to allow only specific frontend origins with credentials. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'my-app' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'cors-server', version: '1.0.0' }, + apps: [MyApp], + http: { + port: Number(process.env['PORT']) || 3001, + cors: { + origin: ['https://myapp.com', 'https://staging.myapp.com'], + credentials: true, + maxAge: 86400, // cache preflight for 24 hours + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Restricting CORS to explicit origins instead of the permissive default +- Enabling `credentials: true` with specific origins (required -- browsers reject `*` with credentials) +- Setting `maxAge` to reduce preflight request overhead +- Reading port from an environment variable with a fallback + +## Related + +- See `configure-http` for the full HTTP configuration reference +- See `configure-throttle` for rate limiting and IP filtering diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md b/libs/skills/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md new file mode 100644 index 000000000..896f66cc4 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md @@ -0,0 +1,72 @@ +--- +name: entry-path-reverse-proxy +reference: configure-http +level: intermediate +description: 'Mount the MCP server under a URL prefix for reverse proxy or multi-service setups.' +tags: [config, nx, http, entry, path, reverse] +features: + - 'Using `entryPath` to mount the server under a URL prefix (no trailing slash)' + - 'All MCP endpoints are prefixed: `/api/mcp/sse`, `/api/mcp/`, etc.' + - 'Using a dynamic CORS origin function to allow wildcard subdomains' + - 'Suitable for running behind nginx, Caddy, or other reverse proxies' +--- + +# Entry Path Prefix Behind a Reverse Proxy + +Mount the MCP server under a URL prefix for reverse proxy or multi-service setups. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'health_check', + description: 'Check service health', + inputSchema: {}, + outputSchema: { status: z.string() }, +}) +class HealthCheckTool extends ToolContext { + async execute() { + return { status: 'ok' }; + } +} + +@App({ + name: 'api', + tools: [HealthCheckTool], +}) +class ApiApp {} + +@FrontMcp({ + info: { name: 'proxy-server', version: '1.0.0' }, + apps: [ApiApp], + http: { + port: 3001, + entryPath: '/api/mcp', // no trailing slash + cors: { + origin: (origin: string) => { + // allow any *.myapp.com subdomain + return origin.endsWith('.myapp.com'); + }, + credentials: true, + }, + }, +}) +class Server {} +// Endpoints become: /api/mcp/sse, /api/mcp/, etc. +``` + +## What This Demonstrates + +- Using `entryPath` to mount the server under a URL prefix (no trailing slash) +- All MCP endpoints are prefixed: `/api/mcp/sse`, `/api/mcp/`, etc. +- Using a dynamic CORS origin function to allow wildcard subdomains +- Suitable for running behind nginx, Caddy, or other reverse proxies + +## Related + +- See `configure-http` for the full HTTP configuration reference +- See `configure-transport` for protocol options behind a proxy diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-http/unix-socket-local.md b/libs/skills/catalog/frontmcp-config/examples/configure-http/unix-socket-local.md new file mode 100644 index 000000000..3f2d4b2c1 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-http/unix-socket-local.md @@ -0,0 +1,64 @@ +--- +name: unix-socket-local +reference: configure-http +level: intermediate +description: 'Bind the server to a unix socket instead of a TCP port for local-only communication.' +tags: [config, unix-socket, cli, local, http, unix] +features: + - 'Using `socketPath` to bind to a unix socket instead of a TCP port' + - 'When `socketPath` is set, the `port` field is ignored' + - 'Disabling CORS with `cors: false` since unix sockets are local-only' + - 'Suitable for CLI tools, daemons, and process manager integrations' +--- + +# Unix Socket for Local Access + +Bind the server to a unix socket instead of a TCP port for local-only communication. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'system_status', + description: 'Get system status', + inputSchema: {}, + outputSchema: { uptime: z.number(), healthy: z.boolean() }, +}) +class SystemStatusTool extends ToolContext { + async execute() { + return { uptime: process.uptime(), healthy: true }; + } +} + +@App({ + name: 'daemon', + tools: [SystemStatusTool], +}) +class DaemonApp {} + +@FrontMcp({ + info: { name: 'daemon-server', version: '1.0.0' }, + apps: [DaemonApp], + http: { + socketPath: '/tmp/my-mcp-server.sock', + cors: false, // no CORS needed for local socket + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `socketPath` to bind to a unix socket instead of a TCP port +- When `socketPath` is set, the `port` field is ignored +- Disabling CORS with `cors: false` since unix sockets are local-only +- Suitable for CLI tools, daemons, and process manager integrations + +## Related + +- See `configure-http` for the full HTTP configuration reference +- See `configure-transport` for transport protocol options diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-session/multi-server-key-prefix.md b/libs/skills/catalog/frontmcp-config/examples/configure-session/multi-server-key-prefix.md new file mode 100644 index 000000000..293d59611 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-session/multi-server-key-prefix.md @@ -0,0 +1,68 @@ +--- +name: multi-server-key-prefix +reference: configure-session +level: intermediate +description: 'Use unique key prefixes when multiple FrontMCP servers share one Redis instance.' +tags: [config, redis, session, multi, key, prefix] +features: + - 'Using unique `keyPrefix` values per server to avoid session key collisions' + - 'Both servers share the same Redis instance but have isolated session namespaces' + - 'Tuning `defaultTtlMs` per server based on workload pattern' + - '`billing-mcp:session:` vs `analytics-mcp:session:` prevents cross-contamination' +--- + +# Multi-Server Key Prefix Isolation + +Use unique key prefixes when multiple FrontMCP servers share one Redis instance. + +## Code + +```typescript +// src/billing-server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'billing-app' }) +class BillingApp {} + +@FrontMcp({ + info: { name: 'billing-server', version: '1.0.0' }, + apps: [BillingApp], + redis: { + provider: 'redis', + host: 'shared-redis.internal', + port: 6379, + keyPrefix: 'billing-mcp:session:', + defaultTtlMs: 86_400_000, // 24 hours for long-running agent workflows + }, +}) +class BillingServer {} + +// src/analytics-server.ts +@App({ name: 'analytics-app' }) +class AnalyticsApp {} + +@FrontMcp({ + info: { name: 'analytics-server', version: '1.0.0' }, + apps: [AnalyticsApp], + redis: { + provider: 'redis', + host: 'shared-redis.internal', + port: 6379, + keyPrefix: 'analytics-mcp:session:', + defaultTtlMs: 600_000, // 10 minutes for short CI/CD operations + }, +}) +class AnalyticsServer {} +``` + +## What This Demonstrates + +- Using unique `keyPrefix` values per server to avoid session key collisions +- Both servers share the same Redis instance but have isolated session namespaces +- Tuning `defaultTtlMs` per server based on workload pattern +- `billing-mcp:session:` vs `analytics-mcp:session:` prevents cross-contamination + +## Related + +- See `configure-session` for the full session configuration reference +- See `setup-redis` for Redis provisioning details diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-session/redis-session-store.md b/libs/skills/catalog/frontmcp-config/examples/configure-session/redis-session-store.md new file mode 100644 index 000000000..bb4a00e21 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-session/redis-session-store.md @@ -0,0 +1,52 @@ +--- +name: redis-session-store +reference: configure-session +level: basic +description: 'Configure Redis-backed session storage for production deployments.' +tags: [config, redis, session, store] +features: + - 'Configuring Redis as the session storage provider for production persistence' + - 'Using `keyPrefix` to namespace session keys and prevent collisions with other servers' + - 'Setting `defaultTtlMs` to control session lifetime (1 hour for interactive use)' + - 'Loading Redis connection details from environment variables' +--- + +# Redis Session Store + +Configure Redis-backed session storage for production deployments. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'my-app' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'prod-server', version: '1.0.0' }, + apps: [MyApp], + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'localhost', + port: Number(process.env['REDIS_PORT'] ?? 6379), + password: process.env['REDIS_PASSWORD'], + keyPrefix: 'myapp-mcp:session:', + defaultTtlMs: 3_600_000, // 1 hour for interactive sessions + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Configuring Redis as the session storage provider for production persistence +- Using `keyPrefix` to namespace session keys and prevent collisions with other servers +- Setting `defaultTtlMs` to control session lifetime (1 hour for interactive use) +- Loading Redis connection details from environment variables + +## Related + +- See `configure-session` for all session storage options +- See `setup-redis` for Redis provisioning details diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md b/libs/skills/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md new file mode 100644 index 000000000..df099a49a --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md @@ -0,0 +1,52 @@ +--- +name: vercel-kv-session +reference: configure-session +level: intermediate +description: 'Configure Vercel KV for session storage in serverless Vercel deployments.' +tags: [config, vercel-kv, vercel, session, transport, serverless] +features: + - "Using `provider: 'vercel-kv'` for Vercel platform deployments" + - 'Vercel automatically injects `KV_REST_API_URL` and `KV_REST_API_TOKEN` environment variables' + - 'Combining with `stateless-api` transport preset for serverless execution' + - 'No explicit host/port needed -- Vercel KV uses REST API under the hood' +--- + +# Vercel KV Session Store + +Configure Vercel KV for session storage in serverless Vercel deployments. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'my-app' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'vercel-server', version: '1.0.0' }, + apps: [MyApp], + redis: { + provider: 'vercel-kv', + // KV_REST_API_URL and KV_REST_API_TOKEN are auto-injected by Vercel + }, + transport: { + protocol: 'stateless-api', + sessionMode: 'stateless', + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `provider: 'vercel-kv'` for Vercel platform deployments +- Vercel automatically injects `KV_REST_API_URL` and `KV_REST_API_TOKEN` environment variables +- Combining with `stateless-api` transport preset for serverless execution +- No explicit host/port needed -- Vercel KV uses REST API under the hood + +## Related + +- See `configure-session` for all session storage options +- See `configure-transport` for transport protocol configuration diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/full-guard-config.md b/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/full-guard-config.md new file mode 100644 index 000000000..6232cd13c --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/full-guard-config.md @@ -0,0 +1,99 @@ +--- +name: full-guard-config +reference: configure-throttle-guard-config +level: advanced +description: 'Complete GuardConfig using every available field for maximum protection.' +tags: [config, redis, session, throttle, guard, full] +features: + - 'Every field in the `GuardConfig` interface used together' + - 'Priority order: IP filter -> global rate limit -> global concurrency -> per-tool limits' + - 'Redis `storage` for shared counters across instances' + - '`keyPrefix` to namespace guard keys in shared Redis' + - "Mixed `partitionBy` strategies: `'ip'` for global, `'session'` for per-tool" + - '`queueTimeoutMs` to briefly queue excess requests instead of rejecting' +--- + +# Full GuardConfig with All Options + +Complete GuardConfig using every available field for maximum protection. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'secure-app' }) +class SecureApp {} + +@FrontMcp({ + info: { name: 'fully-guarded-server', version: '1.0.0' }, + apps: [SecureApp], + throttle: { + enabled: true, + + // Distributed storage for multi-instance deployments + storage: { + type: 'redis', + redis: { + config: { + host: process.env['REDIS_HOST'] ?? 'redis.internal', + port: 6379, + }, + }, + }, + keyPrefix: 'myapp:guard:', + + // Server-wide limits + global: { + maxRequests: 1000, + windowMs: 60000, + partitionBy: 'ip', // per-client IP rate limit + }, + globalConcurrency: { + maxConcurrent: 50, + queueTimeoutMs: 2000, + partitionBy: 'global', + }, + + // Default per-tool limits + defaultRateLimit: { + maxRequests: 100, + windowMs: 60000, + partitionBy: 'session', + }, + defaultConcurrency: { + maxConcurrent: 10, + queueTimeoutMs: 5000, + partitionBy: 'session', + }, + defaultTimeout: { + executeMs: 30000, + }, + + // IP-based access control + ipFilter: { + allowList: ['10.0.0.0/8', '172.16.0.0/12'], + denyList: ['192.168.1.100'], + defaultAction: 'deny', + trustProxy: true, + trustedProxyDepth: 2, + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Every field in the `GuardConfig` interface used together +- Priority order: IP filter -> global rate limit -> global concurrency -> per-tool limits +- Redis `storage` for shared counters across instances +- `keyPrefix` to namespace guard keys in shared Redis +- Mixed `partitionBy` strategies: `'ip'` for global, `'session'` for per-tool +- `queueTimeoutMs` to briefly queue excess requests instead of rejecting + +## Related + +- See `configure-throttle-guard-config` for the complete interface reference +- See `configure-throttle` for practical throttle configuration patterns diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/minimal-guard-config.md b/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/minimal-guard-config.md new file mode 100644 index 000000000..350dad517 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-throttle-guard-config/minimal-guard-config.md @@ -0,0 +1,55 @@ +--- +name: minimal-guard-config +reference: configure-throttle-guard-config +level: basic +description: 'Enable throttle with just a global rate limit and default timeout.' +tags: [config, throttle, guard, minimal] +features: + - 'The minimum fields needed to enable the guard: `enabled`, `global`, and `defaultTimeout`' + - "`partitionBy: 'global'` shares one counter across all clients" + - '`windowMs` defaults to 60000 (1 minute) if omitted' + - 'Other fields (`globalConcurrency`, `ipFilter`, `storage`) are optional' +--- + +# Minimal GuardConfig + +Enable throttle with just a global rate limit and default timeout. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'my-app' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'guarded-server', version: '1.0.0' }, + apps: [MyApp], + throttle: { + enabled: true, + global: { + maxRequests: 1000, + windowMs: 60000, + partitionBy: 'global', + }, + defaultTimeout: { + executeMs: 30000, + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- The minimum fields needed to enable the guard: `enabled`, `global`, and `defaultTimeout` +- `partitionBy: 'global'` shares one counter across all clients +- `windowMs` defaults to 60000 (1 minute) if omitted +- Other fields (`globalConcurrency`, `ipFilter`, `storage`) are optional + +## Related + +- See `configure-throttle-guard-config` for the complete GuardConfig interface +- See `configure-throttle` for practical throttle configuration patterns diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md new file mode 100644 index 000000000..a60962a19 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md @@ -0,0 +1,94 @@ +--- +name: distributed-redis-throttle +reference: configure-throttle +level: advanced +description: 'Configure Redis-backed rate limiting for multi-instance deployments behind a load balancer.' +tags: [config, redis, session, throttle, distributed] +features: + - "Configuring `storage: { type: 'redis' }` so rate limit counters are shared across instances" + - 'Using `keyPrefix` to namespace guard keys in a shared Redis instance' + - "Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool" + - 'In-memory counters are per-process and would allow N times the intended rate with N instances' +--- + +# Distributed Rate Limiting with Redis + +Configure Redis-backed rate limiting for multi-instance deployments behind a load balancer. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'process_payment', + description: 'Process a payment transaction', + inputSchema: { + amount: z.number(), + currency: z.string(), + }, + outputSchema: { transactionId: z.string(), status: z.string() }, + rateLimit: { + maxRequests: 5, + windowMs: 60000, + partitionBy: 'session', + }, + concurrency: { + maxConcurrent: 1, + partitionBy: 'session', + }, +}) +class ProcessPaymentTool extends ToolContext { + async execute(input: { amount: number; currency: string }) { + return { transactionId: 'txn-abc123', status: 'completed' }; + } +} + +@App({ + name: 'payments', + tools: [ProcessPaymentTool], +}) +class PaymentsApp {} + +@FrontMcp({ + info: { name: 'payment-server', version: '1.0.0' }, + apps: [PaymentsApp], + throttle: { + enabled: true, + storage: { + type: 'redis', + redis: { + config: { + host: process.env['REDIS_HOST'] ?? 'redis.internal', + port: Number(process.env['REDIS_PORT'] ?? 6379), + }, + }, + }, + keyPrefix: 'payments:guard:', + global: { + maxRequests: 500, + windowMs: 60000, + partitionBy: 'ip', + }, + globalConcurrency: { + maxConcurrent: 20, + partitionBy: 'global', + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Configuring `storage: { type: 'redis' }` so rate limit counters are shared across instances +- Using `keyPrefix` to namespace guard keys in a shared Redis instance +- Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool +- In-memory counters are per-process and would allow N times the intended rate with N instances + +## Related + +- See `configure-throttle` for the full throttle configuration reference +- See `setup-redis` for Redis provisioning details diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-throttle/per-tool-rate-limit.md b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/per-tool-rate-limit.md new file mode 100644 index 000000000..257c56ec6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/per-tool-rate-limit.md @@ -0,0 +1,92 @@ +--- +name: per-tool-rate-limit +reference: configure-throttle +level: intermediate +description: 'Override server defaults with per-tool rate limits and concurrency caps.' +tags: [config, session, throttle, per, tool, rate] +features: + - 'Setting per-tool `rateLimit`, `concurrency`, and `timeout` on the `@Tool` decorator' + - "Using `partitionBy: 'session'` for per-user fairness on expensive tools" + - 'Setting `queueTimeoutMs` to briefly queue excess requests instead of rejecting immediately' + - 'Tools without overrides (`QuickLookupTool`) inherit server defaults' +--- + +# Per-Tool Rate Limiting + +Override server defaults with per-tool rate limits and concurrency caps. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'expensive_query', + description: 'Run an expensive database query', + inputSchema: { query: z.string() }, + outputSchema: { rows: z.array(z.record(z.unknown())), rowCount: z.number() }, + rateLimit: { + maxRequests: 10, + windowMs: 60000, + partitionBy: 'session', // per-session rate limit + }, + concurrency: { + maxConcurrent: 3, + queueTimeoutMs: 5000, // wait up to 5s for a slot + partitionBy: 'session', + }, + timeout: { + executeMs: 60000, // 60 second timeout for this tool + }, +}) +class ExpensiveQueryTool extends ToolContext { + async execute(input: { query: string }) { + return { rows: [{ id: 1 }], rowCount: 1 }; + } +} + +@Tool({ + name: 'quick_lookup', + description: 'Fast key-value lookup', + inputSchema: { key: z.string() }, + outputSchema: { value: z.string().nullable() }, + // No overrides -- uses server defaults +}) +class QuickLookupTool extends ToolContext { + async execute(input: { key: string }) { + return { value: 'cached-value' }; + } +} + +@App({ + name: 'data-api', + tools: [ExpensiveQueryTool, QuickLookupTool], +}) +class DataApp {} + +@FrontMcp({ + info: { name: 'data-server', version: '1.0.0' }, + apps: [DataApp], + throttle: { + enabled: true, + defaultRateLimit: { maxRequests: 100, windowMs: 60000 }, + defaultConcurrency: { maxConcurrent: 10 }, + defaultTimeout: { executeMs: 30000 }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Setting per-tool `rateLimit`, `concurrency`, and `timeout` on the `@Tool` decorator +- Using `partitionBy: 'session'` for per-user fairness on expensive tools +- Setting `queueTimeoutMs` to briefly queue excess requests instead of rejecting immediately +- Tools without overrides (`QuickLookupTool`) inherit server defaults + +## Related + +- See `configure-throttle` for the full throttle configuration reference +- See `configure-throttle-guard-config` for the complete GuardConfig interface diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-throttle/server-level-rate-limit.md b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/server-level-rate-limit.md new file mode 100644 index 000000000..93b73ceb4 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-throttle/server-level-rate-limit.md @@ -0,0 +1,83 @@ +--- +name: server-level-rate-limit +reference: configure-throttle +level: basic +description: 'Configure global rate limits and IP filtering at the server level.' +tags: [config, throttle, level, rate, limit] +features: + - 'Enabling throttle with `throttle: { enabled: true }`' + - 'Setting `global` rate limit shared across all clients' + - 'Configuring `globalConcurrency` to cap simultaneous executions' + - 'Setting `defaultTimeout` to prevent runaway tool executions' + - 'Using `ipFilter` with deny-by-default posture and an explicit allow list' +--- + +# Server-Level Rate Limiting + +Configure global rate limits and IP filtering at the server level. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'search', + description: 'Search records', + inputSchema: { query: z.string() }, + outputSchema: { results: z.array(z.string()) }, +}) +class SearchTool extends ToolContext { + async execute(input: { query: string }) { + return { results: [`Result for: ${input.query}`] }; + } +} + +@App({ + name: 'api', + tools: [SearchTool], +}) +class ApiApp {} + +@FrontMcp({ + info: { name: 'throttled-server', version: '1.0.0' }, + apps: [ApiApp], + throttle: { + enabled: true, + global: { + maxRequests: 1000, + windowMs: 60000, // 1 minute window + partitionBy: 'global', + }, + globalConcurrency: { + maxConcurrent: 50, + partitionBy: 'global', + }, + defaultTimeout: { + executeMs: 30000, // 30 second timeout + }, + ipFilter: { + allowList: ['10.0.0.0/8'], + defaultAction: 'deny', + trustProxy: true, + trustedProxyDepth: 1, + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Enabling throttle with `throttle: { enabled: true }` +- Setting `global` rate limit shared across all clients +- Configuring `globalConcurrency` to cap simultaneous executions +- Setting `defaultTimeout` to prevent runaway tool executions +- Using `ipFilter` with deny-by-default posture and an explicit allow list + +## Related + +- See `configure-throttle` for the full throttle configuration reference +- See `configure-throttle-guard-config` for the complete GuardConfig interface diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/legacy-preset-nodejs.md b/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/legacy-preset-nodejs.md new file mode 100644 index 000000000..fdd5c1a67 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/legacy-preset-nodejs.md @@ -0,0 +1,65 @@ +--- +name: legacy-preset-nodejs +reference: configure-transport-protocol-presets +level: basic +description: 'Use the default legacy preset for maximum compatibility with all MCP clients.' +tags: [config, anthropic, session, transport, node, protocol] +features: + - "The `'legacy'` preset is the default and can be omitted" + - 'Enables SSE, Streamable HTTP, and Legacy SSE for maximum client compatibility' + - '`strictSession: true` requires `mcp-session-id` header for streamable HTTP' + - 'Best for single-instance Node.js deployments (Claude Desktop, etc.)' +--- + +# Legacy Preset for Node.js + +Use the default legacy preset for maximum compatibility with all MCP clients. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'hello', + description: 'Say hello', + inputSchema: { name: z.string() }, + outputSchema: { greeting: z.string() }, +}) +class HelloTool extends ToolContext { + async execute(input: { name: string }) { + return { greeting: `Hello, ${input.name}!` }; + } +} + +@App({ + name: 'my-app', + tools: [HelloTool], +}) +class MyApp {} + +@FrontMcp({ + info: { name: 'legacy-server', version: '1.0.0' }, + apps: [MyApp], + transport: { + protocol: 'legacy', // default -- can be omitted + }, +}) +class Server {} +// Enables: SSE + Streamable HTTP + Legacy SSE +// Flags: { sse: true, streamable: true, json: false, stateless: false, legacy: true, strictSession: true } +``` + +## What This Demonstrates + +- The `'legacy'` preset is the default and can be omitted +- Enables SSE, Streamable HTTP, and Legacy SSE for maximum client compatibility +- `strictSession: true` requires `mcp-session-id` header for streamable HTTP +- Best for single-instance Node.js deployments (Claude Desktop, etc.) + +## Related + +- See `configure-transport-protocol-presets` for all preset definitions +- See `configure-transport` for full transport configuration diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md b/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md new file mode 100644 index 000000000..f8197e448 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md @@ -0,0 +1,69 @@ +--- +name: stateless-api-serverless +reference: configure-transport-protocol-presets +level: intermediate +description: 'Use the stateless-api preset for Vercel, Lambda, or Cloudflare Workers.' +tags: [config, vercel, lambda, cloudflare, session, transport] +features: + - "The `'stateless-api'` preset disables SSE, streaming, and sessions entirely" + - 'Each request is standalone with no server-side state' + - "Pair with `sessionMode: 'stateless'` for serverless execution" + - 'Required for Vercel, Lambda, Cloudflare Workers where persistent connections are not allowed' +--- + +# Stateless API Preset for Serverless + +Use the stateless-api preset for Vercel, Lambda, or Cloudflare Workers. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'translate', + description: 'Translate text between languages', + inputSchema: { + text: z.string(), + targetLang: z.string(), + }, + outputSchema: { translated: z.string() }, +}) +class TranslateTool extends ToolContext { + async execute(input: { text: string; targetLang: string }) { + return { translated: `[${input.targetLang}] ${input.text}` }; + } +} + +@App({ + name: 'translate-api', + tools: [TranslateTool], +}) +class TranslateApp {} + +@FrontMcp({ + info: { name: 'serverless-translate', version: '1.0.0' }, + apps: [TranslateApp], + transport: { + sessionMode: 'stateless', + protocol: 'stateless-api', + }, +}) +class Server {} +// Enables: Stateless HTTP only +// Flags: { sse: false, streamable: false, json: false, stateless: true, legacy: false, strictSession: false } +``` + +## What This Demonstrates + +- The `'stateless-api'` preset disables SSE, streaming, and sessions entirely +- Each request is standalone with no server-side state +- Pair with `sessionMode: 'stateless'` for serverless execution +- Required for Vercel, Lambda, Cloudflare Workers where persistent connections are not allowed + +## Related + +- See `configure-transport-protocol-presets` for all preset definitions +- See `configure-transport` for full transport configuration diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md b/libs/skills/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md new file mode 100644 index 000000000..15f8dab83 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md @@ -0,0 +1,74 @@ +--- +name: custom-protocol-flags +reference: configure-transport +level: advanced +description: 'Override individual protocol flags instead of using a preset for fine-grained control.' +tags: [config, redis, session, transport, custom, protocol] +features: + - 'Passing an object to `protocol` instead of a preset string for fine-grained control' + - 'Enabling SSE, streamable HTTP, and JSON-only modes simultaneously' + - 'Setting `strictSession: true` to require `mcp-session-id` header on streamable HTTP' + - "Using `distributedMode: 'auto'` to auto-detect based on whether Redis is configured" + - 'Disabling `legacy` SSE while keeping modern SSE support' +--- + +# Custom Protocol Flags + +Override individual protocol flags instead of using a preset for fine-grained control. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'stream_logs', + description: 'Stream application logs', + inputSchema: { service: z.string(), lines: z.number().optional() }, + outputSchema: { logs: z.array(z.string()) }, +}) +class StreamLogsTool extends ToolContext { + async execute(input: { service: string; lines?: number }) { + return { logs: ['[INFO] Service started', '[INFO] Healthy'] }; + } +} + +@App({ + name: 'devtools', + tools: [StreamLogsTool], +}) +class DevtoolsApp {} + +@FrontMcp({ + info: { name: 'custom-protocol-server', version: '1.0.0' }, + apps: [DevtoolsApp], + transport: { + sessionMode: 'stateful', + protocol: { + sse: true, // SSE endpoint enabled + streamable: true, // Streamable HTTP POST enabled + json: true, // JSON-only responses also available + stateless: false, // Sessions required + legacy: false, // No legacy SSE + strictSession: true, // Require mcp-session-id header + }, + distributedMode: 'auto', // auto-detect based on Redis config + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Passing an object to `protocol` instead of a preset string for fine-grained control +- Enabling SSE, streamable HTTP, and JSON-only modes simultaneously +- Setting `strictSession: true` to require `mcp-session-id` header on streamable HTTP +- Using `distributedMode: 'auto'` to auto-detect based on whether Redis is configured +- Disabling `legacy` SSE while keeping modern SSE support + +## Related + +- See `configure-transport` for the full transport configuration reference +- See `configure-transport-protocol-presets` for the built-in preset definitions diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md b/libs/skills/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md new file mode 100644 index 000000000..00c8368ba --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md @@ -0,0 +1,86 @@ +--- +name: distributed-sessions-redis +reference: configure-transport +level: intermediate +description: 'Configure transport with Redis persistence for multi-instance load-balanced deployments.' +tags: [config, redis, session, transport, distributed, sessions] +features: + - 'Using `distributedMode: true` for load-balanced multi-instance deployments' + - 'Redis `persistence` so sessions survive restarts and are shared across instances' + - 'Setting `defaultTtlMs` to prevent sessions from accumulating indefinitely' + - 'Redis-backed `eventStore` for SSE resumability across instances' + - "Using the `'modern'` preset (drops legacy SSE but keeps streamable HTTP)" +--- + +# Distributed Sessions with Redis + +Configure transport with Redis persistence for multi-instance load-balanced deployments. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get_report', + description: 'Generate a report', + inputSchema: { reportId: z.string() }, + outputSchema: { data: z.string(), generatedAt: z.string() }, +}) +class GetReportTool extends ToolContext { + async execute(input: { reportId: string }) { + return { data: 'report-data', generatedAt: new Date().toISOString() }; + } +} + +@App({ + name: 'reports', + tools: [GetReportTool], +}) +class ReportsApp {} + +@FrontMcp({ + info: { name: 'distributed-server', version: '1.0.0' }, + apps: [ReportsApp], + transport: { + sessionMode: 'stateful', + protocol: 'modern', + distributedMode: true, + persistence: { + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'redis.internal', + port: 6379, + }, + defaultTtlMs: 3_600_000, // 1 hour session TTL + }, + eventStore: { + enabled: true, + provider: 'redis', + maxEvents: 10000, + ttlMs: 300_000, // 5 minute event TTL + redis: { + provider: 'redis', + host: process.env['REDIS_HOST'] ?? 'redis.internal', + }, + }, + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `distributedMode: true` for load-balanced multi-instance deployments +- Redis `persistence` so sessions survive restarts and are shared across instances +- Setting `defaultTtlMs` to prevent sessions from accumulating indefinitely +- Redis-backed `eventStore` for SSE resumability across instances +- Using the `'modern'` preset (drops legacy SSE but keeps streamable HTTP) + +## Related + +- See `configure-transport` for the full transport configuration reference +- See `configure-session` for session storage options +- See `setup-redis` for Redis provisioning diff --git a/libs/skills/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md b/libs/skills/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md new file mode 100644 index 000000000..e852c0f75 --- /dev/null +++ b/libs/skills/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md @@ -0,0 +1,69 @@ +--- +name: stateless-serverless +reference: configure-transport +level: basic +description: 'Configure stateless transport for Vercel, Lambda, or Cloudflare deployments.' +tags: [config, vercel, lambda, cloudflare, session, transport] +features: + - "Using `sessionMode: 'stateless'` to disable session management" + - "Using the `'stateless-api'` preset: no SSE, no streaming, pure request/response" + - 'Each request is standalone with no server-side state between invocations' + - 'Required for serverless targets (Vercel, Lambda, Cloudflare Workers)' +--- + +# Stateless Transport for Serverless + +Configure stateless transport for Vercel, Lambda, or Cloudflare deployments. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'convert_currency', + description: 'Convert between currencies', + inputSchema: { + amount: z.number(), + from: z.string(), + to: z.string(), + }, + outputSchema: { result: z.number(), rate: z.number() }, +}) +class ConvertCurrencyTool extends ToolContext { + async execute(input: { amount: number; from: string; to: string }) { + const rate = 1.1; + return { result: input.amount * rate, rate }; + } +} + +@App({ + name: 'currency-api', + tools: [ConvertCurrencyTool], +}) +class CurrencyApp {} + +@FrontMcp({ + info: { name: 'serverless-server', version: '1.0.0' }, + apps: [CurrencyApp], + transport: { + sessionMode: 'stateless', + protocol: 'stateless-api', + }, +}) +class Server {} +``` + +## What This Demonstrates + +- Using `sessionMode: 'stateless'` to disable session management +- Using the `'stateless-api'` preset: no SSE, no streaming, pure request/response +- Each request is standalone with no server-side state between invocations +- Required for serverless targets (Vercel, Lambda, Cloudflare Workers) + +## Related + +- See `configure-transport` for the full transport configuration reference +- See `configure-transport-protocol-presets` for all preset options 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 5b63c92cb..db23be227 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-auth-modes.md @@ -80,3 +80,13 @@ auth: { | Credential vault | No | No | Yes | Yes | | Consent flow | No | No | Optional | Optional | | Federated auth | No | No | Optional | Optional | + +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- | +| [`local-self-signed-tokens`](../examples/configure-auth-modes/local-self-signed-tokens.md) | Intermediate | Configure a server that signs its own JWT tokens with consent and incremental auth enabled. | +| [`remote-enterprise-oauth`](../examples/configure-auth-modes/remote-enterprise-oauth.md) | Advanced | Delegate authentication to an external OAuth orchestrator with Redis-backed token storage. | +| [`transparent-jwt-validation`](../examples/configure-auth-modes/transparent-jwt-validation.md) | Basic | Validate externally-issued JWTs without managing token lifecycle on the server. | + +> See all examples in [`examples/configure-auth-modes/`](../examples/configure-auth-modes/) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-auth.md b/libs/skills/catalog/frontmcp-config/references/configure-auth.md index 15f309511..036125f5c 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-auth.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-auth.md @@ -237,6 +237,16 @@ The `authProviders` accessor (from `@frontmcp/auth`) provides: | `VAULT_SECRET is not defined` error | The vault encryption secret environment variable is missing | Set `VAULT_SECRET` in your environment or `.env` file before starting the server | | OAuth redirect fails in local dev | `remote` mode requires HTTPS and reachable callback URLs | Set `NODE_ENV=development` to relax HTTPS requirements, or use a local OAuth mock server | +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`multi-app-auth`](../examples/configure-auth/multi-app-auth.md) | Advanced | Configure a single FrontMCP server with multiple apps, each using a different auth mode -- public for open endpoints and remote for admin endpoints. | +| [`public-mode-setup`](../examples/configure-auth/public-mode-setup.md) | Basic | Set up a FrontMCP server with public (unauthenticated) access and anonymous scopes. | +| [`remote-oauth-with-vault`](../examples/configure-auth/remote-oauth-with-vault.md) | Intermediate | Configure a FrontMCP server with remote OAuth 2.1 authentication and use the credential vault to call downstream APIs on behalf of the authenticated user. | + +> See all examples in [`examples/configure-auth/`](../examples/configure-auth/) + ## Reference - Docs: [Authentication Overview](https://docs.agentfront.dev/frontmcp/authentication/overview) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md b/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md index c21327018..dcdba9d7a 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-elicitation.md @@ -177,6 +177,15 @@ frontmcp dev | User sees raw JSON instead of a form | The MCP client renders the `requestedSchema` as raw data rather than a form | Use standard JSON Schema types (`boolean`, `string`, `enum`) that clients can render as UI controls | | Tool hangs indefinitely waiting for user response | No timeout configured and user never responds | Implement a timeout or cancellation mechanism in the tool logic to handle non-responsive users | +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------- | +| [`basic-confirmation-gate`](../examples/configure-elicitation/basic-confirmation-gate.md) | Basic | Request user confirmation before executing a destructive action. | +| [`distributed-elicitation-redis`](../examples/configure-elicitation/distributed-elicitation-redis.md) | Intermediate | Configure elicitation with Redis storage for multi-instance production deployments. | + +> See all examples in [`examples/configure-elicitation/`](../examples/configure-elicitation/) + ## Reference - [Elicitation Docs](https://docs.agentfront.dev/frontmcp/servers/elicitation) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-http.md b/libs/skills/catalog/frontmcp-config/references/configure-http.md index 01dfb39da..1ad8950d4 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-http.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-http.md @@ -204,6 +204,16 @@ curl --unix-socket /tmp/my-mcp-server.sock http://localhost/ | Routes return 404 after setting `entryPath` | Client is still requesting the root path without the prefix | Update client base URL to include the entry path (e.g., `http://localhost:3001/api/mcp`) | | Server binds but external clients cannot connect | Server bound to `localhost` or `127.0.0.1` inside a container | Set `host: '0.0.0.0'` or use Docker port mapping to expose the container port | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------ | +| [`cors-restricted-origins`](../examples/configure-http/cors-restricted-origins.md) | Basic | Configure CORS to allow only specific frontend origins with credentials. | +| [`entry-path-reverse-proxy`](../examples/configure-http/entry-path-reverse-proxy.md) | Intermediate | Mount the MCP server under a URL prefix for reverse proxy or multi-service setups. | +| [`unix-socket-local`](../examples/configure-http/unix-socket-local.md) | Intermediate | Bind the server to a unix socket instead of a TCP port for local-only communication. | + +> See all examples in [`examples/configure-http/`](../examples/configure-http/) + ## Reference - [HTTP Server Docs](https://docs.agentfront.dev/frontmcp/deployment/local-dev-server) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-session.md b/libs/skills/catalog/frontmcp-config/references/configure-session.md index eb3a67c14..295977286 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-session.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-session.md @@ -204,6 +204,16 @@ const pubsubStore = createPubsubStore({ | Session key collisions between servers | Multiple servers share the same Redis instance and `keyPrefix` | Set a unique `keyPrefix` per server (e.g., `billing-mcp:session:`, `api-mcp:session:`) | | Pub/sub not working with Vercel KV | Vercel KV does not support pub/sub operations | Add a separate `pubsub` config pointing to a real Redis instance | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------- | +| [`multi-server-key-prefix`](../examples/configure-session/multi-server-key-prefix.md) | Intermediate | Use unique key prefixes when multiple FrontMCP servers share one Redis instance. | +| [`redis-session-store`](../examples/configure-session/redis-session-store.md) | Basic | Configure Redis-backed session storage for production deployments. | +| [`vercel-kv-session`](../examples/configure-session/vercel-kv-session.md) | Intermediate | Configure Vercel KV for session storage in serverless Vercel deployments. | + +> See all examples in [`examples/configure-session/`](../examples/configure-session/) + ## Reference - [Session Storage Docs](https://docs.agentfront.dev/frontmcp/deployment/redis-setup) 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 1b82e9541..653aa81bc 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 @@ -71,3 +71,12 @@ interface IpFilterConfig { 4. Per-tool rate limit — checked per tool 5. Per-tool concurrency — checked per tool 6. Per-tool timeout — enforced during execution + +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------ | +| [`full-guard-config`](../examples/configure-throttle-guard-config/full-guard-config.md) | Advanced | Complete GuardConfig using every available field for maximum protection. | +| [`minimal-guard-config`](../examples/configure-throttle-guard-config/minimal-guard-config.md) | Basic | Enable throttle with just a global rate limit and default timeout. | + +> See all examples in [`examples/configure-throttle-guard-config/`](../examples/configure-throttle-guard-config/) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-throttle.md b/libs/skills/catalog/frontmcp-config/references/configure-throttle.md index a5dcc7df4..f54be1774 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-throttle.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-throttle.md @@ -228,6 +228,16 @@ done | `X-Forwarded-For` header ignored | `ipFilter.trustProxy` not enabled or `trustedProxyDepth` too low | Set `trustProxy: true` and adjust `trustedProxyDepth` to match your proxy chain | | Rate limit resets not aligned with expectations | `windowMs` misunderstood as a sliding window when it is a fixed window | The window is fixed; all counters reset at the end of each `windowMs` interval | +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- | +| [`distributed-redis-throttle`](../examples/configure-throttle/distributed-redis-throttle.md) | Advanced | Configure Redis-backed rate limiting for multi-instance deployments behind a load balancer. | +| [`per-tool-rate-limit`](../examples/configure-throttle/per-tool-rate-limit.md) | Intermediate | Override server defaults with per-tool rate limits and concurrency caps. | +| [`server-level-rate-limit`](../examples/configure-throttle/server-level-rate-limit.md) | Basic | Configure global rate limits and IP filtering at the server level. | + +> See all examples in [`examples/configure-throttle/`](../examples/configure-throttle/) + ## Reference - [Guard Configuration Docs](https://docs.agentfront.dev/frontmcp/servers/guard) 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 684cf181b..fcc18b96f 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 @@ -60,3 +60,12 @@ All protocols enabled. Maximum flexibility. | AWS Lambda | `'stateless-api'` | Stateless execution model | | Cloudflare Workers | `'stateless-api'` | Stateless edge runtime | | Development | `'full'` | Test all protocols | + +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------- | +| [`legacy-preset-nodejs`](../examples/configure-transport-protocol-presets/legacy-preset-nodejs.md) | Basic | Use the default legacy preset for maximum compatibility with all MCP clients. | +| [`stateless-api-serverless`](../examples/configure-transport-protocol-presets/stateless-api-serverless.md) | Intermediate | Use the stateless-api preset for Vercel, Lambda, or Cloudflare Workers. | + +> See all examples in [`examples/configure-transport-protocol-presets/`](../examples/configure-transport-protocol-presets/) diff --git a/libs/skills/catalog/frontmcp-config/references/configure-transport.md b/libs/skills/catalog/frontmcp-config/references/configure-transport.md index d5a45f293..8fcb94398 100644 --- a/libs/skills/catalog/frontmcp-config/references/configure-transport.md +++ b/libs/skills/catalog/frontmcp-config/references/configure-transport.md @@ -194,6 +194,16 @@ curl -X POST http://localhost:3001/ -H 'Content-Type: application/json' -d '{"js | Session not found after server restart | In-memory sessions do not survive restarts | Enable Redis persistence with `distributedMode: true` | | Streamable HTTP returns 404 | Streamable HTTP is not enabled in the current preset | Use `'modern'`, `'legacy'`, or `'full'` preset, or set `streamable: true` in custom config | +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------- | +| [`custom-protocol-flags`](../examples/configure-transport/custom-protocol-flags.md) | Advanced | Override individual protocol flags instead of using a preset for fine-grained control. | +| [`distributed-sessions-redis`](../examples/configure-transport/distributed-sessions-redis.md) | Intermediate | Configure transport with Redis persistence for multi-instance load-balanced deployments. | +| [`stateless-serverless`](../examples/configure-transport/stateless-serverless.md) | Basic | Configure stateless transport for Vercel, Lambda, or Cloudflare deployments. | + +> See all examples in [`examples/configure-transport/`](../examples/configure-transport/) + ## Reference - **Docs:** [Runtime Modes and Transport Configuration](https://docs.agentfront.dev/frontmcp/deployment/runtime-modes) diff --git a/libs/skills/catalog/frontmcp-config/references/setup-redis.md b/libs/skills/catalog/frontmcp-config/references/setup-redis.md index 3d71342d6..3ab9f4bf8 100644 --- a/libs/skills/catalog/frontmcp-config/references/setup-redis.md +++ b/libs/skills/catalog/frontmcp-config/references/setup-redis.md @@ -7,3 +7,8 @@ description: Cross-reference to the full Redis configuration guide in frontmcp-s > This reference is maintained in `frontmcp-setup/references/setup-redis.md`. > See that file for the full Redis configuration guide including connection options, Vercel KV setup, Docker Compose examples, and troubleshooting. + +## Examples + +> Examples for Redis setup are maintained alongside the canonical reference. +> See [`frontmcp-setup/examples/setup-redis/`](../../frontmcp-setup/examples/setup-redis/). diff --git a/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md b/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md index 11ce17b6e..34691c55c 100644 --- a/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md +++ b/libs/skills/catalog/frontmcp-config/references/setup-sqlite.md @@ -7,3 +7,8 @@ description: Cross-reference to the full SQLite configuration guide in frontmcp- > This reference is maintained in `frontmcp-setup/references/setup-sqlite.md`. > See that file for the full SQLite configuration guide including WAL mode, encryption, daemon mode, and troubleshooting. + +## Examples + +> Examples for SQLite setup are maintained alongside the canonical reference. +> See [`frontmcp-setup/examples/setup-sqlite/`](../../frontmcp-setup/examples/setup-sqlite/). diff --git a/libs/skills/catalog/frontmcp-deployment/SKILL.md b/libs/skills/catalog/frontmcp-deployment/SKILL.md index 6db369a35..9b9ffcca1 100644 --- a/libs/skills/catalog/frontmcp-deployment/SKILL.md +++ b/libs/skills/catalog/frontmcp-deployment/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/deployment/overview + docs: https://docs.agentfront.dev/frontmcp/deployment/runtime-modes --- # FrontMCP Deployment Router @@ -148,5 +148,5 @@ Beyond `frontmcp build`, the CLI provides commands for the full deployment lifec ## Reference -- [Deployment Overview](https://docs.agentfront.dev/frontmcp/deployment/overview) +- [Runtime Modes](https://docs.agentfront.dev/frontmcp/deployment/runtime-modes) - Related skills: `deploy-to-node`, `deploy-to-vercel`, `deploy-to-lambda`, `deploy-to-cloudflare`, `build-for-cli`, `build-for-browser`, `build-for-sdk`, `configure-transport` diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md new file mode 100644 index 000000000..3cdfe44d8 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md @@ -0,0 +1,43 @@ +--- +name: browser-build-with-custom-entry +reference: build-for-browser +level: intermediate +description: 'Build a browser bundle using a dedicated client entry file that avoids Node.js-only imports.' +tags: [deployment, browser, node, custom, entry] +features: + - 'Creating a separate browser entry point (`src/client.ts`) that avoids importing Node.js-only modules like `fs` or `node:crypto`' + - 'Using the `-e` and `-o` flags to customize the entry file and output directory' +--- + +# Browser Build with Custom Entry + +Build a browser bundle using a dedicated client entry file that avoids Node.js-only imports. + +## Code + +```typescript +// src/client.ts +// Browser-safe entry point - no Node.js modules imported here +import { FrontMcpProvider, useTools, useResources } from '@frontmcp/react'; + +export { FrontMcpProvider, useTools, useResources }; +``` + +```bash +# Build with custom entry and output directory +frontmcp build --target browser -e ./src/client.ts -o ./dist/browser +``` + +```bash +# Verify output contains no Node.js-only modules +ls dist/browser/ +``` + +## What This Demonstrates + +- Creating a separate browser entry point (`src/client.ts`) that avoids importing Node.js-only modules like `fs` or `node:crypto` +- Using the `-e` and `-o` flags to customize the entry file and output directory + +## Related + +- See `build-for-browser` for the full browser limitations table and verification checklist diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md new file mode 100644 index 000000000..5d88b2e88 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md @@ -0,0 +1,85 @@ +--- +name: browser-crypto-and-storage +reference: build-for-browser +level: advanced +description: 'Use `@frontmcp/utils` crypto functions (WebCrypto API) and in-memory storage in browser environments.' +tags: [deployment, browser, database, remote, node, crypto] +features: + - 'Using `@frontmcp/utils` for PKCE and hashing in the browser (backed by WebCrypto, not `node:crypto`)' + - 'Avoiding filesystem and native database storage in browser builds by relying on a remote server for persistence' +--- + +# Browser-Safe Crypto and Storage + +Use `@frontmcp/utils` crypto functions (WebCrypto API) and in-memory storage in browser environments. + +## Code + +```typescript +// src/browser-auth.ts +import { generateCodeVerifier, generateCodeChallenge, sha256Base64url, randomUUID } from '@frontmcp/utils'; + +// PKCE flow in the browser - uses WebCrypto API automatically +async function startPkceFlow(): Promise<{ + verifier: string; + challenge: string; + state: string; +}> { + const verifier = generateCodeVerifier(); + const challenge = await generateCodeChallenge(verifier); + const state = randomUUID(); + + return { verifier, challenge, state }; +} + +// Hash a value using WebCrypto (works in browsers) +async function hashToken(token: string): Promise { + return sha256Base64url(token); +} + +export { startPkceFlow, hashToken }; +``` + +```typescript +// src/client-app.tsx +import { FrontMcpProvider, useTools } from '@frontmcp/react'; + +// Browser environments cannot use Redis or SQLite. +// Use in-memory stores or connect to a remote server that handles persistence. +function App() { + return ( + + + + ); +} + +function ToolDashboard() { + const { tools, callTool } = useTools(); + + return ( +
+

MCP Tools

+ {tools.map((tool) => ( +
{tool.name}
+ ))} +
+ ); +} + +export default App; +``` + +## What This Demonstrates + +- Using `@frontmcp/utils` for PKCE and hashing in the browser (backed by WebCrypto, not `node:crypto`) +- Avoiding filesystem and native database storage in browser builds by relying on a remote server for persistence + +## Related + +- See `build-for-browser` for the complete browser support table and troubleshooting guide diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md new file mode 100644 index 000000000..9695adec2 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md @@ -0,0 +1,61 @@ +--- +name: react-provider-setup +reference: build-for-browser +level: basic +description: 'Connect a React application to a remote FrontMCP server using `@frontmcp/react`.' +tags: [deployment, react, browser, remote, provider, setup] +features: + - 'Wrapping your React app with `FrontMcpProvider` and pointing it at a remote server URL' + - 'Using the `useTools` hook to list and invoke MCP tools from a React component' +--- + +# React Provider Setup + +Connect a React application to a remote FrontMCP server using `@frontmcp/react`. + +## Code + +```typescript +// src/App.tsx +import { FrontMcpProvider, useTools } from '@frontmcp/react'; + +function App() { + return ( + + + + ); +} + +function ToolUI() { + const { tools, callTool } = useTools(); + + const handleClick = async (toolName: string) => { + const result = await callTool(toolName, { query: 'hello' }); + console.log(result); + }; + + return ( +
    + {tools.map((tool) => ( +
  • + +
  • + ))} +
+ ); +} + +export default App; +``` + +## What This Demonstrates + +- Wrapping your React app with `FrontMcpProvider` and pointing it at a remote server URL +- Using the `useTools` hook to list and invoke MCP tools from a React component + +## Related + +- See `build-for-browser` for the full build command and browser limitations diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md new file mode 100644 index 000000000..903ee9937 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md @@ -0,0 +1,66 @@ +--- +name: cli-binary-build +reference: build-for-cli +level: basic +description: 'Build a FrontMCP server as a standalone binary using Node.js Single Executable Applications (SEA).' +tags: [deployment, cli, local, node, binary] +features: + - 'Building a FrontMCP server as a self-contained binary with `--target cli`' + - 'Using `socketPath` for local communication instead of a TCP port' + - 'The `--js` flag to produce a bundled JS file without the native binary wrapper' +--- + +# CLI Binary Build + +Build a FrontMCP server as a standalone binary using Node.js Single Executable Applications (SEA). + +## Code + +```typescript +// src/main.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greet a user by name', + inputSchema: { name: z.string() }, +}) +class GreetTool extends ToolContext<{ name: string }> { + async execute(input: { name: string }) { + return { content: [{ type: 'text' as const, text: `Hello, ${input.name}!` }] }; + } +} + +@App({ name: 'GreeterApp', tools: [GreetTool] }) +class GreeterApp {} + +@FrontMcp({ + info: { name: 'greeter-cli', version: '1.0.0' }, + apps: [GreeterApp], + http: { socketPath: '/tmp/greeter.sock' }, +}) +class GreeterCLI {} +``` + +```bash +# Build the SEA binary (requires Node.js 24+) +frontmcp build --target cli + +# Test the binary +./dist/greeter-cli --help + +# Or build a JS bundle only (no SEA) +frontmcp build --target cli --js +node dist/greeter-cli.cjs.js +``` + +## What This Demonstrates + +- Building a FrontMCP server as a self-contained binary with `--target cli` +- Using `socketPath` for local communication instead of a TCP port +- The `--js` flag to produce a bundled JS file without the native binary wrapper + +## Related + +- See `build-for-cli` for SEA requirements, process management, and system service installation diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md new file mode 100644 index 000000000..776a6776b --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md @@ -0,0 +1,76 @@ +--- +name: unix-socket-daemon +reference: build-for-cli +level: intermediate +description: 'Run a FrontMCP server as a local daemon accessible via Unix socket for IDE extensions and local MCP clients.' +tags: [deployment, unix-socket, cli, transport, local, unix] +features: + - 'Configuring a FrontMCP server for Unix socket transport instead of TCP' + - 'Running the server as a background daemon with process management (`frontmcp start/stop/status`)' + - 'Installing the daemon as a system service for automatic startup on reboot' +--- + +# Unix Socket Daemon Mode + +Run a FrontMCP server as a local daemon accessible via Unix socket for IDE extensions and local MCP clients. + +## Code + +```typescript +// src/main.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'lookup', + description: 'Look up a term in the local database', + inputSchema: { term: z.string() }, +}) +class LookupTool extends ToolContext<{ term: string }> { + async execute(input: { term: string }) { + return { content: [{ type: 'text' as const, text: `Result for: ${input.term}` }] }; + } +} + +@App({ name: 'DaemonApp', tools: [LookupTool] }) +class DaemonApp {} + +@FrontMcp({ + info: { name: 'my-daemon', version: '1.0.0' }, + apps: [DaemonApp], + http: { socketPath: '/tmp/my-tool.sock' }, + sqlite: { path: '~/.my-tool/data.db' }, +}) +class MyDaemonServer {} +``` + +```bash +# Start daemon in foreground +frontmcp socket ./src/main.ts -s ~/.frontmcp/sockets/my-app.sock + +# Start daemon in background with a local database +frontmcp socket ./src/main.ts -b --db ~/.my-tool/data.db + +# Manage the daemon process +frontmcp start my-daemon -e ./src/main.ts --max-restarts 5 +frontmcp status my-daemon +frontmcp logs my-daemon -F +frontmcp stop my-daemon +``` + +```bash +# Install as a system service for automatic startup +# Linux: creates a systemd unit +# macOS: creates a launchd plist +frontmcp service install my-daemon +``` + +## What This Demonstrates + +- Configuring a FrontMCP server for Unix socket transport instead of TCP +- Running the server as a background daemon with process management (`frontmcp start/stop/status`) +- Installing the daemon as a system service for automatic startup on reboot + +## Related + +- See `build-for-cli` for the full process management and system service reference diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md new file mode 100644 index 000000000..b0b8a1e00 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md @@ -0,0 +1,78 @@ +--- +name: connect-openai +reference: build-for-sdk +level: intermediate +description: "Use `connectOpenAI()` to get tools formatted for OpenAI's function-calling API." +tags: [deployment, sdk, openai, session, connect] +features: + - 'Setting `serve: false` to prevent the HTTP server from starting in library mode' + - 'Using `connectOpenAI()` to get tools in OpenAI function-calling format automatically' + - 'Passing session information via `ConnectOptions` for user context' +--- + +# Connect to OpenAI Function Calling + +Use `connectOpenAI()` to get tools formatted for OpenAI's function-calling API. + +## Code + +```typescript +// src/openai-integration.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { connectOpenAI } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'search_docs', + description: 'Search documentation by keyword', + inputSchema: { query: z.string(), limit: z.number().optional() }, +}) +class SearchDocsTool extends ToolContext<{ query: string; limit?: number }> { + async execute(input: { query: string; limit?: number }) { + return { + content: [{ type: 'text' as const, text: `Found results for: ${input.query}` }], + }; + } +} + +@App({ name: 'DocsApp', tools: [SearchDocsTool] }) +class DocsApp {} + +@FrontMcp({ + info: { name: 'docs-sdk', version: '1.0.0' }, + apps: [DocsApp], + serve: false, // No HTTP server - library mode only +}) +class DocsSDK {} + +// Connect with OpenAI-formatted tools +async function main() { + const client = await connectOpenAI(DocsSDK, { + session: { id: 'user-123', user: { sub: 'user-id' } }, + }); + + // Tools are returned in OpenAI format: + // [{ type: 'function', function: { name, description, parameters, strict: true } }] + const tools = await client.listTools(); + console.log(JSON.stringify(tools, null, 2)); + + // Call a tool + const result = await client.callTool('search_docs', { query: 'authentication' }); + console.log(result); + + // Always clean up + await client.close(); +} + +main(); +``` + +## What This Demonstrates + +- Setting `serve: false` to prevent the HTTP server from starting in library mode +- Using `connectOpenAI()` to get tools in OpenAI function-calling format automatically +- Passing session information via `ConnectOptions` for user context + +## Related + +- See `build-for-sdk` for `connectClaude()`, `connectLangChain()`, and `connectVercelAI()` alternatives diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md new file mode 100644 index 000000000..ae6aa9481 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md @@ -0,0 +1,85 @@ +--- +name: create-flat-config +reference: build-for-sdk +level: basic +description: 'Spin up an in-memory FrontMCP server from a flat config object using `create()`.' +tags: [deployment, sdk, cache, flat, config] +features: + - 'Using `create()` to spin up a server without decorators or classes' + - 'Calling tools directly via `server.callTool()` with zero network overhead' + - 'Using `cacheKey` to reuse the same server instance across multiple calls' +--- + +# Programmatic Server with create() + +Spin up an in-memory FrontMCP server from a flat config object using `create()`. + +## Code + +```typescript +// src/embedded-server.ts +import { create, tool } from '@frontmcp/sdk'; +import { z } from 'zod'; + +async function main() { + const server = await create({ + info: { name: 'my-service', version: '1.0.0' }, + tools: [ + tool({ + name: 'calculate', + description: 'Perform calculation', + inputSchema: { + a: z.number(), + b: z.number(), + operation: z.enum(['add', 'subtract', 'multiply', 'divide']), + }, + outputSchema: { result: z.number() }, + })((input) => { + switch (input.operation) { + case 'add': + return { result: input.a + input.b }; + case 'subtract': + return { result: input.a - input.b }; + case 'multiply': + return { result: input.a * input.b }; + case 'divide': + return { result: input.a / input.b }; + } + }), + ], + cacheKey: 'my-service', // Reuse same instance on repeated calls + }); + + // Call tools directly - no HTTP involved + const result = await server.callTool('calculate', { a: 2, b: 2, operation: 'add' }); + console.log(result); // { result: 4 } + + // List available tools + const { tools } = await server.listTools(); + console.log(tools.map((t) => t.name)); // ['calculate'] + + // Clean up when done + await server.dispose(); +} + +main(); +``` + +```bash +# Build as an SDK library +frontmcp build --target sdk + +# Verify outputs +ls dist/ +# my-service.cjs.js my-service.esm.mjs *.d.ts +``` + +## What This Demonstrates + +- Using `create()` to spin up a server without decorators or classes +- Calling tools directly via `server.callTool()` with zero network overhead +- Using `cacheKey` to reuse the same server instance across multiple calls + +## Related + +- See `build-for-sdk` for the full `CreateConfig` fields and `DirectClient` API diff --git a/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md new file mode 100644 index 000000000..5f79382c6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md @@ -0,0 +1,104 @@ +--- +name: multi-platform-connect +reference: build-for-sdk +level: advanced +description: 'Connect the same FrontMCP server to multiple LLM platforms using platform-specific `connect*()` functions.' +tags: [deployment, sdk, multi, platform, connect] +features: + - 'Connecting a single FrontMCP server to four different LLM platforms with automatic schema translation' + - "Each `connect*()` function returns tools in the platform's native format" + - 'All clients share the same `DirectClient` API (`listTools`, `callTool`, `close`)' +--- + +# Multi-Platform Tool Connection + +Connect the same FrontMCP server to multiple LLM platforms using platform-specific `connect*()` functions. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'translate', + description: 'Translate text to a target language', + inputSchema: { text: z.string(), targetLang: z.string() }, +}) +class TranslateTool extends ToolContext<{ text: string; targetLang: string }> { + async execute(input: { text: string; targetLang: string }) { + return { + content: [{ type: 'text' as const, text: `[${input.targetLang}] ${input.text}` }], + }; + } +} + +@App({ name: 'TranslateApp', tools: [TranslateTool] }) +class TranslateApp {} + +@FrontMcp({ + info: { name: 'translate-sdk', version: '1.0.0' }, + apps: [TranslateApp], + serve: false, +}) +class TranslateSDK {} + +export default TranslateSDK; +``` + +```typescript +// src/connect-all-platforms.ts +import { connectOpenAI, connectClaude, connectLangChain, connectVercelAI } from '@frontmcp/sdk'; +import TranslateSDK from './server'; + +async function main() { + // OpenAI format: [{ type: 'function', function: { name, description, parameters, strict: true } }] + const openaiClient = await connectOpenAI(TranslateSDK, { + clientInfo: { name: 'my-app', version: '1.0' }, + session: { id: 'session-1', user: { sub: 'user-1', name: 'Alice' } }, + }); + const openaiTools = await openaiClient.listTools(); + console.log('OpenAI tools:', openaiTools); + + // Claude format: [{ name, description, input_schema }] + const claudeClient = await connectClaude(TranslateSDK); + const claudeTools = await claudeClient.listTools(); + console.log('Claude tools:', claudeTools); + + // LangChain tool schema format + const langchainClient = await connectLangChain(TranslateSDK); + const langchainTools = await langchainClient.listTools(); + console.log('LangChain tools:', langchainTools); + + // Vercel AI SDK format + const vercelClient = await connectVercelAI(TranslateSDK); + const vercelTools = await vercelClient.listTools(); + console.log('Vercel AI tools:', vercelTools); + + // All clients share the same DirectClient API + const result = await openaiClient.callTool('translate', { + text: 'Hello', + targetLang: 'es', + }); + console.log(result); + + // Clean up all clients + await openaiClient.close(); + await claudeClient.close(); + await langchainClient.close(); + await vercelClient.close(); +} + +main(); +``` + +## What This Demonstrates + +- Connecting a single FrontMCP server to four different LLM platforms with automatic schema translation +- Each `connect*()` function returns tools in the platform's native format +- All clients share the same `DirectClient` API (`listTools`, `callTool`, `close`) + +## Related + +- See `build-for-sdk` for the full `DirectClient` API reference and `ConnectOptions` details diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md new file mode 100644 index 000000000..57d58aca8 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md @@ -0,0 +1,82 @@ +--- +name: basic-worker-deploy +reference: deploy-to-cloudflare +level: basic +description: 'Deploy a FrontMCP server to Cloudflare Workers with a minimal configuration.' +tags: [deployment, cloudflare, transport, local, worker] +features: + - 'A minimal FrontMCP server configured for Cloudflare Workers with SSE transport' + - 'The `wrangler.toml` configuration with `main` pointing to the build output' + - 'Using `wrangler dev` for local testing before deploying with `wrangler deploy`' +--- + +# Basic Cloudflare Workers Deployment + +Deploy a FrontMCP server to Cloudflare Workers with a minimal configuration. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'echo', + description: 'Echo back the input', + inputSchema: { message: z.string() }, +}) +class EchoTool extends ToolContext<{ message: string }> { + async execute(input: { message: string }) { + return { content: [{ type: 'text' as const, text: input.message }] }; + } +} + +@App({ name: 'MyApp', tools: [EchoTool] }) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-worker', version: '1.0.0' }, + apps: [MyApp], + transport: { + protocol: 'legacy', + }, +}) +class MyServer {} + +export default MyServer; +``` + +```toml +# wrangler.toml +name = "frontmcp-worker" +main = "dist/index.js" +compatibility_date = "2024-01-01" + +[vars] +NODE_ENV = "production" +``` + +```bash +# Build for Cloudflare Workers +frontmcp build --target cloudflare + +# Preview locally +wrangler dev + +# Deploy to production +wrangler deploy + +# Verify +curl https://frontmcp-worker.your-subdomain.workers.dev/health +``` + +## What This Demonstrates + +- A minimal FrontMCP server configured for Cloudflare Workers with SSE transport +- The `wrangler.toml` configuration with `main` pointing to the build output +- Using `wrangler dev` for local testing before deploying with `wrangler deploy` + +## Related + +- See `deploy-to-cloudflare` for KV storage, D1, bundle size limits, and troubleshooting diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md new file mode 100644 index 000000000..83f56102c --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md @@ -0,0 +1,97 @@ +--- +name: worker-custom-domain +reference: deploy-to-cloudflare +level: advanced +description: 'Scaffold a FrontMCP project targeting Cloudflare, configure a custom domain, and verify the deployment.' +tags: [deployment, json-rpc, cloudflare, worker, custom, domain] +features: + - 'Using `frontmcp create --target cloudflare` to scaffold a project with `wrangler.toml` and deploy scripts' + - 'Adding a custom domain with `wrangler domains add` for production-ready URLs' + - 'End-to-end verification of both the health check and MCP JSON-RPC endpoint' +--- + +# Cloudflare Worker with Custom Domain and Project Scaffold + +Scaffold a FrontMCP project targeting Cloudflare, configure a custom domain, and verify the deployment. + +## Code + +```bash +# Scaffold a new project targeting Cloudflare +npx frontmcp create my-app --target cloudflare +cd my-app +``` + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'translate', + description: 'Translate text', + inputSchema: { text: z.string(), lang: z.string() }, +}) +class TranslateTool extends ToolContext<{ text: string; lang: string }> { + async execute(input: { text: string; lang: string }) { + return { + content: [{ type: 'text' as const, text: `[${input.lang}] ${input.text}` }], + }; + } +} + +@App({ name: 'TranslateApp', tools: [TranslateTool] }) +class TranslateApp {} + +@FrontMcp({ + info: { name: 'translate-worker', version: '1.0.0' }, + apps: [TranslateApp], + transport: { + type: 'sse', + }, +}) +class TranslateServer {} + +export default TranslateServer; +``` + +```toml +# wrangler.toml +name = "translate-worker" +main = "dist/index.js" +compatibility_date = "2024-01-01" + +[[kv_namespaces]] +binding = "FRONTMCP_KV" +id = "your-kv-namespace-id" + +[vars] +NODE_ENV = "production" +``` + +```bash +# Build and deploy +frontmcp build --target cloudflare +wrangler deploy + +# Add a custom domain +wrangler domains add mcp.example.com + +# Verify health endpoint +curl https://mcp.example.com/health + +# Test MCP endpoint +curl -X POST https://mcp.example.com/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' +``` + +## What This Demonstrates + +- Using `frontmcp create --target cloudflare` to scaffold a project with `wrangler.toml` and deploy scripts +- Adding a custom domain with `wrangler domains add` for production-ready URLs +- End-to-end verification of both the health check and MCP JSON-RPC endpoint + +## Related + +- See `deploy-to-cloudflare` for bundle size limits, CPU time constraints, and the full storage options table diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md new file mode 100644 index 000000000..438e97bf7 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md @@ -0,0 +1,92 @@ +--- +name: worker-with-kv-storage +reference: deploy-to-cloudflare +level: intermediate +description: 'Deploy a FrontMCP server to Cloudflare Workers with KV namespace for session and state storage.' +tags: [deployment, cloudflare, cli, session, worker, kv] +features: + - 'Binding a KV namespace in `wrangler.toml` with `[[kv_namespaces]]`' + - 'Using `wrangler secret put` for sensitive values instead of `[vars]` (which are visible in plaintext)' + - 'Creating the KV namespace via CLI and copying the ID into the configuration' +--- + +# Cloudflare Worker with KV Storage + +Deploy a FrontMCP server to Cloudflare Workers with KV namespace for session and state storage. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'store_value', + description: 'Store a value by key', + inputSchema: { key: z.string(), value: z.string() }, +}) +class StoreValueTool extends ToolContext<{ key: string; value: string }> { + async execute(input: { key: string; value: string }) { + return { + content: [{ type: 'text' as const, text: `Stored: ${input.key}` }], + }; + } +} + +@App({ name: 'StorageApp', tools: [StoreValueTool] }) +class StorageApp {} + +@FrontMcp({ + info: { name: 'my-worker', version: '1.0.0' }, + apps: [StorageApp], + transport: { + type: 'sse', + }, +}) +class MyServer {} + +export default MyServer; +``` + +```toml +# wrangler.toml +name = "frontmcp-worker" +main = "dist/index.js" +compatibility_date = "2024-01-01" + +[[kv_namespaces]] +binding = "FRONTMCP_KV" +id = "your-kv-namespace-id" + +[vars] +NODE_ENV = "production" +``` + +```bash +# Create the KV namespace +wrangler kv:namespace create FRONTMCP_KV +# Copy the returned id into wrangler.toml + +# Store secrets securely (not in [vars]) +wrangler secret put MY_API_KEY + +# Build and deploy +frontmcp build --target cloudflare +wrangler deploy + +# Verify +curl -X POST https://frontmcp-worker.your-subdomain.workers.dev/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' +``` + +## What This Demonstrates + +- Binding a KV namespace in `wrangler.toml` with `[[kv_namespaces]]` +- Using `wrangler secret put` for sensitive values instead of `[vars]` (which are visible in plaintext) +- Creating the KV namespace via CLI and copying the ID into the configuration + +## Related + +- See `deploy-to-cloudflare` for D1, Durable Objects, bundle size limits, and storage comparison table diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md new file mode 100644 index 000000000..32dd26548 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md @@ -0,0 +1,92 @@ +--- +name: cdk-deployment +reference: deploy-to-lambda +level: advanced +description: 'Deploy a FrontMCP server to AWS Lambda using CDK with provisioned concurrency and secrets management.' +tags: [deployment, lambda, performance, cdk] +features: + - 'Using AWS CDK instead of SAM for infrastructure-as-code deployment' + - 'Provisioned concurrency via a Lambda alias to eliminate cold starts on critical endpoints' + - 'Referencing secrets from SSM Parameter Store with `{{resolve:ssm:...}}` instead of hardcoding' +--- + +# CDK Deployment with Provisioned Concurrency + +Deploy a FrontMCP server to AWS Lambda using CDK with provisioned concurrency and secrets management. + +## Code + +```typescript +// lib/frontmcp-stack.ts +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as apigw from 'aws-cdk-lib/aws-apigatewayv2'; +import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; +import { Construct } from 'constructs'; + +export class FrontMcpStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const fn = new lambda.Function(this, 'FrontMcpHandler', { + runtime: lambda.Runtime.NODEJS_24_X, + handler: 'handler.handler', + code: lambda.Code.fromAsset('.'), + memorySize: 512, + timeout: cdk.Duration.seconds(30), + architecture: lambda.Architecture.ARM_64, + environment: { + NODE_ENV: 'production', + LOG_LEVEL: 'info', + // Use SSM for secrets instead of plaintext + FRONTMCP_AUTH_SECRET: cdk.Fn.sub('{{resolve:ssm-secure:/frontmcp/auth-secret}}'), + }, + }); + + // Provisioned concurrency for predictable latency + const alias = new lambda.Alias(this, 'ProdAlias', { + aliasName: 'prod', + version: fn.currentVersion, + provisionedConcurrentExecutions: 5, + }); + + const api = new apigw.HttpApi(this, 'FrontMcpApi', { + defaultIntegration: new integrations.HttpLambdaIntegration( + 'LambdaIntegration', + alias, // Route traffic to the alias with provisioned concurrency + ), + }); + + new cdk.CfnOutput(this, 'ApiEndpoint', { + value: api.apiEndpoint, + }); + } +} +``` + +```bash +# Build the FrontMCP server +frontmcp build --target lambda + +# Store secrets in SSM Parameter Store +aws ssm put-parameter \ + --name "/frontmcp/auth-secret" \ + --type "SecureString" \ + --value "your-secret-value" + +# Deploy with CDK +cdk deploy + +# Verify +curl https://abc123.execute-api.us-east-1.amazonaws.com/health +``` + +## What This Demonstrates + +- Using AWS CDK instead of SAM for infrastructure-as-code deployment +- Provisioned concurrency via a Lambda alias to eliminate cold starts on critical endpoints +- Referencing secrets from SSM Parameter Store with `{{resolve:ssm:...}}` instead of hardcoding + +## Related + +- See `deploy-to-lambda` for the SAM alternative, cold start benchmarks, and VPC configuration for ElastiCache diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md new file mode 100644 index 000000000..eb224abbe --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md @@ -0,0 +1,113 @@ +--- +name: lambda-handler-with-cors +reference: deploy-to-lambda +level: intermediate +description: 'Create a custom Lambda handler with an explicit API Gateway definition for CORS support.' +tags: [deployment, lambda, handler, cors] +features: + - 'Creating a custom Lambda handler with `createLambdaHandler()` from `@frontmcp/adapters/lambda`' + - 'Defining an explicit HTTP API resource with CORS configuration for cross-origin requests' + - 'Linking the function events to the explicit API via `ApiId: !Ref`' +--- + +# Lambda Handler with CORS and API Gateway + +Create a custom Lambda handler with an explicit API Gateway definition for CORS support. + +## Code + +```typescript +// src/lambda.ts +import { createLambdaHandler } from '@frontmcp/adapters/lambda'; +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'analyze', + description: 'Analyze text content', + inputSchema: { text: z.string() }, +}) +class AnalyzeTool extends ToolContext<{ text: string }> { + async execute(input: { text: string }) { + return { + content: [{ type: 'text' as const, text: `Analysis of: ${input.text}` }], + }; + } +} + +@App({ name: 'AnalyzerApp', tools: [AnalyzeTool] }) +class AnalyzerApp {} + +@FrontMcp({ + info: { name: 'analyzer', version: '1.0.0' }, + apps: [AnalyzerApp], +}) +class AnalyzerServer {} + +export const handler = createLambdaHandler(AnalyzerServer, { + streaming: false, +}); +``` + +```yaml +# template.yaml - with explicit API Gateway and CORS +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: FrontMCP server with CORS + +Globals: + Function: + Timeout: 30 + Runtime: nodejs24.x + MemorySize: 512 + +Resources: + FrontMcpApi: + Type: AWS::Serverless::HttpApi + Properties: + StageName: prod + CorsConfiguration: + AllowOrigins: + - 'https://your-domain.com' + AllowMethods: + - GET + - POST + - OPTIONS + AllowHeaders: + - Content-Type + - Authorization + + FrontMcpFunction: + Type: AWS::Serverless::Function + Properties: + Handler: handler.handler + CodeUri: . + Architectures: + - arm64 + Environment: + Variables: + NODE_ENV: production + Events: + McpApi: + Type: HttpApi + Properties: + ApiId: !Ref FrontMcpApi + Path: /{proxy+} + Method: ANY +``` + +```bash +# Build and deploy +frontmcp build --target lambda +sam build && sam deploy +``` + +## What This Demonstrates + +- Creating a custom Lambda handler with `createLambdaHandler()` from `@frontmcp/adapters/lambda` +- Defining an explicit HTTP API resource with CORS configuration for cross-origin requests +- Linking the function events to the explicit API via `ApiId: !Ref` + +## Related + +- See `deploy-to-lambda` for secrets management, provisioned concurrency, and CDK deployment diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md new file mode 100644 index 000000000..7a1ddf590 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md @@ -0,0 +1,100 @@ +--- +name: sam-template-basic +reference: deploy-to-lambda +level: basic +description: 'Deploy a FrontMCP server to AWS Lambda with API Gateway using a SAM template.' +tags: [deployment, lambda, performance, sam, template] +features: + - 'A minimal SAM template with ARM64 architecture for faster cold starts and lower cost' + - "The `/{proxy+}` catch-all route that forwards all requests to FrontMCP's internal router" + - 'CloudWatch log group with 14-day retention' +--- + +# Basic SAM Template for Lambda + +Deploy a FrontMCP server to AWS Lambda with API Gateway using a SAM template. + +## Code + +```yaml +# template.yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: FrontMCP server on AWS Lambda + +Globals: + Function: + Timeout: 30 + Runtime: nodejs24.x + MemorySize: 512 + Environment: + Variables: + NODE_ENV: production + LOG_LEVEL: info + +Resources: + FrontMcpFunction: + Type: AWS::Serverless::Function + Properties: + Handler: handler.handler + CodeUri: . + Description: FrontMCP MCP server + Architectures: + - arm64 + Events: + McpApi: + Type: HttpApi + Properties: + Path: /{proxy+} + Method: ANY + HealthCheck: + Type: HttpApi + Properties: + Path: /health + Method: GET + + FrontMcpLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub /aws/lambda/${FrontMcpFunction} + RetentionInDays: 14 + +Outputs: + ApiEndpoint: + Description: API Gateway endpoint URL + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com' + FunctionArn: + Description: Lambda function ARN + Value: !GetAtt FrontMcpFunction.Arn +``` + +```bash +# Build for Lambda +frontmcp build --target lambda + +# Deploy with guided prompts (first time) +sam build +sam deploy --guided + +# Subsequent deploys +sam build && sam deploy + +# Get the endpoint URL +aws cloudformation describe-stacks \ + --stack-name frontmcp-prod \ + --query "Stacks[0].Outputs[?OutputKey=='ApiEndpoint'].OutputValue" \ + --output text + +# Verify +curl https://abc123.execute-api.us-east-1.amazonaws.com/health +``` + +## What This Demonstrates + +- A minimal SAM template with ARM64 architecture for faster cold starts and lower cost +- The `/{proxy+}` catch-all route that forwards all requests to FrontMCP's internal router +- CloudWatch log group with 14-day retention + +## Related + +- See `deploy-to-lambda` for Redis/ElastiCache integration, CDK alternative, and provisioned concurrency diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md new file mode 100644 index 000000000..bc7bac935 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md @@ -0,0 +1,63 @@ +--- +name: basic-multistage-dockerfile +reference: deploy-to-node-dockerfile +level: basic +description: 'A minimal multi-stage Dockerfile for building and running a FrontMCP server in production.' +tags: [deployment, dockerfile, docker, node, multistage] +features: + - 'Two-stage build: the first stage installs all dependencies and builds; the second copies only production artifacts' + - 'Using `yarn install --production` in the production stage to exclude dev dependencies' + - 'A health check that verifies the server is responding' +--- + +# Basic Multi-Stage Dockerfile + +A minimal multi-stage Dockerfile for building and running a FrontMCP server in production. + +## Code + +```dockerfile +# Dockerfile + +# ---- Build Stage ---- +FROM node:24-alpine AS builder +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY . . +RUN yarn frontmcp build --target node + +# ---- Production Stage ---- +FROM node:24-alpine AS production +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=3000 +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/yarn.lock ./ +RUN yarn install --frozen-lockfile --production && \ + yarn cache clean +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 +CMD ["node", "dist/main.js"] +``` + +```bash +# Build and run the container +docker build -t my-frontmcp-server . +docker run -p 3000:3000 -e NODE_ENV=production my-frontmcp-server + +# Verify +curl http://localhost:3000/health +``` + +## What This Demonstrates + +- Two-stage build: the first stage installs all dependencies and builds; the second copies only production artifacts +- Using `yarn install --production` in the production stage to exclude dev dependencies +- A health check that verifies the server is responding + +## Related + +- See `deploy-to-node-dockerfile` for the complete reference Dockerfile with security hardening diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md new file mode 100644 index 000000000..9b40f783d --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md @@ -0,0 +1,89 @@ +--- +name: secure-nonroot-dockerfile +reference: deploy-to-node-dockerfile +level: advanced +description: 'A production Dockerfile with a non-root user, proper ownership, and security hardening.' +tags: [deployment, dockerfile, docker, security, node, secure] +features: + - 'Creating a dedicated non-root user (`frontmcp`) and switching to it with `USER`' + - 'Setting file ownership before switching users so the process can read its own files' + - 'Combining the Dockerfile with runtime resource limits (`--memory`, `--cpus`)' +--- + +# Secure Non-Root Dockerfile + +A production Dockerfile with a non-root user, proper ownership, and security hardening. + +## Code + +```dockerfile +# Dockerfile + +# ---- Build Stage ---- +FROM node:24-alpine AS builder +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY . . +RUN yarn frontmcp build --target node + +# ---- Production Stage ---- +FROM node:24-alpine AS production +WORKDIR /app + +# Create non-root user for security +RUN addgroup -S frontmcp && adduser -S frontmcp -G frontmcp + +# Copy only production artifacts +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/yarn.lock ./ + +# Install production dependencies only +RUN yarn install --frozen-lockfile --production && \ + yarn cache clean + +# Set ownership to non-root user +RUN chown -R frontmcp:frontmcp /app + +USER frontmcp + +# Environment defaults +ENV NODE_ENV=production +ENV PORT=3000 + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +CMD ["node", "dist/main.js"] +``` + +```bash +# Build +docker build -t my-frontmcp-server:secure . + +# Run with resource limits +docker run -p 3000:3000 \ + --memory=512m \ + --cpus=1.0 \ + -e NODE_ENV=production \ + -e REDIS_URL=redis://redis:6379 \ + my-frontmcp-server:secure + +# Verify the process runs as non-root +docker exec $(docker ps -q -f ancestor=my-frontmcp-server:secure) whoami +# frontmcp +``` + +## What This Demonstrates + +- Creating a dedicated non-root user (`frontmcp`) and switching to it with `USER` +- Setting file ownership before switching users so the process can read its own files +- Combining the Dockerfile with runtime resource limits (`--memory`, `--cpus`) + +## Related + +- See `deploy-to-node-dockerfile` for the complete reference Dockerfile +- See `deploy-to-node` for Docker Compose, PM2, and NGINX deployment patterns diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md new file mode 100644 index 000000000..cf7c34769 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md @@ -0,0 +1,101 @@ +--- +name: docker-compose-with-redis +reference: deploy-to-node +level: basic +description: 'Deploy a FrontMCP server with Redis using Docker Compose for production.' +tags: [deployment, docker-compose, redis, dockerfile, docker, session] +features: + - 'Multi-stage Dockerfile that keeps the production image small and secure' + - 'Docker Compose configuration with Redis for session storage' + - 'Health checks on both the FrontMCP server and Redis, with `depends_on` ensuring Redis starts first' +--- + +# Docker Compose with Redis + +Deploy a FrontMCP server with Redis using Docker Compose for production. + +## Code + +```yaml +# docker-compose.yml +version: '3.9' + +services: + frontmcp: + build: + context: . + dockerfile: Dockerfile + ports: + - '${PORT:-3000}:3000' + environment: + - NODE_ENV=production + - PORT=3000 + - REDIS_URL=redis://redis:6379 + - LOG_LEVEL=info + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/health'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 3s + retries: 5 + restart: unless-stopped + +volumes: + redis-data: +``` + +```dockerfile +# Dockerfile +FROM node:24-alpine AS builder +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +COPY . . +RUN npx frontmcp build --target node + +FROM node:24-alpine AS production +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ +COPY --from=builder /app/yarn.lock ./ +RUN yarn install --frozen-lockfile --production && yarn cache clean +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ + CMD wget -qO- http://localhost:3000/health || exit 1 +CMD ["node", "dist/main.js"] +``` + +```bash +# Build and start +docker compose up -d + +# Verify +docker compose ps +curl http://localhost:3000/health +# {"status":"ok","uptime":12345} +``` + +## What This Demonstrates + +- Multi-stage Dockerfile that keeps the production image small and secure +- Docker Compose configuration with Redis for session storage +- Health checks on both the FrontMCP server and Redis, with `depends_on` ensuring Redis starts first + +## Related + +- See `deploy-to-node` for PM2 process management, NGINX reverse proxy, and environment variable configuration diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md new file mode 100644 index 000000000..20006d20d --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md @@ -0,0 +1,79 @@ +--- +name: pm2-with-nginx +reference: deploy-to-node +level: intermediate +description: 'Deploy a FrontMCP server on bare metal using PM2 for process management and NGINX for TLS termination.' +tags: [deployment, nx, node, pm2, nginx] +features: + - 'Using PM2 with `-i max` for multi-core clustering and automatic restarts' + - 'Configuring NGINX as a reverse proxy for TLS termination in front of the FrontMCP server' + - 'Setting environment variables via `.env` for production configuration' +--- + +# PM2 Process Manager with NGINX Reverse Proxy + +Deploy a FrontMCP server on bare metal using PM2 for process management and NGINX for TLS termination. + +## Code + +```bash +# Build the server +frontmcp build --target node + +# Install PM2 globally +npm install -g pm2 + +# Start with cluster mode (one instance per CPU core) +pm2 start dist/main.js --name frontmcp-server -i max + +# Save the process list for auto-restart on reboot +pm2 save +pm2 startup +``` + +```nginx +# /etc/nginx/sites-available/mcp.example.com +server { + listen 443 ssl; + server_name mcp.example.com; + + ssl_certificate /etc/ssl/certs/mcp.example.com.pem; + ssl_certificate_key /etc/ssl/private/mcp.example.com.key; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +```bash +# .env +PORT=3000 +NODE_ENV=production +HOST=0.0.0.0 +REDIS_URL=redis://localhost:6379 +LOG_LEVEL=info +``` + +```bash +# Enable the NGINX site and reload +sudo ln -s /etc/nginx/sites-available/mcp.example.com /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx + +# Verify +curl https://mcp.example.com/health +``` + +## What This Demonstrates + +- Using PM2 with `-i max` for multi-core clustering and automatic restarts +- Configuring NGINX as a reverse proxy for TLS termination in front of the FrontMCP server +- Setting environment variables via `.env` for production configuration + +## Related + +- See `deploy-to-node` for Docker Compose deployment, resource limits, and the full environment variable reference diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md new file mode 100644 index 000000000..a34b56576 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md @@ -0,0 +1,92 @@ +--- +name: resource-limits +reference: deploy-to-node +level: advanced +description: 'Configure resource limits, health checks, and environment variables for a production FrontMCP deployment.' +tags: [deployment, docker-compose, docker, node, resource, limits] +features: + - 'Setting CPU and memory limits/reservations in Docker Compose to prevent OOM kills' + - 'Using `NODE_OPTIONS=--max-old-space-size` to align the V8 heap limit with the container memory' + - 'Configuring health checks with appropriate `start_period` to allow the server time to initialize' +--- + +# Production Resource Limits and Health Checks + +Configure resource limits, health checks, and environment variables for a production FrontMCP deployment. + +## Code + +```yaml +# docker-compose.yml with resource limits +version: '3.9' + +services: + frontmcp: + build: + context: . + dockerfile: Dockerfile + ports: + - '${PORT:-3000}:3000' + environment: + - NODE_ENV=production + - PORT=3000 + - REDIS_URL=redis://redis:6379 + - LOG_LEVEL=info + - NODE_OPTIONS=--max-old-space-size=384 + depends_on: + redis: + condition: service_healthy + restart: unless-stopped + deploy: + resources: + limits: + memory: 512M + cpus: '1.0' + reservations: + memory: 256M + cpus: '0.5' + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/health'] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + redis: + image: redis:7-alpine + volumes: + - redis-data:/data + deploy: + resources: + limits: + memory: 128M + cpus: '0.5' + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 3s + retries: 5 + restart: unless-stopped + +volumes: + redis-data: +``` + +```bash +# Verify health check endpoint +curl http://localhost:3000/health +# {"status":"ok","uptime":12345} + +# Monitor resource usage +docker stats --no-stream +``` + +## What This Demonstrates + +- Setting CPU and memory limits/reservations in Docker Compose to prevent OOM kills +- Using `NODE_OPTIONS=--max-old-space-size` to align the V8 heap limit with the container memory +- Configuring health checks with appropriate `start_period` to allow the server time to initialize + +## Related + +- See `deploy-to-node` for the full deployment guide including Dockerfile, PM2, and NGINX setup diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md new file mode 100644 index 000000000..d20d07fd9 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md @@ -0,0 +1,49 @@ +--- +name: minimal-vercel-config +reference: deploy-to-vercel-config +level: basic +description: 'The minimum `vercel.json` needed to deploy a FrontMCP server to Vercel.' +tags: [deployment, vercel, serverless, config, minimal] +features: + - 'The catch-all rewrite (`/(.*) -> /api/frontmcp`) routes all requests to the single FrontMCP handler' + - 'Setting `buildCommand` and `outputDirectory` so Vercel uses the FrontMCP build pipeline' + - 'Configuring function memory (512 MB) and max duration (30s) for the serverless function' +--- + +# Minimal vercel.json Configuration + +The minimum `vercel.json` needed to deploy a FrontMCP server to Vercel. + +## Code + +```json +// vercel.json +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "frontmcp build --target vercel", + "outputDirectory": "dist", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/api/frontmcp" + } + ], + "functions": { + "api/frontmcp.js": { + "memory": 512, + "maxDuration": 30 + } + }, + "regions": ["iad1"] +} +``` + +## What This Demonstrates + +- The catch-all rewrite (`/(.*) -> /api/frontmcp`) routes all requests to the single FrontMCP handler +- Setting `buildCommand` and `outputDirectory` so Vercel uses the FrontMCP build pipeline +- Configuring function memory (512 MB) and max duration (30s) for the serverless function + +## Related + +- See `deploy-to-vercel-config` for the full reference configuration with security headers diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md new file mode 100644 index 000000000..aa78270d4 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md @@ -0,0 +1,92 @@ +--- +name: vercel-config-with-security-headers +reference: deploy-to-vercel-config +level: intermediate +description: 'A complete `vercel.json` with per-route security headers for health, MCP, and all other endpoints.' +tags: [deployment, vercel, cache, security, config, headers] +features: + - 'Per-route header configuration: `/health` and `/mcp` get `Cache-Control: no-store` to prevent caching' + - 'Global security headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`) applied to all routes' + - 'Setting `framework: null` to tell Vercel this is not a framework project' +--- + +# vercel.json with Security Headers + +A complete `vercel.json` with per-route security headers for health, MCP, and all other endpoints. + +## Code + +```json +// vercel.json +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": null, + "buildCommand": "frontmcp build --target vercel", + "outputDirectory": "dist", + "rewrites": [ + { + "source": "/(.*)", + "destination": "/api/frontmcp" + } + ], + "functions": { + "api/frontmcp.js": { + "memory": 512, + "maxDuration": 30 + } + }, + "regions": ["iad1"], + "headers": [ + { + "source": "/health", + "headers": [ + { + "key": "Cache-Control", + "value": "no-store" + } + ] + }, + { + "source": "/mcp", + "headers": [ + { + "key": "Cache-Control", + "value": "no-store" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + } + ] + }, + { + "source": "/(.*)", + "headers": [ + { + "key": "X-Frame-Options", + "value": "DENY" + }, + { + "key": "X-Content-Type-Options", + "value": "nosniff" + }, + { + "key": "Referrer-Policy", + "value": "strict-origin-when-cross-origin" + } + ] + } + ] +} +``` + +## What This Demonstrates + +- Per-route header configuration: `/health` and `/mcp` get `Cache-Control: no-store` to prevent caching +- Global security headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`) applied to all routes +- Setting `framework: null` to tell Vercel this is not a framework project + +## Related + +- See `deploy-to-vercel-config` for the full reference configuration +- See `deploy-to-vercel` for the complete deployment guide diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md new file mode 100644 index 000000000..5b0e95223 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md @@ -0,0 +1,69 @@ +--- +name: vercel-mcp-endpoint-test +reference: deploy-to-vercel +level: advanced +description: 'Verify a Vercel-deployed FrontMCP server by testing health, tool listing, and tool invocation.' +tags: [deployment, json-rpc, vercel, mcp, endpoint] +features: + - 'Testing the health endpoint and MCP JSON-RPC API of a deployed Vercel function' + - 'Using preview deployments to validate changes before promoting to production' + - 'Setting `maxDuration` according to your Vercel plan (Hobby: 10s, Pro: 60s, Enterprise: 900s)' +--- + +# Testing a Vercel MCP Endpoint + +Verify a Vercel-deployed FrontMCP server by testing health, tool listing, and tool invocation. + +## Code + +```bash +# Health check +curl https://your-project.vercel.app/health +# {"status":"ok"} + +# List tools via JSON-RPC +curl -X POST https://your-project.vercel.app/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' + +# Call a tool via JSON-RPC +curl -X POST https://your-project.vercel.app/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"greet","arguments":{"name":"World"}},"id":2}' +``` + +```bash +# Preview deployment for PR testing +vercel +# Creates a unique preview URL: https://my-project-abc123.vercel.app + +# Test the preview before promoting to production +curl https://my-project-abc123.vercel.app/health + +# Promote to production +vercel --prod +``` + +```json +// vercel.json - with maxDuration matching your plan +{ + "rewrites": [{ "source": "/(.*)", "destination": "/api/frontmcp" }], + "functions": { + "api/frontmcp.ts": { + "memory": 1024, + "maxDuration": 10 + } + }, + "regions": ["iad1"] +} +``` + +## What This Demonstrates + +- Testing the health endpoint and MCP JSON-RPC API of a deployed Vercel function +- Using preview deployments to validate changes before promoting to production +- Setting `maxDuration` according to your Vercel plan (Hobby: 10s, Pro: 60s, Enterprise: 900s) + +## Related + +- See `deploy-to-vercel` for the full deployment, KV storage, and cold start optimization guide diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md new file mode 100644 index 000000000..6a9b64088 --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md @@ -0,0 +1,82 @@ +--- +name: vercel-with-kv +reference: deploy-to-vercel +level: basic +description: 'Deploy a FrontMCP server to Vercel serverless functions with Vercel KV for session persistence.' +tags: [deployment, vercel-kv, vercel, session, performance, serverless] +features: + - "Configuring `{ provider: 'vercel-kv' }` for automatic Vercel KV session storage" + - 'The `vercel.json` catch-all rewrite that routes all requests to the single FrontMCP handler' + - 'Setting function memory to 1024 MB for faster cold starts' +--- + +# Deploy to Vercel with KV Storage + +Deploy a FrontMCP server to Vercel serverless functions with Vercel KV for session persistence. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet', + description: 'Greet a user', + inputSchema: { name: z.string() }, +}) +class GreetTool extends ToolContext<{ name: string }> { + async execute(input: { name: string }) { + return { content: [{ type: 'text' as const, text: `Hello, ${input.name}!` }] }; + } +} + +@App({ name: 'MyApp', tools: [GreetTool] }) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MyApp], + redis: { provider: 'vercel-kv' }, +}) +class MyServer {} +``` + +```json +// vercel.json +{ + "rewrites": [{ "source": "/(.*)", "destination": "/api/frontmcp" }], + "functions": { + "api/frontmcp.ts": { + "memory": 1024, + "maxDuration": 60 + } + }, + "regions": ["iad1"] +} +``` + +```bash +# Build for Vercel +frontmcp build --target vercel + +# Preview deployment +vercel + +# Production deployment +vercel --prod + +# Verify +curl https://your-project.vercel.app/health +``` + +## What This Demonstrates + +- Configuring `{ provider: 'vercel-kv' }` for automatic Vercel KV session storage +- The `vercel.json` catch-all rewrite that routes all requests to the single FrontMCP handler +- Setting function memory to 1024 MB for faster cold starts + +## Related + +- See `deploy-to-vercel` for KV provisioning, environment variables, and cold start optimization diff --git a/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md new file mode 100644 index 000000000..576ad157d --- /dev/null +++ b/libs/skills/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md @@ -0,0 +1,90 @@ +--- +name: vercel-with-skills-cache +reference: deploy-to-vercel +level: intermediate +description: 'Deploy a FrontMCP server to Vercel with skills enabled and KV-backed skill caching.' +tags: [deployment, vercel-kv, vercel, cache, security, skills] +features: + - 'Enabling skills cache backed by Vercel KV with a 60-second TTL' + - 'Setting environment variables via `vercel env add` instead of hardcoding in source' + - 'Adding security headers (`X-Content-Type-Options`, `X-Frame-Options`) in `vercel.json`' +--- + +# Vercel Deployment with Skills Cache + +Deploy a FrontMCP server to Vercel with skills enabled and KV-backed skill caching. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ name: 'MyApp' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MyApp], + redis: { provider: 'vercel-kv' }, + skillsConfig: { + enabled: true, + cache: { + enabled: true, + redis: { provider: 'vercel-kv' }, + ttlMs: 60000, + }, + }, +}) +class MyServer {} +``` + +```bash +# Set environment variables via Vercel CLI +vercel env add KV_REST_API_URL "https://your-kv-store.kv.vercel-storage.com" +vercel env add KV_REST_API_TOKEN "your-token" +vercel env add NODE_ENV production +vercel env add LOG_LEVEL info +``` + +```json +// vercel.json +{ + "rewrites": [{ "source": "/(.*)", "destination": "/api/frontmcp" }], + "functions": { + "api/frontmcp.ts": { + "memory": 1024, + "maxDuration": 60 + } + }, + "regions": ["iad1"], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-Frame-Options", "value": "DENY" } + ] + } + ] +} +``` + +```bash +# Deploy to production +frontmcp build --target vercel +vercel --prod + +# Add a custom domain +vercel domains add mcp.example.com +``` + +## What This Demonstrates + +- Enabling skills cache backed by Vercel KV with a 60-second TTL +- Setting environment variables via `vercel env add` instead of hardcoding in source +- Adding security headers (`X-Content-Type-Options`, `X-Frame-Options`) in `vercel.json` + +## Related + +- See `deploy-to-vercel` for the full deployment guide including cold start notes and execution limits 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 3950dcd24..bcaacebf1 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-browser.md @@ -137,6 +137,16 @@ ls dist/browser/ | Bundle too large | All server-side code included | Use `--target browser` and a dedicated client entry file | | `@frontmcp/utils` fs throws | File system ops called in browser | Remove fs calls; use API endpoints or in-memory alternatives | +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | +| [`browser-build-with-custom-entry`](../examples/build-for-browser/browser-build-with-custom-entry.md) | Intermediate | Build a browser bundle using a dedicated client entry file that avoids Node.js-only imports. | +| [`browser-crypto-and-storage`](../examples/build-for-browser/browser-crypto-and-storage.md) | Advanced | Use `@frontmcp/utils` crypto functions (WebCrypto API) and in-memory storage in browser environments. | +| [`react-provider-setup`](../examples/build-for-browser/react-provider-setup.md) | Basic | Connect a React application to a remote FrontMCP server using `@frontmcp/react`. | + +> See all examples in [`examples/build-for-browser/`](../examples/build-for-browser/) + ## Reference - **Docs:** 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 82210a92e..3dd10842f 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-cli.md @@ -185,6 +185,15 @@ frontmcp service uninstall my-server | Permission denied on binary | Missing execute permission | Run `chmod +x dist/my-server` | | Binary fails on different OS | SEA binaries are platform-specific | Build on the target OS or use CI matrix builds | +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------ | +| [`cli-binary-build`](../examples/build-for-cli/cli-binary-build.md) | Basic | Build a FrontMCP server as a standalone binary using Node.js Single Executable Applications (SEA). | +| [`unix-socket-daemon`](../examples/build-for-cli/unix-socket-daemon.md) | Intermediate | Run a FrontMCP server as a local daemon accessible via Unix socket for IDE extensions and local MCP clients. | + +> See all examples in [`examples/build-for-cli/`](../examples/build-for-cli/) + ## Reference - **Docs:** 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 85cc7bbb9..d15b648a9 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md +++ b/libs/skills/catalog/frontmcp-deployment/references/build-for-sdk.md @@ -258,6 +258,16 @@ node -e "const { create } = require('./dist/my-sdk.cjs.js'); ..." | `connectOpenAI()` format wrong | Using raw `listTools()` instead of platform client | Use `connectOpenAI()` which formats tools for OpenAI automatically | | Bundle includes `@frontmcp/*` | Build config missing externals | Verify `--target sdk` is set; it marks `@frontmcp/*` as external | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- | +| [`connect-openai`](../examples/build-for-sdk/connect-openai.md) | Intermediate | Use `connectOpenAI()` to get tools formatted for OpenAI's function-calling API. | +| [`create-flat-config`](../examples/build-for-sdk/create-flat-config.md) | Basic | Spin up an in-memory FrontMCP server from a flat config object using `create()`. | +| [`multi-platform-connect`](../examples/build-for-sdk/multi-platform-connect.md) | Advanced | Connect the same FrontMCP server to multiple LLM platforms using platform-specific `connect*()` functions. | + +> See all examples in [`examples/build-for-sdk/`](../examples/build-for-sdk/) + ## Reference - **Docs:** 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 5503af498..73f115eb2 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md @@ -212,6 +212,16 @@ curl -X POST https://frontmcp-worker.your-subdomain.workers.dev/mcp \ - [ ] SSE streaming works end-to-end (if using SSE transport) - [ ] Custom domain resolves correctly (if configured) +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------- | +| [`basic-worker-deploy`](../examples/deploy-to-cloudflare/basic-worker-deploy.md) | Basic | Deploy a FrontMCP server to Cloudflare Workers with a minimal configuration. | +| [`worker-custom-domain`](../examples/deploy-to-cloudflare/worker-custom-domain.md) | Advanced | Scaffold a FrontMCP project targeting Cloudflare, configure a custom domain, and verify the deployment. | +| [`worker-with-kv-storage`](../examples/deploy-to-cloudflare/worker-with-kv-storage.md) | Intermediate | Deploy a FrontMCP server to Cloudflare Workers with KV namespace for session and state storage. | + +> See all examples in [`examples/deploy-to-cloudflare/`](../examples/deploy-to-cloudflare/) + ## Reference - **Docs:** 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 d6d7bae37..0590192b2 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-lambda.md @@ -316,6 +316,16 @@ Lambda cold starts occur when a new execution environment is initialized. Strate | Redis connection refused from Lambda | Lambda not in the same VPC as ElastiCache | Place the Lambda in the ElastiCache VPC with appropriate security group rules | | `sam deploy` fails with IAM error | Insufficient permissions for CloudFormation stack creation | Ensure the deploying IAM user/role has `cloudformation:*`, `lambda:*`, `apigateway:*`, and `iam:PassRole` | +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | +| [`cdk-deployment`](../examples/deploy-to-lambda/cdk-deployment.md) | Advanced | Deploy a FrontMCP server to AWS Lambda using CDK with provisioned concurrency and secrets management. | +| [`lambda-handler-with-cors`](../examples/deploy-to-lambda/lambda-handler-with-cors.md) | Intermediate | Create a custom Lambda handler with an explicit API Gateway definition for CORS support. | +| [`sam-template-basic`](../examples/deploy-to-lambda/sam-template-basic.md) | Basic | Deploy a FrontMCP server to AWS Lambda with API Gateway using a SAM template. | + +> See all examples in [`examples/deploy-to-lambda/`](../examples/deploy-to-lambda/) + ## Reference - **Docs:** https://docs.agentfront.dev/frontmcp/deployment/serverless 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 8ed6eed91..9f67a39da 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 @@ -57,3 +57,12 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 CMD ["node", "dist/main.js"] + +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ | +| [`basic-multistage-dockerfile`](../examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md) | Basic | A minimal multi-stage Dockerfile for building and running a FrontMCP server in production. | +| [`secure-nonroot-dockerfile`](../examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md) | Advanced | A production Dockerfile with a non-root user, proper ownership, and security hardening. | + +> See all examples in [`examples/deploy-to-node-dockerfile/`](../examples/deploy-to-node-dockerfile/) 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 31235bb9f..6b4aec453 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-node.md @@ -256,6 +256,16 @@ services: | Out of memory (OOM kill) | Container memory limit is too low | Increase the memory limit in Docker or set `NODE_OPTIONS="--max-old-space-size=1024"` | | PM2 not restarting on reboot | Startup hook was not saved | Run `pm2 save && pm2 startup` to persist the process list across reboots | +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------- | +| [`docker-compose-with-redis`](../examples/deploy-to-node/docker-compose-with-redis.md) | Basic | Deploy a FrontMCP server with Redis using Docker Compose for production. | +| [`pm2-with-nginx`](../examples/deploy-to-node/pm2-with-nginx.md) | Intermediate | Deploy a FrontMCP server on bare metal using PM2 for process management and NGINX for TLS termination. | +| [`resource-limits`](../examples/deploy-to-node/resource-limits.md) | Advanced | Configure resource limits, health checks, and environment variables for a production FrontMCP deployment. | + +> See all examples in [`examples/deploy-to-node/`](../examples/deploy-to-node/) + ## Reference - **Docs:** https://docs.agentfront.dev/frontmcp/deployment/production-build 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 b99d65389..b80560f4e 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 @@ -63,3 +63,12 @@ description: Reference vercel.json configuration for deploying a FrontMCP server } ] } + +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------- | +| [`minimal-vercel-config`](../examples/deploy-to-vercel-config/minimal-vercel-config.md) | Basic | The minimum `vercel.json` needed to deploy a FrontMCP server to Vercel. | +| [`vercel-config-with-security-headers`](../examples/deploy-to-vercel-config/vercel-config-with-security-headers.md) | Intermediate | A complete `vercel.json` with per-route security headers for health, MCP, and all other endpoints. | + +> See all examples in [`examples/deploy-to-vercel-config/`](../examples/deploy-to-vercel-config/) 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 f69c66690..f9ecdf995 100644 --- a/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md +++ b/libs/skills/catalog/frontmcp-deployment/references/deploy-to-vercel.md @@ -223,6 +223,16 @@ Serverless functions are stateless between invocations. All persistent state mus | Bundle too large | Unnecessary dependencies included | Review dependencies and remove unused packages to reduce bundle size | | Cold starts too slow | Low function memory or large bundle | Increase memory to 1024 MB; audit dependencies; consider Vercel Fluid Compute | +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------- | +| [`vercel-mcp-endpoint-test`](../examples/deploy-to-vercel/vercel-mcp-endpoint-test.md) | Advanced | Verify a Vercel-deployed FrontMCP server by testing health, tool listing, and tool invocation. | +| [`vercel-with-kv`](../examples/deploy-to-vercel/vercel-with-kv.md) | Basic | Deploy a FrontMCP server to Vercel serverless functions with Vercel KV for session persistence. | +| [`vercel-with-skills-cache`](../examples/deploy-to-vercel/vercel-with-skills-cache.md) | Intermediate | Deploy a FrontMCP server to Vercel with skills enabled and KV-backed skill caching. | + +> See all examples in [`examples/deploy-to-vercel/`](../examples/deploy-to-vercel/) + ## Reference - **Docs:** https://docs.agentfront.dev/frontmcp/deployment/serverless diff --git a/libs/skills/catalog/frontmcp-development/SKILL.md b/libs/skills/catalog/frontmcp-development/SKILL.md index 4f4fc7344..f779b4a81 100644 --- a/libs/skills/catalog/frontmcp-development/SKILL.md +++ b/libs/skills/catalog/frontmcp-development/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/servers/overview + docs: https://docs.agentfront.dev/frontmcp/fundamentals/overview --- # FrontMCP Development Router @@ -122,5 +122,5 @@ Entry point for building MCP server components. This skill helps you find the ri ## Reference -- [Server Overview](https://docs.agentfront.dev/frontmcp/servers/overview) +- [FrontMCP Overview](https://docs.agentfront.dev/frontmcp/fundamentals/overview) - Related skills: `create-tool`, `create-resource`, `create-prompt`, `create-agent`, `create-provider`, `create-job`, `create-workflow`, `create-skill`, `create-skill-with-tools`, `decorators-guide`, `official-adapters`, `official-plugins` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-adapter/basic-api-adapter.md b/libs/skills/catalog/frontmcp-development/examples/create-adapter/basic-api-adapter.md new file mode 100644 index 000000000..96b692dae --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-adapter/basic-api-adapter.md @@ -0,0 +1,92 @@ +--- +name: basic-api-adapter +reference: create-adapter +level: basic +description: 'A minimal adapter that fetches operation definitions from an external API and generates MCP tools.' +tags: [development, adapter, api] +features: + - 'Extending `DynamicAdapter` with a typed options interface' + - 'Declaring `__options_brand` for proper TypeScript inference on `init()`' + - 'Implementing `fetch()` to return `FrontMcpAdapterResponse` with tools, resources, and prompts' + - 'Registering the adapter via the static `init()` method in the `adapters` array' +--- + +# Basic Dynamic Adapter + +A minimal adapter that fetches operation definitions from an external API and generates MCP tools. + +## Code + +```typescript +// src/adapters/my-api.adapter.ts +import { DynamicAdapter, type FrontMcpAdapterResponse } from '@frontmcp/sdk'; + +interface MyAdapterOptions { + endpoint: string; + apiKey: string; +} + +class MyApiAdapter extends DynamicAdapter { + declare __options_brand: MyAdapterOptions; + + async fetch(): Promise { + // Fetch definitions from external source + const res = await globalThis.fetch(this.options.endpoint, { + headers: { Authorization: `Bearer ${this.options.apiKey}` }, + }); + const schema = await res.json(); + + // Convert to MCP tool definitions + return { + tools: schema.operations.map((op: { name: string; description: string; params: Record }) => ({ + name: op.name, + description: op.description, + inputSchema: this.convertParams(op.params), + execute: async (input: Record) => { + return this.callApi(op.name, input); + }, + })), + resources: [], + prompts: [], + }; + } + + private convertParams(params: Record) { + // Convert external param definitions to Zod schemas + return {}; + } + + private async callApi(operation: string, input: Record) { + // Call the external API + return {}; + } +} +``` + +```typescript +// src/server.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'MyApp', + adapters: [ + MyApiAdapter.init({ + name: 'my-api', + endpoint: 'https://api.example.com/schema', + apiKey: process.env.API_KEY!, + }), + ], +}) +class MyApp {} +``` + +## What This Demonstrates + +- Extending `DynamicAdapter` with a typed options interface +- Declaring `__options_brand` for proper TypeScript inference on `init()` +- Implementing `fetch()` to return `FrontMcpAdapterResponse` with tools, resources, and prompts +- Registering the adapter via the static `init()` method in the `adapters` array + +## Related + +- See `create-adapter` for namespacing, error handling, and the full adapter response interface diff --git a/libs/skills/catalog/frontmcp-development/examples/create-adapter/namespaced-adapter.md b/libs/skills/catalog/frontmcp-development/examples/create-adapter/namespaced-adapter.md new file mode 100644 index 000000000..30f5e21ec --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-adapter/namespaced-adapter.md @@ -0,0 +1,124 @@ +--- +name: namespaced-adapter +reference: create-adapter +level: intermediate +description: 'An adapter that namespaces generated tools to avoid collisions and includes proper error handling for startup failures.' +tags: [development, adapter, namespaced] +features: + - "Namespacing tools with `name: 'adapter-name:operation-name'` to prevent collisions" + - 'Throwing descriptive errors in `fetch()` so misconfigurations surface at startup' + - 'Registering multiple instances of the same adapter class with different configurations' + - 'Validating the external response shape before generating tool definitions' +--- + +# Namespaced Adapter with Error Handling + +An adapter that namespaces generated tools to avoid collisions and includes proper error handling for startup failures. + +## Code + +```typescript +// src/adapters/graphql-api.adapter.ts +import { DynamicAdapter, type FrontMcpAdapterResponse } from '@frontmcp/sdk'; + +interface GraphqlAdapterOptions { + endpoint: string; + apiKey: string; + namespace?: string; +} + +class GraphqlApiAdapter extends DynamicAdapter { + declare __options_brand: GraphqlAdapterOptions; + + async fetch(): Promise { + const namespace = this.options.namespace ?? this.options.name; + + // Fetch schema from GraphQL introspection + const res = await globalThis.fetch(this.options.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ + query: '{ __schema { queryType { fields { name description } } } }', + }), + }); + + if (!res.ok) { + throw new Error(`GraphQL adapter failed to fetch schema from ${this.options.endpoint}: HTTP ${res.status}`); + } + + const schema = await res.json(); + const fields = schema.data?.__schema?.queryType?.fields; + + if (!fields || !Array.isArray(fields)) { + throw new Error(`GraphQL adapter received unexpected schema format from ${this.options.endpoint}`); + } + + // Namespace tools to prevent collisions across adapters + return { + tools: fields.map((field: { name: string; description: string }) => ({ + name: `${namespace}:${field.name}`, + description: field.description || `Query ${field.name} from GraphQL API`, + inputSchema: {}, + execute: async (input: Record) => { + return this.executeQuery(field.name, input); + }, + })), + }; + } + + private async executeQuery(queryName: string, variables: Record) { + const res = await globalThis.fetch(this.options.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ query: `{ ${queryName} }`, variables }), + }); + return res.json(); + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'DataApp', + adapters: [ + // Each adapter uses its name for tool namespacing + GraphqlApiAdapter.init({ + name: 'users-api', + endpoint: 'https://users.example.com/graphql', + apiKey: process.env.USERS_API_KEY!, + }), + GraphqlApiAdapter.init({ + name: 'orders-api', + endpoint: 'https://orders.example.com/graphql', + apiKey: process.env.ORDERS_API_KEY!, + }), + ], +}) +class DataApp {} + +@FrontMcp({ + info: { name: 'data-server', version: '1.0.0' }, + apps: [DataApp], +}) +class DataServer {} +``` + +## What This Demonstrates + +- Namespacing tools with `name: 'adapter-name:operation-name'` to prevent collisions +- Throwing descriptive errors in `fetch()` so misconfigurations surface at startup +- Registering multiple instances of the same adapter class with different configurations +- Validating the external response shape before generating tool definitions + +## Related + +- See `create-adapter` for the full `FrontMcpAdapterResponse` interface, Nx generator, and verification checklist diff --git a/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/anthropic-config.md b/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/anthropic-config.md new file mode 100644 index 000000000..87e4f6186 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/anthropic-config.md @@ -0,0 +1,81 @@ +--- +name: anthropic-config +reference: create-agent-llm-config +level: basic +description: 'Configuring an agent with the Anthropic provider and common model options.' +tags: [development, anthropic, llm, agent, config] +features: + - "Setting `provider: 'anthropic'` with a supported model (`claude-sonnet-4-20250514` or `claude-opus-4-20250514`)" + - "Using `{ env: 'ANTHROPIC_API_KEY' }` to read the API key from an environment variable" + - 'Setting `maxTokens` at the LLM config level and overriding per-call via `this.completion()` options' + - 'Passing `temperature` as a per-call option for controlling response creativity' +--- + +# Anthropic LLM Configuration + +Configuring an agent with the Anthropic provider and common model options. + +## Code + +```typescript +// src/apps/main/agents/summarizer.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Agent({ + name: 'summarizer', + description: 'Summarizes text using Anthropic Claude', + llm: { + provider: 'anthropic', + model: 'claude-sonnet-4-20250514', + apiKey: { env: 'ANTHROPIC_API_KEY' }, + maxTokens: 4096, + }, + inputSchema: { + text: z.string().describe('Text to summarize'), + }, +}) +class SummarizerAgent extends AgentContext { + async execute(input: { text: string }) { + const result = await this.completion( + { + messages: [{ role: 'user', content: `Summarize this text:\n${input.text}` }], + }, + { + maxTokens: 500, + temperature: 0.3, + }, + ); + + return result.content; + } +} +``` + +```typescript +// For the most capable model: +@Agent({ + name: 'complex_reasoner', + description: 'Handles complex reasoning tasks', + llm: { + provider: 'anthropic', + model: 'claude-opus-4-20250514', + apiKey: { env: 'ANTHROPIC_API_KEY' }, + maxTokens: 4096, + }, + // ... +}) +class ComplexReasonerAgent extends AgentContext {} +``` + +## What This Demonstrates + +- Setting `provider: 'anthropic'` with a supported model (`claude-sonnet-4-20250514` or `claude-opus-4-20250514`) +- Using `{ env: 'ANTHROPIC_API_KEY' }` to read the API key from an environment variable +- Setting `maxTokens` at the LLM config level and overriding per-call via `this.completion()` options +- Passing `temperature` as a per-call option for controlling response creativity + +## Related + +- See `create-agent-llm-config` for all supported providers and the common models table +- See `create-agent` for full agent patterns including inner tools and swarm configuration diff --git a/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/openai-config.md b/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/openai-config.md new file mode 100644 index 000000000..02ead71d6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-agent-llm-config/openai-config.md @@ -0,0 +1,80 @@ +--- +name: openai-config +reference: create-agent-llm-config +level: basic +description: 'Configuring an agent with the OpenAI provider and different model options.' +tags: [development, openai, llm, agent, config] +features: + - "Setting `provider: 'openai'` with `gpt-4o` for general purpose or `gpt-4o-mini` for cost-effective tasks" + - "The API key pattern `{ env: 'OPENAI_API_KEY' }` works the same across all providers" + - 'Combining LLM config with inner tools -- the agent uses OpenAI to reason about tool invocations' + - 'Choosing the right model for the task: `gpt-4o` for complex workflows, `gpt-4o-mini` for fast classification' +--- + +# OpenAI LLM Configuration + +Configuring an agent with the OpenAI provider and different model options. + +## Code + +```typescript +// src/apps/main/agents/data-pipeline.agent.ts +import { Agent, AgentContext, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'extract_data', + description: 'Extract data from a source', + inputSchema: { source: z.string() }, +}) +class ExtractTool extends ToolContext { + async execute(input: { source: string }) { + return { data: `extracted from ${input.source}` }; + } +} + +@Agent({ + name: 'data_pipeline', + description: 'Data processing pipeline agent', + llm: { + provider: 'openai', + model: 'gpt-4o', + apiKey: { env: 'OPENAI_API_KEY' }, + maxTokens: 4096, + }, + inputSchema: { + source: z.string().describe('Data source to process'), + }, + tools: [ExtractTool], + systemInstructions: 'You are a data processing agent. Extract and transform data from the given source.', +}) +class DataPipelineAgent extends AgentContext {} +``` + +```typescript +// For a cost-effective model: +@Agent({ + name: 'quick_classifier', + description: 'Fast classification of incoming requests', + llm: { + provider: 'openai', + model: 'gpt-4o-mini', + apiKey: { env: 'OPENAI_API_KEY' }, + maxTokens: 1024, + }, + // ... +}) +class QuickClassifierAgent extends AgentContext {} +``` + +## What This Demonstrates + +- Setting `provider: 'openai'` with `gpt-4o` for general purpose or `gpt-4o-mini` for cost-effective tasks +- The API key pattern `{ env: 'OPENAI_API_KEY' }` works the same across all providers +- Combining LLM config with inner tools -- the agent uses OpenAI to reason about tool invocations +- Choosing the right model for the task: `gpt-4o` for complex workflows, `gpt-4o-mini` for fast classification + +## Related + +- See `create-agent-llm-config` for the complete common models table and API key source options +- See `create-agent` for sub-agents using different providers for specialized tasks diff --git a/libs/skills/catalog/frontmcp-development/examples/create-agent/basic-agent-with-tools.md b/libs/skills/catalog/frontmcp-development/examples/create-agent/basic-agent-with-tools.md new file mode 100644 index 000000000..8e63ed082 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-agent/basic-agent-with-tools.md @@ -0,0 +1,121 @@ +--- +name: basic-agent-with-tools +reference: create-agent +level: basic +description: 'An autonomous agent that uses inner tools to review GitHub pull requests.' +tags: [development, anthropic, agent, tools] +features: + - 'Creating an agent with `@Agent` decorator, `llm` config, and `inputSchema`' + - 'Defining inner tools in the `tools` array that the agent can invoke during its reasoning loop' + - "Using `{ env: 'ANTHROPIC_API_KEY' }` for safe API key configuration" + - 'Inner tools are private to the agent and not exposed to external MCP clients' + - 'The default `execute()` runs the full agent loop without needing an override' +--- + +# Basic Agent with Inner Tools + +An autonomous agent that uses inner tools to review GitHub pull requests. + +## Code + +```typescript +// src/apps/review/tools/fetch-pr.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'fetch_pr', + description: 'Fetch pull request details from GitHub', + inputSchema: { + owner: z.string(), + repo: z.string(), + number: z.number(), + }, +}) +class FetchPRTool extends ToolContext { + async execute(input: { owner: string; repo: string; number: number }) { + const response = await this.fetch( + `https://api.github.com/repos/${input.owner}/${input.repo}/pulls/${input.number}`, + ); + return response.json(); + } +} +``` + +```typescript +// src/apps/review/tools/post-review.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'post_review_comment', + description: 'Post a review comment on a PR', + inputSchema: { + owner: z.string(), + repo: z.string(), + number: z.number(), + body: z.string(), + }, +}) +class PostReviewCommentTool extends ToolContext { + async execute(input: { owner: string; repo: string; number: number; body: string }) { + await this.fetch(`https://api.github.com/repos/${input.owner}/${input.repo}/pulls/${input.number}/reviews`, { + method: 'POST', + body: JSON.stringify({ body: input.body, event: 'COMMENT' }), + }); + return 'Comment posted'; + } +} +``` + +```typescript +// src/apps/review/agents/pr-reviewer.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Agent({ + name: 'pr_reviewer', + description: 'Autonomously reviews GitHub pull requests', + llm: { + provider: 'anthropic', + model: 'claude-sonnet-4-20250514', + apiKey: { env: 'ANTHROPIC_API_KEY' }, + }, + inputSchema: { + owner: z.string().describe('Repository owner'), + repo: z.string().describe('Repository name'), + prNumber: z.number().describe('PR number to review'), + }, + systemInstructions: 'You are a senior code reviewer. Fetch the PR, analyze changes, and post a thorough review.', + tools: [FetchPRTool, PostReviewCommentTool], +}) +class PRReviewerAgent extends AgentContext { + // Default execute() runs the agent loop. + // The agent will autonomously call FetchPRTool, analyze the diff, + // and call PostReviewCommentTool to leave a review. +} +``` + +```typescript +// src/apps/review/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'review-app', + agents: [PRReviewerAgent], +}) +class ReviewApp {} +``` + +## What This Demonstrates + +- Creating an agent with `@Agent` decorator, `llm` config, and `inputSchema` +- Defining inner tools in the `tools` array that the agent can invoke during its reasoning loop +- Using `{ env: 'ANTHROPIC_API_KEY' }` for safe API key configuration +- Inner tools are private to the agent and not exposed to external MCP clients +- The default `execute()` runs the full agent loop without needing an override + +## Related + +- See `create-agent` for custom execute, sub-agents, swarm configuration, and exported tools +- See `create-agent-llm-config` for all supported LLM providers and model options diff --git a/libs/skills/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md b/libs/skills/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md new file mode 100644 index 000000000..3aff3c114 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md @@ -0,0 +1,95 @@ +--- +name: custom-multi-pass-agent +reference: create-agent +level: intermediate +description: 'An agent that overrides `execute()` to perform multi-pass LLM reasoning with `this.completion()`.' +tags: [development, security, agent, custom, multi, pass] +features: + - 'Overriding `execute()` for custom multi-pass orchestration instead of the default agent loop' + - 'Using `this.completion()` to make individual LLM calls with full control over prompts' + - 'Using `this.mark(stage)` to track execution stages (security-pass, quality-pass, synthesis)' + - 'Defining `outputSchema` with Zod to validate and type-check the structured return value' +--- + +# Custom Multi-Pass Agent with Structured Output + +An agent that overrides `execute()` to perform multi-pass LLM reasoning with `this.completion()`. + +## Code + +```typescript +// src/apps/review/agents/structured-reviewer.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Agent({ + name: 'structured_reviewer', + description: 'Reviews code with a structured multi-pass approach', + llm: { + provider: 'anthropic', + model: 'claude-sonnet-4-20250514', + apiKey: { env: 'ANTHROPIC_API_KEY' }, + }, + inputSchema: { + code: z.string().describe('Source code to review'), + }, + outputSchema: { + issues: z.array( + z.object({ + severity: z.enum(['error', 'warning', 'info']), + line: z.number(), + message: z.string(), + }), + ), + summary: z.string(), + }, +}) +class StructuredReviewerAgent extends AgentContext { + async execute(input: { code: string }) { + this.mark('security-pass'); + const securityReview = await this.completion({ + messages: [{ role: 'user', content: `Review this code for security issues:\n${input.code}` }], + }); + + this.mark('quality-pass'); + const qualityReview = await this.completion({ + messages: [{ role: 'user', content: `Review this code for quality issues:\n${input.code}` }], + }); + + this.mark('synthesis'); + const finalReview = await this.completion({ + messages: [ + { + role: 'user', + content: `Combine these reviews into a structured JSON report with "issues" (array of {severity, line, message}) and "summary" (string):\nSecurity: ${securityReview.content}\nQuality: ${qualityReview.content}`, + }, + ], + }); + + return JSON.parse(finalReview.content); + } +} +``` + +```typescript +// src/apps/review/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'review-app', + agents: [StructuredReviewerAgent], +}) +class ReviewApp {} +``` + +## What This Demonstrates + +- Overriding `execute()` for custom multi-pass orchestration instead of the default agent loop +- Using `this.completion()` to make individual LLM calls with full control over prompts +- Using `this.mark(stage)` to track execution stages (security-pass, quality-pass, synthesis) +- Defining `outputSchema` with Zod to validate and type-check the structured return value + +## Related + +- See `create-agent` for streaming with `streamCompletion()`, sub-agents, and swarm handoff +- See `create-agent-llm-config` for provider-specific options like `maxTokens` and `temperature` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md b/libs/skills/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md new file mode 100644 index 000000000..05a8791a9 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md @@ -0,0 +1,111 @@ +--- +name: nested-agents-with-swarm +reference: create-agent +level: advanced +description: 'Composing specialized sub-agents and configuring swarm-based handoff between agents.' +tags: [development, agent, nested, agents, swarm] +features: + - "Configuring `swarm` with `role: 'coordinator'` for the triage agent and `role: 'specialist'` for domain agents" + - 'Defining `handoff` rules with `agent` name and `condition` for declarative LLM-driven routing' + - 'Specialist agents can hand back to the triage agent when a request falls outside their scope' + - 'Each agent has its own `llm` config, `tools`, and `systemInstructions` for specialization' +--- + +# Nested Sub-Agents and Swarm Handoff + +Composing specialized sub-agents and configuring swarm-based handoff between agents. + +## Code + +```typescript +// src/apps/support/agents/billing.agent.ts +import { Agent, AgentContext, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'lookup_invoice', + description: 'Look up an invoice by ID', + inputSchema: { invoiceId: z.string() }, +}) +class LookupInvoiceTool extends ToolContext { + async execute(input: { invoiceId: string }) { + return { id: input.invoiceId, amount: 99.99, status: 'paid' }; + } +} + +@Agent({ + name: 'billing_agent', + description: 'Handles billing and payment inquiries', + llm: { provider: 'anthropic', model: 'claude-sonnet-4-20250514', apiKey: { env: 'ANTHROPIC_API_KEY' } }, + tools: [LookupInvoiceTool], + swarm: { + role: 'specialist', + handoff: [{ agent: 'triage_agent', condition: 'Request is outside billing scope' }], + }, +}) +class BillingAgent extends AgentContext {} +``` + +```typescript +// src/apps/support/agents/technical.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; + +@Agent({ + name: 'technical_agent', + description: 'Handles technical support issues', + llm: { provider: 'anthropic', model: 'claude-sonnet-4-20250514', apiKey: { env: 'ANTHROPIC_API_KEY' } }, + systemInstructions: 'You are a technical support specialist. Diagnose issues and provide solutions.', + swarm: { + role: 'specialist', + handoff: [{ agent: 'triage_agent', condition: 'Request is outside technical scope' }], + }, +}) +class TechnicalAgent extends AgentContext {} +``` + +```typescript +// src/apps/support/agents/triage.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Agent({ + name: 'triage_agent', + description: 'Triages incoming requests and hands off to specialists', + llm: { provider: 'anthropic', model: 'claude-sonnet-4-20250514', apiKey: { env: 'ANTHROPIC_API_KEY' } }, + inputSchema: { + request: z.string().describe('The incoming user request'), + }, + swarm: { + role: 'coordinator', + handoff: [ + { agent: 'billing_agent', condition: 'Request is about billing or payments' }, + { agent: 'technical_agent', condition: 'Request is about technical issues' }, + ], + }, + systemInstructions: 'Analyze the request and hand off to the appropriate specialist agent.', +}) +class TriageAgent extends AgentContext {} +``` + +```typescript +// src/apps/support/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'support-app', + agents: [TriageAgent, BillingAgent, TechnicalAgent], +}) +class SupportApp {} +``` + +## What This Demonstrates + +- Configuring `swarm` with `role: 'coordinator'` for the triage agent and `role: 'specialist'` for domain agents +- Defining `handoff` rules with `agent` name and `condition` for declarative LLM-driven routing +- Specialist agents can hand back to the triage agent when a request falls outside their scope +- Each agent has its own `llm` config, `tools`, and `systemInstructions` for specialization + +## Related + +- See `create-agent` for exported tools, function-style builder, providers, and rate limiting +- See `create-agent-llm-config` for using different LLM providers per agent diff --git a/libs/skills/catalog/frontmcp-development/examples/create-job/basic-report-job.md b/libs/skills/catalog/frontmcp-development/examples/create-job/basic-report-job.md new file mode 100644 index 000000000..daad71d96 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-job/basic-report-job.md @@ -0,0 +1,87 @@ +--- +name: basic-report-job +reference: create-job +level: basic +description: 'A minimal job that generates a report with progress tracking and structured output.' +tags: [development, job, report] +features: + - 'Defining a job with `@Job` decorator including `inputSchema`, `outputSchema`, and `timeout`' + - 'Reporting progress at each stage using `this.progress(pct, total, message)`' + - 'Using `this.log()` for persistent, queryable log entries' +--- + +# Basic Report Generation Job + +A minimal job that generates a report with progress tracking and structured output. + +## Code + +```typescript +// src/jobs/generate-report.job.ts +import { Job, JobContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Job({ + name: 'generate-report', + description: 'Generate a PDF report from data', + inputSchema: { + reportType: z.enum(['sales', 'inventory', 'users']).describe('Type of report'), + dateRange: z.object({ + from: z.string().describe('Start date (ISO 8601)'), + to: z.string().describe('End date (ISO 8601)'), + }), + format: z.enum(['pdf', 'csv']).default('pdf').describe('Output format'), + }, + outputSchema: { + url: z.string().url(), + pageCount: z.number().int(), + generatedAt: z.string(), + }, + timeout: 120000, +}) +class GenerateReportJob extends JobContext { + async execute(input: { + reportType: 'sales' | 'inventory' | 'users'; + dateRange: { from: string; to: string }; + format: 'pdf' | 'csv'; + }) { + this.log(`Starting ${input.reportType} report generation`); + + this.progress(10, 100, 'Fetching data'); + const data = await this.fetchReportData(input.reportType, input.dateRange); + + this.progress(50, 100, 'Generating document'); + const document = await this.buildDocument(data, input.format); + + this.progress(90, 100, 'Uploading'); + const url = await this.uploadDocument(document); + + this.progress(100, 100, 'Complete'); + return { + url, + pageCount: document.pages, + generatedAt: new Date().toISOString(), + }; + } + + private async fetchReportData(type: string, range: { from: string; to: string }) { + return { rows: [], count: 0 }; + } + private async buildDocument(data: unknown, format: string) { + return { pages: 5, buffer: Buffer.alloc(0) }; + } + private async uploadDocument(doc: { buffer: Buffer }) { + return 'https://storage.example.com/reports/report-001.pdf'; + } +} +``` + +## What This Demonstrates + +- Defining a job with `@Job` decorator including `inputSchema`, `outputSchema`, and `timeout` +- Reporting progress at each stage using `this.progress(pct, total, message)` +- Using `this.log()` for persistent, queryable log entries + +## Related + +- See `create-job` for the full API reference including retry policies and permissions diff --git a/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-permissions.md b/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-permissions.md new file mode 100644 index 000000000..b600664dd --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-permissions.md @@ -0,0 +1,117 @@ +--- +name: job-with-permissions +reference: create-job +level: advanced +description: 'A data export job with declarative permission controls, plus a function-style job for simple tasks.' +tags: [development, redis, job, permissions] +features: + - 'Declarative `permissions` with `actions`, `roles`, `scopes`, and a custom `predicate`' + - 'Using `tags` and `labels` for categorization and filtering' + - 'The `job()` function builder for simple jobs that need no class' + - 'Full server registration with `jobs.enabled: true` and a Redis store' +--- + +# Job with Permissions, Tags, and Function Builder + +A data export job with declarative permission controls, plus a function-style job for simple tasks. + +## Code + +```typescript +// src/jobs/data-export.job.ts +import { Job, JobContext, job } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Job({ + name: 'data-export', + description: 'Export data to external storage', + inputSchema: { + dataset: z.string(), + destination: z.string().url(), + }, + outputSchema: { + exportedRows: z.number().int(), + location: z.string().url(), + }, + tags: ['export', 'data'], + labels: { team: 'data-engineering', priority: 'high' }, + permissions: { + actions: ['create', 'read', 'execute', 'list'], + roles: ['admin', 'data-engineer'], + scopes: ['jobs:write', 'data:export'], + predicate: (ctx) => ctx.user?.department === 'engineering', + }, +}) +class DataExportJob extends JobContext { + async execute(input: { dataset: string; destination: string }) { + this.log(`Exporting dataset: ${input.dataset}`); + const rows = await this.exportData(input.dataset, input.destination); + return { exportedRows: rows, location: input.destination }; + } + + private async exportData(dataset: string, destination: string) { + return 1000; + } +} + +// Function-style job for simple tasks +const CleanupTempFiles = job({ + name: 'cleanup-temp-files', + description: 'Remove temporary files older than the specified age', + inputSchema: { + directory: z.string().describe('Directory to clean'), + maxAgeDays: z.number().int().min(1).default(7), + }, + outputSchema: { + deleted: z.number().int(), + freedBytes: z.number().int(), + }, +})((input, ctx) => { + ctx.log(`Cleaning ${input.directory}, max age: ${input.maxAgeDays} days`); + ctx.progress(0, 100, 'Scanning directory'); + + // ... scan and delete logic ... + + ctx.progress(100, 100, 'Cleanup complete'); + return { deleted: 42, freedBytes: 1024000 }; +}); +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'data-app', + jobs: [DataExportJob, CleanupTempFiles], +}) +class DataApp {} + +@FrontMcp({ + info: { name: 'data-server', version: '1.0.0' }, + apps: [DataApp], + jobs: { + enabled: true, + store: { + redis: { + provider: 'redis', + host: 'localhost', + port: 6379, + keyPrefix: 'mcp:jobs:', + }, + }, + }, +}) +class DataServer {} +``` + +## What This Demonstrates + +- Declarative `permissions` with `actions`, `roles`, `scopes`, and a custom `predicate` +- Using `tags` and `labels` for categorization and filtering +- The `job()` function builder for simple jobs that need no class +- Full server registration with `jobs.enabled: true` and a Redis store + +## Related + +- See `create-job` for the complete permissions reference and all job registration options diff --git a/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-retry.md b/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-retry.md new file mode 100644 index 000000000..e20e7c778 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-job/job-with-retry.md @@ -0,0 +1,88 @@ +--- +name: job-with-retry +reference: create-job +level: intermediate +description: 'A job that syncs data from an external API with automatic retry, exponential backoff, and batch progress tracking.' +tags: [development, job, retry] +features: + - 'Configuring `retry` with `maxAttempts`, `backoffMs`, `backoffMultiplier`, and `maxBackoffMs`' + - 'Using `this.attempt` to log retry context (1-based attempt counter)' + - 'Using `this.fail()` to abort execution and trigger the retry flow' + - 'Combining batch processing with `this.progress()` for granular tracking' +--- + +# Job with Retry Policy and Batch Processing + +A job that syncs data from an external API with automatic retry, exponential backoff, and batch progress tracking. + +## Code + +```typescript +// src/jobs/sync-external-api.job.ts +import { Job, JobContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Job({ + name: 'sync-external-api', + description: 'Synchronize data from an external API', + inputSchema: { + endpoint: z.string().url().describe('API endpoint to sync from'), + batchSize: z.number().int().min(1).max(1000).default(100), + }, + outputSchema: { + synced: z.number().int(), + errors: z.number().int(), + }, + timeout: 600000, // 10 minutes + retry: { + maxAttempts: 5, + backoffMs: 2000, + backoffMultiplier: 2, + maxBackoffMs: 60000, + }, +}) +class SyncExternalApiJob extends JobContext { + async execute(input: { endpoint: string; batchSize: number }) { + this.log(`Attempt ${this.attempt}: syncing from ${input.endpoint}`); + + const response = await this.fetch(input.endpoint); + if (!response.ok) { + this.fail(new Error(`API returned ${response.status}`)); + } + + const data = await response.json(); + let synced = 0; + let errors = 0; + + for (let i = 0; i < data.items.length; i += input.batchSize) { + const batch = data.items.slice(i, i + input.batchSize); + this.progress(i, data.items.length, `Processing batch ${Math.floor(i / input.batchSize) + 1}`); + + try { + await this.processBatch(batch); + synced += batch.length; + } catch (err) { + errors += batch.length; + this.log(`Batch error: ${err}`); + } + } + + return { synced, errors }; + } + + private async processBatch(batch: unknown[]) { + // process batch + } +} +``` + +## What This Demonstrates + +- Configuring `retry` with `maxAttempts`, `backoffMs`, `backoffMultiplier`, and `maxBackoffMs` +- Using `this.attempt` to log retry context (1-based attempt counter) +- Using `this.fail()` to abort execution and trigger the retry flow +- Combining batch processing with `this.progress()` for granular tracking + +## Related + +- See `create-job` for the full retry policy reference and backoff schedule diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/basic-logging-plugin.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/basic-logging-plugin.md new file mode 100644 index 000000000..c521f2ef6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/basic-logging-plugin.md @@ -0,0 +1,69 @@ +--- +name: basic-logging-plugin +reference: create-plugin-hooks +level: basic +description: 'Demonstrates a plugin that logs tool execution using `@Will` and `@Did` hook decorators from the pre-built `ToolHook` export.' +tags: [development, plugin-hooks, plugin, hooks, logging] +features: + - "Using `ToolHook` pre-built export instead of calling `FlowHooksOf('tools:call-tool')` directly" + - 'Destructuring `Will` and `Did` decorators from the hook object' + - 'Setting `priority: 100` on `@Will` to ensure the logging hook runs early' + - 'Registering a plugin in the `plugins` array of `@App`' +--- + +# Basic Logging Plugin with @Will and @Did + +Demonstrates a plugin that logs tool execution using `@Will` and `@Did` hook decorators from the pre-built `ToolHook` export. + +## Code + +```typescript +// src/plugins/logging.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import { ToolHook } from '@frontmcp/sdk'; + +const { Will, Did } = ToolHook; + +@Plugin({ name: 'logging-plugin' }) +export class LoggingPlugin { + @Will('execute', { priority: 100 }) + logBefore(ctx) { + console.log(`[LOG] Tool "${ctx.toolName}" called with`, ctx.input); + } + + @Did('execute') + logAfter(ctx) { + console.log(`[LOG] Tool "${ctx.toolName}" completed in ${ctx.elapsed()}ms`); + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { LoggingPlugin } from './plugins/logging.plugin'; + +@App({ + name: 'my-app', + plugins: [LoggingPlugin], +}) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MyApp], +}) +class MyServer {} +``` + +## What This Demonstrates + +- Using `ToolHook` pre-built export instead of calling `FlowHooksOf('tools:call-tool')` directly +- Destructuring `Will` and `Did` decorators from the hook object +- Setting `priority: 100` on `@Will` to ensure the logging hook runs early +- Registering a plugin in the `plugins` array of `@App` + +## Related + +- See `create-plugin-hooks` for the full hook decorator API reference +- See `official-plugins` for ready-made plugins that include logging capabilities diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/caching-with-around.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/caching-with-around.md new file mode 100644 index 000000000..25462e615 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/caching-with-around.md @@ -0,0 +1,80 @@ +--- +name: caching-with-around +reference: create-plugin-hooks +level: intermediate +description: 'Demonstrates wrapping tool execution with an `@Around` hook to implement result caching with TTL-based expiry.' +tags: [development, cache, plugin-hooks, plugin, hooks, caching] +features: + - 'Using `@Around` to wrap the `execute` stage with before-and-after logic' + - 'Calling `await next()` to invoke the original stage and capture its result' + - 'Short-circuiting execution by returning cached data without calling `next()`' + - 'Building a cache key from `ctx.toolName` and `ctx.input`' +--- + +# Caching Plugin with @Around Hook + +Demonstrates wrapping tool execution with an `@Around` hook to implement result caching with TTL-based expiry. + +## Code + +```typescript +// src/plugins/cache.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import { ToolHook } from '@frontmcp/sdk'; + +const { Around } = ToolHook; + +@Plugin({ name: 'cache-plugin' }) +export class CachePlugin { + private cache = new Map(); + + @Around('execute', { priority: 90 }) + async cacheResults(ctx, next) { + const key = `${ctx.toolName}:${JSON.stringify(ctx.input)}`; + const cached = this.cache.get(key); + + if (cached && cached.expiry > Date.now()) { + return cached.data; + } + + const result = await next(); + + this.cache.set(key, { + data: result, + expiry: Date.now() + 60_000, + }); + + return result; + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { CachePlugin } from './plugins/cache.plugin'; + +@App({ + name: 'my-app', + plugins: [CachePlugin], +}) +class MyApp {} + +@FrontMcp({ + info: { name: 'cached-server', version: '1.0.0' }, + apps: [MyApp], +}) +class MyServer {} +``` + +## What This Demonstrates + +- Using `@Around` to wrap the `execute` stage with before-and-after logic +- Calling `await next()` to invoke the original stage and capture its result +- Short-circuiting execution by returning cached data without calling `next()` +- Building a cache key from `ctx.toolName` and `ctx.input` + +## Related + +- See `create-plugin-hooks` for all hook decorator types and their timing +- See `official-plugins` for the production-ready `CachePlugin` from `@frontmcp/plugin-cache` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md new file mode 100644 index 000000000..c0cc19e03 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md @@ -0,0 +1,100 @@ +--- +name: tool-level-hooks-and-stage-replacement +reference: create-plugin-hooks +level: advanced +description: 'Demonstrates two advanced patterns: adding `@Will`/`@Did` hooks directly on a `@Tool` class (scoped to that tool only), and using `@Stage` in a plugin to replace a flow stage entirely with a filtered mock.' +tags: [development, plugin-hooks, plugin, hooks, tool, level] +features: + - "Placing `@Will('execute')` and `@Did('execute')` directly on a `@Tool` class so hooks fire only for that tool" + - 'Using `this.fail()` in a `@Will` hook to abort execution when preconditions are not met' + - 'Using `this.mark()` to record lifecycle checkpoints during hook execution' + - 'Using `@Stage` with a `filter` predicate to replace the `execute` stage only for a specific tool name' + - 'The difference between tool-level hooks (scoped to one tool) and plugin-level hooks (fire for all tools)' +--- + +# Tool-Level Hooks and Stage Replacement + +Demonstrates two advanced patterns: adding `@Will`/`@Did` hooks directly on a `@Tool` class (scoped to that tool only), and using `@Stage` in a plugin to replace a flow stage entirely with a filtered mock. + +## Code + +```typescript +// src/tools/process-order.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { ToolHook } from '@frontmcp/sdk'; +import { z } from 'zod'; + +const { Will, Did } = ToolHook; + +@Tool({ + name: 'process_order', + description: 'Process a customer order', + inputSchema: { + orderId: z.string(), + amount: z.number(), + }, + outputSchema: { status: z.string(), receipt: z.string() }, +}) +class ProcessOrderTool extends ToolContext { + @Will('execute', { priority: 10 }) + async beforeExecute() { + const db = this.get(DB_TOKEN); + const order = await db.findOrder(this.input.orderId); + if (!order) { + this.fail(new Error(`Order ${this.input.orderId} not found`)); + } + if (order.status === 'completed') { + this.fail(new Error('Order already processed')); + } + this.mark('validated'); + } + + async execute(input: { orderId: string; amount: number }) { + const payment = this.get(PAYMENT_TOKEN); + const receipt = await payment.charge(input.orderId, input.amount); + return { status: 'completed', receipt: receipt.id }; + } + + @Did('execute') + async afterExecute() { + const analytics = this.tryGet(ANALYTICS_TOKEN); + if (analytics) { + await analytics.track('order_processed', { + orderId: this.input.orderId, + amount: this.input.amount, + }); + } + } +} +``` + +```typescript +// src/plugins/mock.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import { ToolHook } from '@frontmcp/sdk'; + +const { Stage } = ToolHook; + +@Plugin({ name: 'mock-plugin' }) +export class MockPlugin { + @Stage('execute', { + filter: (ctx) => ctx.toolName === 'fetch_weather', + }) + mockWeather(ctx) { + return { content: [{ type: 'text', text: '72F and sunny' }] }; + } +} +``` + +## What This Demonstrates + +- Placing `@Will('execute')` and `@Did('execute')` directly on a `@Tool` class so hooks fire only for that tool +- Using `this.fail()` in a `@Will` hook to abort execution when preconditions are not met +- Using `this.mark()` to record lifecycle checkpoints during hook execution +- Using `@Stage` with a `filter` predicate to replace the `execute` stage only for a specific tool name +- The difference between tool-level hooks (scoped to one tool) and plugin-level hooks (fire for all tools) + +## Related + +- See `create-plugin-hooks` for the full list of hookable stages in the `call-tool` flow +- See `decorators-guide` for the complete decorator hierarchy including `@Tool` and `@Plugin` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin/basic-plugin-with-provider.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin/basic-plugin-with-provider.md new file mode 100644 index 000000000..145f02f58 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin/basic-plugin-with-provider.md @@ -0,0 +1,69 @@ +--- +name: basic-plugin-with-provider +reference: create-plugin +level: basic +description: 'A minimal plugin that contributes an injectable service via the `providers` and `exports` arrays.' +tags: [development, plugin, provider] +features: + - 'Creating a plugin with `@Plugin` decorator that bundles a `@Provider` class' + - 'Listing providers in both `providers` (for DI registration) and `exports` (for external access)' + - 'Registering a plugin in the `plugins` array of `@FrontMcp`' +--- + +# Basic Plugin with a Provider + +A minimal plugin that contributes an injectable service via the `providers` and `exports` arrays. + +## Code + +```typescript +// src/plugins/audit-log/providers/audit-logger.provider.ts +import { Provider } from '@frontmcp/sdk'; + +@Provider() +export class AuditLogger { + async logToolCall(toolName: string, userId: string, input: unknown): Promise { + console.log(`[AUDIT] ${userId} called ${toolName}`, input); + } +} +``` + +```typescript +// src/plugins/audit-log/audit-log.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import { AuditLogger } from './providers/audit-logger.provider'; + +@Plugin({ + name: 'audit-log', + description: 'Logs tool executions for audit compliance', + providers: [AuditLogger], + exports: [AuditLogger], +}) +export default class AuditLogPlugin {} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import AuditLogPlugin from './plugins/audit-log/audit-log.plugin'; + +@App({ name: 'MyApp' }) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MyApp], + plugins: [AuditLogPlugin], +}) +class MyServer {} +``` + +## What This Demonstrates + +- Creating a plugin with `@Plugin` decorator that bundles a `@Provider` class +- Listing providers in both `providers` (for DI registration) and `exports` (for external access) +- Registering a plugin in the `plugins` array of `@FrontMcp` + +## Related + +- See `create-plugin` for context extensions, DynamicPlugin, and metadata augmentation diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin/configurable-dynamic-plugin.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin/configurable-dynamic-plugin.md new file mode 100644 index 000000000..06cb985cb --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin/configurable-dynamic-plugin.md @@ -0,0 +1,178 @@ +--- +name: configurable-dynamic-plugin +reference: create-plugin +level: advanced +description: 'A plugin that accepts runtime configuration via `DynamicPlugin` and extends decorator metadata with custom fields.' +tags: [development, plugin, configurable, dynamic] +features: + - 'Extending `DynamicPlugin` for runtime-configurable plugins' + - 'Implementing `static dynamicProviders()` to create providers from the input options' + - 'Using `TInput` with optional fields and applying defaults in the constructor' + - 'Extending decorator metadata via `declare global { interface ExtendFrontMcpToolMetadata }`' + - 'Augmenting both `ExecutionContextBase` and `PromptContext` for full context extension coverage' + - 'Registering the plugin with `MyPlugin.init({ ... })` in the `plugins` array' +--- + +# Configurable Plugin with DynamicPlugin and Metadata Extension + +A plugin that accepts runtime configuration via `DynamicPlugin` and extends decorator metadata with custom fields. + +## Code + +```typescript +// src/plugins/my-plugin/my-plugin.types.ts +export interface MyPluginOptions { + endpoint: string; + refreshIntervalMs: number; +} + +export type MyPluginOptionsInput = Omit & { + refreshIntervalMs?: number; +}; + +// Extend the @Tool decorator metadata with a custom field +declare global { + interface ExtendFrontMcpToolMetadata { + audit?: { + enabled: boolean; + level: 'info' | 'warn' | 'critical'; + }; + } +} +``` + +```typescript +// src/plugins/my-plugin/my-plugin.symbols.ts +import type { Token } from '@frontmcp/sdk'; +import type { MyService } from './providers/my-service.provider'; + +export const MyServiceToken: Token = Symbol('MyService'); +``` + +```typescript +// src/plugins/my-plugin/providers/my-service.provider.ts +import { Provider } from '@frontmcp/sdk'; +import type { MyPluginOptions } from '../my-plugin.types'; + +@Provider() +export class MyService { + private readonly endpoint: string; + private readonly refreshIntervalMs: number; + + constructor(options: MyPluginOptions) { + this.endpoint = options.endpoint; + this.refreshIntervalMs = options.refreshIntervalMs; + } + + async query(params: Record): Promise { + const res = await globalThis.fetch(this.endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + return res.json(); + } +} +``` + +```typescript +// src/plugins/my-plugin/my-plugin.context-extension.ts +import type { MyService } from './providers/my-service.provider'; + +declare module '@frontmcp/sdk' { + interface ExecutionContextBase { + readonly myService: MyService; + } + interface PromptContext { + readonly myService: MyService; + } +} +``` + +```typescript +// src/plugins/my-plugin/my-plugin.plugin.ts +import { Plugin, DynamicPlugin, ProviderType } from '@frontmcp/sdk'; +import { MyService } from './providers/my-service.provider'; +import { MyServiceToken } from './my-plugin.symbols'; +import type { MyPluginOptions, MyPluginOptionsInput } from './my-plugin.types'; +import './my-plugin.context-extension'; + +@Plugin({ + name: 'my-plugin', + description: 'A configurable plugin with context extensions', + contextExtensions: [ + { + property: 'myService', + token: MyServiceToken, + errorMessage: 'MyPlugin is not installed.', + }, + ], +}) +export default class MyPlugin extends DynamicPlugin { + options: MyPluginOptions; + + constructor(options: MyPluginOptionsInput = { endpoint: '' }) { + super(); + this.options = { refreshIntervalMs: 30_000, ...options }; + } + + static override dynamicProviders(options: MyPluginOptionsInput): ProviderType[] { + return [ + { + provide: MyServiceToken, + useFactory: () => + new MyService({ + refreshIntervalMs: 30_000, + ...options, + }), + }, + ]; + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App, Tool, ToolContext } from '@frontmcp/sdk'; +import MyPlugin from './plugins/my-plugin/my-plugin.plugin'; + +// Tool using the extended metadata field and context extension +@Tool({ + name: 'delete_user', + audit: { enabled: true, level: 'critical' }, // Custom metadata from ExtendFrontMcpToolMetadata +}) +class DeleteUserTool extends ToolContext { + async execute(input: { userId: string }) { + const result = await this.myService.query({ action: 'delete', userId: input.userId }); + return result; + } +} + +@App({ name: 'MyApp', tools: [DeleteUserTool] }) +class MyApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MyApp], + plugins: [ + MyPlugin.init({ + endpoint: 'https://api.example.com', + refreshIntervalMs: 60_000, + }), + ], +}) +class MyServer {} +``` + +## What This Demonstrates + +- Extending `DynamicPlugin` for runtime-configurable plugins +- Implementing `static dynamicProviders()` to create providers from the input options +- Using `TInput` with optional fields and applying defaults in the constructor +- Extending decorator metadata via `declare global { interface ExtendFrontMcpToolMetadata }` +- Augmenting both `ExecutionContextBase` and `PromptContext` for full context extension coverage +- Registering the plugin with `MyPlugin.init({ ... })` in the `plugins` array + +## Related + +- See `create-plugin` for the full list of extensible metadata interfaces and the recommended folder structure diff --git a/libs/skills/catalog/frontmcp-development/examples/create-plugin/plugin-with-context-extension.md b/libs/skills/catalog/frontmcp-development/examples/create-plugin/plugin-with-context-extension.md new file mode 100644 index 000000000..c12ce591a --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-plugin/plugin-with-context-extension.md @@ -0,0 +1,107 @@ +--- +name: plugin-with-context-extension +reference: create-plugin +level: intermediate +description: 'A plugin that adds a `this.auditLog` property to all execution contexts using context extensions and module augmentation.' +tags: [development, sdk, plugin, context, extension] +features: + - "Defining a typed DI token with `Token = Symbol('...')` in a dedicated symbols file" + - "Module augmentation via `declare module '@frontmcp/sdk'` to add `readonly auditLog` to `ExecutionContextBase`" + - 'Registering `contextExtensions` in `@Plugin` metadata with `property`, `token`, and `errorMessage`' + - 'Side-effect import of the context extension file in both the plugin and the barrel export' + - 'Accessing the extended property (`this.auditLog`) in tool execution contexts' +--- + +# Plugin with Context Extension + +A plugin that adds a `this.auditLog` property to all execution contexts using context extensions and module augmentation. + +## Code + +```typescript +// src/plugins/audit-log/audit-log.symbols.ts +import type { Token } from '@frontmcp/sdk'; +import type { AuditLogger } from './providers/audit-logger.provider'; + +export const AuditLoggerToken: Token = Symbol('AuditLogger'); +``` + +```typescript +// src/plugins/audit-log/providers/audit-logger.provider.ts +import { Provider } from '@frontmcp/sdk'; + +@Provider() +export class AuditLogger { + async logToolCall(toolName: string, userId: string, input: unknown): Promise { + console.log(`[AUDIT] ${userId} called ${toolName}`, input); + } +} +``` + +```typescript +// src/plugins/audit-log/audit-log.context-extension.ts +import type { AuditLogger } from './providers/audit-logger.provider'; + +declare module '@frontmcp/sdk' { + interface ExecutionContextBase { + /** Audit logger provided by AuditLogPlugin */ + readonly auditLog: AuditLogger; + } +} +``` + +```typescript +// src/plugins/audit-log/audit-log.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import { AuditLogger } from './providers/audit-logger.provider'; +import { AuditLoggerToken } from './audit-log.symbols'; +import './audit-log.context-extension'; // Side-effect import for type augmentation + +@Plugin({ + name: 'audit-log', + description: 'Logs tool executions for audit compliance', + providers: [{ provide: AuditLoggerToken, useClass: AuditLogger }], + exports: [AuditLoggerToken], + contextExtensions: [ + { + property: 'auditLog', + token: AuditLoggerToken, + errorMessage: 'AuditLogPlugin is not installed. Add it to your @FrontMcp plugins array.', + }, + ], +}) +export default class AuditLogPlugin {} +``` + +```typescript +// src/plugins/audit-log/index.ts +import './audit-log.context-extension'; // Side-effect import for type augmentation +export { default as AuditLogPlugin } from './audit-log.plugin'; +export { AuditLoggerToken } from './audit-log.symbols'; +``` + +```typescript +// src/tools/delete-record.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; + +@Tool({ name: 'delete_record' }) +class DeleteRecordTool extends ToolContext { + async execute(input: { recordId: string }) { + // this.auditLog is available because AuditLogPlugin is installed + await this.auditLog.logToolCall('delete_record', this.scope.userId, input); + return { deleted: true }; + } +} +``` + +## What This Demonstrates + +- Defining a typed DI token with `Token = Symbol('...')` in a dedicated symbols file +- Module augmentation via `declare module '@frontmcp/sdk'` to add `readonly auditLog` to `ExecutionContextBase` +- Registering `contextExtensions` in `@Plugin` metadata with `property`, `token`, and `errorMessage` +- Side-effect import of the context extension file in both the plugin and the barrel export +- Accessing the extended property (`this.auditLog`) in tool execution contexts + +## Related + +- See `create-plugin` for the full context extension pattern, metadata extensions, and DynamicPlugin diff --git a/libs/skills/catalog/frontmcp-development/examples/create-prompt/basic-prompt.md b/libs/skills/catalog/frontmcp-development/examples/create-prompt/basic-prompt.md new file mode 100644 index 000000000..abd384993 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-prompt/basic-prompt.md @@ -0,0 +1,72 @@ +--- +name: basic-prompt +reference: create-prompt +level: basic +description: 'A simple prompt that generates a structured code review message from user-provided arguments.' +tags: [development, prompt, create-prompt] +features: + - 'Extending `PromptContext` and implementing `execute(args)` returning `GetPromptResult`' + - 'Declaring prompt `arguments` with `required: true` for mandatory parameters' + - 'Framework validates required arguments before `execute()` runs' + - 'Registering the prompt in the `prompts` array of `@App`' +--- + +# Basic Prompt with Arguments + +A simple prompt that generates a structured code review message from user-provided arguments. + +## Code + +```typescript +// src/apps/main/prompts/code-review.prompt.ts +import { Prompt, PromptContext } from '@frontmcp/sdk'; +import { GetPromptResult } from '@frontmcp/protocol'; + +@Prompt({ + name: 'code-review', + description: 'Generate a structured code review for the given code', + arguments: [ + { name: 'code', description: 'The code to review', required: true }, + { name: 'language', description: 'Programming language', required: false }, + ], +}) +class CodeReviewPrompt extends PromptContext { + async execute(args: Record): Promise { + const language = args.language ?? 'unknown language'; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Please review the following ${language} code. Focus on correctness, performance, and maintainability.\n\n\`\`\`${language}\n${args.code}\n\`\`\``, + }, + }, + ], + }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + prompts: [CodeReviewPrompt], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Extending `PromptContext` and implementing `execute(args)` returning `GetPromptResult` +- Declaring prompt `arguments` with `required: true` for mandatory parameters +- Framework validates required arguments before `execute()` runs +- Registering the prompt in the `prompts` array of `@App` + +## Related + +- See `create-prompt` for multi-turn conversations, resource embedding, and function-style builders diff --git a/libs/skills/catalog/frontmcp-development/examples/create-prompt/dynamic-rag-prompt.md b/libs/skills/catalog/frontmcp-development/examples/create-prompt/dynamic-rag-prompt.md new file mode 100644 index 000000000..721ee5dc9 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-prompt/dynamic-rag-prompt.md @@ -0,0 +1,92 @@ +--- +name: dynamic-rag-prompt +reference: create-prompt +level: advanced +description: 'A prompt that queries a knowledge base via DI to build context-aware messages at runtime.' +tags: [development, prompt, dynamic, rag] +features: + - 'Performing async operations (knowledge base search) inside `execute()` to generate context-aware prompts' + - 'Resolving a DI provider via `this.get(KNOWLEDGE_BASE)` for service access' + - 'Using `this.mark(stage)` for execution stage tracking in complex prompt generation' + - 'Building dynamic message content from external data sources at runtime' +--- + +# Dynamic RAG Prompt with Dependency Injection + +A prompt that queries a knowledge base via DI to build context-aware messages at runtime. + +## Code + +```typescript +// src/apps/main/tokens.ts +import type { Token } from '@frontmcp/di'; + +export interface KnowledgeBase { + search(query: string, limit: number): Promise>; +} + +export const KNOWLEDGE_BASE: Token = Symbol('knowledge-base'); +``` + +```typescript +// src/apps/main/prompts/rag-query.prompt.ts +import { Prompt, PromptContext } from '@frontmcp/sdk'; +import { GetPromptResult } from '@frontmcp/protocol'; +import { KNOWLEDGE_BASE } from '../tokens'; + +@Prompt({ + name: 'rag-query', + description: 'Answer a question using knowledge base context', + arguments: [ + { name: 'question', description: 'The question to answer', required: true }, + { name: 'maxSources', description: 'Maximum number of sources to include', required: false }, + ], +}) +class RagQueryPrompt extends PromptContext { + async execute(args: Record): Promise { + this.mark('search'); + const kb = this.get(KNOWLEDGE_BASE); + const maxSources = parseInt(args.maxSources ?? '3', 10); + const sources = await kb.search(args.question, maxSources); + + this.mark('compose'); + const contextBlock = sources.map((s, i) => `### Source ${i + 1}: ${s.title}\n${s.content}`).join('\n\n'); + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `Answer the following question using only the provided sources. If the sources do not contain enough information, say so clearly.\n\n**Question:** ${args.question}\n\n---\n\n${contextBlock}`, + }, + }, + ], + }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + providers: [KnowledgeBaseProvider], + prompts: [RagQueryPrompt], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Performing async operations (knowledge base search) inside `execute()` to generate context-aware prompts +- Resolving a DI provider via `this.get(KNOWLEDGE_BASE)` for service access +- Using `this.mark(stage)` for execution stage tracking in complex prompt generation +- Building dynamic message content from external data sources at runtime + +## Related + +- See `create-prompt` for resource embedding, function-style builders, and error handling with `this.fail()` +- See `create-provider` for implementing the `KnowledgeBaseProvider` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-prompt/multi-turn-debug-session.md b/libs/skills/catalog/frontmcp-development/examples/create-prompt/multi-turn-debug-session.md new file mode 100644 index 000000000..90b036b4d --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-prompt/multi-turn-debug-session.md @@ -0,0 +1,86 @@ +--- +name: multi-turn-debug-session +reference: create-prompt +level: intermediate +description: 'A prompt that uses alternating user/assistant messages to guide a structured debugging conversation.' +tags: [development, session, prompt, multi, turn, debug] +features: + - 'Using `assistant` role messages to prime expected response patterns and guide LLM behavior' + - 'Alternating `user` and `assistant` roles to create a structured multi-turn conversation' + - 'Optional arguments that conditionally add content to the prompt' + - 'The assistant message establishes a systematic debugging approach the LLM will follow' +--- + +# Multi-Turn Debug Session Prompt + +A prompt that uses alternating user/assistant messages to guide a structured debugging conversation. + +## Code + +```typescript +// src/apps/main/prompts/debug-session.prompt.ts +import { Prompt, PromptContext } from '@frontmcp/sdk'; +import { GetPromptResult } from '@frontmcp/protocol'; + +@Prompt({ + name: 'debug-session', + description: 'Start a structured debugging session', + arguments: [ + { name: 'error', description: 'The error message or stack trace', required: true }, + { name: 'context', description: 'Additional context about what was happening', required: false }, + ], +}) +class DebugSessionPrompt extends PromptContext { + async execute(args: Record): Promise { + const contextNote = args.context ? `\n\nAdditional context: ${args.context}` : ''; + + return { + messages: [ + { + role: 'user', + content: { + type: 'text', + text: `I encountered an error and need help debugging it.\n\nError:\n\`\`\`\n${args.error}\n\`\`\`${contextNote}`, + }, + }, + { + role: 'assistant', + content: { + type: 'text', + text: "I'll help you debug this. Let me analyze the error systematically.\n\n**Step 1: Error Classification**\nLet me first identify what type of error this is and its likely root cause.\n\n", + }, + }, + { + role: 'user', + content: { + type: 'text', + text: 'Please continue with your analysis and suggest specific fixes.', + }, + }, + ], + }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + prompts: [DebugSessionPrompt], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Using `assistant` role messages to prime expected response patterns and guide LLM behavior +- Alternating `user` and `assistant` roles to create a structured multi-turn conversation +- Optional arguments that conditionally add content to the prompt +- The assistant message establishes a systematic debugging approach the LLM will follow + +## Related + +- See `create-prompt` for dynamic prompt generation with DI, resource embedding, and error handling diff --git a/libs/skills/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md b/libs/skills/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md new file mode 100644 index 000000000..e822e2378 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md @@ -0,0 +1,113 @@ +--- +name: basic-database-provider +reference: create-provider +level: basic +description: 'A provider that manages a database connection pool with `onInit()` and `onDestroy()` lifecycle hooks.' +tags: [development, database, provider] +features: + - 'Defining a typed token with `Token` using a `Symbol` for DI identification' + - 'Using `@Provider` decorator with `onInit()` for async startup and `onDestroy()` for cleanup' + - 'Consuming the provider in a tool via `this.get(DB_TOKEN)` with full type safety' + - 'Registering the provider in the `providers` array so tools can resolve it' +--- + +# Basic Database Provider with Lifecycle + +A provider that manages a database connection pool with `onInit()` and `onDestroy()` lifecycle hooks. + +## Code + +```typescript +// src/apps/main/tokens.ts +import type { Token } from '@frontmcp/di'; + +export interface DatabaseService { + query(sql: string, params?: unknown[]): Promise; + close(): Promise; +} + +export const DB_TOKEN: Token = Symbol('DatabaseService'); +``` + +```typescript +// src/apps/main/providers/database.provider.ts +import { Provider } from '@frontmcp/sdk'; +import { createPool, Pool } from 'your-db-driver'; + +@Provider({ name: 'DatabaseProvider' }) +class DatabaseProvider implements DatabaseService { + private pool!: Pool; + + async onInit() { + this.pool = await createPool({ + connectionString: process.env.DATABASE_URL, + max: 20, + }); + } + + async query(sql: string, params?: unknown[]) { + return this.pool.query(sql, params); + } + + async close() { + await this.pool.end(); + } + + async onDestroy() { + await this.pool.end(); + } +} +``` + +```typescript +// src/apps/main/tools/query-users.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { DB_TOKEN } from '../tokens'; + +@Tool({ + name: 'query_users', + description: 'Query users from the database', + inputSchema: { + filter: z.string().optional(), + limit: z.number().default(10), + }, + outputSchema: { + users: z.array(z.object({ id: z.string(), name: z.string(), email: z.string() })), + }, +}) +class QueryUsersTool extends ToolContext { + async execute(input: { filter?: string; limit: number }) { + const db = this.get(DB_TOKEN); + const users = await db.query('SELECT id, name, email FROM users WHERE name LIKE $1 LIMIT $2', [ + `%${input.filter ?? ''}%`, + input.limit, + ]); + return { users }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + providers: [DatabaseProvider], + tools: [QueryUsersTool], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Defining a typed token with `Token` using a `Symbol` for DI identification +- Using `@Provider` decorator with `onInit()` for async startup and `onDestroy()` for cleanup +- Consuming the provider in a tool via `this.get(DB_TOKEN)` with full type safety +- Registering the provider in the `providers` array so tools can resolve it + +## Related + +- See `create-provider` for configuration providers, HTTP API clients, and cache providers +- See `create-tool` for more patterns using DI in tool execution diff --git a/libs/skills/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md b/libs/skills/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md new file mode 100644 index 000000000..b68caacc6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md @@ -0,0 +1,107 @@ +--- +name: config-and-api-providers +reference: create-provider +level: intermediate +description: 'A configuration provider with readonly environment settings and an HTTP API client provider.' +tags: [development, provider, config, api, providers] +features: + - 'A configuration provider using `readonly` properties from environment variables (no lifecycle needed)' + - 'An API client provider using `onInit()` for async setup of credentials' + - 'Registering providers at `@FrontMcp` level for server-wide sharing across all apps' + - 'Separating token definitions from provider implementations for clean dependency boundaries' +--- + +# Configuration and API Client Providers + +A configuration provider with readonly environment settings and an HTTP API client provider. + +## Code + +```typescript +// src/apps/main/tokens.ts +import type { Token } from '@frontmcp/di'; + +export interface AppConfig { + apiBaseUrl: string; + maxRetries: number; + debug: boolean; +} + +export const CONFIG_TOKEN: Token = Symbol('AppConfig'); + +export interface ApiClient { + get(path: string): Promise; + post(path: string, body: unknown): Promise; +} + +export const API_TOKEN: Token = Symbol('ApiClient'); +``` + +```typescript +// src/apps/main/providers/config.provider.ts +import { Provider } from '@frontmcp/sdk'; +import type { AppConfig } from '../tokens'; + +@Provider({ name: 'ConfigProvider' }) +class ConfigProvider implements AppConfig { + readonly apiBaseUrl = process.env.API_BASE_URL ?? 'https://api.example.com'; + readonly maxRetries = Number(process.env.MAX_RETRIES ?? 3); + readonly debug = process.env.DEBUG === 'true'; +} +``` + +```typescript +// src/apps/main/providers/api-client.provider.ts +import { Provider } from '@frontmcp/sdk'; +import type { ApiClient } from '../tokens'; + +@Provider({ name: 'ApiClientProvider' }) +class ApiClientProvider implements ApiClient { + private baseUrl!: string; + private apiKey!: string; + + async onInit() { + this.baseUrl = process.env.API_URL!; + this.apiKey = process.env.API_KEY!; + } + + async get(path: string) { + const res = await fetch(`${this.baseUrl}${path}`, { + headers: { Authorization: `Bearer ${this.apiKey}` }, + }); + return res.json(); + } + + async post(path: string, body: unknown) { + const res = await fetch(`${this.baseUrl}${path}`, { + method: 'POST', + headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return res.json(); + } +} +``` + +```typescript +// src/index.ts +import { FrontMcp } from '@frontmcp/sdk'; + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [MainApp], + providers: [ConfigProvider, ApiClientProvider], // Server-scoped: shared across all apps +}) +class MyServer {} +``` + +## What This Demonstrates + +- A configuration provider using `readonly` properties from environment variables (no lifecycle needed) +- An API client provider using `onInit()` for async setup of credentials +- Registering providers at `@FrontMcp` level for server-wide sharing across all apps +- Separating token definitions from provider implementations for clean dependency boundaries + +## Related + +- See `create-provider` for cache providers, lifecycle details, and the `tryGet()` safe access pattern diff --git a/libs/skills/catalog/frontmcp-development/examples/create-resource/basic-static-resource.md b/libs/skills/catalog/frontmcp-development/examples/create-resource/basic-static-resource.md new file mode 100644 index 000000000..90ff1ef9f --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-resource/basic-static-resource.md @@ -0,0 +1,72 @@ +--- +name: basic-static-resource +reference: create-resource +level: basic +description: 'A static resource that exposes application configuration at a fixed URI.' +tags: [development, resource, static] +features: + - 'Using `@Resource` with a fixed URI that follows RFC 3986 (has a valid scheme)' + - 'Returning a `ReadResourceResult` with `contents` array containing `uri`, `mimeType`, and `text`' + - 'Setting `mimeType` to indicate the content type of the resource' + - 'Registering the resource in the `resources` array of `@App`' +--- + +# Basic Static Resource + +A static resource that exposes application configuration at a fixed URI. + +## Code + +```typescript +// src/apps/main/resources/app-config.resource.ts +import { Resource, ResourceContext } from '@frontmcp/sdk'; +import { ReadResourceResult } from '@frontmcp/protocol'; + +@Resource({ + name: 'app-config', + uri: 'config://app/settings', + description: 'Current application configuration', + mimeType: 'application/json', +}) +class AppConfigResource extends ResourceContext { + async execute(uri: string, params: Record): Promise { + const config = { + version: '2.1.0', + environment: 'production', + features: { darkMode: true, notifications: true }, + }; + + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(config, null, 2), + }, + ], + }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + resources: [AppConfigResource], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Using `@Resource` with a fixed URI that follows RFC 3986 (has a valid scheme) +- Returning a `ReadResourceResult` with `contents` array containing `uri`, `mimeType`, and `text` +- Setting `mimeType` to indicate the content type of the resource +- Registering the resource in the `resources` array of `@App` + +## Related + +- See `create-resource` for resource templates, binary content, and function-style builders diff --git a/libs/skills/catalog/frontmcp-development/examples/create-resource/binary-and-multi-content.md b/libs/skills/catalog/frontmcp-development/examples/create-resource/binary-and-multi-content.md new file mode 100644 index 000000000..2de505208 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-resource/binary-and-multi-content.md @@ -0,0 +1,111 @@ +--- +name: binary-and-multi-content +reference: create-resource +level: advanced +description: 'A resource serving binary blob data and a resource returning multiple content items.' +tags: [development, cli, resource, binary, multi, content] +features: + - 'Returning binary data as base64-encoded `blob` (not `text`) for images and other binary assets' + - 'Using `@frontmcp/utils` for file system operations (`readFileBuffer`) instead of `fs` directly' + - 'Returning multiple content items from a single resource using fragment URIs (`#metrics`, `#charts`)' + - 'Each content item has its own `uri`, `mimeType`, and `text` or `blob` field' +--- + +# Binary Content and Multi-Content Resource + +A resource serving binary blob data and a resource returning multiple content items. + +## Code + +```typescript +// src/apps/main/resources/app-logo.resource.ts +import { Resource, ResourceContext } from '@frontmcp/sdk'; +import { ReadResourceResult } from '@frontmcp/protocol'; + +@Resource({ + name: 'app-logo', + uri: 'assets://logo.png', + description: 'Application logo image', + mimeType: 'image/png', +}) +class AppLogoResource extends ResourceContext { + async execute(uri: string, params: Record): Promise { + const { readFileBuffer } = await import('@frontmcp/utils'); + const buffer = await readFileBuffer('/assets/logo.png'); + + return { + contents: [ + { + uri, + mimeType: 'image/png', + blob: buffer.toString('base64'), + }, + ], + }; + } +} +``` + +```typescript +// src/apps/main/resources/dashboard.resource.ts +import { Resource, ResourceContext } from '@frontmcp/sdk'; +import { ReadResourceResult } from '@frontmcp/protocol'; + +@Resource({ + name: 'dashboard-data', + uri: 'dashboard://overview', + description: 'Dashboard overview with metrics and chart data', + mimeType: 'application/json', +}) +class DashboardResource extends ResourceContext { + async execute(uri: string, params: Record): Promise { + const metrics = await this.loadMetrics(); + const chartData = await this.loadChartData(); + + return { + contents: [ + { + uri: `${uri}#metrics`, + mimeType: 'application/json', + text: JSON.stringify(metrics), + }, + { + uri: `${uri}#charts`, + mimeType: 'application/json', + text: JSON.stringify(chartData), + }, + ], + }; + } + + private async loadMetrics() { + return { users: 1500, revenue: 42000 }; + } + + private async loadChartData() { + return { labels: ['Jan', 'Feb'], values: [100, 200] }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + resources: [AppLogoResource, DashboardResource], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Returning binary data as base64-encoded `blob` (not `text`) for images and other binary assets +- Using `@frontmcp/utils` for file system operations (`readFileBuffer`) instead of `fs` directly +- Returning multiple content items from a single resource using fragment URIs (`#metrics`, `#charts`) +- Each content item has its own `uri`, `mimeType`, and `text` or `blob` field + +## Related + +- See `create-resource` for function-style builders, simplified return values, and ESM/remote loading diff --git a/libs/skills/catalog/frontmcp-development/examples/create-resource/parameterized-template.md b/libs/skills/catalog/frontmcp-development/examples/create-resource/parameterized-template.md new file mode 100644 index 000000000..40e58f13f --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-resource/parameterized-template.md @@ -0,0 +1,84 @@ +--- +name: parameterized-template +reference: create-resource +level: intermediate +description: 'A resource template with typed URI parameters and argument autocompletion.' +tags: [development, resource, parameterized, template] +features: + - 'Using `@ResourceTemplate` with `uriTemplate` containing `{param}` placeholders' + - 'Typing the `ResourceContext` generic parameter for compile-time parameter checking' + - 'Implementing a convention-based completer (`userIdCompleter`) for argument autocompletion' + - 'Accessing DI providers via `this.get()` in both `execute()` and completer methods' +--- + +# Parameterized Resource Template + +A resource template with typed URI parameters and argument autocompletion. + +## Code + +```typescript +// src/apps/main/resources/user-profile.resource.ts +import { ResourceTemplate, ResourceContext } from '@frontmcp/sdk'; +import type { ResourceCompletionResult } from '@frontmcp/sdk'; +import { ReadResourceResult } from '@frontmcp/protocol'; +import type { Token } from '@frontmcp/di'; + +interface UserService { + findById(id: string): Promise<{ id: string; name: string; email: string }>; + search(partial: string): Promise>; +} + +const USER_SERVICE: Token = Symbol('UserService'); + +@ResourceTemplate({ + name: 'user-profile', + uriTemplate: 'users://{userId}/profile', + description: 'User profile by ID', + mimeType: 'application/json', +}) +class UserProfileResource extends ResourceContext<{ userId: string }> { + async execute(uri: string, params: { userId: string }): Promise { + const user = await this.get(USER_SERVICE).findById(params.userId); + + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(user), + }, + ], + }; + } + + async userIdCompleter(partial: string): Promise { + const users = await this.get(USER_SERVICE).search(partial); + return { values: users.map((u) => u.id), total: users.length }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + providers: [UserServiceProvider], + resources: [UserProfileResource], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Using `@ResourceTemplate` with `uriTemplate` containing `{param}` placeholders +- Typing the `ResourceContext` generic parameter for compile-time parameter checking +- Implementing a convention-based completer (`userIdCompleter`) for argument autocompletion +- Accessing DI providers via `this.get()` in both `execute()` and completer methods + +## Related + +- See `create-resource` for binary blob content, multiple content items, and function-style builders +- See `create-provider` for implementing the `UserServiceProvider` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/basic-tool-orchestration.md b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/basic-tool-orchestration.md new file mode 100644 index 000000000..871b186c2 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/basic-tool-orchestration.md @@ -0,0 +1,76 @@ +--- +name: basic-tool-orchestration +reference: create-skill-with-tools +level: basic +description: 'A skill that guides an AI client through a deploy workflow using referenced MCP tools.' +tags: [development, skill, tools, tool, orchestration] +features: + - "Referencing tools by class (`BuildProjectTool`) and by string name (`'health_check'`)" + - 'Mixing class references and string names in a single `tools` array' + - 'Writing step-by-step instructions that guide the AI to use specific tools' + - 'The `skill()` function builder for tool-referencing skills that need no class' +--- + +# Basic Skill with Tool References + +A skill that guides an AI client through a deploy workflow using referenced MCP tools. + +## Code + +```typescript +// src/skills/deploy-service.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; +import { BuildProjectTool } from '../tools/build-project.tool'; +import { RunTestsTool } from '../tools/run-tests.tool'; +import { DeployToEnvTool } from '../tools/deploy-to-env.tool'; + +@Skill({ + name: 'deploy-service', + description: 'Deploy a service through the build, test, and release pipeline', + instructions: `# Deploy Service Workflow + +## Step 1: Build +Use the \`build_project\` tool to compile the service. +Pass the service name and target environment. + +## Step 2: Run Tests +Use the \`run_tests\` tool to execute the test suite. +If tests fail, stop and report the failures. + +## Step 3: Deploy +Use the \`deploy_to_env\` tool to push the build to the target environment. +Verify the deployment using \`health_check\` tool. + +## Step 4: Notify +Use the \`send_notification\` tool to notify the team of the deployment status.`, + tools: [BuildProjectTool, RunTestsTool, DeployToEnvTool, 'health_check', 'send_notification'], +}) +class DeployServiceSkill extends SkillContext {} +``` + +```typescript +// src/skills/quick-deploy.skill.ts +import { skill } from '@frontmcp/sdk'; + +// Function-style skill with tool references +const QuickDeploySkill = skill({ + name: 'quick-deploy', + description: 'Quick deployment to staging', + instructions: `# Quick Deploy +1. Use build_project to compile. +2. Use deploy_to_env with environment=staging. +3. Use health_check to verify.`, + tools: ['build_project', 'deploy_to_env', 'health_check'], +}); +``` + +## What This Demonstrates + +- Referencing tools by class (`BuildProjectTool`) and by string name (`'health_check'`) +- Mixing class references and string names in a single `tools` array +- Writing step-by-step instructions that guide the AI to use specific tools +- The `skill()` function builder for tool-referencing skills that need no class + +## Related + +- See `create-skill-with-tools` for all three tool reference styles and validation modes diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/directory-skill-with-tools.md b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/directory-skill-with-tools.md new file mode 100644 index 000000000..2d9b5c0ea --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/directory-skill-with-tools.md @@ -0,0 +1,149 @@ +--- +name: directory-skill-with-tools +reference: create-skill-with-tools +level: advanced +description: 'A directory-based skill loaded with `skillDir()`, plus a class-based skill using Agent Skills spec metadata fields.' +tags: [development, skill, tools, directory] +features: + - 'Loading a directory-based skill with `skillDir()` including SKILL.md frontmatter with tool entries' + - 'Mixing all three tool reference styles in one `tools` array: class, string, and object' + - 'Agent Skills spec fields: `priority`, `license`, `compatibility`, `allowedTools`, `specMetadata`' + - 'Bundled resource directories: `scripts`, `references`, `assets`' + - "File-based instructions with `{ file: './docs/codebase-audit.md' }`" +--- + +# Directory-Based Skill with Tools, Agent Skills Spec Fields, and Registration + +A directory-based skill loaded with `skillDir()`, plus a class-based skill using Agent Skills spec metadata fields. + +## Code + +```text +skills/ + deploy-service/ + SKILL.md # Instructions with YAML frontmatter + scripts/ + validate.sh # Helper scripts + smoke-test.sh + references/ + architecture.md # Reference documentation + runbook.md + assets/ + topology.png # Visual assets +``` + +```markdown +## + +name: deploy-service +description: Deploy a service through the full pipeline +tags: [deploy, ci-cd, production] +tools: + +- name: build_project + purpose: Compile the service + required: true +- name: run_tests + purpose: Execute test suite + required: true +- name: deploy_to_env + purpose: Push build to target environment + required: true + parameters: +- name: environment + description: Target deployment environment + type: string + required: true + examples: +- scenario: Deploy to staging + expected-outcome: Service deployed and health check passes + +--- + +# Deploy Service + +Follow these steps to deploy the service... +``` + +```typescript +// src/skills/load-skills.ts +import { skillDir } from '@frontmcp/sdk'; + +const DeployServiceSkill = await skillDir('./skills/deploy-service'); +``` + +```typescript +// src/skills/audit.skill.ts +import { Skill, SkillContext, Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'analyze_codebase', + description: 'Analyze a codebase for patterns and issues', + inputSchema: { + path: z.string().describe('Path to the codebase'), + checks: z.array(z.string()).describe('Checks to run'), + }, +}) +class AnalyzeCodebaseTool extends ToolContext { + async execute(input: { path: string; checks: string[] }) { + return { issues: [], score: 95 }; + } +} + +@Skill({ + name: 'codebase-audit', + description: 'Perform a comprehensive codebase audit with reporting and issue creation', + instructions: { file: './docs/codebase-audit.md' }, + tools: [ + AnalyzeCodebaseTool, + 'generate_report', + { name: 'create_issue', purpose: 'File GitHub issues for critical findings', required: false }, + ], + toolValidation: 'strict', + priority: 10, + license: 'MIT', + compatibility: 'Node.js 24+', + allowedTools: 'Read Edit Bash(git status)', + specMetadata: { + author: 'platform-team', + version: '2.0.0', + }, + resources: { + scripts: './scripts', + references: './references', + assets: './assets', + }, +}) +class CodebaseAuditSkill extends SkillContext {} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'audit-app', + skills: [DeployServiceSkill, CodebaseAuditSkill], + tools: [AnalyzeCodebaseTool, GenerateReportTool, CreateIssueTool], +}) +class AuditApp {} + +@FrontMcp({ + info: { name: 'audit-server', version: '1.0.0' }, + apps: [AuditApp], +}) +class AuditServer {} +``` + +## What This Demonstrates + +- Loading a directory-based skill with `skillDir()` including SKILL.md frontmatter with tool entries +- Mixing all three tool reference styles in one `tools` array: class, string, and object +- Agent Skills spec fields: `priority`, `license`, `compatibility`, `allowedTools`, `specMetadata` +- Bundled resource directories: `scripts`, `references`, `assets` +- File-based instructions with `{ file: './docs/codebase-audit.md' }` + +## Related + +- See `create-skill-with-tools` for the full Agent Skills spec fields reference and CodeCall compatibility diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/incident-response-skill.md b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/incident-response-skill.md new file mode 100644 index 000000000..938efd243 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill-with-tools/incident-response-skill.md @@ -0,0 +1,92 @@ +--- +name: incident-response-skill +reference: create-skill-with-tools +level: intermediate +description: 'A skill that uses object-style tool references with purpose descriptions and required flags, plus strict validation.' +tags: [development, skill, tools, incident, response] +features: + - 'Object-style tool references with `name`, `purpose`, and `required` fields' + - "Using `toolValidation: 'strict'` to fail at startup if any referenced tool is missing" + - 'Combining tool references with `parameters` and `examples` for full skill metadata' + - "Setting `visibility: 'mcp'` to restrict discovery to MCP protocol only" + - 'Registering both skills and their referenced tools in the same `@App`' +--- + +# Incident Response Skill with Detailed Tool Metadata + +A skill that uses object-style tool references with purpose descriptions and required flags, plus strict validation. + +## Code + +```typescript +// src/skills/incident-response.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; + +@Skill({ + name: 'incident-response', + description: 'Respond to production incidents', + instructions: `# Incident Response + +## Step 1: Gather Information +Use check_service_health to determine which services are affected. +Use query_logs to find error patterns. + +## Step 2: Mitigate +Use rollback_deployment if a recent deploy caused the issue. +Use scale_service if the issue is load-related. + +## Step 3: Communicate +Use send_notification to update the incident channel.`, + tools: [ + { name: 'check_service_health', purpose: 'Check health status of services', required: true }, + { name: 'query_logs', purpose: 'Search application logs for errors', required: true }, + { name: 'rollback_deployment', purpose: 'Rollback to previous deployment', required: false }, + { name: 'scale_service', purpose: 'Scale service replicas up or down', required: false }, + { name: 'send_notification', purpose: 'Send notification to Slack channel', required: true }, + ], + toolValidation: 'strict', // Fail at startup if any required tool is missing + parameters: [ + { name: 'severity', description: 'Incident severity level', type: 'string', required: true }, + { name: 'auto-rollback', description: 'Whether to auto-rollback on detection', type: 'boolean', default: false }, + ], + examples: [ + { + scenario: 'API latency spike after a deployment', + expectedOutcome: 'Health checked, logs queried, deployment rolled back, team notified', + }, + ], + tags: ['incident', 'ops', 'on-call'], + visibility: 'mcp', +}) +class IncidentResponseSkill extends SkillContext {} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'ops-app', + skills: [IncidentResponseSkill], + tools: [CheckServiceHealthTool, QueryLogsTool, RollbackDeploymentTool, ScaleServiceTool, SendNotificationTool], +}) +class OpsApp {} + +@FrontMcp({ + info: { name: 'ops-server', version: '1.0.0' }, + apps: [OpsApp], +}) +class OpsServer {} +``` + +## What This Demonstrates + +- Object-style tool references with `name`, `purpose`, and `required` fields +- Using `toolValidation: 'strict'` to fail at startup if any referenced tool is missing +- Combining tool references with `parameters` and `examples` for full skill metadata +- Setting `visibility: 'mcp'` to restrict discovery to MCP protocol only +- Registering both skills and their referenced tools in the same `@App` + +## Related + +- See `create-skill-with-tools` for all tool validation modes and the CodeCall compatibility section diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill/basic-inline-skill.md b/libs/skills/catalog/frontmcp-development/examples/create-skill/basic-inline-skill.md new file mode 100644 index 000000000..87d4fbbec --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill/basic-inline-skill.md @@ -0,0 +1,96 @@ +--- +name: basic-inline-skill +reference: create-skill +level: basic +description: 'A minimal instruction-only skill with inline content and the function builder alternative.' +tags: [development, skill, inline] +features: + - 'Creating a class-based instruction-only skill with `@Skill` and `SkillContext`' + - 'Using inline string instructions for short, self-contained guides' + - 'The `skill()` function builder as a lighter alternative when no `build()` override is needed' + - 'Setting `visibility` to control where the skill is discoverable' +--- + +# Basic Inline Instruction Skill + +A minimal instruction-only skill with inline content and the function builder alternative. + +## Code + +```typescript +// src/skills/typescript-conventions.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; + +@Skill({ + name: 'typescript-conventions', + description: 'TypeScript coding conventions and patterns for the project', + instructions: `# TypeScript Conventions + +## Naming +- Use PascalCase for classes and interfaces +- Use camelCase for variables, functions, and methods +- Use UPPER_SNAKE_CASE for constants +- Use kebab-case for file names + +## Types +- Always use explicit return types on public methods +- Prefer \`unknown\` over \`any\` for generic defaults +- Use strict mode (\`strict: true\` in tsconfig) +- Define shared types in a common directory + +## Error Handling +- Use specific error classes, not raw Error +- Never use non-null assertions (\`!\`) -- throw proper errors +- Use \`this.fail(err)\` in execution contexts + +## Imports +- Use barrel exports (index.ts) for public APIs +- No circular dependencies +- Group imports: external, internal, relative`, +}) +class TypeScriptConventionsSkill extends SkillContext {} +``` + +```typescript +// src/skills/code-review-checklist.skill.ts +import { skill } from '@frontmcp/sdk'; + +// Function-style skill -- no class needed for simple instruction-only skills +const CodeReviewChecklist = skill({ + name: 'code-review-checklist', + description: 'Checklist for reviewing pull requests', + instructions: `# Code Review Checklist + +## Correctness +- Does the code do what it claims? +- Are edge cases handled? +- Are error paths covered? + +## Style +- Does it follow project conventions? +- Are names descriptive and consistent? +- Is the code self-documenting? + +## Testing +- Are there tests for new functionality? +- Do tests cover edge cases? +- Is coverage above 95%? + +## Security +- No secrets in code or config? +- Input validation present? +- Proper error handling without leaking internals?`, + visibility: 'both', +}); +``` + +## What This Demonstrates + +- Creating a class-based instruction-only skill with `@Skill` and `SkillContext` +- Using inline string instructions for short, self-contained guides +- The `skill()` function builder as a lighter alternative when no `build()` override is needed +- Setting `visibility` to control where the skill is discoverable + +## Related + +- See `create-skill` for file-based instructions, parameters, and directory-based skills diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill/directory-based-skill.md b/libs/skills/catalog/frontmcp-development/examples/create-skill/directory-based-skill.md new file mode 100644 index 000000000..c99a16221 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill/directory-based-skill.md @@ -0,0 +1,115 @@ +--- +name: directory-based-skill +reference: create-skill +level: advanced +description: 'A skill loaded from a directory structure with SKILL.md frontmatter, plus file-based and URL-based instruction sources.' +tags: [development, remote, skill, directory, based] +features: + - 'Loading a skill from a directory with `skillDir()` including SKILL.md frontmatter and bundled resources' + - 'The SKILL.md YAML frontmatter format for metadata (name, description, tags, parameters, examples)' + - "File-based instructions with `{ file: './path.md' }` resolved relative to the skill file" + - "URL-based instructions with `{ url: '...' }` fetched at build time" + - 'ESM loading with `Skill.esm()` and remote loading with `Skill.remote()`' +--- + +# Directory-Based Skill with File References and Registration + +A skill loaded from a directory structure with SKILL.md frontmatter, plus file-based and URL-based instruction sources. + +## Code + +```text +skills/ + coding-standards/ + SKILL.md # Instructions with YAML frontmatter + scripts/ + lint-check.sh # Helper scripts referenced in instructions + references/ + patterns.md # Reference documentation appended to context + assets/ + diagram.png # Visual assets +``` + +```markdown +## + +name: coding-standards +description: Project coding standards and patterns +tags: [standards, conventions, quality] +parameters: + +- name: language + description: Target programming language + type: string + default: typescript + examples: +- scenario: Apply coding standards to a new module + expected-outcome: Code follows all project conventions + +--- + +# Coding Standards + +Follow these standards when writing code for this project... +``` + +```typescript +// src/skills/load-skills.ts +import { skillDir, skill } from '@frontmcp/sdk'; + +// Load a directory-based skill with bundled scripts, references, and assets +const CodingStandards = await skillDir('./skills/coding-standards'); + +// File-based instructions -- path resolves relative to this file's directory +const DeployGuide = skill({ + name: 'deploy-guide', + description: 'Step-by-step deployment checklist', + instructions: { file: './docs/deploy-guide.md' }, // resolves to src/skills/docs/deploy-guide.md +}); +``` + +```typescript +// src/server.ts +import { FrontMcp, App, Skill, SkillContext } from '@frontmcp/sdk'; + +// URL-based instructions fetched at build time +@Skill({ + name: 'api-standards', + description: 'REST API design standards', + instructions: { url: 'https://docs.example.com/standards/api-design.md' }, +}) +class ApiStandardsSkill extends SkillContext {} + +// ESM and remote loading +const ExternalGuide = Skill.esm('@my-org/skills@^1.0.0', 'ExternalGuide', { + description: 'A skill loaded from an ES module', +}); + +const CloudGuide = Skill.remote('https://example.com/skills/style-guide', 'CloudGuide', { + description: 'A skill loaded from a remote server', +}); + +@App({ + name: 'standards-app', + skills: [CodingStandards, DeployGuide, ApiStandardsSkill, ExternalGuide, CloudGuide], +}) +class StandardsApp {} + +@FrontMcp({ + info: { name: 'dev-server', version: '1.0.0' }, + apps: [StandardsApp], +}) +class DevServer {} +``` + +## What This Demonstrates + +- Loading a skill from a directory with `skillDir()` including SKILL.md frontmatter and bundled resources +- The SKILL.md YAML frontmatter format for metadata (name, description, tags, parameters, examples) +- File-based instructions with `{ file: './path.md' }` resolved relative to the skill file +- URL-based instructions with `{ url: '...' }` fetched at build time +- ESM loading with `Skill.esm()` and remote loading with `Skill.remote()` + +## Related + +- See `create-skill` for the complete `skillDir()` reference, instruction resolution, and HTTP discovery diff --git a/libs/skills/catalog/frontmcp-development/examples/create-skill/parameterized-skill.md b/libs/skills/catalog/frontmcp-development/examples/create-skill/parameterized-skill.md new file mode 100644 index 000000000..6055a79f0 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-skill/parameterized-skill.md @@ -0,0 +1,96 @@ +--- +name: parameterized-skill +reference: create-skill +level: intermediate +description: 'A skill with customizable parameters, usage examples for AI guidance, and controlled visibility.' +tags: [development, skill, parameterized] +features: + - 'Defining `parameters` to let callers customize skill behavior at invocation time' + - 'Providing `examples` with `scenario` and `expectedOutcome` to guide AI application' + - 'Using `tags` for skill categorization and filtering' + - "Controlling discovery with `visibility: 'mcp'` (MCP-only) vs `visibility: 'both'` (default)" + - 'Using `hideFromDiscovery: true` to register a skill that is invocable by name but not listed' +--- + +# Parameterized Skill with Examples and Visibility + +A skill with customizable parameters, usage examples for AI guidance, and controlled visibility. + +## Code + +```typescript +// src/skills/api-design-guide.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; + +@Skill({ + name: 'api-design-guide', + description: 'REST API design guidelines', + instructions: `# API Design Guide + +Design APIs following these conventions. +Adapt the versioning strategy based on the api-style parameter. +Use the auth-required parameter to determine if authentication sections apply.`, + parameters: [ + { name: 'api-style', description: 'API style to follow', type: 'string', default: 'rest' }, + { name: 'auth-required', description: 'Whether to include auth guidelines', type: 'boolean', default: true }, + { name: 'version-strategy', description: 'API versioning approach', type: 'string', default: 'url-path' }, + ], + examples: [ + { + scenario: 'Adding error handling to a new API endpoint', + expectedOutcome: + 'Endpoint uses specific error classes with MCP error codes, validates input, and returns structured error responses', + }, + { + scenario: 'Refactoring try-catch blocks in existing code', + expectedOutcome: 'Generic catches replaced with specific error types, proper error propagation chain established', + }, + ], + tags: ['api', 'design', 'standards'], + visibility: 'both', +}) +class ApiDesignGuideSkill extends SkillContext {} +``` + +```typescript +// src/skills/internal-runbook.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; + +@Skill({ + name: 'internal-runbook', + description: 'Internal operations runbook', + instructions: `# Operations Runbook + +## Incident Response +1. Check monitoring dashboards +2. Identify affected services +3. Escalate if severity is P0 or P1`, + visibility: 'mcp', // Only visible to MCP clients, not HTTP discovery +}) +class InternalRunbookSkill extends SkillContext {} +``` + +```typescript +// src/skills/admin-procedures.skill.ts +import { Skill, SkillContext } from '@frontmcp/sdk'; + +@Skill({ + name: 'admin-procedures', + description: 'Administrative procedures for internal use', + instructions: '...', + hideFromDiscovery: true, // Registered but hidden from listing endpoints +}) +class AdminProceduresSkill extends SkillContext {} +``` + +## What This Demonstrates + +- Defining `parameters` to let callers customize skill behavior at invocation time +- Providing `examples` with `scenario` and `expectedOutcome` to guide AI application +- Using `tags` for skill categorization and filtering +- Controlling discovery with `visibility: 'mcp'` (MCP-only) vs `visibility: 'both'` (default) +- Using `hideFromDiscovery: true` to register a skill that is invocable by name but not listed + +## Related + +- See `create-skill` for the full parameters, examples, and visibility reference diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md b/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md new file mode 100644 index 000000000..b28d29096 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md @@ -0,0 +1,94 @@ +--- +name: destructive-delete-tool +reference: create-tool-annotations +level: intermediate +description: 'Demonstrates annotating a tool that deletes data, enabling MCP clients to warn users before execution.' +tags: [development, elicitation, tool, annotations, destructive, delete] +features: + - 'Setting `destructiveHint: true` on the delete tool so MCP clients can trigger confirmation warnings' + - 'Setting `idempotentHint: true` on the delete tool because deleting the same user twice produces the same outcome' + - 'Setting `openWorldHint: true` on the email tool because it interacts with an external SMTP service' + - 'Setting `idempotentHint: false` on the email tool because each call sends a new email' + - 'How different annotation combinations express different behavioral contracts' +--- + +# Destructive Delete Tool with Annotations + +Demonstrates annotating a tool that deletes data, enabling MCP clients to warn users before execution. + +## Code + +```typescript +// src/tools/delete-user.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'delete_user', + description: 'Permanently delete a user account and all associated data', + inputSchema: { + userId: z.string().describe('ID of the user to delete'), + confirm: z.boolean().describe('Must be true to confirm deletion'), + }, + annotations: { + title: 'Delete User Account', + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, +}) +class DeleteUserTool extends ToolContext { + async execute(input: { userId: string; confirm: boolean }) { + if (!input.confirm) { + return { deleted: false, reason: 'Confirmation required' }; + } + const db = this.get(DatabaseToken); + await db.deleteUser(input.userId); + return { deleted: true, userId: input.userId }; + } +} +``` + +```typescript +// src/tools/send-email.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'send_email', + description: 'Send an email to a recipient via external SMTP service', + inputSchema: { + to: z.string().email().describe('Recipient email address'), + subject: z.string().describe('Email subject'), + body: z.string().describe('Email body text'), + }, + annotations: { + title: 'Send Email', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, +}) +class SendEmailTool extends ToolContext { + async execute(input: { to: string; subject: string; body: string }) { + const mailer = this.get(MailerToken); + const result = await mailer.send(input.to, input.subject, input.body); + return { sent: true, messageId: result.id }; + } +} +``` + +## What This Demonstrates + +- Setting `destructiveHint: true` on the delete tool so MCP clients can trigger confirmation warnings +- Setting `idempotentHint: true` on the delete tool because deleting the same user twice produces the same outcome +- Setting `openWorldHint: true` on the email tool because it interacts with an external SMTP service +- Setting `idempotentHint: false` on the email tool because each call sends a new email +- How different annotation combinations express different behavioral contracts + +## Related + +- See `create-tool-annotations` for all annotation fields and their default values +- See `decorators-guide` for the full `@Tool` decorator field reference diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md b/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md new file mode 100644 index 000000000..c9c925697 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md @@ -0,0 +1,60 @@ +--- +name: readonly-query-tool +reference: create-tool-annotations +level: basic +description: 'Demonstrates annotating a tool that only reads data, signaling to MCP clients that it has no side effects and is safe to retry.' +tags: [development, database, local, tool, annotations, readonly] +features: + - 'Setting `readOnlyHint: true` to indicate the tool performs no mutations' + - 'Setting `destructiveHint: false` to tell clients no data will be deleted or overwritten' + - 'Setting `idempotentHint: true` because repeated calls with the same input produce the same result' + - 'Setting `openWorldHint: false` because the tool only accesses local database data' + - 'Using `title` to provide a human-friendly display name for MCP client UIs' +--- + +# Read-Only Query Tool with Annotations + +Demonstrates annotating a tool that only reads data, signaling to MCP clients that it has no side effects and is safe to retry. + +## Code + +```typescript +// src/tools/search-users.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'search_users', + description: 'Search for users by name or email', + inputSchema: { + query: z.string().describe('Search query'), + limit: z.number().optional().default(10), + }, + annotations: { + title: 'Search Users', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, +}) +class SearchUsersTool extends ToolContext { + async execute(input: { query: string; limit: number }) { + const db = this.get(DatabaseToken); + const users = await db.searchUsers(input.query, input.limit); + return { users }; + } +} +``` + +## What This Demonstrates + +- Setting `readOnlyHint: true` to indicate the tool performs no mutations +- Setting `destructiveHint: false` to tell clients no data will be deleted or overwritten +- Setting `idempotentHint: true` because repeated calls with the same input produce the same result +- Setting `openWorldHint: false` because the tool only accesses local database data +- Using `title` to provide a human-friendly display name for MCP client UIs + +## Related + +- See `create-tool-annotations` for the full fields reference and default values diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md new file mode 100644 index 000000000..fd2239b1c --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md @@ -0,0 +1,104 @@ +--- +name: primitive-and-media-outputs +reference: create-tool-output-schema-types +level: intermediate +description: 'Demonstrates using primitive string literals and media types as `outputSchema` for tools that return plain text, images, or multi-content arrays.' +tags: [development, output-schema, tool, output, schema, types] +features: + - "Using `'string'` literal to return plain text output" + - "Using `'image'` literal to return base64 image data" + - "Using `['string', 'image']` array to return multi-content (text plus image) in a single response" + - "Other available primitives: `'number'`, `'boolean'`, `'date'`" + - "Other available media types: `'audio'`, `'resource'`, `'resource_link'`" +--- + +# Primitive Literal and Media Type Output Schemas + +Demonstrates using primitive string literals and media types as `outputSchema` for tools that return plain text, images, or multi-content arrays. + +## Code + +```typescript +// src/tools/summarize.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// Primitive literal: returns plain text +@Tool({ + name: 'summarize_text', + description: 'Summarize a long text into a short paragraph', + inputSchema: { + text: z.string().describe('The text to summarize'), + }, + outputSchema: 'string', +}) +class SummarizeTextTool extends ToolContext { + async execute(input: { text: string }) { + const summary = await this.get(LlmService).summarize(input.text); + return summary; + } +} +``` + +```typescript +// src/tools/generate-chart.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// Media type: returns base64 image data +@Tool({ + name: 'generate_chart', + description: 'Generate a chart image from data points', + inputSchema: { + data: z.array(z.object({ label: z.string(), value: z.number() })), + chartType: z.enum(['bar', 'line', 'pie']), + }, + outputSchema: 'image', +}) +class GenerateChartTool extends ToolContext { + async execute(input: { data: Array<{ label: string; value: number }>; chartType: string }) { + const chartService = this.get(ChartService); + const imageBase64 = await chartService.render(input.data, input.chartType); + return imageBase64; + } +} +``` + +```typescript +// src/tools/analyze-document.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// Multi-content array: returns text + image +@Tool({ + name: 'analyze_document', + description: 'Analyze a document and return summary with visual highlights', + inputSchema: { + documentId: z.string().describe('Document ID to analyze'), + }, + outputSchema: ['string', 'image'], +}) +class AnalyzeDocumentTool extends ToolContext { + async execute(input: { documentId: string }) { + const doc = this.get(DocumentService); + const analysis = await doc.analyze(input.documentId); + return { + text: analysis.summary, + image: analysis.highlightImageBase64, + }; + } +} +``` + +## What This Demonstrates + +- Using `'string'` literal to return plain text output +- Using `'image'` literal to return base64 image data +- Using `['string', 'image']` array to return multi-content (text plus image) in a single response +- Other available primitives: `'number'`, `'boolean'`, `'date'` +- Other available media types: `'audio'`, `'resource'`, `'resource_link'` + +## Related + +- See `create-tool-output-schema-types` for the complete list of supported output schema types +- See `decorators-guide` for the full `@Tool` decorator field reference diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md new file mode 100644 index 000000000..97cd681ea --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md @@ -0,0 +1,63 @@ +--- +name: zod-raw-shape-output +reference: create-tool-output-schema-types +level: basic +description: 'Demonstrates the recommended approach of using a Zod raw shape as `outputSchema` for structured, validated JSON output.' +tags: [development, codecall, output-schema, tool, output, schema] +features: + - 'Using a Zod raw shape (plain object with Zod types) as `outputSchema` for structured output' + - 'The output is validated at runtime against the schema before being returned to the client' + - 'This is the recommended pattern for CodeCall compatibility and data leak prevention' + - 'The `execute()` return type is automatically inferred from the output schema' +--- + +# Zod Raw Shape Output Schema + +Demonstrates the recommended approach of using a Zod raw shape as `outputSchema` for structured, validated JSON output. + +## Code + +```typescript +// src/tools/get-user-profile.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get_user_profile', + description: 'Retrieve a user profile by ID', + inputSchema: { + userId: z.string().describe('The user ID to look up'), + }, + outputSchema: { + name: z.string(), + email: z.string().email(), + age: z.number(), + roles: z.array(z.string()), + active: z.boolean(), + }, +}) +class GetUserProfileTool extends ToolContext { + async execute(input: { userId: string }) { + const db = this.get(DatabaseToken); + const user = await db.findUser(input.userId); + return { + name: user.name, + email: user.email, + age: user.age, + roles: user.roles, + active: user.active, + }; + } +} +``` + +## What This Demonstrates + +- Using a Zod raw shape (plain object with Zod types) as `outputSchema` for structured output +- The output is validated at runtime against the schema before being returned to the client +- This is the recommended pattern for CodeCall compatibility and data leak prevention +- The `execute()` return type is automatically inferred from the output schema + +## Related + +- See `create-tool-output-schema-types` for all supported output schema formats diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md new file mode 100644 index 000000000..f76413744 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md @@ -0,0 +1,103 @@ +--- +name: zod-schema-advanced-output +reference: create-tool-output-schema-types +level: advanced +description: 'Demonstrates using full Zod schema objects (not raw shapes) as `outputSchema`, including `z.object()`, `z.array()`, `z.union()`, and `z.discriminatedUnion()`.' +tags: [development, output-schema, tool, output, schema, types] +features: + - 'Using `z.object()` for structured output with nested arrays and nullable fields' + - 'Using `z.discriminatedUnion()` to return different output shapes based on a discriminant field' + - 'Full Zod schemas provide the same validation as raw shapes but support more complex types' + - 'Output is validated at runtime -- mismatched return values trigger validation errors' +--- + +# Advanced Zod Schema Output Types + +Demonstrates using full Zod schema objects (not raw shapes) as `outputSchema`, including `z.object()`, `z.array()`, `z.union()`, and `z.discriminatedUnion()`. + +## Code + +```typescript +// src/tools/list-products.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// z.object() -- structured object output +@Tool({ + name: 'get_order_status', + description: 'Get the current status of an order', + inputSchema: { + orderId: z.string(), + }, + outputSchema: z.object({ + orderId: z.string(), + status: z.enum(['pending', 'processing', 'shipped', 'delivered']), + estimatedDelivery: z.string().nullable(), + items: z.array( + z.object({ + name: z.string(), + quantity: z.number(), + }), + ), + }), +}) +class GetOrderStatusTool extends ToolContext { + async execute(input: { orderId: string }) { + const order = await this.get(OrderService).getStatus(input.orderId); + return { + orderId: order.id, + status: order.status, + estimatedDelivery: order.estimatedDelivery, + items: order.items.map((i) => ({ name: i.name, quantity: i.quantity })), + }; + } +} +``` + +```typescript +// src/tools/search-catalog.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// z.discriminatedUnion() -- different shapes based on a type field +const ProductResult = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('physical'), + name: z.string(), + weight: z.number(), + dimensions: z.object({ width: z.number(), height: z.number(), depth: z.number() }), + }), + z.object({ + type: z.literal('digital'), + name: z.string(), + downloadUrl: z.string(), + fileSizeMb: z.number(), + }), +]); + +@Tool({ + name: 'get_product', + description: 'Retrieve product details by ID', + inputSchema: { + productId: z.string(), + }, + outputSchema: ProductResult, +}) +class GetProductTool extends ToolContext { + async execute(input: { productId: string }) { + const product = await this.get(CatalogService).findById(input.productId); + return product; + } +} +``` + +## What This Demonstrates + +- Using `z.object()` for structured output with nested arrays and nullable fields +- Using `z.discriminatedUnion()` to return different output shapes based on a discriminant field +- Full Zod schemas provide the same validation as raw shapes but support more complex types +- Output is validated at runtime -- mismatched return values trigger validation errors + +## Related + +- See `create-tool-output-schema-types` for all supported output schema formats including primitives and media types diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md b/libs/skills/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md new file mode 100644 index 000000000..c1003f35d --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md @@ -0,0 +1,62 @@ +--- +name: basic-class-tool +reference: create-tool +level: basic +description: 'A minimal tool using the class-based pattern with Zod input validation and output schema.' +tags: [development, tool, class] +features: + - 'Extending `ToolContext` and implementing the `execute()` method' + - 'Using a Zod raw shape for `inputSchema` (not wrapped in `z.object()`)' + - 'Defining `outputSchema` to validate and restrict output fields' + - 'Registering the tool in an `@App` via the `tools` array' +--- + +# Basic Class-Based Tool + +A minimal tool using the class-based pattern with Zod input validation and output schema. + +## Code + +```typescript +// src/apps/main/tools/greet-user.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'greet_user', + description: 'Greet a user by name', + inputSchema: { + name: z.string().describe('The name of the user to greet'), + }, + outputSchema: { + greeting: z.string(), + }, +}) +class GreetUserTool extends ToolContext { + async execute(input: { name: string }): Promise<{ greeting: string }> { + return { greeting: `Hello, ${input.name}!` }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + tools: [GreetUserTool], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Extending `ToolContext` and implementing the `execute()` method +- Using a Zod raw shape for `inputSchema` (not wrapped in `z.object()`) +- Defining `outputSchema` to validate and restrict output fields +- Registering the tool in an `@App` via the `tools` array + +## Related + +- See `create-tool` for the full API reference including annotations, rate limiting, and elicitation diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md b/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md new file mode 100644 index 000000000..104ad864d --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md @@ -0,0 +1,84 @@ +--- +name: tool-with-di-and-errors +reference: create-tool +level: intermediate +description: 'A tool that resolves a database service via DI and uses `this.fail()` for business-logic errors.' +tags: [development, database, tool, di, errors] +features: + - 'Defining a typed DI token with `Token` and resolving it via `this.get()`' + - 'Using `this.fail()` with `ResourceNotFoundError` for MCP-compliant error responses' + - 'Letting infrastructure errors (database failures) propagate naturally to the framework' + - 'Registering both the provider and tool in the same `@App`' +--- + +# Tool with Dependency Injection and Error Handling + +A tool that resolves a database service via DI and uses `this.fail()` for business-logic errors. + +## Code + +```typescript +// src/apps/main/tokens.ts +import type { Token } from '@frontmcp/di'; + +export interface DatabaseService { + query(sql: string, params: unknown[]): Promise; +} + +export const DATABASE: Token = Symbol('database'); +``` + +```typescript +// src/apps/main/tools/delete-record.tool.ts +import { Tool, ToolContext, ResourceNotFoundError } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { DATABASE } from '../tokens'; + +@Tool({ + name: 'delete_record', + description: 'Delete a record by ID', + inputSchema: { + id: z.string().uuid().describe('Record UUID'), + }, + outputSchema: { + message: z.string(), + }, +}) +class DeleteRecordTool extends ToolContext { + async execute(input: { id: string }): Promise<{ message: string }> { + const db = this.get(DATABASE); + const rows = await db.query('SELECT * FROM records WHERE id = $1', [input.id]); + + if (rows.length === 0) { + this.fail(new ResourceNotFoundError(`Record ${input.id}`)); + } + + await db.query('DELETE FROM records WHERE id = $1', [input.id]); + return { message: `Record ${input.id} deleted successfully` }; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + providers: [DatabaseProvider], + tools: [DeleteRecordTool], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Defining a typed DI token with `Token` and resolving it via `this.get()` +- Using `this.fail()` with `ResourceNotFoundError` for MCP-compliant error responses +- Letting infrastructure errors (database failures) propagate naturally to the framework +- Registering both the provider and tool in the same `@App` + +## Related + +- See `create-tool` for all context methods and error handling patterns +- See `create-provider` for how to implement the `DatabaseProvider` class diff --git a/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md b/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md new file mode 100644 index 000000000..d0cdc07d2 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md @@ -0,0 +1,93 @@ +--- +name: tool-with-rate-limiting-and-progress +reference: create-tool +level: advanced +description: 'A batch processing tool that uses rate limiting, concurrency control, progress notifications, and annotations.' +tags: [development, throttle, tool, rate, limiting, progress] +features: + - 'Configuring `rateLimit`, `concurrency`, and `timeout` for throttling protection' + - 'Sending progress updates to the client with `this.respondProgress(value, total)`' + - 'Using `this.mark(stage)` for execution stage tracking and debugging' + - 'Sending log-level notifications with `this.notify(message, level)`' + - 'Setting tool `annotations` to communicate behavioral hints to clients' +--- + +# Tool with Rate Limiting, Progress, and Annotations + +A batch processing tool that uses rate limiting, concurrency control, progress notifications, and annotations. + +## Code + +```typescript +// src/apps/main/tools/batch-process.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'batch_process', + description: 'Process a batch of items with progress tracking', + inputSchema: { + items: z.array(z.string()).min(1).describe('Items to process'), + }, + outputSchema: { + processed: z.number(), + results: z.array(z.string()), + }, + annotations: { + title: 'Batch Processor', + readOnlyHint: false, + idempotentHint: true, + openWorldHint: false, + }, + rateLimit: { maxRequests: 10, windowMs: 60_000 }, + concurrency: { maxConcurrent: 2 }, + timeout: { executeMs: 30_000 }, +}) +class BatchProcessTool extends ToolContext { + async execute(input: { items: string[] }): Promise<{ processed: number; results: string[] }> { + this.mark('validation'); + if (input.items.some((item) => item.trim() === '')) { + this.fail(new Error('Items must not be empty strings')); + } + + this.mark('processing'); + const results: string[] = []; + for (let i = 0; i < input.items.length; i++) { + await this.respondProgress(i + 1, input.items.length); + const result = await this.processItem(input.items[i]); + results.push(result); + } + + this.mark('complete'); + await this.notify(`Processed ${results.length} items`, 'info'); + return { processed: results.length, results }; + } + + private async processItem(item: string): Promise { + return `processed:${item}`; + } +} +``` + +```typescript +// src/apps/main/index.ts +import { App } from '@frontmcp/sdk'; + +@App({ + name: 'main', + tools: [BatchProcessTool], +}) +class MainApp {} +``` + +## What This Demonstrates + +- Configuring `rateLimit`, `concurrency`, and `timeout` for throttling protection +- Sending progress updates to the client with `this.respondProgress(value, total)` +- Using `this.mark(stage)` for execution stage tracking and debugging +- Sending log-level notifications with `this.notify(message, level)` +- Setting tool `annotations` to communicate behavioral hints to clients + +## Related + +- See `create-tool` for all annotation fields, elicitation, and auth provider patterns diff --git a/libs/skills/catalog/frontmcp-development/examples/create-workflow/basic-deploy-pipeline.md b/libs/skills/catalog/frontmcp-development/examples/create-workflow/basic-deploy-pipeline.md new file mode 100644 index 000000000..ea5c2336d --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-workflow/basic-deploy-pipeline.md @@ -0,0 +1,91 @@ +--- +name: basic-deploy-pipeline +reference: create-workflow +level: basic +description: 'A linear workflow that builds, tests, and deploys a service with step dependencies and dynamic input.' +tags: [development, workflow, pipeline] +features: + - 'Defining a workflow with `@Workflow` decorator and sequential `steps`' + - 'Using `dependsOn` to establish step execution order' + - 'Passing dynamic input from a previous step using the callback form `(steps) => ({...})`' + - 'Registering both jobs and workflows in `@App` with jobs enabled' +--- + +# Basic Deploy Pipeline Workflow + +A linear workflow that builds, tests, and deploys a service with step dependencies and dynamic input. + +## Code + +```typescript +// src/workflows/deploy-pipeline.workflow.ts +import { Workflow } from '@frontmcp/sdk'; + +@Workflow({ + name: 'deploy-pipeline', + description: 'Build, test, and deploy a service', + steps: [ + { + id: 'build', + jobName: 'build-project', + input: { target: 'production', optimize: true }, + }, + { + id: 'test', + jobName: 'run-tests', + input: { suite: 'all', coverage: true }, + dependsOn: ['build'], + }, + { + id: 'deploy', + jobName: 'deploy-to-env', + input: (steps) => ({ + artifact: steps.get('build').outputs.artifactUrl, + environment: 'production', + }), + dependsOn: ['test'], + }, + ], +}) +class DeployPipeline {} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'pipeline-app', + jobs: [BuildProjectJob, RunTestsJob, DeployToEnvJob], + workflows: [DeployPipeline], +}) +class PipelineApp {} + +@FrontMcp({ + info: { name: 'pipeline-server', version: '1.0.0' }, + apps: [PipelineApp], + jobs: { + enabled: true, + store: { + redis: { + provider: 'redis', + host: 'localhost', + port: 6379, + keyPrefix: 'mcp:jobs:', + }, + }, + }, +}) +class PipelineServer {} +``` + +## What This Demonstrates + +- Defining a workflow with `@Workflow` decorator and sequential `steps` +- Using `dependsOn` to establish step execution order +- Passing dynamic input from a previous step using the callback form `(steps) => ({...})` +- Registering both jobs and workflows in `@App` with jobs enabled + +## Related + +- See `create-workflow` for the full API reference including triggers, conditions, and error handling diff --git a/libs/skills/catalog/frontmcp-development/examples/create-workflow/parallel-validation-pipeline.md b/libs/skills/catalog/frontmcp-development/examples/create-workflow/parallel-validation-pipeline.md new file mode 100644 index 000000000..23b1a7cb8 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-workflow/parallel-validation-pipeline.md @@ -0,0 +1,90 @@ +--- +name: parallel-validation-pipeline +reference: create-workflow +level: intermediate +description: 'A workflow that validates multiple datasets in parallel, then conditionally merges results or notifies on failure.' +tags: [development, workflow, parallel, validation, pipeline] +features: + - 'Running steps in parallel by omitting `dependsOn` (no mutual dependencies)' + - 'Using `maxConcurrency` to limit how many steps run at the same time' + - 'Conditional steps with `condition` that check `.state` of previous steps' + - 'Fan-out/fan-in pattern: parallel validation steps converge into a merge step' + - 'Branching: separate success and failure notification paths' +--- + +# Parallel Validation Pipeline with Conditional Steps + +A workflow that validates multiple datasets in parallel, then conditionally merges results or notifies on failure. + +## Code + +```typescript +// src/workflows/data-validation.workflow.ts +import { Workflow } from '@frontmcp/sdk'; + +@Workflow({ + name: 'data-validation-pipeline', + description: 'Validate data from multiple sources in parallel, then merge', + maxConcurrency: 3, + steps: [ + // These three steps have no dependencies -- they run in parallel + { + id: 'validate-users', + jobName: 'validate-dataset', + input: { dataset: 'users', rules: ['no-nulls', 'email-format'] }, + }, + { + id: 'validate-orders', + jobName: 'validate-dataset', + input: { dataset: 'orders', rules: ['no-nulls', 'positive-amounts'] }, + }, + { + id: 'validate-products', + jobName: 'validate-dataset', + input: { dataset: 'products', rules: ['no-nulls', 'unique-sku'] }, + }, + // This step depends on all three -- runs after all complete + { + id: 'merge-results', + jobName: 'merge-validations', + dependsOn: ['validate-users', 'validate-orders', 'validate-products'], + condition: (steps) => + steps.get('validate-users').state === 'completed' && + steps.get('validate-orders').state === 'completed' && + steps.get('validate-products').state === 'completed', + input: (steps) => ({ + userReport: steps.get('validate-users').outputs, + orderReport: steps.get('validate-orders').outputs, + productReport: steps.get('validate-products').outputs, + }), + }, + // Notify on any failure + { + id: 'notify-failure', + jobName: 'send-notification', + dependsOn: ['validate-users', 'validate-orders', 'validate-products'], + condition: (steps) => + steps.get('validate-users').state === 'failed' || + steps.get('validate-orders').state === 'failed' || + steps.get('validate-products').state === 'failed', + input: { + channel: '#alerts', + message: 'Data validation pipeline encountered failures', + }, + }, + ], +}) +class DataValidationPipeline {} +``` + +## What This Demonstrates + +- Running steps in parallel by omitting `dependsOn` (no mutual dependencies) +- Using `maxConcurrency` to limit how many steps run at the same time +- Conditional steps with `condition` that check `.state` of previous steps +- Fan-out/fan-in pattern: parallel validation steps converge into a merge step +- Branching: separate success and failure notification paths + +## Related + +- See `create-workflow` for the full DAG execution model, diamond dependencies, and `continueOnError` diff --git a/libs/skills/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md b/libs/skills/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md new file mode 100644 index 000000000..051a26730 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md @@ -0,0 +1,136 @@ +--- +name: webhook-triggered-workflow +reference: create-workflow +level: advanced +description: 'A CI/CD workflow triggered by a webhook, featuring `continueOnError`, per-step conditions, and the `workflow()` function builder.' +tags: [development, workflow, webhook, triggered] +features: + - "Webhook trigger with `trigger: 'webhook'` and `webhook: { path, secret, methods }`" + - 'Using `continueOnError: true` to allow the workflow to proceed past non-critical step failures' + - 'Conditional branching: separate success and failure notification steps based on prior step state' + - 'Workflow-level `permissions` for access control' + - 'The `workflow()` function builder as a lighter alternative to the class pattern' +--- + +# Webhook-Triggered Workflow with Error Resilience + +A CI/CD workflow triggered by a webhook, featuring `continueOnError`, per-step conditions, and the `workflow()` function builder. + +## Code + +```typescript +// src/workflows/github-deploy.workflow.ts +import { Workflow } from '@frontmcp/sdk'; + +@Workflow({ + name: 'github-deploy', + description: 'Deploy on GitHub push events', + trigger: 'webhook', + webhook: { + path: '/webhooks/github-deploy', + secret: process.env.WEBHOOK_SECRET, + methods: ['POST'], + }, + timeout: 900000, // 15 minutes + maxConcurrency: 3, + permissions: { + actions: ['create', 'read', 'execute', 'list'], + roles: ['admin', 'ci-bot'], + }, + steps: [ + { + id: 'build', + jobName: 'build-project', + input: { branch: 'main' }, + }, + { + id: 'lint', + jobName: 'run-linter', + dependsOn: ['build'], + continueOnError: true, // lint failures are non-blocking + input: (steps) => ({ + workDir: steps.get('build').outputs.workDir, + }), + }, + { + id: 'test', + jobName: 'run-unit-tests', + dependsOn: ['build'], + input: (steps) => ({ + workDir: steps.get('build').outputs.workDir, + coverage: true, + }), + }, + { + id: 'deploy', + jobName: 'deploy-artifact', + dependsOn: ['lint', 'test'], + condition: (steps) => steps.get('test').state === 'completed' && steps.get('test').outputs.passed === true, + input: (steps) => ({ + artifactUrl: steps.get('build').outputs.artifactUrl, + environment: 'staging', + }), + }, + { + id: 'notify-success', + jobName: 'notify-team', + dependsOn: ['deploy'], + condition: (steps) => steps.get('deploy').state === 'completed', + input: (steps) => ({ + channel: '#deployments', + message: `Deployed to ${steps.get('deploy').outputs.url}`, + }), + }, + { + id: 'notify-failure', + jobName: 'notify-team', + dependsOn: ['test'], + condition: (steps) => steps.get('test').state === 'failed', + input: { + channel: '#alerts', + message: 'CI pipeline failed -- check test results', + }, + }, + ], +}) +class GithubDeploy {} +``` + +```typescript +// src/workflows/quick-deploy.workflow.ts +import { workflow } from '@frontmcp/sdk'; + +// Function-style workflow for simpler cases +const QuickDeploy = workflow({ + name: 'quick-deploy', + description: 'Simplified deployment workflow', + steps: [ + { + id: 'build', + jobName: 'build-project', + input: { target: 'production' }, + }, + { + id: 'deploy', + jobName: 'deploy-to-env', + dependsOn: ['build'], + input: (steps) => ({ + artifact: steps.get('build').outputs.artifactUrl, + environment: 'staging', + }), + }, + ], +}); +``` + +## What This Demonstrates + +- Webhook trigger with `trigger: 'webhook'` and `webhook: { path, secret, methods }` +- Using `continueOnError: true` to allow the workflow to proceed past non-critical step failures +- Conditional branching: separate success and failure notification steps based on prior step state +- Workflow-level `permissions` for access control +- The `workflow()` function builder as a lighter alternative to the class pattern + +## Related + +- See `create-workflow` for the full trigger types (manual, webhook, event), error handling, and registration diff --git a/libs/skills/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md new file mode 100644 index 000000000..a97f34f5e --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md @@ -0,0 +1,145 @@ +--- +name: agent-skill-job-workflow +reference: decorators-guide +level: advanced +description: 'Demonstrates the advanced decorator types: `@Agent` for autonomous AI agents, `@Skill` for knowledge packages, `@Job` for background tasks, and `@Workflow` for multi-step orchestration.' +tags: [development, decorators, agent, skill, job, workflow] +features: + - '`@Agent` with LLM config, input schema, and delegated tools for autonomous task execution' + - '`@Skill` with inline instructions and tool references for reusable knowledge packages' + - '`@Job` with retry policy, timeout, and typed input/output for background processing' + - '`@Workflow` with ordered steps, `dependsOn` for sequencing, and `condition` for conditional execution' + - 'Enabling jobs and skills at the server level via `jobs: { enabled: true }` and `skillsConfig: { enabled: true }`' + - 'All advanced decorators registered in a single `@App` module' +--- + +# Agents, Skills, Jobs, and Workflows + +Demonstrates the advanced decorator types: `@Agent` for autonomous AI agents, `@Skill` for knowledge packages, `@Job` for background tasks, and `@Workflow` for multi-step orchestration. + +## Code + +```typescript +// src/agents/research.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Agent({ + name: 'research_agent', + description: 'Researches topics and produces summaries', + llm: { model: 'claude-sonnet-4-20250514', provider: 'anthropic' }, + inputSchema: { + topic: z.string().describe('Topic to research'), + }, + tools: [WebSearchTool, SummarizeTool], +}) +class ResearchAgent extends AgentContext { + async execute(input: { topic: string }) { + return this.run(`Research and summarize: ${input.topic}`); + } +} +``` + +```typescript +// src/skills/code-migration.skill.ts +import { Skill } from '@frontmcp/sdk'; + +@Skill({ + name: 'code_migration', + description: 'Guides migration of code between frameworks', + instructions: ` + 1. Analyze the source codebase structure + 2. Identify framework-specific patterns + 3. Generate migration plan + 4. Apply transformations using the provided tools + `, + tools: [AnalyzeTool, TransformTool, ValidateTool], + visibility: 'both', +}) +class CodeMigrationSkill {} +``` + +```typescript +// src/jobs/sync-data.job.ts +import { Job, JobContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Job({ + name: 'sync_data', + description: 'Synchronize data from external sources', + 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(input: { source: string }) { + const count = await this.get(SyncService).runFullSync(input.source); + return { synced: count }; + } +} +``` + +```typescript +// src/workflows/deploy-pipeline.workflow.ts +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: [ + { 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 {} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'platform', + agents: [ResearchAgent], + skills: [CodeMigrationSkill], + jobs: [SyncDataJob], + workflows: [DeployPipeline], +}) +class PlatformApp {} + +@FrontMcp({ + info: { name: 'advanced-server', version: '1.0.0' }, + apps: [PlatformApp], + jobs: { enabled: true }, + skillsConfig: { enabled: true }, +}) +class MyServer {} +``` + +## What This Demonstrates + +- `@Agent` with LLM config, input schema, and delegated tools for autonomous task execution +- `@Skill` with inline instructions and tool references for reusable knowledge packages +- `@Job` with retry policy, timeout, and typed input/output for background processing +- `@Workflow` with ordered steps, `dependsOn` for sequencing, and `condition` for conditional execution +- Enabling jobs and skills at the server level via `jobs: { enabled: true }` and `skillsConfig: { enabled: true }` +- All advanced decorators registered in a single `@App` module + +## Related + +- See `decorators-guide` for the complete decorator hierarchy and all field definitions +- See `create-plugin-hooks` for attaching lifecycle hooks to any flow diff --git a/libs/skills/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md new file mode 100644 index 000000000..6bcd7db5f --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md @@ -0,0 +1,124 @@ +--- +name: basic-server-with-app-and-tools +reference: decorators-guide +level: basic +description: 'Demonstrates the minimal decorator hierarchy to create a working FrontMCP server with one app containing a tool and a resource.' +tags: [development, decorators, app, tools] +features: + - 'The `@FrontMcp` -> `@App` -> `@Tool`/`@Resource`/`@Prompt` nesting hierarchy' + - 'Tool classes extend `ToolContext` and implement `execute()`' + - 'Resource classes extend `ResourceContext` and implement `read()`' + - 'Prompt classes extend `PromptContext` and implement `execute()`' + - 'Apps group related tools, resources, and prompts into logical modules' +--- + +# Basic Server with @FrontMcp, @App, and @Tool + +Demonstrates the minimal decorator hierarchy to create a working FrontMCP server with one app containing a tool and a resource. + +## Code + +```typescript +// src/tools/search-users.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'search_users', + description: 'Search for users by name or email', + inputSchema: { + query: z.string().describe('Search query'), + limit: z.number().optional().default(10), + }, +}) +class SearchUsersTool extends ToolContext { + async execute(input: { query: string; limit: number }) { + const users = await this.get(UserService).search(input.query, input.limit); + return { users }; + } +} +``` + +```typescript +// src/resources/app-config.resource.ts +import { Resource, ResourceContext } from '@frontmcp/sdk'; + +@Resource({ + name: 'app_config', + uri: 'config://app/settings', + description: 'Current application settings', + mimeType: 'application/json', +}) +class AppConfigResource extends ResourceContext { + async read() { + const config = await this.get(ConfigService).getAll(); + return { contents: [{ uri: this.uri, text: JSON.stringify(config) }] }; + } +} +``` + +```typescript +// src/prompts/code-review.prompt.ts +import { Prompt, PromptContext } from '@frontmcp/sdk'; + +@Prompt({ + name: 'code_review', + description: 'Generate a code review for the given code', + arguments: [ + { name: 'code', description: 'The code to review', required: true }, + { name: 'language', description: 'Programming language' }, + ], +}) +class CodeReviewPrompt extends PromptContext { + async execute(args: { code: string; language?: string }) { + return { + messages: [ + { + role: 'user' as const, + content: { + type: 'text' as const, + text: `Review this ${args.language ?? ''} code:\n\n${args.code}`, + }, + }, + ], + }; + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { SearchUsersTool } from './tools/search-users.tool'; +import { AppConfigResource } from './resources/app-config.resource'; +import { CodeReviewPrompt } from './prompts/code-review.prompt'; + +@App({ + name: 'analytics', + tools: [SearchUsersTool], + resources: [AppConfigResource], + prompts: [CodeReviewPrompt], +}) +class AnalyticsApp {} + +@FrontMcp({ + info: { name: 'my-server', version: '1.0.0' }, + apps: [AnalyticsApp], + transport: 'modern', + http: { port: 3000 }, +}) +class MyServer {} +``` + +## What This Demonstrates + +- The `@FrontMcp` -> `@App` -> `@Tool`/`@Resource`/`@Prompt` nesting hierarchy +- Tool classes extend `ToolContext` and implement `execute()` +- Resource classes extend `ResourceContext` and implement `read()` +- Prompt classes extend `PromptContext` and implement `execute()` +- Apps group related tools, resources, and prompts into logical modules + +## Related + +- See `decorators-guide` for the full decorator reference including all field options +- See `create-tool` for step-by-step tool creation patterns diff --git a/libs/skills/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md new file mode 100644 index 000000000..d63f10d28 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md @@ -0,0 +1,149 @@ +--- +name: multi-app-with-plugins-and-providers +reference: decorators-guide +level: intermediate +description: 'Demonstrates a server with multiple `@App` modules, a `@Provider` for dependency injection, and a `@Plugin` for cross-cutting concerns.' +tags: [development, database, multi-app, decorators, multi, app] +features: + - 'Organizing a server into multiple `@App` modules (`analytics` and `admin`)' + - 'Using `@Provider` with `useFactory` to register a database client for dependency injection' + - 'Accessing injected dependencies via `this.get(DatabaseToken)` in tools and resources' + - 'Using `@ResourceTemplate` with URI parameters (`{dashboardId}`) for dynamic resources' + - 'Registering a `@Plugin` at the server level so it applies across all apps' + - 'Global plugins go in `@FrontMcp({ plugins })`, app-scoped providers go in `@App({ providers })`' +--- + +# Multi-App Server with Plugins and Providers + +Demonstrates a server with multiple `@App` modules, a `@Provider` for dependency injection, and a `@Plugin` for cross-cutting concerns. + +## Code + +```typescript +// src/providers/database.provider.ts +import { Provider } from '@frontmcp/sdk'; + +export const DatabaseToken = Symbol('Database'); + +@Provider({ + name: 'database', + provide: DatabaseToken, + useFactory: () => new DatabaseClient(process.env.DB_URL), +}) +class DatabaseProvider {} +``` + +```typescript +// src/plugins/audit.plugin.ts +import { Plugin } from '@frontmcp/sdk'; + +@Plugin({ + name: 'audit-log', + providers: [AuditLogProvider], + contextExtensions: [installAuditExtension], +}) +class AuditPlugin {} +``` + +```typescript +// src/tools/query.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'run_query', + description: 'Run an analytics query', + inputSchema: { + sql: z.string().describe('SQL query to execute'), + }, +}) +class QueryTool extends ToolContext { + async execute(input: { sql: string }) { + const db = this.get(DatabaseToken); + const results = await db.query(input.sql); + return { rows: results }; + } +} +``` + +```typescript +// src/tools/report.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'generate_report', + description: 'Generate a formatted report from a dataset', + inputSchema: { + datasetId: z.string(), + format: z.enum(['csv', 'json', 'pdf']), + }, +}) +class ReportTool extends ToolContext { + async execute(input: { datasetId: string; format: string }) { + const db = this.get(DatabaseToken); + const data = await db.getDataset(input.datasetId); + return { report: `Report in ${input.format} format`, rows: data.length }; + } +} +``` + +```typescript +// src/resources/dashboard.resource.ts +import { ResourceTemplate, ResourceContext } from '@frontmcp/sdk'; + +@ResourceTemplate({ + name: 'dashboard', + uriTemplate: 'dashboards://{dashboardId}', + description: 'Dashboard data by ID', + mimeType: 'application/json', +}) +class DashboardResource extends ResourceContext { + async read(uri: string, params: { dashboardId: string }) { + const db = this.get(DatabaseToken); + const data = await db.getDashboard(params.dashboardId); + return { contents: [{ uri, text: JSON.stringify(data) }] }; + } +} +``` + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; + +@App({ + name: 'analytics', + tools: [QueryTool, ReportTool], + resources: [DashboardResource], + providers: [DatabaseProvider], +}) +class AnalyticsApp {} + +@App({ + name: 'admin', + tools: [ManageUsersTool], +}) +class AdminApp {} + +@FrontMcp({ + info: { name: 'multi-app-server', version: '1.0.0' }, + apps: [AnalyticsApp, AdminApp], + plugins: [AuditPlugin], + http: { port: 3000 }, +}) +class MyServer {} +``` + +## What This Demonstrates + +- Organizing a server into multiple `@App` modules (`analytics` and `admin`) +- Using `@Provider` with `useFactory` to register a database client for dependency injection +- Accessing injected dependencies via `this.get(DatabaseToken)` in tools and resources +- Using `@ResourceTemplate` with URI parameters (`{dashboardId}`) for dynamic resources +- Registering a `@Plugin` at the server level so it applies across all apps +- Global plugins go in `@FrontMcp({ plugins })`, app-scoped providers go in `@App({ providers })` + +## Related + +- See `decorators-guide` for the complete list of all decorator fields and their types +- See `create-plugin-hooks` for adding lifecycle hooks to plugins diff --git a/libs/skills/catalog/frontmcp-development/examples/official-adapters/authenticated-adapter-with-polling.md b/libs/skills/catalog/frontmcp-development/examples/official-adapters/authenticated-adapter-with-polling.md new file mode 100644 index 000000000..122816bc9 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-adapters/authenticated-adapter-with-polling.md @@ -0,0 +1,84 @@ +--- +name: authenticated-adapter-with-polling +reference: official-adapters +level: intermediate +description: 'Demonstrates configuring authentication (API key and bearer token) and automatic spec polling for OpenAPI adapters.' +tags: [development, auth, openapi, security, adapters, authenticated] +features: + - 'Three authentication methods: `staticAuth.apiKey`, `staticAuth.jwt`, and dynamic `securityResolver`' + - 'Using `securityResolver` for per-request dynamic authentication based on the calling context' + - 'Enabling `polling` to automatically refresh tool definitions when the upstream spec changes' + - 'Loading secrets from environment variables instead of hardcoding them' + - 'Each adapter has a unique `name` to avoid tool naming collisions' +--- + +# Authenticated Adapter with Spec Polling + +Demonstrates configuring authentication (API key and bearer token) and automatic spec polling for OpenAPI adapters. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + name: 'integrations', + adapters: [ + // API key authentication via staticAuth + OpenapiAdapter.init({ + name: 'analytics-api', + url: 'https://api.analytics.example.com/openapi.json', + baseUrl: 'https://api.analytics.example.com', + staticAuth: { + apiKey: process.env.ANALYTICS_API_KEY!, + }, + }), + + // Bearer token authentication via staticAuth + OpenapiAdapter.init({ + name: 'crm-api', + url: 'https://crm.example.com/openapi.json', + baseUrl: 'https://crm.example.com', + staticAuth: { + jwt: process.env.CRM_API_TOKEN!, + }, + }), + + // Dynamic auth with spec polling + OpenapiAdapter.init({ + name: 'evolving-api', + url: 'https://api.example.com/openapi.json', + baseUrl: 'https://api.example.com', + securityResolver: (tool, ctx) => { + return { jwt: ctx.authInfo?.token }; + }, + polling: { + intervalMs: 300000, // Re-fetch spec every 5 minutes + }, + }), + ], +}) +class IntegrationsApp {} + +@FrontMcp({ + info: { name: 'integration-hub', version: '1.0.0' }, + apps: [IntegrationsApp], + http: { port: 3000 }, +}) +class MyServer {} +``` + +## What This Demonstrates + +- Three authentication methods: `staticAuth.apiKey`, `staticAuth.jwt`, and dynamic `securityResolver` +- Using `securityResolver` for per-request dynamic authentication based on the calling context +- Enabling `polling` to automatically refresh tool definitions when the upstream spec changes +- Loading secrets from environment variables instead of hardcoding them +- Each adapter has a unique `name` to avoid tool naming collisions + +## Related + +- See `official-adapters` for inline specs, multiple adapter registration, and troubleshooting +- See `decorators-guide` for the full `@App` and `@FrontMcp` field reference diff --git a/libs/skills/catalog/frontmcp-development/examples/official-adapters/basic-openapi-adapter.md b/libs/skills/catalog/frontmcp-development/examples/official-adapters/basic-openapi-adapter.md new file mode 100644 index 000000000..1fd50a9f8 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-adapters/basic-openapi-adapter.md @@ -0,0 +1,54 @@ +--- +name: basic-openapi-adapter +reference: official-adapters +level: basic +description: 'Demonstrates converting an OpenAPI specification into MCP tools automatically using `OpenapiAdapter` with minimal configuration.' +tags: [development, openapi, adapters, adapter] +features: + - 'Using `OpenapiAdapter.init()` with just `name` and `url` to auto-generate MCP tools' + - 'Each OpenAPI operation becomes a tool named `:`' + - 'The adapter is registered in the `adapters` array of `@App`, not in `plugins`' + - 'The `name` field serves as the namespace prefix to prevent tool name collisions' +--- + +# Basic OpenAPI Adapter + +Demonstrates converting an OpenAPI specification into MCP tools automatically using `OpenapiAdapter` with minimal configuration. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + name: 'petstore', + adapters: [ + OpenapiAdapter.init({ + name: 'petstore', + url: 'https://petstore3.swagger.io/api/v3/openapi.json', + }), + ], +}) +class PetstoreApp {} + +@FrontMcp({ + info: { name: 'petstore-server', version: '1.0.0' }, + apps: [PetstoreApp], + http: { port: 3000 }, +}) +class MyServer {} +// Generated tools: petstore:addPet, petstore:getPetById, petstore:deletePet, etc. +``` + +## What This Demonstrates + +- Using `OpenapiAdapter.init()` with just `name` and `url` to auto-generate MCP tools +- Each OpenAPI operation becomes a tool named `:` +- The adapter is registered in the `adapters` array of `@App`, not in `plugins` +- The `name` field serves as the namespace prefix to prevent tool name collisions + +## Related + +- See `official-adapters` for authentication options, spec polling, and inline specs diff --git a/libs/skills/catalog/frontmcp-development/examples/official-adapters/multi-api-hub-with-inline-spec.md b/libs/skills/catalog/frontmcp-development/examples/official-adapters/multi-api-hub-with-inline-spec.md new file mode 100644 index 000000000..e147a623e --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-adapters/multi-api-hub-with-inline-spec.md @@ -0,0 +1,130 @@ +--- +name: multi-api-hub-with-inline-spec +reference: official-adapters +level: advanced +description: 'Demonstrates registering multiple OpenAPI adapters from different APIs in a single app, including one with an inline spec definition instead of a remote URL.' +tags: [development, openapi, remote, adapters, multi, api] +features: + - 'Registering multiple adapters in a single `@App` with unique names for tool namespacing' + - 'Using `additionalHeaders` for header-based authentication (GitHub token)' + - 'Providing an inline `spec` object instead of a remote `url` for APIs without hosted specs' + - "Each adapter's tools are namespaced: `github:*`, `jira:*`, `internal:*`" + - 'Only one of `url` or `spec` should be provided per adapter; `spec` takes precedence' +--- + +# Multi-API Hub with Inline Spec + +Demonstrates registering multiple OpenAPI adapters from different APIs in a single app, including one with an inline spec definition instead of a remote URL. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import { OpenapiAdapter } from '@frontmcp/adapters'; + +@App({ + name: 'integration-hub', + adapters: [ + // Remote specs from public APIs + OpenapiAdapter.init({ + name: 'github', + url: 'https://api.github.com/openapi.json', + additionalHeaders: { + Authorization: `token ${process.env.GITHUB_TOKEN!}`, + }, + }), + + OpenapiAdapter.init({ + name: 'jira', + url: 'https://jira.example.com/openapi.json', + staticAuth: { + apiKey: process.env.JIRA_API_KEY!, + }, + }), + + // Inline spec for an internal API without a hosted spec URL + OpenapiAdapter.init({ + name: 'internal', + spec: { + openapi: '3.0.0', + info: { title: 'Internal API', version: '1.0.0' }, + paths: { + '/health': { + get: { + operationId: 'getHealth', + summary: 'Health check endpoint', + responses: { + '200': { + description: 'Service is healthy', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + status: { type: 'string' }, + uptime: { type: 'number' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/users/{id}': { + get: { + operationId: 'getUserById', + summary: 'Get user by ID', + parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }], + responses: { + '200': { + description: 'User found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + baseUrl: 'https://internal.example.com', + staticAuth: { + jwt: process.env.INTERNAL_API_TOKEN!, + }, + }), + ], +}) +class IntegrationHub {} +// Tools: github:createIssue, jira:createTicket, internal:getHealth, internal:getUserById + +@FrontMcp({ + info: { name: 'multi-api-server', version: '1.0.0' }, + apps: [IntegrationHub], + http: { port: 3000 }, +}) +class MyServer {} +``` + +## What This Demonstrates + +- Registering multiple adapters in a single `@App` with unique names for tool namespacing +- Using `additionalHeaders` for header-based authentication (GitHub token) +- Providing an inline `spec` object instead of a remote `url` for APIs without hosted specs +- Each adapter's tools are namespaced: `github:*`, `jira:*`, `internal:*` +- Only one of `url` or `spec` should be provided per adapter; `spec` takes precedence + +## Related + +- See `official-adapters` for spec polling, `securityResolver`, and the adapter vs plugin comparison +- See `decorators-guide` for the `@Adapter` decorator and how adapters fit in the hierarchy diff --git a/libs/skills/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md b/libs/skills/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md new file mode 100644 index 000000000..ae67ee9bd --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md @@ -0,0 +1,117 @@ +--- +name: cache-and-feature-flags +reference: official-plugins +level: intermediate +description: 'Demonstrates combining the Cache plugin for tool result caching with the Feature Flags plugin for gating tools behind flags.' +tags: [development, feature-flags, cache, plugins, feature, flags] +features: + - 'Combining `CachePlugin` and `FeatureFlagPlugin` in the same server' + - 'Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration' + - 'Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits' + - 'Using `cache: true` for simple default-TTL caching' + - "Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off" + - 'Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks' +--- + +# Cache Plugin and Feature Flags Plugin + +Demonstrates combining the Cache plugin for tool result caching with the Feature Flags plugin for gating tools behind flags. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import CachePlugin from '@frontmcp/plugin-cache'; +import FeatureFlagPlugin from '@frontmcp/plugin-feature-flags'; + +@App({ + name: 'api', + tools: [GetWeatherTool, GetUserProfileTool, BetaSearchTool], +}) +class ApiApp {} + +@FrontMcp({ + info: { name: 'cached-flagged-server', version: '1.0.0' }, + apps: [ApiApp], + plugins: [ + CachePlugin.init({ + type: 'memory', + defaultTTL: 3600, + toolPatterns: ['api:get-*', 'search:*'], + bypassHeader: 'x-frontmcp-disable-cache', + }), + FeatureFlagPlugin.init({ + adapter: 'static', + flags: { + 'beta-search': true, + 'experimental-agent': false, + }, + }), + ], +}) +class MyServer {} +``` + +```typescript +// src/tools/get-weather.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// Per-tool cache metadata with custom TTL and sliding window +@Tool({ + name: 'get_weather', + description: 'Get current weather for a city', + inputSchema: { + city: z.string().describe('City name'), + }, + cache: { + ttl: 1800, + slideWindow: true, + }, +}) +class GetWeatherTool extends ToolContext { + async execute(input: { city: string }) { + const weather = await this.get(WeatherService).getCurrent(input.city); + return { city: input.city, temperature: weather.temp, condition: weather.condition }; + } +} +``` + +```typescript +// src/tools/beta-search.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +// Tool gated behind a feature flag -- hidden from list_tools when flag is off +@Tool({ + name: 'beta_search', + description: 'New search algorithm (beta)', + inputSchema: { + query: z.string(), + }, + featureFlag: 'beta-search', + cache: true, +}) +class BetaSearchTool extends ToolContext { + async execute(input: { query: string }) { + const enabled = await this.featureFlags.isEnabled('beta-search'); + const results = await this.get(SearchService).search(input.query); + return { results, algorithm: 'v2-beta' }; + } +} +``` + +## What This Demonstrates + +- Combining `CachePlugin` and `FeatureFlagPlugin` in the same server +- Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration +- Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits +- Using `cache: true` for simple default-TTL caching +- Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off +- Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks + +## Related + +- See `official-plugins` for all plugin configuration options, Redis cache, and external flag adapters +- See `create-tool-annotations` for additional tool metadata like `readOnlyHint` and `destructiveHint` diff --git a/libs/skills/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md b/libs/skills/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md new file mode 100644 index 000000000..67bdba3a6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md @@ -0,0 +1,147 @@ +--- +name: production-multi-plugin-setup +reference: official-plugins +level: advanced +description: 'Demonstrates a production-ready server configuration combining CodeCall, Remember, Approval, Cache, and Feature Flags plugins with Redis storage and external flag services.' +tags: [development, feature-flags, redis, keyword-search, cache, approval] +features: + - 'Configuring all 5 stable official plugins together in a production server' + - 'CodeCall in `codecall_only` mode with TF-IDF search and synonym expansion for semantic tool discovery' + - 'Remember plugin with Redis storage, encryption enabled, and LLM-accessible memory tools' + - 'Approval plugin in `webhook` mode with external PKCE-secured approval flow and audit logging' + - 'Cache plugin with Redis storage and 24-hour default TTL' + - 'Feature Flags with LaunchDarkly integration for external flag management' + - 'Per-tool `approval` metadata with risk level and scope configuration' + - 'Per-tool `featureFlag` with a `defaultValue` fallback if flag evaluation fails' + - 'Using `this.approval.isApproved()` and `this.remember.set()` together in a single tool' +--- + +# Production Multi-Plugin Setup + +Demonstrates a production-ready server configuration combining CodeCall, Remember, Approval, Cache, and Feature Flags plugins with Redis storage and external flag services. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import CodeCallPlugin from '@frontmcp/plugin-codecall'; +import RememberPlugin from '@frontmcp/plugin-remember'; +import { ApprovalPlugin } from '@frontmcp/plugin-approval'; +import CachePlugin from '@frontmcp/plugin-cache'; +import FeatureFlagPlugin from '@frontmcp/plugin-feature-flags'; + +@App({ name: 'core', tools: [ReadDataTool, WriteDataTool, DeleteDataTool] }) +class CoreApp {} + +@FrontMcp({ + info: { name: 'production-server', version: '1.0.0' }, + apps: [CoreApp], + plugins: [ + CodeCallPlugin.init({ + mode: 'codecall_only', + topK: 8, + maxDefinitions: 8, + vm: { + preset: 'secure', + timeoutMs: 5000, + allowLoops: false, + }, + embedding: { + strategy: 'tfidf', + synonymExpansion: { enabled: true }, + }, + }), + + RememberPlugin.init({ + type: 'redis', + config: { host: process.env.REDIS_HOST!, port: 6379 }, + keyPrefix: 'remember:', + encryption: { enabled: true }, + tools: { enabled: true }, + }), + + ApprovalPlugin.init({ + mode: 'webhook', + webhook: { + url: 'https://approval.example.com/webhook', + challengeTtl: 300, + callbackPath: '/approval/callback', + }, + enableAudit: true, + maxDelegationDepth: 3, + }), + + CachePlugin.init({ + type: 'redis', + config: { host: process.env.REDIS_HOST!, port: 6379 }, + defaultTTL: 86400, + }), + + FeatureFlagPlugin.init({ + adapter: 'launchdarkly', + config: { sdkKey: process.env.LD_SDK_KEY! }, + }), + ], +}) +class ProductionServer {} +``` + +```typescript +// src/tools/delete-data.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'delete_data', + description: 'Permanently delete a data record', + inputSchema: { + recordId: z.string(), + confirm: z.boolean(), + }, + approval: { + required: true, + defaultScope: 'session', + category: 'write', + riskLevel: 'high', + approvalMessage: 'Allow data deletion for this session?', + }, + featureFlag: { key: 'enable-delete', defaultValue: false }, +}) +class DeleteDataTool extends ToolContext { + async execute(input: { recordId: string; confirm: boolean }) { + if (!input.confirm) { + return { deleted: false, reason: 'Confirmation required' }; + } + + const isApproved = await this.approval.isApproved('delete_data'); + if (!isApproved) { + return { deleted: false, reason: 'Awaiting approval' }; + } + + const db = this.get(DatabaseToken); + await db.deleteRecord(input.recordId); + + await this.remember.set(`deleted:${input.recordId}`, new Date().toISOString()); + + return { deleted: true, recordId: input.recordId }; + } +} +``` + +## What This Demonstrates + +- Configuring all 5 stable official plugins together in a production server +- CodeCall in `codecall_only` mode with TF-IDF search and synonym expansion for semantic tool discovery +- Remember plugin with Redis storage, encryption enabled, and LLM-accessible memory tools +- Approval plugin in `webhook` mode with external PKCE-secured approval flow and audit logging +- Cache plugin with Redis storage and 24-hour default TTL +- Feature Flags with LaunchDarkly integration for external flag management +- Per-tool `approval` metadata with risk level and scope configuration +- Per-tool `featureFlag` with a `defaultValue` fallback if flag evaluation fails +- Using `this.approval.isApproved()` and `this.remember.set()` together in a single tool + +## Related + +- See `official-plugins` for all plugin options, storage types, and troubleshooting +- See `create-plugin-hooks` for adding custom lifecycle hooks alongside official plugins diff --git a/libs/skills/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md b/libs/skills/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md new file mode 100644 index 000000000..e4896e26c --- /dev/null +++ b/libs/skills/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md @@ -0,0 +1,104 @@ +--- +name: remember-plugin-session-memory +reference: official-plugins +level: basic +description: 'Demonstrates installing the Remember plugin and using `this.remember` in tools to store and retrieve session memory.' +tags: [development, session, plugins, remember, plugin, memory] +features: + - "Installing `RememberPlugin` with `type: 'memory'` for development" + - 'Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.)' + - 'Using `this.remember.set()` with default `session` scope and explicit `user` scope' + - 'Using `this.remember.get()` with a `defaultValue` fallback' + - 'Using `this.remember.knows()` to check key existence without retrieving the value' +--- + +# Remember Plugin for Session Memory + +Demonstrates installing the Remember plugin and using `this.remember` in tools to store and retrieve session memory. + +## Code + +```typescript +// src/server.ts +import { FrontMcp, App } from '@frontmcp/sdk'; +import RememberPlugin from '@frontmcp/plugin-remember'; + +@App({ name: 'my-app', tools: [PreferencesTool, GreetingTool] }) +class MyApp {} + +@FrontMcp({ + info: { name: 'memory-server', version: '1.0.0' }, + apps: [MyApp], + plugins: [ + RememberPlugin.init({ + type: 'memory', + tools: { enabled: true }, + }), + ], +}) +class MyServer {} +``` + +```typescript +// src/tools/preferences.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'set_preferences', + description: 'Save user preferences for the session', + inputSchema: { + theme: z.enum(['light', 'dark']).describe('UI theme preference'), + language: z.string().describe('Preferred language code'), + }, +}) +class PreferencesTool extends ToolContext { + async execute(input: { theme: string; language: string }) { + await this.remember.set('theme', input.theme); + await this.remember.set('language', input.language, { scope: 'user' }); + + return { saved: true, theme: input.theme, language: input.language }; + } +} +``` + +```typescript +// src/tools/greeting.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get_greeting', + description: 'Get a personalized greeting using remembered preferences', + inputSchema: { + name: z.string().describe('User name'), + }, +}) +class GreetingTool extends ToolContext { + async execute(input: { name: string }) { + const theme = await this.remember.get('theme', { defaultValue: 'light' }); + const language = await this.remember.get('language', { defaultValue: 'en' }); + const hasOnboarded = await this.remember.knows('onboarding_complete'); + + return { + greeting: `Hello ${input.name}!`, + theme, + language, + showOnboarding: !hasOnboarded, + }; + } +} +``` + +## What This Demonstrates + +- Installing `RememberPlugin` with `type: 'memory'` for development +- Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.) +- Using `this.remember.set()` with default `session` scope and explicit `user` scope +- Using `this.remember.get()` with a `defaultValue` fallback +- Using `this.remember.knows()` to check key existence without retrieving the value + +## Related + +- See `official-plugins` for Redis storage, Vercel KV, memory scopes, and all Remember API methods +- See `create-plugin-hooks` for building custom plugins with lifecycle hooks diff --git a/libs/skills/catalog/frontmcp-development/references/create-adapter.md b/libs/skills/catalog/frontmcp-development/references/create-adapter.md index 40e654b68..6777627a6 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-adapter.md +++ b/libs/skills/catalog/frontmcp-development/references/create-adapter.md @@ -164,6 +164,15 @@ Creates a `DynamicAdapter` subclass in `src/adapters/my-adapter.adapter.ts`. | Duplicate tool name error | Multiple adapters produce tools with the same name | Use unique `name` parameter in `init()` to namespace tools | | Adapter not found at runtime | Registered in wrong `@App` or not in `adapters` array | Ensure `.init()` result is in the `adapters` array of the correct `@App` | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| [`basic-api-adapter`](../examples/create-adapter/basic-api-adapter.md) | Basic | A minimal adapter that fetches operation definitions from an external API and generates MCP tools. | +| [`namespaced-adapter`](../examples/create-adapter/namespaced-adapter.md) | Intermediate | An adapter that namespaces generated tools to avoid collisions and includes proper error handling for startup failures. | + +> See all examples in [`examples/create-adapter/`](../examples/create-adapter/) + ## Reference - [Adapter Documentation](https://docs.agentfront.dev/frontmcp/adapters/overview) 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 620d46a70..02d522785 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 @@ -49,3 +49,12 @@ apiKey: 'sk-...'; | Anthropic | `claude-opus-4-20250514` | Most capable | | OpenAI | `gpt-4o` | General purpose | | OpenAI | `gpt-4o-mini` | Fast, cost-effective | + +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------- | ----- | -------------------------------------------------------------------------- | +| [`anthropic-config`](../examples/create-agent-llm-config/anthropic-config.md) | Basic | Configuring an agent with the Anthropic provider and common model options. | +| [`openai-config`](../examples/create-agent-llm-config/openai-config.md) | Basic | Configuring an agent with the OpenAI provider and different model options. | + +> See all examples in [`examples/create-agent-llm-config/`](../examples/create-agent-llm-config/) diff --git a/libs/skills/catalog/frontmcp-development/references/create-agent.md b/libs/skills/catalog/frontmcp-development/references/create-agent.md index 49cb935c7..456812e2f 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-agent.md +++ b/libs/skills/catalog/frontmcp-development/references/create-agent.md @@ -600,6 +600,16 @@ class DocsAgent extends AgentContext {} | Agent times out | No timeout or rate limit configured | Add `timeout: { executeMs: 120_000 }` and `rateLimit` to `@Agent` options | | Swarm handoff fails | Target agent name does not match any registered agent | Ensure `handoff.agent` matches the `name` of a registered agent in the same scope | +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- | +| [`basic-agent-with-tools`](../examples/create-agent/basic-agent-with-tools.md) | Basic | An autonomous agent that uses inner tools to review GitHub pull requests. | +| [`custom-multi-pass-agent`](../examples/create-agent/custom-multi-pass-agent.md) | Intermediate | An agent that overrides `execute()` to perform multi-pass LLM reasoning with `this.completion()`. | +| [`nested-agents-with-swarm`](../examples/create-agent/nested-agents-with-swarm.md) | Advanced | Composing specialized sub-agents and configuring swarm-based handoff between agents. | + +> See all examples in [`examples/create-agent/`](../examples/create-agent/) + ## Reference - [Agents Documentation](https://docs.agentfront.dev/frontmcp/servers/agents) diff --git a/libs/skills/catalog/frontmcp-development/references/create-job.md b/libs/skills/catalog/frontmcp-development/references/create-job.md index c3562c08f..f3fada9d1 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-job.md +++ b/libs/skills/catalog/frontmcp-development/references/create-job.md @@ -607,6 +607,16 @@ class DataServer {} | Job times out unexpectedly | Default 5-minute timeout too short | Set `timeout` in `@Job` to a higher value (e.g., `600000` for 10 minutes) | | Permission denied error | User lacks required roles or scopes | Verify user has one of the `roles` and all `scopes` defined in `permissions` | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------------------------ | +| [`basic-report-job`](../examples/create-job/basic-report-job.md) | Basic | A minimal job that generates a report with progress tracking and structured output. | +| [`job-with-permissions`](../examples/create-job/job-with-permissions.md) | Advanced | A data export job with declarative permission controls, plus a function-style job for simple tasks. | +| [`job-with-retry`](../examples/create-job/job-with-retry.md) | Intermediate | A job that syncs data from an external API with automatic retry, exponential backoff, and batch progress tracking. | + +> See all examples in [`examples/create-job/`](../examples/create-job/) + ## Reference - [Jobs Documentation](https://docs.agentfront.dev/frontmcp/servers/jobs) 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 c1ae97aa1..62bcc672a 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md +++ b/libs/skills/catalog/frontmcp-development/references/create-plugin-hooks.md @@ -369,6 +369,16 @@ Any stage can have `@Will`, `@Did`, `@Stage`, or `@Around` hooks. | Multiple hooks execute in wrong order | Priorities not set or conflicting | Set explicit `priority` values; higher numbers execute first | | `@Stage` replacement causes downstream errors | Return value shape does not match stage contract | Ensure the return matches what the next stage expects (e.g., MCP response format) | +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`basic-logging-plugin`](../examples/create-plugin-hooks/basic-logging-plugin.md) | Basic | Demonstrates a plugin that logs tool execution using `@Will` and `@Did` hook decorators from the pre-built `ToolHook` export. | +| [`caching-with-around`](../examples/create-plugin-hooks/caching-with-around.md) | Intermediate | Demonstrates wrapping tool execution with an `@Around` hook to implement result caching with TTL-based expiry. | +| [`tool-level-hooks-and-stage-replacement`](../examples/create-plugin-hooks/tool-level-hooks-and-stage-replacement.md) | Advanced | Demonstrates two advanced patterns: adding `@Will`/`@Did` hooks directly on a `@Tool` class (scoped to that tool only), and using `@Stage` in a plugin to replace a flow stage entirely with a filtered mock. | + +> See all examples in [`examples/create-plugin-hooks/`](../examples/create-plugin-hooks/) + ## Reference - [Plugin Hooks Documentation](https://docs.agentfront.dev/frontmcp/plugins/creating-plugins) diff --git a/libs/skills/catalog/frontmcp-development/references/create-plugin.md b/libs/skills/catalog/frontmcp-development/references/create-plugin.md index 93ccad0a2..1a9bc805a 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-plugin.md +++ b/libs/skills/catalog/frontmcp-development/references/create-plugin.md @@ -500,6 +500,16 @@ plugins/ | `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 | +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- | +| [`basic-plugin-with-provider`](../examples/create-plugin/basic-plugin-with-provider.md) | Basic | A minimal plugin that contributes an injectable service via the `providers` and `exports` arrays. | +| [`configurable-dynamic-plugin`](../examples/create-plugin/configurable-dynamic-plugin.md) | Advanced | A plugin that accepts runtime configuration via `DynamicPlugin` and extends decorator metadata with custom fields. | +| [`plugin-with-context-extension`](../examples/create-plugin/plugin-with-context-extension.md) | Intermediate | A plugin that adds a `this.auditLog` property to all execution contexts using context extensions and module augmentation. | + +> See all examples in [`examples/create-plugin/`](../examples/create-plugin/) + ## Reference - [Plugin System Documentation](https://docs.agentfront.dev/frontmcp/plugins/creating-plugins) diff --git a/libs/skills/catalog/frontmcp-development/references/create-prompt.md b/libs/skills/catalog/frontmcp-development/references/create-prompt.md index 5b28c9e5e..0786965b9 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-prompt.md +++ b/libs/skills/catalog/frontmcp-development/references/create-prompt.md @@ -437,6 +437,16 @@ This creates the prompt file, spec file, and updates barrel exports. | Type error on `execute()` return | Returning plain string instead of `GetPromptResult` | Wrap return in `{ messages: [{ role: 'user', content: { type: 'text', text: '...' } }] }` | | `this.get(TOKEN)` throws DependencyNotFoundError | Provider not registered in scope | Register provider in `providers` array of `@App` or `@FrontMcp` | +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------- | +| [`basic-prompt`](../examples/create-prompt/basic-prompt.md) | Basic | A simple prompt that generates a structured code review message from user-provided arguments. | +| [`dynamic-rag-prompt`](../examples/create-prompt/dynamic-rag-prompt.md) | Advanced | A prompt that queries a knowledge base via DI to build context-aware messages at runtime. | +| [`multi-turn-debug-session`](../examples/create-prompt/multi-turn-debug-session.md) | Intermediate | A prompt that uses alternating user/assistant messages to guide a structured debugging conversation. | + +> See all examples in [`examples/create-prompt/`](../examples/create-prompt/) + ## Reference - [Prompts Documentation](https://docs.agentfront.dev/frontmcp/servers/prompts) diff --git a/libs/skills/catalog/frontmcp-development/references/create-provider.md b/libs/skills/catalog/frontmcp-development/references/create-provider.md index 72d6ae84f..9cd4af4b4 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-provider.md +++ b/libs/skills/catalog/frontmcp-development/references/create-provider.md @@ -267,6 +267,15 @@ frontmcp dev | Type mismatch on `this.get(TOKEN)` | Token typed with wrong interface | Ensure `Token` generic matches the provider's implemented interface | | Provider not destroyed on shutdown | Missing `onDestroy()` method | Implement `onDestroy()` to close connections and release resources | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | +| [`basic-database-provider`](../examples/create-provider/basic-database-provider.md) | Basic | A provider that manages a database connection pool with `onInit()` and `onDestroy()` lifecycle hooks. | +| [`config-and-api-providers`](../examples/create-provider/config-and-api-providers.md) | Intermediate | A configuration provider with readonly environment settings and an HTTP API client provider. | + +> See all examples in [`examples/create-provider/`](../examples/create-provider/) + ## Reference - [Providers Documentation](https://docs.agentfront.dev/frontmcp/extensibility/providers) diff --git a/libs/skills/catalog/frontmcp-development/references/create-resource.md b/libs/skills/catalog/frontmcp-development/references/create-resource.md index fdbdbc525..332e07853 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-resource.md +++ b/libs/skills/catalog/frontmcp-development/references/create-resource.md @@ -465,24 +465,55 @@ type ResourceArgumentCompleter = (partial: string) => Promise { + 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 }; + } + + async userIdCompleter(partial: string): Promise { + const users = await this.get(UserService).search(partial); + return { values: users.map((u) => u.id), total: users.length }; + } +} +``` + +The naming convention is `${argName}Completer` -- for a URI parameter `{accountName}`, define `accountNameCompleter(partial)`. + +#### Override-Based + +Override the `getArgumentCompleter(argName)` method for dynamic dispatch across multiple parameters. Return a completer function for argument names you support, or `null` for unknown arguments. ```typescript getArgumentCompleter(argName: string): ResourceArgumentCompleter | null { - if (argName === 'myParam') { + if (argName === 'userId') { return async (partial) => { - // Search or filter based on partial input - const matches = await findMatches(partial); - return { values: matches, total: matches.length }; + const users = await this.get(UserService).search(partial); + return { values: users.map((u) => u.id), total: users.length }; }; } return null; } ``` +Convention-based completers take priority when both are present on the same class. + ### Complete Example -A user profile template resource that autocompletes user IDs by searching a user service: +A user profile template resource that autocompletes user IDs using the convention-based approach: ```typescript @ResourceTemplate({ @@ -497,14 +528,9 @@ class UserProfileResource extends ResourceContext<{ userId: string }> { 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; + async userIdCompleter(partial: string): Promise { + const users = await this.get(UserService).search(partial); + return { values: users.map((u) => u.id), total: users.length }; } } ``` @@ -540,8 +566,9 @@ When a client requests completions for the `userId` parameter with a partial str ### Autocompletion -- [ ] Template resources with dynamic params implement `getArgumentCompleter()` +- [ ] Template resources with dynamic params define `${argName}Completer` methods or override `getArgumentCompleter()` - [ ] Completer returns `{ values, total?, hasMore? }` matching the partial input +- [ ] Completers use `this.get()` for DI (both convention and override patterns support full DI) ## Troubleshooting @@ -553,6 +580,16 @@ When a client requests completions for the `userId` parameter with a partial str | Binary content is garbled | Returning raw buffer in `text` field | Use `blob: buffer.toString('base64')` instead of `text` for binary data | | `this.get(TOKEN)` throws DependencyNotFoundError | Provider not registered in scope | Register provider in `providers` array of `@App` or `@FrontMcp` | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------ | +| [`basic-static-resource`](../examples/create-resource/basic-static-resource.md) | Basic | A static resource that exposes application configuration at a fixed URI. | +| [`binary-and-multi-content`](../examples/create-resource/binary-and-multi-content.md) | Advanced | A resource serving binary blob data and a resource returning multiple content items. | +| [`parameterized-template`](../examples/create-resource/parameterized-template.md) | Intermediate | A resource template with typed URI parameters and argument autocompletion. | + +> See all examples in [`examples/create-resource/`](../examples/create-resource/) + ## Reference - [Resources Documentation](https://docs.agentfront.dev/frontmcp/servers/resources) 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 25099a2f6..ed81bd36b 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 @@ -727,6 +727,16 @@ class DeployServiceSkill extends SkillContext {} | Instructions are empty at runtime | `{ file: './path.md' }` path is relative to wrong directory | Use a path relative to the skill file's location, not the project root | | Parameters not visible to AI client | `parameters` defined as a plain object instead of an array | Use array format: `[{ name, description, type, required }]` | +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------- | +| [`basic-tool-orchestration`](../examples/create-skill-with-tools/basic-tool-orchestration.md) | Basic | A skill that guides an AI client through a deploy workflow using referenced MCP tools. | +| [`directory-skill-with-tools`](../examples/create-skill-with-tools/directory-skill-with-tools.md) | Advanced | A directory-based skill loaded with `skillDir()`, plus a class-based skill using Agent Skills spec metadata fields. | +| [`incident-response-skill`](../examples/create-skill-with-tools/incident-response-skill.md) | Intermediate | A skill that uses object-style tool references with purpose descriptions and required flags, plus strict validation. | + +> See all examples in [`examples/create-skill-with-tools/`](../examples/create-skill-with-tools/) + ## Reference - [Skills Documentation](https://docs.agentfront.dev/frontmcp/servers/skills) diff --git a/libs/skills/catalog/frontmcp-development/references/create-skill.md b/libs/skills/catalog/frontmcp-development/references/create-skill.md index b1dcc9e5c..da43c1b9c 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-skill.md +++ b/libs/skills/catalog/frontmcp-development/references/create-skill.md @@ -606,6 +606,16 @@ class DevServer {} | Skill parameters are ignored by the AI | Parameters are declared but not referenced in the instruction text | Mention each parameter by name in the instructions so the AI knows how to apply them | | Directory-based skill missing bundled files | Subdirectories are not named `scripts/`, `references/`, or `assets/` | Use the exact conventional directory names; other names are not auto-bundled | +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------- | +| [`basic-inline-skill`](../examples/create-skill/basic-inline-skill.md) | Basic | A minimal instruction-only skill with inline content and the function builder alternative. | +| [`directory-based-skill`](../examples/create-skill/directory-based-skill.md) | Advanced | A skill loaded from a directory structure with SKILL.md frontmatter, plus file-based and URL-based instruction sources. | +| [`parameterized-skill`](../examples/create-skill/parameterized-skill.md) | Intermediate | A skill with customizable parameters, usage examples for AI guidance, and controlled visibility. | + +> See all examples in [`examples/create-skill/`](../examples/create-skill/) + ## Reference - **Docs:** 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 8a536e404..e5eadd6fe 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md +++ b/libs/skills/catalog/frontmcp-development/references/create-tool-annotations.md @@ -37,3 +37,12 @@ Annotations provide hints to MCP clients about tool behavior: - Set `destructiveHint: true` for delete/overwrite operations (triggers client warnings) - Set `idempotentHint: true` for safe-to-retry tools - Set `openWorldHint: false` for tools that only access local data + +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| [`destructive-delete-tool`](../examples/create-tool-annotations/destructive-delete-tool.md) | Intermediate | Demonstrates annotating a tool that deletes data, enabling MCP clients to warn users before execution. | +| [`readonly-query-tool`](../examples/create-tool-annotations/readonly-query-tool.md) | Basic | Demonstrates annotating a tool that only reads data, signaling to MCP clients that it has no side effects and is safe to retry. | + +> See all examples in [`examples/create-tool-annotations/`](../examples/create-tool-annotations/) 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 d10332907..1b6e43e10 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 @@ -59,3 +59,13 @@ When `outputSchema` is omitted, the tool returns unvalidated content. This: - Risks leaking internal fields to the client - Prevents CodeCall from inferring return types - Loses compile-time type checking on `Out` generic + +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`primitive-and-media-outputs`](../examples/create-tool-output-schema-types/primitive-and-media-outputs.md) | Intermediate | Demonstrates using primitive string literals and media types as `outputSchema` for tools that return plain text, images, or multi-content arrays. | +| [`zod-raw-shape-output`](../examples/create-tool-output-schema-types/zod-raw-shape-output.md) | Basic | Demonstrates the recommended approach of using a Zod raw shape as `outputSchema` for structured, validated JSON output. | +| [`zod-schema-advanced-output`](../examples/create-tool-output-schema-types/zod-schema-advanced-output.md) | Advanced | Demonstrates using full Zod schema objects (not raw shapes) as `outputSchema`, including `z.object()`, `z.array()`, `z.union()`, and `z.discriminatedUnion()`. | + +> See all examples in [`examples/create-tool-output-schema-types/`](../examples/create-tool-output-schema-types/) diff --git a/libs/skills/catalog/frontmcp-development/references/create-tool.md b/libs/skills/catalog/frontmcp-development/references/create-tool.md index f9d63386c..bab580360 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-tool.md +++ b/libs/skills/catalog/frontmcp-development/references/create-tool.md @@ -581,6 +581,16 @@ class ConvertCurrencyTool extends ToolContext { | Output contains unexpected fields | No `outputSchema` defined | Add `outputSchema` to strip unvalidated fields from response | | Tool times out | No timeout configured for long operation | Add `timeout: { executeMs: 30_000 }` to `@Tool` options | +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------- | +| [`basic-class-tool`](../examples/create-tool/basic-class-tool.md) | Basic | A minimal tool using the class-based pattern with Zod input validation and output schema. | +| [`tool-with-di-and-errors`](../examples/create-tool/tool-with-di-and-errors.md) | Intermediate | A tool that resolves a database service via DI and uses `this.fail()` for business-logic errors. | +| [`tool-with-rate-limiting-and-progress`](../examples/create-tool/tool-with-rate-limiting-and-progress.md) | Advanced | A batch processing tool that uses rate limiting, concurrency control, progress notifications, and annotations. | + +> See all examples in [`examples/create-tool/`](../examples/create-tool/) + ## Reference - [Tools Documentation](https://docs.agentfront.dev/frontmcp/servers/tools) diff --git a/libs/skills/catalog/frontmcp-development/references/create-workflow.md b/libs/skills/catalog/frontmcp-development/references/create-workflow.md index 926a098af..a7770ac9d 100644 --- a/libs/skills/catalog/frontmcp-development/references/create-workflow.md +++ b/libs/skills/catalog/frontmcp-development/references/create-workflow.md @@ -750,6 +750,16 @@ class CiServer {} | Webhook trigger does not fire | Missing or mismatched `webhook.secret` | Ensure `webhook.secret` matches the sender's HMAC secret and `webhook.path` is correct | | Workflow exceeds timeout | Total step execution time exceeds the default 600000 ms | Increase `timeout` at the workflow level or add per-step `timeout` overrides | +## Examples + +| Example | Level | Description | +| --------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| [`basic-deploy-pipeline`](../examples/create-workflow/basic-deploy-pipeline.md) | Basic | A linear workflow that builds, tests, and deploys a service with step dependencies and dynamic input. | +| [`parallel-validation-pipeline`](../examples/create-workflow/parallel-validation-pipeline.md) | Intermediate | A workflow that validates multiple datasets in parallel, then conditionally merges results or notifies on failure. | +| [`webhook-triggered-workflow`](../examples/create-workflow/webhook-triggered-workflow.md) | Advanced | A CI/CD workflow triggered by a webhook, featuring `continueOnError`, per-step conditions, and the `workflow()` function builder. | + +> See all examples in [`examples/create-workflow/`](../examples/create-workflow/) + ## Reference - [Workflows Documentation](https://docs.agentfront.dev/frontmcp/servers/workflows) diff --git a/libs/skills/catalog/frontmcp-development/references/decorators-guide.md b/libs/skills/catalog/frontmcp-development/references/decorators-guide.md index 6762d9a57..39b1dc4cc 100644 --- a/libs/skills/catalog/frontmcp-development/references/decorators-guide.md +++ b/libs/skills/catalog/frontmcp-development/references/decorators-guide.md @@ -744,6 +744,16 @@ class AuditHooks { --- +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`agent-skill-job-workflow`](../examples/decorators-guide/agent-skill-job-workflow.md) | Advanced | Demonstrates the advanced decorator types: `@Agent` for autonomous AI agents, `@Skill` for knowledge packages, `@Job` for background tasks, and `@Workflow` for multi-step orchestration. | +| [`basic-server-with-app-and-tools`](../examples/decorators-guide/basic-server-with-app-and-tools.md) | Basic | Demonstrates the minimal decorator hierarchy to create a working FrontMCP server with one app containing a tool and a resource. | +| [`multi-app-with-plugins-and-providers`](../examples/decorators-guide/multi-app-with-plugins-and-providers.md) | Intermediate | Demonstrates a server with multiple `@App` modules, a `@Provider` for dependency injection, and a `@Plugin` for cross-cutting concerns. | + +> See all examples in [`examples/decorators-guide/`](../examples/decorators-guide/) + ## Reference - **Official docs:** [FrontMCP Decorators Overview](https://docs.agentfront.dev/frontmcp/sdk-reference/decorators/overview) diff --git a/libs/skills/catalog/frontmcp-development/references/official-adapters.md b/libs/skills/catalog/frontmcp-development/references/official-adapters.md index 4f103f3cc..4b53adf88 100644 --- a/libs/skills/catalog/frontmcp-development/references/official-adapters.md +++ b/libs/skills/catalog/frontmcp-development/references/official-adapters.md @@ -193,6 +193,16 @@ class IntegrationHub {} | Stale tools after API update | Spec polling not configured | Add `polling: { intervalMs: 300000 }` to refresh every 5 minutes | | TypeScript error importing adapter | Wrong import path | Import from `@frontmcp/adapters`: `import { OpenapiAdapter } from '@frontmcp/adapters'` | +## Examples + +| Example | Level | Description | +| ----------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`authenticated-adapter-with-polling`](../examples/official-adapters/authenticated-adapter-with-polling.md) | Intermediate | Demonstrates configuring authentication (API key and bearer token) and automatic spec polling for OpenAPI adapters. | +| [`basic-openapi-adapter`](../examples/official-adapters/basic-openapi-adapter.md) | Basic | Demonstrates converting an OpenAPI specification into MCP tools automatically using `OpenapiAdapter` with minimal configuration. | +| [`multi-api-hub-with-inline-spec`](../examples/official-adapters/multi-api-hub-with-inline-spec.md) | Advanced | Demonstrates registering multiple OpenAPI adapters from different APIs in a single app, including one with an inline spec definition instead of a remote URL. | + +> See all examples in [`examples/official-adapters/`](../examples/official-adapters/) + ## Reference - [Adapter Overview Documentation](https://docs.agentfront.dev/frontmcp/adapters/overview) diff --git a/libs/skills/catalog/frontmcp-development/references/official-plugins.md b/libs/skills/catalog/frontmcp-development/references/official-plugins.md index 09ff58378..c4a360e62 100644 --- a/libs/skills/catalog/frontmcp-development/references/official-plugins.md +++ b/libs/skills/catalog/frontmcp-development/references/official-plugins.md @@ -731,6 +731,16 @@ class ProductionServer {} | Dashboard returns 404 | Plugin is in beta and auto-disabled in production (`NODE_ENV=production`) | Dashboard is unstable — avoid in production. For dev: set `enabled: true` explicitly | | Approval webhook times out | Callback URL not reachable from the external approval service | Verify `callbackPath` is publicly accessible and matches the webhook configuration | +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`cache-and-feature-flags`](../examples/official-plugins/cache-and-feature-flags.md) | Intermediate | Demonstrates combining the Cache plugin for tool result caching with the Feature Flags plugin for gating tools behind flags. | +| [`production-multi-plugin-setup`](../examples/official-plugins/production-multi-plugin-setup.md) | Advanced | Demonstrates a production-ready server configuration combining CodeCall, Remember, Approval, Cache, and Feature Flags plugins with Redis storage and external flag services. | +| [`remember-plugin-session-memory`](../examples/official-plugins/remember-plugin-session-memory.md) | Basic | Demonstrates installing the Remember plugin and using `this.remember` in tools to store and retrieve session memory. | + +> See all examples in [`examples/official-plugins/`](../examples/official-plugins/) + ## Reference - [Plugins Overview Documentation](https://docs.agentfront.dev/frontmcp/plugins/overview) diff --git a/libs/skills/catalog/frontmcp-extensibility/SKILL.md b/libs/skills/catalog/frontmcp-extensibility/SKILL.md index a84b9483b..168df687e 100644 --- a/libs/skills/catalog/frontmcp-extensibility/SKILL.md +++ b/libs/skills/catalog/frontmcp-extensibility/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/extensibility/overview + docs: https://docs.agentfront.dev/frontmcp/extensibility/providers --- # FrontMCP Extensibility diff --git a/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md new file mode 100644 index 000000000..2df9a5cf4 --- /dev/null +++ b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md @@ -0,0 +1,175 @@ +--- +name: product-catalog-search +reference: vectoriadb +level: advanced +description: 'Shows advanced VectoriaDB usage with typed document metadata, batch operations, filtered search by multiple criteria, and batch indexing of a product catalog.' +tags: [extensibility, vectoriadb, product, catalog, search] +features: + - 'Typed document metadata with `ProductDoc extends DocumentMetadata`' + - 'Batch operations with `db.addMany()` for efficient catalog indexing' + - 'Multi-criteria filtered search combining category, price, and stock status' + - '`maxDocuments` option for DoS protection on large datasets' + - '`FileStorageAdapter` for persisting the entire product index to disk' + - 'Semantic matching: "something to block office noise" finds "noise-canceling headphones"' +--- + +# VectoriaDB: Product Catalog with Typed Metadata and Batch Operations + +Shows advanced VectoriaDB usage with typed document metadata, batch operations, filtered search by multiple criteria, and batch indexing of a product catalog. + +## Code + +```typescript +// src/providers/product-search.provider.ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; +import { VectoriaDB, FileStorageAdapter } from 'vectoriadb'; +import type { DocumentMetadata } from 'vectoriadb'; + +export const ProductSearch = Symbol('ProductSearch'); + +// Typed metadata for product documents +interface ProductDoc extends DocumentMetadata { + name: string; + category: string; + price: number; + inStock: boolean; +} + +@Provider({ name: 'product-search', provide: ProductSearch, scope: ProviderScope.GLOBAL }) +export class ProductSearchProvider { + private db: VectoriaDB; + private ready: Promise; + + constructor() { + this.db = new VectoriaDB({ + modelName: 'Xenova/all-MiniLM-L6-v2', + cacheDir: './.cache/transformers', + useHNSW: true, + defaultSimilarityThreshold: 0.3, + defaultTopK: 10, + maxDocuments: 100000, // DoS protection + storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/product-vectors' }), + }); + this.ready = this.db.initialize(); + } + + // Batch indexing for large catalogs + async indexProducts( + products: Array<{ + id: string; + description: string; + name: string; + category: string; + price: number; + inStock: boolean; + }>, + ) { + await this.ready; + + // Use addMany for efficient batch operations + await this.db.addMany( + products.map((p) => ({ + id: p.id, + text: `${p.name}: ${p.description}`, + metadata: { + id: p.id, + name: p.name, + category: p.category, + price: p.price, + inStock: p.inStock, + }, + })), + ); + + await this.db.saveToStorage(); + } + + // Multi-criteria filtered search + async search(query: string, filters?: { category?: string; maxPrice?: number; inStockOnly?: boolean }, limit = 10) { + await this.ready; + + return this.db.search(query, { + topK: limit, + threshold: 0.4, + filter: (meta) => { + if (filters?.category && meta.category !== filters.category) return false; + if (filters?.maxPrice !== undefined && meta.price > filters.maxPrice) return false; + if (filters?.inStockOnly && !meta.inStock) return false; + return true; + }, + }); + } +} +``` + +```typescript +// src/tools/find-products.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { ProductSearch } from '../providers/product-search.provider'; + +@Tool({ + name: 'find_products', + description: 'Find products using natural language (e.g., "something to block office noise")', + inputSchema: { + query: z.string().min(1).describe('Natural language product search'), + category: z.string().optional().describe('Filter by category'), + maxPrice: z.number().positive().optional().describe('Maximum price'), + inStockOnly: z.boolean().default(true).describe('Only show in-stock products'), + limit: z.number().int().min(1).max(20).default(5).describe('Max results'), + }, + outputSchema: { + products: z.array( + z.object({ + id: z.string(), + name: z.string(), + category: z.string(), + price: z.number(), + score: z.number(), + inStock: z.boolean(), + }), + ), + total: z.number(), + }, +}) +export class FindProductsTool extends ToolContext { + async execute(input: { query: string; category?: string; maxPrice?: number; inStockOnly: boolean; limit: number }) { + const search = this.get(ProductSearch); + + const results = await search.search( + input.query, + { + category: input.category, + maxPrice: input.maxPrice, + inStockOnly: input.inStockOnly, + }, + input.limit, + ); + + return { + products: results.map((r) => ({ + id: r.id, + name: r.metadata.name, + category: r.metadata.category, + price: r.metadata.price, + score: r.score, + inStock: r.metadata.inStock, + })), + total: results.length, + }; + } +} +``` + +## What This Demonstrates + +- Typed document metadata with `ProductDoc extends DocumentMetadata` +- Batch operations with `db.addMany()` for efficient catalog indexing +- Multi-criteria filtered search combining category, price, and stock status +- `maxDocuments` option for DoS protection on large datasets +- `FileStorageAdapter` for persisting the entire product index to disk +- Semantic matching: "something to block office noise" finds "noise-canceling headphones" + +## Related + +- See `vectoriadb` for the full configuration reference, engine comparison, and TFIDFVectoria examples diff --git a/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md new file mode 100644 index 000000000..87d42901f --- /dev/null +++ b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md @@ -0,0 +1,138 @@ +--- +name: semantic-search-with-persistence +reference: vectoriadb +level: intermediate +description: 'Shows how to use `VectoriaDB` for semantic search with transformer models, filtered search, and `FileStorageAdapter` for persistence across restarts.' +tags: [extensibility, vectoriadb, semantic-search, semantic, search, persistence] +features: + - 'Using `VectoriaDB` with transformer models for true semantic search' + - 'Configuring HNSW index (`useHNSW: true`) for fast O(log n) search on large datasets' + - 'Filtered search with a callback: `filter: (m) => m.category === category`' + - '`FileStorageAdapter` for persisting vectors to disk (restored without re-embedding)' + - 'Async initialization with `await db.initialize()` (downloads model on first run)' + - 'Update-or-add pattern with `db.has(id)` check' +--- + +# VectoriaDB: Semantic ML Search with Persistence + +Shows how to use `VectoriaDB` for semantic search with transformer models, filtered search, and `FileStorageAdapter` for persistence across restarts. + +## Code + +```typescript +// src/providers/knowledge-base.provider.ts +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
({ + modelName: 'Xenova/all-MiniLM-L6-v2', // Default transformer model + cacheDir: './.cache/transformers', // Model cache directory + useHNSW: true, // HNSW index for O(log n) search + defaultSimilarityThreshold: 0.4, + defaultTopK: 10, + storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/kb-vectors' }), + }); + // Initialize async — downloads model on first run + 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, + // Filtered search: narrow results by category + filter: options?.category ? (m) => m.category === options.category : undefined, + }); + } + + async index(id: string, text: string, metadata: Article) { + await this.ready; + // Update if exists, add if new + if (this.db.has(id)) { + await this.db.update(id, { text, metadata }); + } else { + await this.db.add(id, text, metadata); + } + // Persist to disk — restored without re-embedding on next startup + await this.db.saveToStorage(); + } + + async loadFromDisk() { + await this.ready; + await this.db.loadFromStorage(); + } +} +``` + +```typescript +// src/tools/semantic-search.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { KnowledgeBase } from '../providers/knowledge-base.provider'; + +@Tool({ + name: 'semantic_search', + description: 'Search the knowledge base using natural language (understands meaning, not just keywords)', + inputSchema: { + query: z.string().min(1).describe('Natural language search query'), + category: z.string().optional().describe('Filter by category'), + limit: z.number().int().min(1).max(20).default(5).describe('Max results'), + }, + outputSchema: { + results: z.array( + z.object({ + id: z.string(), + score: z.number(), + title: z.string(), + category: z.string(), + }), + ), + }, +}) +export class SemanticSearchTool extends ToolContext { + async execute(input: { query: string; category?: string; limit: number }) { + const kb = this.get(KnowledgeBase); + + const results = await kb.search(input.query, { + category: input.category, + limit: input.limit, + }); + + return { + results: results.map((r) => ({ + id: r.id, + score: r.score, + title: r.metadata.title, + category: r.metadata.category, + })), + }; + } +} +``` + +## What This Demonstrates + +- Using `VectoriaDB` with transformer models for true semantic search +- Configuring HNSW index (`useHNSW: true`) for fast O(log n) search on large datasets +- Filtered search with a callback: `filter: (m) => m.category === category` +- `FileStorageAdapter` for persisting vectors to disk (restored without re-embedding) +- Async initialization with `await db.initialize()` (downloads model on first run) +- Update-or-add pattern with `db.has(id)` check + +## Related + +- See `vectoriadb` for the full configuration reference and engine comparison diff --git a/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md new file mode 100644 index 000000000..17c5af6fb --- /dev/null +++ b/libs/skills/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md @@ -0,0 +1,103 @@ +--- +name: tfidf-keyword-search +reference: vectoriadb +level: basic +description: 'Shows how to use `TFIDFVectoria` for zero-dependency keyword search in a FrontMCP provider, with field weights and index building.' +tags: [extensibility, vectoriadb, keyword-search, tfidf, keyword, search] +features: + - 'Using `TFIDFVectoria` for zero-dependency keyword search (no model downloads)' + - 'Configuring field weights to control scoring influence' + - 'Calling `buildIndex()` after adding documents (required for TFIDFVectoria)' + - 'Wrapping the search engine in a FrontMCP provider with `ProviderScope.GLOBAL`' + - 'Injecting the provider into tools via `this.get(FAQSearch)`' +--- + +# TFIDFVectoria: Lightweight Keyword Search Provider + +Shows how to use `TFIDFVectoria` for zero-dependency keyword search in a FrontMCP provider, with field weights and index building. + +## Code + +```typescript +// src/providers/faq-search.provider.ts +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 }, // Question matches are 3x more important + answer: { weight: 1 }, // Answer matches are baseline + tags: { weight: 2 }, // Tag matches are 2x + }, + }); + + 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, + }); + } + // Required after adding documents — builds the TF-IDF index + this.db.buildIndex(); + } + + search(query: string, limit = 5) { + return this.db.search(query, limit); + } +} +``` + +```typescript +// src/tools/search-faq.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { FAQSearch } from '../providers/faq-search.provider'; + +@Tool({ + name: 'search_faq', + description: 'Search the FAQ knowledge base using keyword matching', + inputSchema: { + query: z.string().min(1).describe('Search query'), + limit: z.number().int().min(1).max(20).default(5).describe('Max results'), + }, + outputSchema: { + results: z.array( + z.object({ + id: z.string(), + score: z.number(), + }), + ), + }, +}) +export class SearchFaqTool extends ToolContext { + async execute(input: { query: string; limit: number }) { + const faqSearch = this.get(FAQSearch); + const results = faqSearch.search(input.query, input.limit); + + return { + results: results.map((r) => ({ + id: r.id, + score: r.score, + })), + }; + } +} +``` + +## What This Demonstrates + +- Using `TFIDFVectoria` for zero-dependency keyword search (no model downloads) +- Configuring field weights to control scoring influence +- Calling `buildIndex()` after adding documents (required for TFIDFVectoria) +- Wrapping the search engine in a FrontMCP provider with `ProviderScope.GLOBAL` +- Injecting the provider into tools via `this.get(FAQSearch)` + +## Related + +- See `vectoriadb` for the full API reference and engine comparison diff --git a/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md b/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md index 1227ef65d..ab2f5fdb0 100644 --- a/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md +++ b/libs/skills/catalog/frontmcp-extensibility/references/vectoriadb.md @@ -282,6 +282,16 @@ export class KnowledgeBaseProvider { - [ ] Storage adapter configured if persistence is needed - [ ] Search tool injects provider via `this.get(TOKEN)` +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`product-catalog-search`](../examples/vectoriadb/product-catalog-search.md) | Advanced | Shows advanced VectoriaDB usage with typed document metadata, batch operations, filtered search by multiple criteria, and batch indexing of a product catalog. | +| [`semantic-search-with-persistence`](../examples/vectoriadb/semantic-search-with-persistence.md) | Intermediate | Shows how to use `VectoriaDB` for semantic search with transformer models, filtered search, and `FileStorageAdapter` for persistence across restarts. | +| [`tfidf-keyword-search`](../examples/vectoriadb/tfidf-keyword-search.md) | Basic | Shows how to use `TFIDFVectoria` for zero-dependency keyword search in a FrontMCP provider, with field weights and index building. | + +> See all examples in [`examples/vectoriadb/`](../examples/vectoriadb/) + ## Reference - [VectoriaDB Documentation](https://docs.agentfront.dev/vectoriadb/get-started/welcome) diff --git a/libs/skills/catalog/frontmcp-guides/SKILL.md b/libs/skills/catalog/frontmcp-guides/SKILL.md index db39144fb..aa48da33b 100644 --- a/libs/skills/catalog/frontmcp-guides/SKILL.md +++ b/libs/skills/catalog/frontmcp-guides/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/guides/overview + docs: https://docs.agentfront.dev/frontmcp/guides/your-first-tool examples: - scenario: Build a simple weather API MCP server from scratch expected-outcome: Working server with tools, resources, and tests deployed to Node @@ -415,6 +415,6 @@ export class ResearcherAgent extends AgentContext { ## Reference -- [Guides Documentation](https://docs.agentfront.dev/frontmcp/guides/overview) +- [Your First Tool](https://docs.agentfront.dev/frontmcp/guides/your-first-tool) - Domain routers: `frontmcp-development`, `frontmcp-deployment`, `frontmcp-testing`, `frontmcp-config` - Core skills: `setup-project`, `create-tool`, `create-resource`, `create-provider`, `create-agent`, `configure-auth`, `setup-testing` diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md new file mode 100644 index 000000000..32933260f --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md @@ -0,0 +1,160 @@ +--- +name: agent-and-plugin +reference: example-knowledge-base +level: advanced +description: 'Shows an autonomous research agent with inner tools and configurable depth, and a plugin that hooks into tool execution for audit logging.' +tags: [guides, knowledge-base, knowledge, base, agent, plugin] +features: + - 'Agent with `@Agent` decorator, LLM config, inner tools, and system instructions' + - 'Using `this.run(prompt, { maxIterations })` to execute the LLM tool-use loop' + - "Configurable behavior via input schema (`depth: 'shallow' | 'deep'`)" + - 'Plugin hooks: `onToolExecuteBefore`, `onToolExecuteAfter`, `onToolExecuteError`' + - 'Using `ctx.state.set/get()` for flow state instead of mutating `rawInput`' + - 'Non-blocking audit logging (`.catch()` prevents audit failures from breaking tools)' +--- + +# Knowledge Base: Research Agent and Audit Log Plugin + +Shows an autonomous research agent with inner tools and configurable depth, and a plugin that hooks into tool execution for audit logging. + +## Code + +```typescript +// src/research/agents/researcher.agent.ts +import { Agent, AgentContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { SearchDocsTool } from '../../search/tools/search-docs.tool'; +import { IngestDocumentTool } from '../../ingestion/tools/ingest-document.tool'; + +@Agent({ + name: 'research_topic', + description: 'Research a topic across the knowledge base and synthesize findings into a structured report', + inputSchema: { + topic: z.string().min(1).describe('Research topic or question'), + depth: z.enum(['shallow', 'deep']).default('shallow').describe('Research depth'), + }, + outputSchema: { + topic: z.string(), + summary: z.string(), + sources: z.array( + z.object({ + documentId: z.string(), + title: z.string(), + relevance: z.string(), + }), + ), + confidence: z.enum(['low', 'medium', 'high']), + }, + llm: { + provider: 'anthropic', + model: 'claude-sonnet-4-20250514', + apiKey: { env: 'ANTHROPIC_API_KEY' }, + maxTokens: 4096, + }, + // Inner tools: the agent can call these during its execution + tools: [SearchDocsTool, IngestDocumentTool], + systemInstructions: `You are a research assistant with access to a knowledge base. +Your job is to: +1. Search the knowledge base for relevant documents using the search_docs tool. +2. Analyze the results and identify key themes. +3. If depth is "deep", perform multiple searches with refined queries. +4. Synthesize findings into a structured summary with source attribution. +Always cite which documents support your findings.`, +}) +export class ResearcherAgent extends AgentContext { + async execute(input: { topic: string; depth: 'shallow' | 'deep' }) { + const maxIterations = input.depth === 'deep' ? 5 : 2; + const prompt = [ + `Research the following topic: "${input.topic}"`, + `Depth: ${input.depth} (max ${maxIterations} search iterations)`, + 'Search the knowledge base, analyze results, and produce a structured summary.', + 'Return your findings as JSON matching the output schema.', + ].join('\n'); + + // this.run() executes the LLM loop with inner tools + return this.run(prompt, { maxIterations }); + } +} +``` + +```typescript +// src/plugins/audit-log.plugin.ts +import { Plugin } from '@frontmcp/sdk'; +import type { PluginHookContext } from '@frontmcp/sdk'; + +@Plugin({ + name: 'AuditLog', + description: 'Logs all tool invocations for audit compliance', +}) +export class AuditLogPlugin { + private readonly logs: Array<{ + timestamp: string; + tool: string; + userId: string | undefined; + duration: number; + success: boolean; + }> = []; + + async onToolExecuteBefore(ctx: PluginHookContext): Promise { + // Store start time in flow state (not in rawInput) + ctx.state.set('audit:startTime', Date.now()); + } + + async onToolExecuteAfter(ctx: PluginHookContext): Promise { + const startTime = ctx.state.get('audit:startTime') as number; + const duration = Date.now() - startTime; + + const entry = { + timestamp: new Date().toISOString(), + tool: ctx.toolName, + userId: ctx.session?.userId, + duration, + success: true, + }; + this.logs.push(entry); + + // In production, send to an external logging service + if (process.env.AUDIT_LOG_ENDPOINT) { + await ctx + .fetch(process.env.AUDIT_LOG_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(entry), + }) + .catch(() => { + // Audit logging should not block tool execution + }); + } + } + + async onToolExecuteError(ctx: PluginHookContext): Promise { + const startTime = ctx.state.get('audit:startTime') as number; + const duration = Date.now() - startTime; + + this.logs.push({ + timestamp: new Date().toISOString(), + tool: ctx.toolName, + userId: ctx.session?.userId, + duration, + success: false, + }); + } + + getLogs(): typeof this.logs { + return [...this.logs]; + } +} +``` + +## What This Demonstrates + +- Agent with `@Agent` decorator, LLM config, inner tools, and system instructions +- Using `this.run(prompt, { maxIterations })` to execute the LLM tool-use loop +- Configurable behavior via input schema (`depth: 'shallow' | 'deep'`) +- Plugin hooks: `onToolExecuteBefore`, `onToolExecuteAfter`, `onToolExecuteError` +- Using `ctx.state.set/get()` for flow state instead of mutating `rawInput` +- Non-blocking audit logging (`.catch()` prevents audit failures from breaking tools) + +## Related + +- See `example-knowledge-base` for the full knowledge base example with vector store and tests diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/multi-app-composition.md b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/multi-app-composition.md new file mode 100644 index 000000000..e157eda03 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/multi-app-composition.md @@ -0,0 +1,92 @@ +--- +name: multi-app-composition +reference: example-knowledge-base +level: basic +description: 'Shows how to compose multiple apps (Ingestion, Search, Research) into a single server with shared providers, plugins, and agent registration.' +tags: [guides, multi-app, knowledge-base, knowledge, base, multi] +features: + - 'Composing three apps into one server: Ingestion (tools + providers), Search (tools + resources), Research (agents)' + - 'Sharing providers across apps (VectorStoreProvider used by both Ingestion and Search)' + - 'Registering plugins at the server level (AuditLogPlugin applies to all tools)' + - 'Registering agents in a dedicated app for AI-powered features' +--- + +# Knowledge Base: Multi-App Composition + +Shows how to compose multiple apps (Ingestion, Search, Research) into a single server with shared providers, plugins, and agent registration. + +## Code + +```typescript +// src/main.ts +import { FrontMcp } from '@frontmcp/sdk'; +import { IngestionApp } from './ingestion/ingestion.app'; +import { SearchApp } from './search/search.app'; +import { ResearchApp } from './research/research.app'; +import { AuditLogPlugin } from './plugins/audit-log.plugin'; + +@FrontMcp({ + info: { name: 'knowledge-base', version: '1.0.0' }, + apps: [IngestionApp, SearchApp, ResearchApp], + plugins: [AuditLogPlugin], + auth: { mode: 'remote', provider: 'https://auth.example.com', clientId: 'my-client-id' }, + redis: { provider: 'redis', host: process.env.REDIS_URL ?? 'localhost' }, +}) +export default class KnowledgeBaseServer {} +``` + +```typescript +// src/ingestion/ingestion.app.ts +import { App } from '@frontmcp/sdk'; +import { VectorStoreProvider } from './providers/vector-store.provider'; +import { IngestDocumentTool } from './tools/ingest-document.tool'; + +@App({ + name: 'Ingestion', + description: 'Document ingestion and chunking pipeline', + providers: [VectorStoreProvider], + tools: [IngestDocumentTool], +}) +export class IngestionApp {} +``` + +```typescript +// src/search/search.app.ts +import { App } from '@frontmcp/sdk'; +import { VectorStoreProvider } from '../ingestion/providers/vector-store.provider'; +import { SearchDocsTool } from './tools/search-docs.tool'; +import { DocResource } from './resources/doc.resource'; + +@App({ + name: 'Search', + description: 'Semantic search and document retrieval', + providers: [VectorStoreProvider], + tools: [SearchDocsTool], + resources: [DocResource], +}) +export class SearchApp {} +``` + +```typescript +// src/research/research.app.ts +import { App } from '@frontmcp/sdk'; +import { ResearcherAgent } from './agents/researcher.agent'; + +@App({ + name: 'Research', + description: 'AI-powered research agent for knowledge synthesis', + agents: [ResearcherAgent], +}) +export class ResearchApp {} +``` + +## What This Demonstrates + +- Composing three apps into one server: Ingestion (tools + providers), Search (tools + resources), Research (agents) +- Sharing providers across apps (VectorStoreProvider used by both Ingestion and Search) +- Registering plugins at the server level (AuditLogPlugin applies to all tools) +- Registering agents in a dedicated app for AI-powered features + +## Related + +- See `example-knowledge-base` for the full knowledge base example with vector store, search, and agent code diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md new file mode 100644 index 000000000..19fa93ab6 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md @@ -0,0 +1,135 @@ +--- +name: vector-search-and-resources +reference: example-knowledge-base +level: intermediate +description: 'Shows a semantic search tool with embedding generation and a resource template for retrieving documents by ID using URI parameters.' +tags: [guides, openai, semantic-search, knowledge-base, knowledge, base] +features: + - 'Semantic search tool that generates query embeddings via `this.fetch()` to OpenAI' + - 'Using `this.mark()` for execution phase tracing' + - "Resource template with `uriTemplate: 'kb://documents/{documentId}'` for parameterized URIs" + - 'Typed params via `ResourceContext<{ documentId: string }>` for type-safe URI parameters' + - 'Returning `ReadResourceResult` with proper MCP protocol structure' +--- + +# Knowledge Base: Semantic Search Tool and Resource Template + +Shows a semantic search tool with embedding generation and a resource template for retrieving documents by ID using URI parameters. + +## Code + +```typescript +// src/search/tools/search-docs.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider'; + +@Tool({ + name: 'search_docs', + description: 'Semantic search across the knowledge base', + inputSchema: { + query: z.string().min(1).describe('Natural language search query'), + topK: z.number().int().min(1).max(20).default(5).describe('Number of results'), + }, + outputSchema: { + results: z.array( + z.object({ + documentId: z.string(), + content: z.string(), + score: z.number(), + title: z.string(), + }), + ), + total: z.number(), + }, +}) +export class SearchDocsTool extends ToolContext { + async execute(input: { query: string; topK: number }) { + const store = this.get(VECTOR_STORE); + + // Mark execution phases for observability + this.mark('embedding-query'); + const queryEmbedding = await this.generateQueryEmbedding(input.query); + + this.mark('searching'); + const chunks = await store.search(queryEmbedding, input.topK); + + const results = chunks.map((chunk) => ({ + documentId: chunk.documentId, + content: chunk.content, + score: chunk.metadata.score ? parseFloat(chunk.metadata.score) : 0, + title: chunk.metadata.title ?? 'Untitled', + })); + + return { results, total: results.length }; + } + + private async generateQueryEmbedding(query: string): Promise { + const response = await this.fetch('https://api.openai.com/v1/embeddings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + }, + body: JSON.stringify({ input: query, model: 'text-embedding-3-small' }), + }); + const data = await response.json(); + return data.data[0].embedding; + } +} +``` + +```typescript +// src/search/resources/doc.resource.ts +import { ResourceTemplate, ResourceContext } from '@frontmcp/sdk'; +import type { ReadResourceResult } from '@frontmcp/protocol'; +import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider'; + +@ResourceTemplate({ + name: 'document', + uriTemplate: 'kb://documents/{documentId}', + description: 'Retrieve all chunks of a document by its ID', + mimeType: 'application/json', +}) +export class DocResource extends ResourceContext<{ documentId: string }> { + async execute(uri: string, params: { documentId: string }): Promise { + const store = this.get(VECTOR_STORE); + const chunks = await store.getByDocumentId(params.documentId); + + if (chunks.length === 0) { + this.fail(new Error(`Document not found: ${params.documentId}`)); + } + + const document = { + documentId: params.documentId, + title: chunks[0].metadata.title ?? 'Untitled', + chunks: chunks.map((c) => ({ + chunkIndex: c.metadata.chunkIndex, + content: c.content, + })), + }; + + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(document, null, 2), + }, + ], + }; + } +} +``` + +## What This Demonstrates + +- Semantic search tool that generates query embeddings via `this.fetch()` to OpenAI +- Using `this.mark()` for execution phase tracing +- Resource template with `uriTemplate: 'kb://documents/{documentId}'` for parameterized URIs +- Typed params via `ResourceContext<{ documentId: string }>` for type-safe URI parameters +- Returning `ReadResourceResult` with proper MCP protocol structure + +## Related + +- See `example-knowledge-base` for the full knowledge base example with ingestion, agent, and plugin code diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md new file mode 100644 index 000000000..0861e4722 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md @@ -0,0 +1,135 @@ +--- +name: auth-and-crud-tools +reference: example-task-manager +level: basic +description: 'Shows how to create CRUD tools with authentication, using `this.context.session` for user isolation and `this.get()` for dependency injection.' +tags: [guides, auth, session, task-manager, task, manager] +features: + - 'Using `this.context.session?.userId` for per-user data isolation' + - 'Using `this.get(TASK_STORE)` for dependency injection of providers' + - 'Enforcing authentication with `this.fail()` when no session exists' + - 'Optional input fields with `.optional()` for filtering' + - '`outputSchema` with nested `z.array(z.object(...))` for structured responses' +--- + +# Task Manager: Authenticated CRUD Tools + +Shows how to create CRUD tools with authentication, using `this.context.session` for user isolation and `this.get()` for dependency injection. + +## Code + +```typescript +// src/tools/create-task.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { TASK_STORE } from '../providers/task-store.provider'; + +@Tool({ + name: 'create_task', + description: 'Create a new task for the authenticated user', + inputSchema: { + title: z.string().min(1).max(200).describe('Task title'), + priority: z.enum(['low', 'medium', 'high']).default('medium').describe('Task priority'), + }, + outputSchema: { + id: z.string(), + title: z.string(), + priority: z.string(), + status: z.string(), + createdAt: z.string(), + }, +}) +export class CreateTaskTool extends ToolContext { + async execute(input: { title: string; priority: 'low' | 'medium' | 'high' }) { + // Inject the task store via DI + const store = this.get(TASK_STORE); + + // Get the authenticated user's ID from the session + const userId = this.context.session?.userId; + if (!userId) { + this.fail(new Error('Authentication required')); + } + + const task = await store.create({ + title: input.title, + priority: input.priority, + status: 'pending', + userId, + }); + + return { + id: task.id, + title: task.title, + priority: task.priority, + status: task.status, + createdAt: task.createdAt, + }; + } +} +``` + +```typescript +// src/tools/list-tasks.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { TASK_STORE } from '../providers/task-store.provider'; + +@Tool({ + name: 'list_tasks', + description: 'List all tasks for the authenticated user', + inputSchema: { + status: z.enum(['pending', 'in_progress', 'done']).optional().describe('Filter by status'), + }, + outputSchema: { + tasks: z.array( + z.object({ + id: z.string(), + title: z.string(), + priority: z.string(), + status: z.string(), + createdAt: z.string(), + }), + ), + total: z.number(), + }, +}) +export class ListTasksTool extends ToolContext { + async execute(input: { status?: 'pending' | 'in_progress' | 'done' }) { + const store = this.get(TASK_STORE); + const userId = this.context.session?.userId; + + if (!userId) { + this.fail(new Error('Authentication required')); + } + + let tasks = await store.list(userId); + + if (input.status) { + tasks = tasks.filter((t) => t.status === input.status); + } + + return { + tasks: tasks.map((t) => ({ + id: t.id, + title: t.title, + priority: t.priority, + status: t.status, + createdAt: t.createdAt, + })), + total: tasks.length, + }; + } +} +``` + +## What This Demonstrates + +- Using `this.context.session?.userId` for per-user data isolation +- Using `this.get(TASK_STORE)` for dependency injection of providers +- Enforcing authentication with `this.fail()` when no session exists +- Optional input fields with `.optional()` for filtering +- `outputSchema` with nested `z.array(z.object(...))` for structured responses + +## Related + +- See `example-task-manager` for the full task manager example with provider, auth, and tests diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md new file mode 100644 index 000000000..2da93bddf --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md @@ -0,0 +1,148 @@ +--- +name: authenticated-e2e-tests +reference: example-task-manager +level: advanced +description: 'Shows how to write E2E tests with authentication using `TestTokenFactory`, and unit tests for tools that require session context.' +tags: [guides, auth, session, e2e, unit-test, task-manager] +features: + - 'Using `TestTokenFactory` to create JWT tokens for authenticated E2E tests' + - 'Chaining `.withToken(token).buildAndConnect()` for authenticated clients' + - 'Unit testing with mocked DI tokens via `this.get()` mock' + - 'Mocking session context (`context: { session: { userId } }`) for auth-dependent tools' + - 'Testing the unauthenticated error path (no session)' +--- + +# Task Manager: Authenticated E2E Tests + +Shows how to write E2E tests with authentication using `TestTokenFactory`, and unit tests for tools that require session context. + +## Code + +```typescript +// test/tasks.e2e.spec.ts +import { McpTestClient, TestServer, TestTokenFactory } from '@frontmcp/testing'; + +describe('Task Manager E2E', () => { + let client: McpTestClient; + let server: TestServer; + + beforeAll(async () => { + server = await TestServer.start({ command: 'npx tsx src/main.ts' }); + + // Create a test token for authenticated requests + const tokenFactory = new TestTokenFactory(); + const token = await tokenFactory.createTestToken({ sub: 'user-e2e', scopes: ['tasks'] }); + + // Build client with the auth token + client = await McpTestClient.create({ baseUrl: server.info.baseUrl }).withToken(token).buildAndConnect(); + }); + + afterAll(async () => { + await client.disconnect(); + await server.stop(); + }); + + it('should list all CRUD tools', async () => { + const { tools } = await client.listTools(); + const names = tools.map((t) => t.name); + + expect(names).toContain('create_task'); + expect(names).toContain('list_tasks'); + expect(names).toContain('update_task'); + expect(names).toContain('delete_task'); + }); + + it('should create and list a task', async () => { + const createResult = await client.callTool('create_task', { + title: 'E2E test task', + priority: 'high', + }); + expect(createResult).toBeSuccessful(); + + const listResult = await client.callTool('list_tasks', {}); + expect(listResult).toBeSuccessful(); + + const parsed = JSON.parse(listResult.content[0].text); + expect(parsed.tasks.length).toBeGreaterThan(0); + expect(parsed.tasks.some((t: { title: string }) => t.title === 'E2E test task')).toBe(true); + }); +}); +``` + +```typescript +// test/create-task.tool.spec.ts — Unit test with mocked session +import { ToolContext } from '@frontmcp/sdk'; +import { CreateTaskTool } from '../src/tools/create-task.tool'; +import { TASK_STORE, type TaskStore } from '../src/providers/task-store.provider'; +import type { Task } from '../src/types/task'; + +describe('CreateTaskTool', () => { + let tool: CreateTaskTool; + let mockStore: jest.Mocked; + + beforeEach(() => { + tool = new CreateTaskTool(); + mockStore = { + create: jest.fn(), + list: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }; + }); + + function applyContext(userId: string | undefined): void { + const ctx = { + get: jest.fn((token: symbol) => { + if (token === TASK_STORE) return mockStore; + throw new Error(`Unknown token: ${String(token)}`); + }), + tryGet: jest.fn(), + fail: jest.fn((err: Error) => { + throw err; + }), + mark: jest.fn(), + notify: jest.fn(), + respondProgress: jest.fn(), + context: { session: userId ? { userId } : undefined }, + } as unknown as ToolContext; + Object.assign(tool, ctx); + } + + it('should create a task for an authenticated user', async () => { + const mockTask: Task = { + id: 'task-001', + title: 'Write tests', + priority: 'high', + status: 'pending', + userId: 'user-123', + createdAt: '2026-03-27T10:00:00.000Z', + }; + mockStore.create.mockResolvedValue(mockTask); + applyContext('user-123'); + + const result = await tool.execute({ title: 'Write tests', priority: 'high' }); + + expect(result.id).toBe('task-001'); + expect(result.title).toBe('Write tests'); + expect(mockStore.create).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-123', status: 'pending' })); + }); + + it('should fail when user is not authenticated', async () => { + applyContext(undefined); + + await expect(tool.execute({ title: 'Write tests', priority: 'medium' })).rejects.toThrow('Authentication required'); + }); +}); +``` + +## What This Demonstrates + +- Using `TestTokenFactory` to create JWT tokens for authenticated E2E tests +- Chaining `.withToken(token).buildAndConnect()` for authenticated clients +- Unit testing with mocked DI tokens via `this.get()` mock +- Mocking session context (`context: { session: { userId } }`) for auth-dependent tools +- Testing the unauthenticated error path (no session) + +## Related + +- See `example-task-manager` for the full task manager example with server setup and Vercel deployment diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md new file mode 100644 index 000000000..e467bf2d2 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md @@ -0,0 +1,129 @@ +--- +name: redis-provider-with-di +reference: example-task-manager +level: intermediate +description: 'Shows how to create a Redis-backed provider with a DI token, lifecycle hooks (`onInit`/`onDestroy`), and how tools inject it.' +tags: [guides, redis, node, task-manager, task, manager] +features: + - "Defining an interface and DI token (`Symbol('TaskStore')`) for the provider" + - 'Using `@Provider({ token: TASK_STORE })` to register the provider for DI' + - 'Lifecycle hooks: `onInit()` for connection setup, `onDestroy()` for cleanup' + - 'Lazy-loading `ioredis` via dynamic `import()` in `onInit()`' + - 'Using `@frontmcp/utils` for `randomUUID()` instead of `node:crypto`' + - 'Per-user data isolation using Redis hash keys (`tasks:${userId}`)' +--- + +# Task Manager: Redis Provider with Dependency Injection + +Shows how to create a Redis-backed provider with a DI token, lifecycle hooks (`onInit`/`onDestroy`), and how tools inject it. + +## Code + +```typescript +// src/types/task.ts +export interface Task { + id: string; + title: string; + priority: 'low' | 'medium' | 'high'; + status: 'pending' | 'in_progress' | 'done'; + userId: string; + createdAt: string; +} +``` + +```typescript +// src/providers/task-store.provider.ts +import { Provider } from '@frontmcp/sdk'; +import type { Token } from '@frontmcp/di'; +import type { Task } from '../types/task'; + +export interface TaskStore { + create(task: Omit): Promise; + list(userId: string): Promise; + update(id: string, userId: string, data: Partial>): Promise; + delete(id: string, userId: string): Promise; +} + +// DI token — tools use this.get(TASK_STORE) to access the provider +export const TASK_STORE: Token = Symbol('TaskStore'); + +@Provider({ token: TASK_STORE }) +export class RedisTaskStoreProvider implements TaskStore { + private redis!: import('ioredis').default; + + // Lifecycle: initialize Redis connection + async onInit(): Promise { + const Redis = (await import('ioredis')).default; + this.redis = new Redis(process.env.REDIS_URL ?? 'redis://localhost:6379'); + } + + async create(input: Omit): Promise { + const { randomUUID } = await import('@frontmcp/utils'); + const task: Task = { + ...input, + id: randomUUID(), + createdAt: new Date().toISOString(), + }; + await this.redis.hset(`tasks:${task.userId}`, task.id, JSON.stringify(task)); + return task; + } + + async list(userId: string): Promise { + const entries = await this.redis.hgetall(`tasks:${userId}`); + return Object.values(entries).map((v) => JSON.parse(v) as Task); + } + + async update(id: string, userId: string, data: Partial>): Promise { + const raw = await this.redis.hget(`tasks:${userId}`, id); + if (!raw) { + throw new Error(`Task not found: ${id}`); + } + const task: Task = { ...(JSON.parse(raw) as Task), ...data }; + await this.redis.hset(`tasks:${userId}`, id, JSON.stringify(task)); + return task; + } + + async delete(id: string, userId: string): Promise { + const removed = await this.redis.hdel(`tasks:${userId}`, id); + if (removed === 0) { + throw new Error(`Task not found: ${id}`); + } + } + + // Lifecycle: close Redis connection on shutdown + async onDestroy(): Promise { + await this.redis.quit(); + } +} +``` + +```typescript +// src/tasks.app.ts +import { App } from '@frontmcp/sdk'; +import { RedisTaskStoreProvider } from './providers/task-store.provider'; +import { CreateTaskTool } from './tools/create-task.tool'; +import { ListTasksTool } from './tools/list-tasks.tool'; +import { UpdateTaskTool } from './tools/update-task.tool'; +import { DeleteTaskTool } from './tools/delete-task.tool'; + +@App({ + name: 'Tasks', + description: 'Task management with CRUD operations', + providers: [RedisTaskStoreProvider], + tools: [CreateTaskTool, ListTasksTool, UpdateTaskTool, DeleteTaskTool], +}) +export class TasksApp {} +``` + +## What This Demonstrates + +- Defining an interface and DI token (`Symbol('TaskStore')`) for the provider +- Using `@Provider({ token: TASK_STORE })` to register the provider for DI +- Lifecycle hooks: `onInit()` for connection setup, `onDestroy()` for cleanup +- Lazy-loading `ioredis` via dynamic `import()` in `onInit()` +- Using `@frontmcp/utils` for `randomUUID()` instead of `node:crypto` +- Per-user data isolation using Redis hash keys (`tasks:${userId}`) + +## Related + +- See `example-task-manager` for the full task manager example with auth and E2E tests diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md new file mode 100644 index 000000000..643f2c8a3 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md @@ -0,0 +1,75 @@ +--- +name: server-and-app-setup +reference: example-weather-api +level: basic +description: 'Shows the server entry point, app registration, and static resource for a beginner FrontMCP weather API server.' +tags: [guides, weather, api, app, setup] +features: + - 'Server entry point with `@FrontMcp` decorator and `info` configuration' + - 'App registration with `@App` grouping tools and resources together' + - 'Static resource that returns JSON data via `read()`' + - 'Clean separation between server, app, tools, and resources' +--- + +# Weather API: Server and App Setup + +Shows the server entry point, app registration, and static resource for a beginner FrontMCP weather API server. + +## Code + +```typescript +// src/main.ts +import { FrontMcp } from '@frontmcp/sdk'; +import { WeatherApp } from './weather.app'; + +@FrontMcp({ + info: { name: 'weather-api', version: '1.0.0' }, + apps: [WeatherApp], +}) +export default class WeatherServer {} +``` + +```typescript +// src/weather.app.ts +import { App } from '@frontmcp/sdk'; +import { GetWeatherTool } from './tools/get-weather.tool'; +import { CitiesResource } from './resources/cities.resource'; + +@App({ + name: 'Weather', + description: 'Weather lookup tools and city data', + tools: [GetWeatherTool], + resources: [CitiesResource], +}) +export class WeatherApp {} +``` + +```typescript +// src/resources/cities.resource.ts +import { Resource, ResourceContext } from '@frontmcp/sdk'; + +const SUPPORTED_CITIES = ['London', 'Tokyo', 'New York', 'Paris', 'Sydney', 'Berlin', 'Toronto', 'Mumbai']; + +@Resource({ + uri: 'weather://cities', + name: 'Supported Cities', + description: 'List of cities with available weather data', + mimeType: 'application/json', +}) +export class CitiesResource extends ResourceContext { + async read() { + return JSON.stringify(SUPPORTED_CITIES); + } +} +``` + +## What This Demonstrates + +- Server entry point with `@FrontMcp` decorator and `info` configuration +- App registration with `@App` grouping tools and resources together +- Static resource that returns JSON data via `read()` +- Clean separation between server, app, tools, and resources + +## Related + +- See `example-weather-api` for the full end-to-end weather API example diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md new file mode 100644 index 000000000..ba6150373 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md @@ -0,0 +1,142 @@ +--- +name: unit-and-e2e-tests +reference: example-weather-api +level: intermediate +description: 'Shows how to write unit tests for tools by mocking context methods, and E2E tests using `McpTestClient` and `TestServer`.' +tags: [guides, e2e, unit-test, weather, api, unit] +features: + - 'Unit testing tools by mocking `this.fetch()`, `this.fail()`, and other context methods' + - 'Using `Object.assign(tool, ctx)` to inject mock context into the tool instance' + - 'E2E testing with `TestServer.start()` and `McpTestClient.create()`' + - 'Using `toContainTool()` custom matcher for asserting tool presence' + - 'Proper cleanup with `client.disconnect()` and `server.stop()` in `afterAll`' +--- + +# Weather API: Unit and E2E Tests + +Shows how to write unit tests for tools by mocking context methods, and E2E tests using `McpTestClient` and `TestServer`. + +## Code + +```typescript +// test/get-weather.tool.spec.ts +import { ToolContext } from '@frontmcp/sdk'; +import { GetWeatherTool } from '../src/tools/get-weather.tool'; + +describe('GetWeatherTool', () => { + let tool: GetWeatherTool; + + beforeEach(() => { + tool = new GetWeatherTool(); + }); + + it('should return weather data for a valid city', async () => { + const mockResponse = { + ok: true, + json: async () => ({ + temp: 22, + condition: 'Sunny', + humidity: 45, + }), + } as unknown as Response; + + const ctx = { + fetch: jest.fn().mockResolvedValue(mockResponse), + fail: jest.fn((err: Error) => { + throw err; + }), + mark: jest.fn(), + get: jest.fn(), + tryGet: jest.fn(), + notify: jest.fn(), + respondProgress: jest.fn(), + } as unknown as ToolContext; + Object.assign(tool, ctx); + + const result = await tool.execute({ city: 'London', units: 'celsius' }); + + expect(result).toEqual({ + temperature: 22, + condition: 'Sunny', + humidity: 45, + city: 'London', + }); + expect(ctx.fetch).toHaveBeenCalledWith(expect.stringContaining('city=London')); + }); + + it('should fail when the weather API returns an error', async () => { + const mockResponse = { + ok: false, + status: 404, + statusText: 'Not Found', + } as unknown as Response; + + const failFn = jest.fn((err: Error) => { + throw err; + }); + const ctx = { + fetch: jest.fn().mockResolvedValue(mockResponse), + fail: failFn, + mark: jest.fn(), + get: jest.fn(), + tryGet: jest.fn(), + notify: jest.fn(), + respondProgress: jest.fn(), + } as unknown as ToolContext; + Object.assign(tool, ctx); + + await expect(tool.execute({ city: 'Atlantis', units: 'celsius' })).rejects.toThrow( + 'Weather API error: 404 Not Found', + ); + expect(failFn).toHaveBeenCalled(); + }); +}); +``` + +```typescript +// test/weather.e2e.spec.ts +import { McpTestClient, TestServer } from '@frontmcp/testing'; + +describe('Weather Server E2E', () => { + let client: McpTestClient; + let server: TestServer; + + beforeAll(async () => { + server = await TestServer.start({ command: 'npx tsx src/main.ts' }); + client = await McpTestClient.create({ baseUrl: server.info.baseUrl }).buildAndConnect(); + }); + + afterAll(async () => { + await client.disconnect(); + await server.stop(); + }); + + it('should list tools including get_weather', async () => { + const { tools } = await client.listTools(); + + expect(tools.length).toBeGreaterThan(0); + expect(tools).toContainTool('get_weather'); + }); + + it('should read the cities resource', async () => { + const result = await client.readResource('weather://cities'); + const cities = JSON.parse(result.contents[0].text); + + expect(Array.isArray(cities)).toBe(true); + expect(cities).toContain('London'); + expect(cities).toContain('Tokyo'); + }); +}); +``` + +## What This Demonstrates + +- Unit testing tools by mocking `this.fetch()`, `this.fail()`, and other context methods +- Using `Object.assign(tool, ctx)` to inject mock context into the tool instance +- E2E testing with `TestServer.start()` and `McpTestClient.create()` +- Using `toContainTool()` custom matcher for asserting tool presence +- Proper cleanup with `client.disconnect()` and `server.stop()` in `afterAll` + +## Related + +- See `example-weather-api` for the full end-to-end weather API example diff --git a/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/weather-tool-with-schemas.md b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/weather-tool-with-schemas.md new file mode 100644 index 000000000..576500d98 --- /dev/null +++ b/libs/skills/catalog/frontmcp-guides/examples/example-weather-api/weather-tool-with-schemas.md @@ -0,0 +1,74 @@ +--- +name: weather-tool-with-schemas +reference: example-weather-api +level: basic +description: 'Shows how to create a tool with Zod input and output schemas, use `this.fetch()` for HTTP calls, and handle errors with `this.fail()`.' +tags: [guides, weather, api, tool, schemas] +features: + - 'Defining Zod `inputSchema` with validation (`.min(1)`, `.enum()`, `.default()`)' + - 'Defining `outputSchema` to prevent data leaks and ensure type safety' + - 'Using `this.fetch()` for HTTP calls within tools' + - 'Using `this.fail()` for business-logic error handling' + - 'Using `.describe()` on schema fields for LLM-friendly tool descriptions' +--- + +# Weather Tool with Zod Input/Output Schemas + +Shows how to create a tool with Zod input and output schemas, use `this.fetch()` for HTTP calls, and handle errors with `this.fail()`. + +## Code + +```typescript +// src/tools/get-weather.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'get_weather', + description: 'Get current weather for a city', + inputSchema: { + city: z.string().min(1).describe('City name'), + units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'), + }, + outputSchema: { + temperature: z.number(), + condition: z.string(), + humidity: z.number(), + city: z.string(), + }, +}) +export class GetWeatherTool extends ToolContext { + async execute(input: { city: string; units: 'celsius' | 'fahrenheit' }) { + const url = `https://api.weather.example.com/v1/current?city=${encodeURIComponent(input.city)}&units=${input.units}`; + + // Use this.fetch() for HTTP calls — the framework handles errors in the tool execution flow. + const response = await this.fetch(url); + + if (!response.ok) { + // Use this.fail() for business-logic errors + this.fail(new Error(`Weather API error: ${response.status} ${response.statusText}`)); + } + + const data = await response.json(); + + return { + temperature: data.temp, + condition: data.condition, + humidity: data.humidity, + city: input.city, + }; + } +} +``` + +## What This Demonstrates + +- Defining Zod `inputSchema` with validation (`.min(1)`, `.enum()`, `.default()`) +- Defining `outputSchema` to prevent data leaks and ensure type safety +- Using `this.fetch()` for HTTP calls within tools +- Using `this.fail()` for business-logic error handling +- Using `.describe()` on schema fields for LLM-friendly tool descriptions + +## Related + +- See `example-weather-api` for the full end-to-end weather API example with tests 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 985a65258..8992f2b53 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-knowledge-base.md @@ -639,3 +639,13 @@ describe('AuditLogPlugin', () => { }); }); ``` + +## Examples + +| Example | Level | Description | +| -------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [`agent-and-plugin`](../examples/example-knowledge-base/agent-and-plugin.md) | Advanced | Shows an autonomous research agent with inner tools and configurable depth, and a plugin that hooks into tool execution for audit logging. | +| [`multi-app-composition`](../examples/example-knowledge-base/multi-app-composition.md) | Basic | Shows how to compose multiple apps (Ingestion, Search, Research) into a single server with shared providers, plugins, and agent registration. | +| [`vector-search-and-resources`](../examples/example-knowledge-base/vector-search-and-resources.md) | Intermediate | Shows a semantic search tool with embedding generation and a resource template for retrieving documents by ID using URI parameters. | + +> See all examples in [`examples/example-knowledge-base/`](../examples/example-knowledge-base/) 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 6f88807dc..b32f32cf5 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-task-manager.md @@ -509,3 +509,13 @@ describe('Task Manager E2E', () => { }); }); ``` + +## Examples + +| Example | Level | Description | +| ---------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| [`auth-and-crud-tools`](../examples/example-task-manager/auth-and-crud-tools.md) | Basic | Shows how to create CRUD tools with authentication, using `this.context.session` for user isolation and `this.get()` for dependency injection. | +| [`authenticated-e2e-tests`](../examples/example-task-manager/authenticated-e2e-tests.md) | Advanced | Shows how to write E2E tests with authentication using `TestTokenFactory`, and unit tests for tools that require session context. | +| [`redis-provider-with-di`](../examples/example-task-manager/redis-provider-with-di.md) | Intermediate | Shows how to create a Redis-backed provider with a DI token, lifecycle hooks (`onInit`/`onDestroy`), and how tools inject it. | + +> See all examples in [`examples/example-task-manager/`](../examples/example-task-manager/) 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 f255e088d..eff17b1ab 100644 --- a/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md +++ b/libs/skills/catalog/frontmcp-guides/references/example-weather-api.md @@ -292,3 +292,13 @@ describe('Weather Server E2E', () => { }); }); ``` + +## Examples + +| Example | Level | Description | +| ------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| [`server-and-app-setup`](../examples/example-weather-api/server-and-app-setup.md) | Basic | Shows the server entry point, app registration, and static resource for a beginner FrontMCP weather API server. | +| [`unit-and-e2e-tests`](../examples/example-weather-api/unit-and-e2e-tests.md) | Intermediate | Shows how to write unit tests for tools by mocking context methods, and E2E tests using `McpTestClient` and `TestServer`. | +| [`weather-tool-with-schemas`](../examples/example-weather-api/weather-tool-with-schemas.md) | Basic | Shows how to create a tool with Zod input and output schemas, use `this.fetch()` for HTTP calls, and handle errors with `this.fail()`. | + +> See all examples in [`examples/example-weather-api/`](../examples/example-weather-api/) diff --git a/libs/skills/catalog/frontmcp-production-readiness/SKILL.md b/libs/skills/catalog/frontmcp-production-readiness/SKILL.md index 0c41371ce..1cdbf989b 100644 --- a/libs/skills/catalog/frontmcp-production-readiness/SKILL.md +++ b/libs/skills/catalog/frontmcp-production-readiness/SKILL.md @@ -9,7 +9,7 @@ priority: 10 visibility: both license: Apache-2.0 metadata: - docs: https://docs.agentfront.dev/frontmcp/production/overview + docs: https://docs.agentfront.dev/frontmcp/deployment/production-build --- # FrontMCP Production Readiness Audit @@ -94,5 +94,5 @@ After completing both common and target-specific checklists: ## Reference -- [FrontMCP Production Guide](https://docs.agentfront.dev/frontmcp/production) +- [Production Build](https://docs.agentfront.dev/frontmcp/deployment/production-build) - Related skills: `frontmcp-config`, `frontmcp-deployment`, `frontmcp-testing`, `frontmcp-setup` diff --git a/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md new file mode 100644 index 000000000..5918d5768 --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md @@ -0,0 +1,102 @@ +--- +name: caching-and-performance +reference: common-checklist +level: advanced +description: 'Shows how to configure caching with TTL, optimize responses, and manage memory with proper provider lifecycle cleanup.' +tags: [production, redis, cache, session, performance, checklist] +features: + - 'Configuring per-tool cache TTL instead of a single global value' + - 'Using Redis-backed cache for multi-instance consistency' + - 'Setting session TTL to prevent unbounded storage growth' + - 'Implementing `onDestroy()` in providers for proper connection cleanup' + - 'Using connection pool limits and timeouts to prevent resource exhaustion' +--- + +# Caching and Performance Configuration + +Shows how to configure caching with TTL, optimize responses, and manage memory with proper provider lifecycle cleanup. + +## Code + +```typescript +// src/main.ts +import { FrontMcp } from '@frontmcp/sdk'; +import { CachePlugin } from '@frontmcp/plugins'; +import { MyApp } from './my.app'; + +@FrontMcp({ + info: { name: 'perf-server', version: '1.0.0' }, + apps: [MyApp], + plugins: [ + new CachePlugin({ + // Per-tool TTL tuning (not one-size-fits-all) + ttl: { + get_weather: 300_000, // 5 minutes — data changes slowly + list_tasks: 10_000, // 10 seconds — data changes frequently + }, + defaultTtl: 60_000, // 1 minute default + }), + ], + + // Redis for multi-instance cache consistency + redis: { + provider: 'redis', + host: process.env.REDIS_HOST ?? 'localhost', + port: 6379, + }, + + // Session TTL to prevent unbounded growth + session: { + ttl: 3600_000, // 1 hour + }, +}) +export default class PerfServer {} +``` + +```typescript +// src/providers/db-connection.provider.ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +export const DB_POOL = Symbol('DbPool'); + +@Provider({ token: DB_POOL, scope: ProviderScope.GLOBAL }) +export class DbConnectionProvider { + private pool!: { query: Function; end: Function }; + + async onInit(): Promise { + // Connection pool with limits — prevents resource exhaustion + this.pool = await this.createPool({ + host: process.env.DB_HOST, + max: 20, // Maximum connections + idleTimeoutMs: 30_000, // Close idle connections after 30s + connectionTimeoutMs: 5_000, // Don't hang on connection attempts + }); + } + + async query(sql: string, params: unknown[]): Promise { + return this.pool.query(sql, params); // Parameterized — no SQL injection + } + + async onDestroy(): Promise { + // Clean up on shutdown — prevents connection leaks + await this.pool.end(); + } + + private async createPool(config: Record): Promise<{ query: Function; end: Function }> { + // Replace with your database driver (e.g., pg, mysql2) + throw new Error('Implement with your database driver'); + } +} +``` + +## What This Demonstrates + +- Configuring per-tool cache TTL instead of a single global value +- Using Redis-backed cache for multi-instance consistency +- Setting session TTL to prevent unbounded storage growth +- Implementing `onDestroy()` in providers for proper connection cleanup +- Using connection pool limits and timeouts to prevent resource exhaustion + +## Related + +- See `common-checklist` for the full performance and memory management checklist diff --git a/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md new file mode 100644 index 000000000..467cac248 --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md @@ -0,0 +1,104 @@ +--- +name: observability-setup +reference: common-checklist +level: intermediate +description: 'Shows how to configure structured logging, error handling with MCP error codes, and monitoring integration for production.' +tags: [production, observability, checklist, setup] +features: + - 'Using `this.mark()` to annotate execution phases for tracing' + - 'Using `this.fail()` for business-logic errors without exposing internals' + - 'Setting timeouts on all external calls via `AbortSignal.timeout()`' + - 'Implementing health check providers that verify downstream dependencies' +--- + +# Observability and Error Handling Setup + +Shows how to configure structured logging, error handling with MCP error codes, and monitoring integration for production. + +## Code + +```typescript +// src/tools/monitored-tool.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'monitored_operation', + description: 'A tool with proper error handling and observability markers', + inputSchema: { + operationId: z.string().min(1).describe('Operation identifier'), + }, + outputSchema: { + status: z.string(), + operationId: z.string(), + }, +}) +export class MonitoredOperationTool extends ToolContext { + async execute(input: { operationId: string }) { + // Mark execution phases for tracing and duration metrics + this.mark('validation'); + // ... validate business rules ... + + this.mark('processing'); + const result = await this.processOperation(input.operationId); + + if (!result) { + // Use this.fail() with specific errors — never expose stack traces + this.fail(new Error(`Operation not found: ${input.operationId}`)); + } + + // Report progress for long-running operations + await this.respondProgress(1, 1); + + return { status: 'completed', operationId: input.operationId }; + } + + private async processOperation(id: string): Promise { + // External call with timeout — always set timeouts for external services + const response = await this.fetch(`https://api.example.com/operations/${id}`, { + signal: AbortSignal.timeout(5000), // 5 second timeout + }); + return response.ok; + } +} +``` + +```typescript +// src/providers/health-check.provider.ts +import { Provider, ProviderScope } from '@frontmcp/sdk'; + +export const HEALTH_CHECK = Symbol('HealthCheck'); + +@Provider({ token: HEALTH_CHECK, scope: ProviderScope.GLOBAL }) +export class HealthCheckProvider { + async checkRedis(): Promise { + // Verify downstream dependency is reachable + try { + // ... ping Redis ... + return true; + } catch { + return false; + } + } + + async checkDatabase(): Promise { + try { + // ... run a lightweight query ... + return true; + } catch { + return false; + } + } +} +``` + +## What This Demonstrates + +- Using `this.mark()` to annotate execution phases for tracing +- Using `this.fail()` for business-logic errors without exposing internals +- Setting timeouts on all external calls via `AbortSignal.timeout()` +- Implementing health check providers that verify downstream dependencies + +## Related + +- See `common-checklist` for the full observability and monitoring checklist diff --git a/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md new file mode 100644 index 000000000..6e17f65f3 --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md @@ -0,0 +1,95 @@ +--- +name: security-hardening +reference: common-checklist +level: basic +description: 'Shows how to configure authentication, CORS, input validation, and rate limiting for a production FrontMCP server.' +tags: [production, redis, session, security, throttle, checklist] +features: + - "Restricting CORS origins to known domains instead of using `'*'`" + - 'Configuring rate limiting via the `throttle` option' + - 'Using Redis for session storage in multi-instance deployments' + - 'Defining both `inputSchema` and `outputSchema` on tools to prevent data leaks' +--- + +# Security Hardening Configuration + +Shows how to configure authentication, CORS, input validation, and rate limiting for a production FrontMCP server. + +## Code + +```typescript +// src/main.ts +import { FrontMcp } from '@frontmcp/sdk'; +import { z } from 'zod'; +import { MyApp } from './my.app'; + +@FrontMcp({ + info: { name: 'secure-server', version: '1.0.0' }, + apps: [MyApp], + + // Authentication: use remote OAuth provider + auth: { + mode: 'remote', + provider: 'https://auth.example.com', + clientId: process.env.AUTH_CLIENT_ID!, + }, + + // CORS: restrict to known origins (never use '*' in production) + cors: { + origin: ['https://app.example.com', 'https://admin.example.com'], + credentials: true, + maxAge: 86400, // Cache preflight for 24 hours + }, + + // Rate limiting: prevent abuse + throttle: { + windowMs: 60_000, // 1 minute window + max: 100, // 100 requests per window per client + }, + + // Session storage: use Redis (not in-memory) for multi-instance + redis: { + provider: 'redis', + host: process.env.REDIS_HOST ?? 'localhost', + port: 6379, + }, +}) +export default class SecureServer {} +``` + +```typescript +// src/tools/safe-query.tool.ts +import { Tool, ToolContext } from '@frontmcp/sdk'; +import { z } from 'zod'; + +@Tool({ + name: 'safe_query', + description: 'Query data with validated and sanitized input', + inputSchema: { + query: z.string().min(1).max(500).describe('Search query'), + limit: z.number().int().min(1).max(100).default(10).describe('Max results'), + }, + outputSchema: { + results: z.array(z.object({ id: z.string(), title: z.string() })), + total: z.number(), + }, +}) +export class SafeQueryTool extends ToolContext { + async execute(input: { query: string; limit: number }) { + // Zod already validated input — safe to use + // outputSchema prevents accidental data leaks + return { results: [], total: 0 }; + } +} +``` + +## What This Demonstrates + +- Restricting CORS origins to known domains instead of using `'*'` +- Configuring rate limiting via the `throttle` option +- Using Redis for session storage in multi-instance deployments +- Defining both `inputSchema` and `outputSchema` on tools to prevent data leaks + +## Related + +- See `common-checklist` for the full security, performance, and reliability checklist diff --git a/libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/browser-bundle-config.md b/libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/browser-bundle-config.md new file mode 100644 index 000000000..7012256c5 --- /dev/null +++ b/libs/skills/catalog/frontmcp-production-readiness/examples/production-browser/browser-bundle-config.md @@ -0,0 +1,93 @@ +--- +name: browser-bundle-config +reference: production-browser +level: basic +description: 'Shows how to configure package.json for browser-compatible SDK distribution with ESM/CJS/UMD entry points, TypeScript declarations, and CDN support.' +tags: [production, browser, sdk, node, bundle, config] +features: + - 'Correct `main`, `module`, `browser`, `types`, and `exports` fields for browser distribution' + - 'Using the `browser` field to point bundlers to the browser-specific build' + - 'Browser-safe imports with no Node.js-only APIs' + - 'CDN-friendly distribution that works via ` +``` + +## What This Demonstrates + +- Correct `main`, `module`, `browser`, `types`, and `exports` fields for browser distribution +- Using the `browser` field to point bundlers to the browser-specific build +- Browser-safe imports with no Node.js-only APIs +- CDN-friendly distribution that works via `