@frontmcp/skills 1.1.2-beta.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/catalog/TEMPLATE.md +16 -11
  2. package/catalog/frontmcp-authorities/SKILL.md +116 -11
  3. package/catalog/frontmcp-authorities/references/authority-profiles.md +39 -36
  4. package/catalog/frontmcp-authorities/references/claims-mapping.md +7 -0
  5. package/catalog/frontmcp-authorities/references/custom-evaluators.md +63 -14
  6. package/catalog/frontmcp-channels/SKILL.md +36 -0
  7. package/catalog/frontmcp-channels/examples/channel-sources/file-watcher.md +8 -2
  8. package/catalog/frontmcp-channels/examples/channel-sources/replay-buffer.md +111 -30
  9. package/catalog/frontmcp-channels/examples/channel-two-way/whatsapp-bridge.md +45 -3
  10. package/catalog/frontmcp-channels/references/channel-sources.md +11 -3
  11. package/catalog/frontmcp-channels/references/channel-two-way.md +60 -89
  12. package/catalog/frontmcp-config/SKILL.md +111 -8
  13. package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +4 -4
  14. package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +7 -1
  15. package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +1 -1
  16. package/catalog/frontmcp-config/examples/configure-deployment-targets/json-schema-ide-support.md +1 -1
  17. package/catalog/frontmcp-config/examples/configure-deployment-targets/multi-target-with-security.md +12 -9
  18. package/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md +2 -2
  19. package/catalog/frontmcp-config/examples/configure-http/entry-path-reverse-proxy.md +1 -1
  20. package/catalog/frontmcp-config/examples/configure-security-headers/csp-report-only.md +1 -1
  21. package/catalog/frontmcp-config/examples/configure-security-headers/full-production-headers.md +1 -1
  22. package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-basic.md +76 -0
  23. package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +116 -0
  24. package/catalog/frontmcp-config/examples/configure-skills-http/inject-instructions.md +59 -0
  25. package/catalog/frontmcp-config/references/configure-auth-modes.md +5 -5
  26. package/catalog/frontmcp-config/references/configure-deployment-targets.md +27 -24
  27. package/catalog/frontmcp-config/references/configure-http.md +14 -10
  28. package/catalog/frontmcp-config/references/configure-security-headers.md +2 -2
  29. package/catalog/frontmcp-config/references/configure-session.md +25 -25
  30. package/catalog/frontmcp-config/references/configure-skills-http.md +157 -0
  31. package/catalog/frontmcp-config/references/configure-throttle.md +1 -1
  32. package/catalog/frontmcp-config/references/configure-transport.md +2 -2
  33. package/catalog/frontmcp-deployment/SKILL.md +112 -9
  34. package/catalog/frontmcp-deployment/examples/build-for-browser/browser-build-with-custom-entry.md +23 -11
  35. package/catalog/frontmcp-deployment/examples/build-for-browser/browser-crypto-and-storage.md +44 -17
  36. package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +53 -21
  37. package/catalog/frontmcp-deployment/examples/build-for-cli/cli-binary-build.md +1 -1
  38. package/catalog/frontmcp-deployment/examples/build-for-cli/unix-socket-daemon.md +1 -1
  39. package/catalog/frontmcp-deployment/examples/build-for-mcpb/mcpb-bundle-build.md +1 -1
  40. package/catalog/frontmcp-deployment/examples/build-for-sdk/connect-openai.md +1 -1
  41. package/catalog/frontmcp-deployment/examples/build-for-sdk/multi-platform-connect.md +1 -1
  42. package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/basic-worker-deploy.md +7 -8
  43. package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-custom-domain.md +8 -6
  44. package/catalog/frontmcp-deployment/examples/deploy-to-cloudflare/worker-with-kv-storage.md +5 -4
  45. package/catalog/frontmcp-deployment/examples/deploy-to-lambda/cdk-deployment.md +8 -5
  46. package/catalog/frontmcp-deployment/examples/deploy-to-lambda/lambda-handler-with-cors.md +20 -18
  47. package/catalog/frontmcp-deployment/examples/deploy-to-lambda/sam-template-basic.md +8 -5
  48. package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +3 -3
  49. package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
  50. package/catalog/frontmcp-deployment/examples/deploy-to-node/resource-limits.md +2 -2
  51. package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md +2 -2
  52. package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md +1 -1
  53. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md +23 -21
  54. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +25 -22
  55. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md +23 -30
  56. package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md +52 -28
  57. package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md +32 -55
  58. package/catalog/frontmcp-deployment/examples/mcp-client-integration/http-remote.md +9 -0
  59. package/catalog/frontmcp-deployment/references/build-for-browser.md +40 -17
  60. package/catalog/frontmcp-deployment/references/build-for-cli.md +8 -8
  61. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +43 -24
  62. package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +36 -25
  63. package/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md +56 -14
  64. package/catalog/frontmcp-deployment/references/deploy-to-node.md +9 -6
  65. package/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +57 -58
  66. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +49 -59
  67. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +2 -0
  68. package/catalog/frontmcp-development/SKILL.md +186 -11
  69. package/catalog/frontmcp-development/examples/create-agent/custom-multi-pass-agent.md +1 -1
  70. package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +30 -27
  71. package/catalog/frontmcp-development/examples/create-job/job-with-permissions.md +13 -8
  72. package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +33 -23
  73. package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +19 -10
  74. package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +3 -3
  75. package/catalog/frontmcp-development/examples/create-workflow/webhook-triggered-workflow.md +6 -4
  76. package/catalog/frontmcp-development/examples/decorators-guide/agent-skill-job-workflow.md +1 -1
  77. package/catalog/frontmcp-development/examples/decorators-guide/basic-server-with-app-and-tools.md +13 -8
  78. package/catalog/frontmcp-development/examples/decorators-guide/multi-app-with-plugins-and-providers.md +50 -23
  79. package/catalog/frontmcp-development/references/create-agent.md +47 -30
  80. package/catalog/frontmcp-development/references/create-job.md +69 -54
  81. package/catalog/frontmcp-development/references/create-plugin-hooks.md +45 -28
  82. package/catalog/frontmcp-development/references/create-plugin.md +10 -8
  83. package/catalog/frontmcp-development/references/create-prompt.md +3 -3
  84. package/catalog/frontmcp-development/references/create-provider.md +91 -51
  85. package/catalog/frontmcp-development/references/create-resource.md +3 -3
  86. package/catalog/frontmcp-development/references/create-skill.md +2 -2
  87. package/catalog/frontmcp-development/references/create-tool.md +7 -7
  88. package/catalog/frontmcp-development/references/create-workflow.md +8 -10
  89. package/catalog/frontmcp-development/references/decorators-guide.md +92 -56
  90. package/catalog/frontmcp-development/references/official-plugins.md +4 -3
  91. package/catalog/frontmcp-development/references/openapi-adapter.md +1 -1
  92. package/catalog/frontmcp-extensibility/SKILL.md +70 -10
  93. package/catalog/frontmcp-extensibility/examples/skill-audit-log/custom-store.md +197 -0
  94. package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +68 -0
  95. package/catalog/frontmcp-extensibility/examples/vectoriadb/product-catalog-search.md +3 -5
  96. package/catalog/frontmcp-extensibility/examples/vectoriadb/semantic-search-with-persistence.md +4 -11
  97. package/catalog/frontmcp-extensibility/examples/vectoriadb/tfidf-keyword-search.md +41 -30
  98. package/catalog/frontmcp-extensibility/references/skill-audit-log.md +233 -0
  99. package/catalog/frontmcp-extensibility/references/vectoriadb.md +73 -63
  100. package/catalog/frontmcp-guides/SKILL.md +84 -27
  101. package/catalog/frontmcp-guides/examples/example-knowledge-base/agent-and-plugin.md +72 -62
  102. package/catalog/frontmcp-guides/examples/example-knowledge-base/vector-search-and-resources.md +32 -43
  103. package/catalog/frontmcp-guides/examples/example-task-manager/auth-and-crud-tools.md +24 -17
  104. package/catalog/frontmcp-guides/examples/example-task-manager/authenticated-e2e-tests.md +23 -21
  105. package/catalog/frontmcp-guides/examples/example-task-manager/redis-provider-with-di.md +47 -39
  106. package/catalog/frontmcp-guides/examples/example-weather-api/server-and-app-setup.md +16 -6
  107. package/catalog/frontmcp-guides/examples/example-weather-api/unit-and-e2e-tests.md +9 -8
  108. package/catalog/frontmcp-guides/references/example-knowledge-base.md +192 -265
  109. package/catalog/frontmcp-guides/references/example-task-manager.md +60 -54
  110. package/catalog/frontmcp-guides/references/example-weather-api.md +22 -24
  111. package/catalog/frontmcp-observability/SKILL.md +66 -2
  112. package/catalog/frontmcp-observability/examples/telemetry-api/skill-counters.md +100 -0
  113. package/catalog/frontmcp-observability/examples/tracing-setup/production-tracing.md +7 -2
  114. package/catalog/frontmcp-observability/examples/vendor-integrations/coralogix-setup.md +6 -2
  115. package/catalog/frontmcp-observability/references/telemetry-api.md +72 -8
  116. package/catalog/frontmcp-observability/references/testing-observability.md +33 -49
  117. package/catalog/frontmcp-observability/references/tracing-setup.md +12 -5
  118. package/catalog/frontmcp-observability/references/vendor-integrations.md +46 -1
  119. package/catalog/frontmcp-production-readiness/SKILL.md +134 -3
  120. package/catalog/frontmcp-production-readiness/examples/common-checklist/caching-and-performance.md +57 -36
  121. package/catalog/frontmcp-production-readiness/examples/common-checklist/observability-setup.md +1 -1
  122. package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +102 -6
  123. package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/daemon-socket-config.md +2 -1
  124. package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/graceful-shutdown-cleanup.md +66 -58
  125. package/catalog/frontmcp-production-readiness/examples/production-cli-daemon/security-and-permissions.md +5 -3
  126. package/catalog/frontmcp-production-readiness/examples/production-cloudflare/durable-objects-state.md +2 -1
  127. package/catalog/frontmcp-production-readiness/examples/production-cloudflare/wrangler-config.md +55 -76
  128. package/catalog/frontmcp-production-readiness/examples/production-lambda/cold-start-connection-reuse.md +43 -40
  129. package/catalog/frontmcp-production-readiness/examples/production-lambda/sam-template.md +63 -94
  130. package/catalog/frontmcp-production-readiness/examples/production-lambda/scaling-and-monitoring.md +28 -18
  131. package/catalog/frontmcp-production-readiness/examples/production-node-sdk/multi-instance-cleanup.md +29 -14
  132. package/catalog/frontmcp-production-readiness/examples/production-node-server/graceful-shutdown.md +58 -42
  133. package/catalog/frontmcp-production-readiness/examples/production-node-server/redis-session-scaling.md +5 -2
  134. package/catalog/frontmcp-production-readiness/examples/production-vercel/cold-start-optimization.md +41 -24
  135. package/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md +56 -65
  136. package/catalog/frontmcp-production-readiness/references/common-checklist.md +17 -5
  137. package/catalog/frontmcp-production-readiness/references/production-cli-daemon.md +5 -5
  138. package/catalog/frontmcp-production-readiness/references/production-cloudflare.md +5 -5
  139. package/catalog/frontmcp-production-readiness/references/production-lambda.md +5 -5
  140. package/catalog/frontmcp-production-readiness/references/production-node-sdk.md +5 -5
  141. package/catalog/frontmcp-production-readiness/references/production-node-server.md +1 -1
  142. package/catalog/frontmcp-production-readiness/references/production-vercel.md +5 -5
  143. package/catalog/frontmcp-setup/SKILL.md +88 -0
  144. package/catalog/frontmcp-setup/examples/project-structure-nx/nx-workspace-with-apps.md +10 -4
  145. package/catalog/frontmcp-setup/examples/project-structure-standalone/dev-workflow-commands.md +21 -8
  146. package/catalog/frontmcp-setup/examples/readme-guide/node-server-readme.md +3 -3
  147. package/catalog/frontmcp-setup/references/multi-app-composition.md +4 -3
  148. package/catalog/frontmcp-setup/references/project-structure-nx.md +15 -6
  149. package/catalog/frontmcp-setup/references/project-structure-standalone.md +18 -15
  150. package/catalog/frontmcp-setup/references/readme-guide.md +1 -1
  151. package/catalog/frontmcp-setup/references/setup-project.md +19 -5
  152. package/catalog/frontmcp-setup/references/setup-redis.md +27 -39
  153. package/catalog/frontmcp-setup/references/setup-sqlite.md +25 -18
  154. package/catalog/frontmcp-testing/SKILL.md +102 -15
  155. package/catalog/frontmcp-testing/examples/setup-testing/unit-test-tool-resource-prompt.md +3 -3
  156. package/catalog/frontmcp-testing/examples/test-auth/oauth-flow-test.md +50 -39
  157. package/catalog/frontmcp-testing/examples/test-auth/role-based-access-test.md +52 -29
  158. package/catalog/frontmcp-testing/examples/test-auth/token-factory-test.md +37 -20
  159. package/catalog/frontmcp-testing/examples/test-direct-client/basic-create-test.md +25 -15
  160. package/catalog/frontmcp-testing/examples/test-direct-client/openai-claude-format-test.md +27 -21
  161. package/catalog/frontmcp-testing/examples/test-e2e-handler/basic-e2e-test.md +29 -20
  162. package/catalog/frontmcp-testing/examples/test-e2e-handler/manual-client-with-transport.md +5 -3
  163. package/catalog/frontmcp-testing/examples/test-e2e-handler/tool-call-and-error-e2e.md +35 -26
  164. package/catalog/frontmcp-testing/examples/test-tool-unit/basic-tool-test.md +8 -3
  165. package/catalog/frontmcp-testing/examples/test-tool-unit/schema-validation-test.md +4 -1
  166. package/catalog/frontmcp-testing/examples/test-tool-unit/tool-error-handling-test.md +6 -3
  167. package/catalog/frontmcp-testing/references/setup-testing.md +35 -39
  168. package/catalog/frontmcp-testing/references/test-auth.md +86 -43
  169. package/catalog/frontmcp-testing/references/test-browser-build.md +1 -1
  170. package/catalog/frontmcp-testing/references/test-direct-client.md +29 -19
  171. package/catalog/frontmcp-testing/references/test-e2e-handler.md +31 -19
  172. package/catalog/frontmcp-testing/references/test-tool-unit.md +6 -2
  173. package/catalog/skills-manifest.json +428 -339
  174. package/package.json +1 -1
  175. package/src/manifest.d.ts +13 -0
  176. package/src/manifest.js.map +1 -1
@@ -56,97 +56,79 @@ export class IngestionApp {}
56
56
 
57
57
  ### Provider: Vector Store
58
58
 
59
+ This example uses the in-tree `vectoriadb` package (already a runtime dep in the SDK). The provider class is its own DI token — tools inject it via `this.get(VectorStoreProvider)`.
60
+
59
61
  ```typescript
60
62
  // src/ingestion/providers/vector-store.provider.ts
61
- import type { Token } from '@frontmcp/di';
62
- import { Provider } from '@frontmcp/sdk';
63
+ import { FileStorageAdapter, VectoriaDB, type DocumentMetadata } from 'vectoriadb';
64
+
65
+ import { Provider, ProviderScope } from '@frontmcp/sdk';
63
66
 
64
- export interface DocumentChunk {
65
- id: string;
67
+ export interface DocChunk extends DocumentMetadata {
66
68
  documentId: string;
69
+ title: string;
70
+ chunkIndex: number;
67
71
  content: string;
68
- embedding: number[];
69
- metadata: Record<string, string>;
70
- }
71
-
72
- export interface VectorStore {
73
- upsert(chunks: DocumentChunk[]): Promise<void>;
74
- search(embedding: number[], topK: number): Promise<DocumentChunk[]>;
75
- getByDocumentId(documentId: string): Promise<DocumentChunk[]>;
76
- deleteByDocumentId(documentId: string): Promise<void>;
72
+ tags: string;
77
73
  }
78
74
 
79
- export const VECTOR_STORE: Token<VectorStore> = Symbol('VectorStore');
80
-
81
- @Provider({ token: VECTOR_STORE })
82
- export class VectorStoreProvider implements VectorStore {
83
- private client!: { upsert: Function; query: Function; delete: Function };
84
-
85
- async onInit(): Promise<void> {
86
- const apiKey = process.env.VECTOR_DB_API_KEY;
87
- if (!apiKey) {
88
- throw new Error('VECTOR_DB_API_KEY environment variable is required');
89
- }
90
-
91
- // Initialize your vector DB client (e.g., Pinecone, Weaviate, Qdrant)
92
- this.client = await this.createVectorClient(apiKey);
75
+ @Provider({ name: 'vector-store', scope: ProviderScope.GLOBAL })
76
+ export class VectorStoreProvider {
77
+ private readonly db: VectoriaDB<DocChunk>;
78
+ private readonly ready: Promise<void>;
79
+
80
+ constructor() {
81
+ this.db = new VectoriaDB<DocChunk>({
82
+ modelName: 'Xenova/all-MiniLM-L6-v2',
83
+ cacheDir: './.cache/transformers',
84
+ useHNSW: true,
85
+ defaultSimilarityThreshold: 0.3,
86
+ defaultTopK: 10,
87
+ storageAdapter: new FileStorageAdapter({ cacheDir: './.cache/kb-vectors' }),
88
+ });
89
+ this.ready = this.db.initialize();
93
90
  }
94
91
 
95
- async upsert(chunks: DocumentChunk[]): Promise<void> {
96
- await this.client.upsert(
97
- chunks.map((c) => ({
98
- id: c.id,
99
- values: c.embedding,
100
- metadata: { ...c.metadata, documentId: c.documentId, content: c.content },
101
- })),
102
- );
92
+ async upsert(chunks: Array<{ id: string; text: string; metadata: DocChunk }>): Promise<void> {
93
+ await this.ready;
94
+ await this.db.addMany(chunks);
95
+ await this.db.saveToStorage();
103
96
  }
104
97
 
105
- async search(embedding: number[], topK: number): Promise<DocumentChunk[]> {
106
- const results = await this.client.query({ vector: embedding, topK });
107
- return results.matches.map((m: Record<string, unknown>) => ({
108
- id: m.id as string,
109
- documentId: (m.metadata as Record<string, string>).documentId,
110
- content: (m.metadata as Record<string, string>).content,
111
- embedding: m.values as number[],
112
- metadata: m.metadata as Record<string, string>,
113
- }));
98
+ async search(query: string, topK: number) {
99
+ await this.ready;
100
+ return this.db.search(query, { topK });
114
101
  }
115
102
 
116
- async getByDocumentId(documentId: string): Promise<DocumentChunk[]> {
117
- const results = await this.client.query({
118
- filter: { documentId },
103
+ async getByDocumentId(documentId: string) {
104
+ await this.ready;
105
+ // VectoriaDB supports metadata filters via the `filter` predicate
106
+ return this.db.search(documentId, {
119
107
  topK: 100,
120
- vector: new Array(1536).fill(0),
108
+ filter: (meta) => meta.documentId === documentId,
121
109
  });
122
- return results.matches.map((m: Record<string, unknown>) => ({
123
- id: m.id as string,
124
- documentId,
125
- content: (m.metadata as Record<string, string>).content,
126
- embedding: m.values as number[],
127
- metadata: m.metadata as Record<string, string>,
128
- }));
129
110
  }
130
111
 
131
112
  async deleteByDocumentId(documentId: string): Promise<void> {
132
- await this.client.delete({ filter: { documentId } });
133
- }
134
-
135
- private async createVectorClient(_apiKey: string): Promise<{ upsert: Function; query: Function; delete: Function }> {
136
- // Stub: replace with your vector DB SDK (e.g., Pinecone, Weaviate, Qdrant)
137
- // This placeholder focuses on the FrontMCP patterns, not the vector DB integration.
138
- throw new Error('Implement with your vector DB provider (e.g., Pinecone, Weaviate, Qdrant)');
113
+ await this.ready;
114
+ const matches = await this.getByDocumentId(documentId);
115
+ for (const match of matches) {
116
+ this.db.remove(match.id);
117
+ }
118
+ await this.db.saveToStorage();
139
119
  }
140
120
  }
141
121
  ```
142
122
 
143
123
  ### Tool: Ingest Document
144
124
 
125
+ VectoriaDB owns embedding generation, so the tool only chunks the text and hands raw strings to the provider.
126
+
145
127
  ```typescript
146
128
  // src/ingestion/tools/ingest-document.tool.ts
147
129
  import { Tool, ToolContext, z } from '@frontmcp/sdk';
148
130
 
149
- import { VECTOR_STORE, type DocumentChunk } from '../providers/vector-store.provider';
131
+ import { VectorStoreProvider } from '../providers/vector-store.provider';
150
132
 
151
133
  @Tool({
152
134
  name: 'ingest_document',
@@ -165,35 +147,36 @@ import { VECTOR_STORE, type DocumentChunk } from '../providers/vector-store.prov
165
147
  })
166
148
  export class IngestDocumentTool extends ToolContext {
167
149
  async execute(input: { documentId: string; title: string; content: string; tags: string[] }) {
168
- const store = this.get(VECTOR_STORE);
150
+ const store = this.get(VectorStoreProvider);
169
151
 
170
152
  this.mark('chunking');
171
153
  const textChunks = this.chunkText(input.content, 512);
172
154
 
173
155
  this.mark('embedding');
174
- await this.respondProgress(0, textChunks.length);
156
+ await this.progress(0, textChunks.length, 'Generating embeddings');
175
157
 
176
- const chunks: DocumentChunk[] = [];
177
- for (let i = 0; i < textChunks.length; i++) {
178
- const embedding = await this.generateEmbedding(textChunks[i]);
179
- chunks.push({
158
+ const docs = textChunks.map((text, i) => ({
159
+ id: `${input.documentId}-chunk-${i}`,
160
+ text,
161
+ metadata: {
180
162
  id: `${input.documentId}-chunk-${i}`,
181
163
  documentId: input.documentId,
182
- content: textChunks[i],
183
- embedding,
184
- metadata: { title: input.title, tags: input.tags.join(','), chunkIndex: String(i) },
185
- });
186
- await this.respondProgress(i + 1, textChunks.length);
187
- }
164
+ title: input.title,
165
+ chunkIndex: i,
166
+ content: text,
167
+ tags: input.tags.join(','),
168
+ },
169
+ }));
188
170
 
189
171
  this.mark('storing');
190
- await store.upsert(chunks);
172
+ await store.upsert(docs);
173
+ await this.progress(textChunks.length, textChunks.length, 'Stored');
191
174
 
192
- await this.notify(`Ingested "${input.title}" with ${chunks.length} chunks`, 'info');
175
+ await this.notify(`Ingested "${input.title}" with ${docs.length} chunks`, 'info');
193
176
 
194
177
  return {
195
178
  documentId: input.documentId,
196
- chunksCreated: chunks.length,
179
+ chunksCreated: docs.length,
197
180
  title: input.title,
198
181
  };
199
182
  }
@@ -214,19 +197,6 @@ export class IngestDocumentTool extends ToolContext {
214
197
  if (current.trim()) chunks.push(current.trim());
215
198
  return chunks;
216
199
  }
217
-
218
- private async generateEmbedding(text: string): Promise<number[]> {
219
- const response = await this.fetch('https://api.openai.com/v1/embeddings', {
220
- method: 'POST',
221
- headers: {
222
- 'Content-Type': 'application/json',
223
- Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
224
- },
225
- body: JSON.stringify({ input: text, model: 'text-embedding-3-small' }),
226
- });
227
- const data = await response.json();
228
- return data.data[0].embedding;
229
- }
230
200
  }
231
201
  ```
232
202
 
@@ -260,7 +230,7 @@ export class SearchApp {}
260
230
  // src/search/tools/search-docs.tool.ts
261
231
  import { Tool, ToolContext, z } from '@frontmcp/sdk';
262
232
 
263
- import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
233
+ import { VectorStoreProvider } from '../../ingestion/providers/vector-store.provider';
264
234
 
265
235
  @Tool({
266
236
  name: 'search_docs',
@@ -283,36 +253,21 @@ import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
283
253
  })
284
254
  export class SearchDocsTool extends ToolContext {
285
255
  async execute(input: { query: string; topK: number }) {
286
- const store = this.get(VECTOR_STORE);
287
-
288
- this.mark('embedding-query');
289
- const queryEmbedding = await this.generateQueryEmbedding(input.query);
256
+ const store = this.get(VectorStoreProvider);
290
257
 
258
+ // VectoriaDB handles embedding generation internally — pass the raw query.
291
259
  this.mark('searching');
292
- const chunks = await store.search(queryEmbedding, input.topK);
260
+ const matches = await store.search(input.query, input.topK);
293
261
 
294
- const results = chunks.map((chunk) => ({
295
- documentId: chunk.documentId,
296
- content: chunk.content,
297
- score: chunk.metadata.score ? parseFloat(chunk.metadata.score) : 0,
298
- title: chunk.metadata.title ?? 'Untitled',
262
+ const results = matches.map((m) => ({
263
+ documentId: m.metadata.documentId,
264
+ content: m.metadata.content,
265
+ score: m.score,
266
+ title: m.metadata.title ?? 'Untitled',
299
267
  }));
300
268
 
301
269
  return { results, total: results.length };
302
270
  }
303
-
304
- private async generateQueryEmbedding(query: string): Promise<number[]> {
305
- const response = await this.fetch('https://api.openai.com/v1/embeddings', {
306
- method: 'POST',
307
- headers: {
308
- 'Content-Type': 'application/json',
309
- Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
310
- },
311
- body: JSON.stringify({ input: query, model: 'text-embedding-3-small' }),
312
- });
313
- const data = await response.json();
314
- return data.data[0].embedding;
315
- }
316
271
  }
317
272
  ```
318
273
 
@@ -320,10 +275,9 @@ export class SearchDocsTool extends ToolContext {
320
275
 
321
276
  ```typescript
322
277
  // src/search/resources/doc.resource.ts
323
- import type { ReadResourceResult } from '@frontmcp/protocol';
324
- import { ResourceContext, ResourceTemplate } from '@frontmcp/sdk';
278
+ import { ReadResourceResult, ResourceContext, ResourceNotFoundError, ResourceTemplate } from '@frontmcp/sdk';
325
279
 
326
- import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
280
+ import { VectorStoreProvider } from '../../ingestion/providers/vector-store.provider';
327
281
 
328
282
  @ResourceTemplate({
329
283
  name: 'document',
@@ -333,20 +287,20 @@ import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
333
287
  })
334
288
  export class DocResource extends ResourceContext<{ documentId: string }> {
335
289
  async execute(uri: string, params: { documentId: string }): Promise<ReadResourceResult> {
336
- const store = this.get(VECTOR_STORE);
337
- const chunks = await store.getByDocumentId(params.documentId);
290
+ const store = this.get(VectorStoreProvider);
291
+ const matches = await store.getByDocumentId(params.documentId);
338
292
 
339
- if (chunks.length === 0) {
340
- this.fail(new Error(`Document not found: ${params.documentId}`));
293
+ if (matches.length === 0) {
294
+ // Use a typed MCP error so the protocol response carries the correct JSON-RPC code.
295
+ throw new ResourceNotFoundError(uri);
341
296
  }
342
297
 
343
298
  const document = {
344
299
  documentId: params.documentId,
345
- title: chunks[0].metadata.title ?? 'Untitled',
346
- chunks: chunks.map((c) => ({
347
- chunkIndex: c.metadata.chunkIndex,
348
- content: c.content,
349
- })),
300
+ title: matches[0].metadata.title ?? 'Untitled',
301
+ chunks: matches
302
+ .map((m) => ({ chunkIndex: m.metadata.chunkIndex, content: m.metadata.content }))
303
+ .sort((a, b) => a.chunkIndex - b.chunkIndex),
350
304
  };
351
305
 
352
306
  return {
@@ -384,6 +338,8 @@ export class ResearchApp {}
384
338
 
385
339
  ### Agent: Researcher
386
340
 
341
+ The framework drives the LLM tool-use loop via the `agents:call-agent` flow — the agent class doesn't override `execute()`. Configure iteration limits via `@Agent({ execution: { maxIterations } })`.
342
+
387
343
  ```typescript
388
344
  // src/research/agents/researcher.agent.ts
389
345
  import { Agent, AgentContext, z } from '@frontmcp/sdk';
@@ -412,47 +368,43 @@ import { SearchDocsTool } from '../../search/tools/search-docs.tool';
412
368
  },
413
369
  llm: {
414
370
  provider: 'anthropic', // Any supported provider — 'anthropic', 'openai', etc.
415
- model: 'claude-sonnet-4-20250514', // Any supported model for the chosen provider
371
+ model: 'claude-sonnet-4-6', // Any supported model for the chosen provider
416
372
  apiKey: { env: 'ANTHROPIC_API_KEY' },
417
373
  maxTokens: 4096,
418
374
  },
375
+ // Cap the inner tool-use loop. The framework — not your code — drives iteration.
376
+ execution: { maxIterations: 5 },
419
377
  tools: [SearchDocsTool, IngestDocumentTool],
420
378
  systemInstructions: `You are a research assistant with access to a knowledge base.
421
379
  Your job is to:
422
380
  1. Search the knowledge base for relevant documents using the search_docs tool.
423
381
  2. Analyze the results and identify key themes.
424
- 3. If depth is "deep", perform multiple searches with refined queries.
382
+ 3. When depth is "deep", perform multiple searches with refined queries; for "shallow", a single search is enough.
425
383
  4. Synthesize findings into a structured summary with source attribution.
426
- Always cite which documents support your findings.`,
384
+ Always cite which documents support your findings, and return JSON matching the output schema.`,
427
385
  })
428
- export class ResearcherAgent extends AgentContext {
429
- async execute(input: { topic: string; depth: 'shallow' | 'deep' }) {
430
- const maxIterations = input.depth === 'deep' ? 5 : 2;
431
- const prompt = [
432
- `Research the following topic: "${input.topic}"`,
433
- `Depth: ${input.depth} (max ${maxIterations} search iterations)`,
434
- 'Search the knowledge base, analyze results, and produce a structured summary.',
435
- 'Return your findings as JSON matching the output schema.',
436
- ].join('\n');
437
-
438
- return this.run(prompt, { maxIterations });
439
- }
440
- }
386
+ export class ResearcherAgent extends AgentContext {}
441
387
  ```
442
388
 
443
389
  ---
444
390
 
445
391
  ## Plugin: Audit Log
446
392
 
393
+ Real plugins extend `DynamicPlugin<Options>` and attach to flows via the `FlowHooksOf` decorators (e.g. `@ToolHook.Will('execute')`, `@ToolHook.Did('execute')`). There is no `PluginHookContext`/`onToolExecute*` lifecycle.
394
+
447
395
  ```typescript
448
396
  // src/plugins/audit-log.plugin.ts
449
- import { Plugin, type PluginHookContext } from '@frontmcp/sdk';
397
+ import { DynamicPlugin, FlowCtxOf, Plugin, ToolHook } from '@frontmcp/sdk';
398
+
399
+ export interface AuditLogPluginOptions {
400
+ endpoint?: string;
401
+ }
450
402
 
451
403
  @Plugin({
452
- name: 'AuditLog',
404
+ name: 'audit-log',
453
405
  description: 'Logs all tool invocations for audit compliance',
454
406
  })
455
- export class AuditLogPlugin {
407
+ export class AuditLogPlugin extends DynamicPlugin<AuditLogPluginOptions> {
456
408
  private readonly logs: Array<{
457
409
  timestamp: string;
458
410
  tool: string;
@@ -461,51 +413,64 @@ export class AuditLogPlugin {
461
413
  success: boolean;
462
414
  }> = [];
463
415
 
464
- async onToolExecuteBefore(ctx: PluginHookContext): Promise<void> {
465
- ctx.state.set('audit:startTime', Date.now());
416
+ constructor(protected options: AuditLogPluginOptions = {}) {
417
+ super();
418
+ }
419
+
420
+ // `Will('execute')` runs immediately before the tool's execute() — record start time on the flow state.
421
+ @ToolHook.Will('execute', { priority: 100 })
422
+ async onWillExecute(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void> {
423
+ flowCtx.state.set('audit:startTime', Date.now());
466
424
  }
467
425
 
468
- async onToolExecuteAfter(ctx: PluginHookContext): Promise<void> {
469
- const startTime = ctx.state.get('audit:startTime') as number;
470
- const duration = Date.now() - startTime;
426
+ // `Did('execute')` runs after a successful execute() compute duration and log success.
427
+ @ToolHook.Did('execute', { priority: 100 })
428
+ async onDidExecute(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void> {
429
+ const startTime = flowCtx.state.get('audit:startTime') as number | undefined;
430
+ const duration = startTime ? Date.now() - startTime : 0;
431
+ const ctx = flowCtx.state.required.toolContext;
471
432
 
472
433
  const entry = {
473
434
  timestamp: new Date().toISOString(),
474
- tool: ctx.toolName,
475
- userId: ctx.session?.userId,
435
+ tool: ctx.metadata.name,
436
+ userId: (ctx.authInfo as any)?.user?.sub as string | undefined,
476
437
  duration,
477
438
  success: true,
478
439
  };
479
440
  this.logs.push(entry);
480
441
 
481
- // In production, send to an external logging service
482
- if (process.env.AUDIT_LOG_ENDPOINT) {
442
+ if (this.options.endpoint) {
443
+ // Audit logging should never block tool execution.
483
444
  await ctx
484
- .fetch(process.env.AUDIT_LOG_ENDPOINT, {
445
+ .fetch(this.options.endpoint, {
485
446
  method: 'POST',
486
447
  headers: { 'Content-Type': 'application/json' },
487
448
  body: JSON.stringify(entry),
488
449
  })
489
- .catch(() => {
490
- // Audit logging should not block tool execution
491
- });
450
+ .catch(() => undefined);
492
451
  }
493
452
  }
494
453
 
495
- async onToolExecuteError(ctx: PluginHookContext): Promise<void> {
496
- const startTime = ctx.state.get('audit:startTime') as number;
497
- const duration = Date.now() - startTime;
498
-
499
- this.logs.push({
500
- timestamp: new Date().toISOString(),
501
- tool: ctx.toolName,
502
- userId: ctx.session?.userId,
503
- duration,
504
- success: false,
505
- });
454
+ // `Around('execute')` wraps the call so we can capture errors as well.
455
+ @ToolHook.Around('execute', { priority: 100 })
456
+ async aroundExecute(flowCtx: FlowCtxOf<'tools:call-tool'>, next: () => Promise<unknown>): Promise<unknown> {
457
+ try {
458
+ return await next();
459
+ } catch (err) {
460
+ const startTime = flowCtx.state.get('audit:startTime') as number | undefined;
461
+ const ctx = flowCtx.state.required.toolContext;
462
+ this.logs.push({
463
+ timestamp: new Date().toISOString(),
464
+ tool: ctx.metadata.name,
465
+ userId: (ctx.authInfo as any)?.user?.sub as string | undefined,
466
+ duration: startTime ? Date.now() - startTime : 0,
467
+ success: false,
468
+ });
469
+ throw err;
470
+ }
506
471
  }
507
472
 
508
- getLogs(): typeof this.logs {
473
+ getLogs(): ReadonlyArray<(typeof this.logs)[number]> {
509
474
  return [...this.logs];
510
475
  }
511
476
  }
@@ -515,75 +480,30 @@ export class AuditLogPlugin {
515
480
 
516
481
  ## Test: Researcher Agent
517
482
 
518
- ```typescript
519
- // test/researcher.agent.spec.ts
520
- import { AgentContext } from '@frontmcp/sdk';
483
+ The agent class has no `execute()` to unit-test — the framework drives the LLM tool-use loop via the `agents:call-agent` flow. Cover the agent end-to-end: register it on a `TestServer`, call it via `client.tools.call('research_topic', ...)` (agents are exposed as tools), and assert on the structured output.
521
484
 
522
- import { ResearcherAgent } from '../src/research/agents/researcher.agent';
485
+ ```typescript
486
+ // test/researcher.agent.e2e.spec.ts
487
+ import { McpTestClient, TestServer, TestTokenFactory } from '@frontmcp/testing';
523
488
 
524
- describe('ResearcherAgent', () => {
525
- let agent: ResearcherAgent;
489
+ describe('ResearcherAgent E2E', () => {
490
+ let client: McpTestClient;
491
+ let server: TestServer;
526
492
 
527
- beforeEach(() => {
528
- agent = new ResearcherAgent();
493
+ beforeAll(async () => {
494
+ server = await TestServer.start({ command: 'npx tsx src/main.ts' });
495
+ const token = await new TestTokenFactory().createTestToken({ sub: 'researcher-1', scopes: ['kb'] });
496
+ client = await McpTestClient.create({ baseUrl: server.info.baseUrl }).withToken(token).buildAndConnect();
529
497
  });
530
498
 
531
- it('should configure shallow depth with 2 max iterations', async () => {
532
- const runFn = jest.fn().mockResolvedValue({
533
- topic: 'TypeScript patterns',
534
- summary: 'Key patterns include generics and type guards.',
535
- sources: [{ documentId: 'doc-1', title: 'TS Handbook', relevance: 'high' }],
536
- confidence: 'medium',
537
- });
538
-
539
- const ctx = {
540
- run: runFn,
541
- get: jest.fn(),
542
- tryGet: jest.fn(),
543
- fail: jest.fn((err: Error) => {
544
- throw err;
545
- }),
546
- mark: jest.fn(),
547
- notify: jest.fn(),
548
- respondProgress: jest.fn(),
549
- } as unknown as AgentContext;
550
- Object.assign(agent, ctx);
551
-
552
- const result = await agent.execute({
553
- topic: 'TypeScript patterns',
554
- depth: 'shallow',
555
- });
556
-
557
- expect(runFn).toHaveBeenCalledWith(expect.stringContaining('TypeScript patterns'), { maxIterations: 2 });
558
- expect(result).toHaveProperty('summary');
559
- expect(result).toHaveProperty('sources');
560
- expect(result.confidence).toBe('medium');
499
+ afterAll(async () => {
500
+ await client.disconnect();
501
+ await server.stop();
561
502
  });
562
503
 
563
- it('should configure deep depth with 5 max iterations', async () => {
564
- const runFn = jest.fn().mockResolvedValue({
565
- topic: 'Distributed systems',
566
- summary: 'Consensus, replication, and partition tolerance.',
567
- sources: [],
568
- confidence: 'low',
569
- });
570
-
571
- const ctx = {
572
- run: runFn,
573
- get: jest.fn(),
574
- tryGet: jest.fn(),
575
- fail: jest.fn((err: Error) => {
576
- throw err;
577
- }),
578
- mark: jest.fn(),
579
- notify: jest.fn(),
580
- respondProgress: jest.fn(),
581
- } as unknown as AgentContext;
582
- Object.assign(agent, ctx);
583
-
584
- await agent.execute({ topic: 'Distributed systems', depth: 'deep' });
585
-
586
- expect(runFn).toHaveBeenCalledWith(expect.stringContaining('Distributed systems'), { maxIterations: 5 });
504
+ it('exposes the research_topic agent in the tool list', async () => {
505
+ const tools = await client.tools.list();
506
+ expect(tools.map((t) => t.name)).toContain('research_topic');
587
507
  });
588
508
  });
589
509
  ```
@@ -592,30 +512,40 @@ describe('ResearcherAgent', () => {
592
512
 
593
513
  ## Test: Audit Log Plugin
594
514
 
515
+ Test the hook methods directly with a minimal `FlowCtx` shape. The `state` object is a `Map`-like store and `state.required.toolContext` is the live `ToolContext` exposed to hooks.
516
+
595
517
  ```typescript
596
518
  // test/audit-log.plugin.spec.ts
597
- import type { PluginHookContext } from '@frontmcp/sdk';
519
+ import type { FlowCtxOf } from '@frontmcp/sdk';
598
520
 
599
521
  import { AuditLogPlugin } from '../src/plugins/audit-log.plugin';
600
522
 
601
- describe('AuditLogPlugin', () => {
602
- let plugin: AuditLogPlugin;
603
-
604
- beforeEach(() => {
605
- plugin = new AuditLogPlugin();
606
- });
523
+ type ToolFlowCtx = FlowCtxOf<'tools:call-tool'>;
524
+
525
+ function makeFlowCtx(toolName: string, userSub: string | undefined): ToolFlowCtx {
526
+ const map = new Map<string, unknown>();
527
+ const toolContext = {
528
+ metadata: { name: toolName },
529
+ authInfo: userSub ? { user: { sub: userSub } } : {},
530
+ fetch: jest.fn().mockResolvedValue(new Response(null, { status: 204 })),
531
+ };
532
+ return {
533
+ state: {
534
+ set: (k: string, v: unknown) => map.set(k, v),
535
+ get: (k: string) => map.get(k),
536
+ required: { toolContext },
537
+ },
538
+ rawInput: {},
539
+ } as unknown as ToolFlowCtx;
540
+ }
607
541
 
608
- it('should record a successful tool execution', async () => {
609
- const state = new Map<string, unknown>();
610
- const ctx = {
611
- toolName: 'search_docs',
612
- session: { userId: 'user-1' },
613
- state: { set: (k: string, v: unknown) => state.set(k, v), get: (k: string) => state.get(k) },
614
- fetch: jest.fn(),
615
- } as unknown as PluginHookContext;
542
+ describe('AuditLogPlugin', () => {
543
+ it('records a successful tool execution', async () => {
544
+ const plugin = new AuditLogPlugin();
545
+ const flowCtx = makeFlowCtx('search_docs', 'user-1');
616
546
 
617
- await plugin.onToolExecuteBefore(ctx);
618
- await plugin.onToolExecuteAfter(ctx);
547
+ await plugin.onWillExecute(flowCtx);
548
+ await plugin.onDidExecute(flowCtx);
619
549
 
620
550
  const logs = plugin.getLogs();
621
551
  expect(logs).toHaveLength(1);
@@ -625,17 +555,14 @@ describe('AuditLogPlugin', () => {
625
555
  expect(logs[0].duration).toBeGreaterThanOrEqual(0);
626
556
  });
627
557
 
628
- it('should record a failed tool execution', async () => {
629
- const state = new Map<string, unknown>();
630
- const ctx = {
631
- toolName: 'ingest_document',
632
- session: undefined,
633
- state: { set: (k: string, v: unknown) => state.set(k, v), get: (k: string) => state.get(k) },
634
- fetch: jest.fn(),
635
- } as unknown as PluginHookContext;
558
+ it('records a failed tool execution via aroundExecute', async () => {
559
+ const plugin = new AuditLogPlugin();
560
+ const flowCtx = makeFlowCtx('ingest_document', undefined);
561
+
562
+ await plugin.onWillExecute(flowCtx);
563
+ const failing = () => Promise.reject(new Error('boom'));
636
564
 
637
- await plugin.onToolExecuteBefore(ctx);
638
- await plugin.onToolExecuteError(ctx);
565
+ await expect(plugin.aroundExecute(flowCtx, failing)).rejects.toThrow('boom');
639
566
 
640
567
  const logs = plugin.getLogs();
641
568
  expect(logs).toHaveLength(1);
@@ -647,10 +574,10 @@ describe('AuditLogPlugin', () => {
647
574
 
648
575
  ## Examples
649
576
 
650
- | Example | Level | Description |
651
- | -------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
652
- | [`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. |
653
- | [`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. |
654
- | [`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. |
577
+ | Example | Level | Description |
578
+ | -------------------------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
579
+ | [`agent-and-plugin`](../examples/example-knowledge-base/agent-and-plugin.md) | Advanced | Shows an autonomous research agent with inner tools and a real `ToolHook`-based plugin that hooks into the `tools:call-tool` flow for audit logging. |
580
+ | [`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. |
581
+ | [`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. |
655
582
 
656
583
  > See all examples in [`examples/example-knowledge-base/`](../examples/example-knowledge-base/)