@frontmcp/skills 0.0.1 → 1.0.0-beta.11

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 (104) hide show
  1. package/README.md +2 -2
  2. package/catalog/TEMPLATE.md +58 -13
  3. package/catalog/frontmcp-config/SKILL.md +156 -0
  4. package/catalog/{auth/configure-auth/references/auth-modes.md → frontmcp-config/references/configure-auth-modes.md} +5 -0
  5. package/catalog/frontmcp-config/references/configure-auth.md +243 -0
  6. package/catalog/frontmcp-config/references/configure-elicitation.md +183 -0
  7. package/catalog/frontmcp-config/references/configure-http.md +210 -0
  8. package/catalog/frontmcp-config/references/configure-session.md +210 -0
  9. package/catalog/{config/configure-throttle/references/guard-config.md → frontmcp-config/references/configure-throttle-guard-config.md} +5 -0
  10. package/catalog/frontmcp-config/references/configure-throttle.md +234 -0
  11. package/catalog/{config/configure-transport/references/protocol-presets.md → frontmcp-config/references/configure-transport-protocol-presets.md} +5 -0
  12. package/catalog/frontmcp-config/references/configure-transport.md +200 -0
  13. package/catalog/frontmcp-config/references/setup-redis.md +9 -0
  14. package/catalog/frontmcp-config/references/setup-sqlite.md +9 -0
  15. package/catalog/frontmcp-deployment/SKILL.md +152 -0
  16. package/catalog/frontmcp-deployment/references/build-for-browser.md +143 -0
  17. package/catalog/frontmcp-deployment/references/build-for-cli.md +191 -0
  18. package/catalog/{deployment/build-for-sdk/SKILL.md → frontmcp-deployment/references/build-for-sdk.md} +66 -20
  19. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +218 -0
  20. package/catalog/{deployment/deploy-to-lambda/SKILL.md → frontmcp-deployment/references/deploy-to-lambda.md} +77 -59
  21. package/catalog/{deployment/deploy-to-node/references/Dockerfile.example → frontmcp-deployment/references/deploy-to-node-dockerfile.md} +18 -4
  22. package/catalog/{deployment/deploy-to-node/SKILL.md → frontmcp-deployment/references/deploy-to-node.md} +69 -36
  23. package/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +65 -0
  24. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +229 -0
  25. package/catalog/frontmcp-development/SKILL.md +126 -0
  26. package/catalog/frontmcp-development/references/create-adapter.md +170 -0
  27. package/catalog/{development/create-agent/references/llm-config.md → frontmcp-development/references/create-agent-llm-config.md} +10 -5
  28. package/catalog/{development/create-agent/SKILL.md → frontmcp-development/references/create-agent.md} +83 -40
  29. package/catalog/{development/create-job/SKILL.md → frontmcp-development/references/create-job.md} +62 -15
  30. package/catalog/{plugins/create-plugin-hooks/SKILL.md → frontmcp-development/references/create-plugin-hooks.md} +100 -7
  31. package/catalog/frontmcp-development/references/create-plugin.md +506 -0
  32. package/catalog/{development/create-prompt/SKILL.md → frontmcp-development/references/create-prompt.md} +65 -22
  33. package/catalog/{development/create-provider/SKILL.md → frontmcp-development/references/create-provider.md} +63 -23
  34. package/catalog/{development/create-resource/SKILL.md → frontmcp-development/references/create-resource.md} +148 -26
  35. package/catalog/{development/create-skill-with-tools/SKILL.md → frontmcp-development/references/create-skill-with-tools.md} +174 -20
  36. package/catalog/{development/create-skill/SKILL.md → frontmcp-development/references/create-skill.md} +114 -28
  37. package/catalog/{development/create-tool/references/tool-annotations.md → frontmcp-development/references/create-tool-annotations.md} +5 -0
  38. package/catalog/{development/create-tool/references/output-schema-types.md → frontmcp-development/references/create-tool-output-schema-types.md} +5 -0
  39. package/catalog/{development/create-tool/SKILL.md → frontmcp-development/references/create-tool.md} +172 -23
  40. package/catalog/{development/create-workflow/SKILL.md → frontmcp-development/references/create-workflow.md} +61 -14
  41. package/catalog/frontmcp-development/references/decorators-guide.md +754 -0
  42. package/catalog/frontmcp-development/references/official-adapters.md +199 -0
  43. package/catalog/{plugins/official-plugins/SKILL.md → frontmcp-development/references/official-plugins.md} +97 -27
  44. package/catalog/frontmcp-extensibility/SKILL.md +103 -0
  45. package/catalog/frontmcp-extensibility/references/vectoriadb.md +289 -0
  46. package/catalog/frontmcp-guides/SKILL.md +420 -0
  47. package/catalog/frontmcp-guides/references/example-knowledge-base.md +641 -0
  48. package/catalog/frontmcp-guides/references/example-task-manager.md +517 -0
  49. package/catalog/frontmcp-guides/references/example-weather-api.md +297 -0
  50. package/catalog/frontmcp-production-readiness/SKILL.md +98 -0
  51. package/catalog/frontmcp-production-readiness/references/common-checklist.md +156 -0
  52. package/catalog/frontmcp-production-readiness/references/production-browser.md +46 -0
  53. package/catalog/frontmcp-production-readiness/references/production-cli-binary.md +62 -0
  54. package/catalog/frontmcp-production-readiness/references/production-cli-daemon.md +61 -0
  55. package/catalog/frontmcp-production-readiness/references/production-cloudflare.md +52 -0
  56. package/catalog/frontmcp-production-readiness/references/production-lambda.md +53 -0
  57. package/catalog/frontmcp-production-readiness/references/production-node-sdk.md +66 -0
  58. package/catalog/frontmcp-production-readiness/references/production-node-server.md +61 -0
  59. package/catalog/frontmcp-production-readiness/references/production-vercel.md +52 -0
  60. package/catalog/frontmcp-setup/SKILL.md +132 -0
  61. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +280 -0
  62. package/catalog/{setup/multi-app-composition/SKILL.md → frontmcp-setup/references/multi-app-composition.md} +66 -19
  63. package/catalog/{setup/nx-workflow/SKILL.md → frontmcp-setup/references/nx-workflow.md} +79 -17
  64. package/catalog/frontmcp-setup/references/project-structure-nx.md +251 -0
  65. package/catalog/frontmcp-setup/references/project-structure-standalone.md +217 -0
  66. package/catalog/frontmcp-setup/references/readme-guide.md +226 -0
  67. package/catalog/{setup/setup-project/SKILL.md → frontmcp-setup/references/setup-project.md} +63 -58
  68. package/catalog/{setup/setup-redis/SKILL.md → frontmcp-setup/references/setup-redis.md} +60 -82
  69. package/catalog/{setup/setup-sqlite/SKILL.md → frontmcp-setup/references/setup-sqlite.md} +65 -72
  70. package/catalog/frontmcp-testing/SKILL.md +135 -0
  71. package/catalog/{testing/setup-testing/SKILL.md → frontmcp-testing/references/setup-testing.md} +79 -63
  72. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-auth.md +5 -0
  73. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-browser-build.md +5 -0
  74. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-cli-binary.md +5 -0
  75. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-direct-client.md +5 -0
  76. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-e2e-handler.md +5 -0
  77. package/catalog/{testing/setup-testing → frontmcp-testing}/references/test-tool-unit.md +6 -0
  78. package/catalog/skills-manifest.json +337 -382
  79. package/package.json +2 -2
  80. package/src/index.d.ts +1 -1
  81. package/src/index.js.map +1 -1
  82. package/src/loader.js +0 -1
  83. package/src/loader.js.map +1 -1
  84. package/src/manifest.d.ts +15 -3
  85. package/src/manifest.js +3 -3
  86. package/src/manifest.js.map +1 -1
  87. package/catalog/adapters/create-adapter/SKILL.md +0 -127
  88. package/catalog/adapters/official-adapters/SKILL.md +0 -136
  89. package/catalog/auth/configure-auth/SKILL.md +0 -250
  90. package/catalog/auth/configure-session/SKILL.md +0 -201
  91. package/catalog/config/configure-elicitation/SKILL.md +0 -136
  92. package/catalog/config/configure-http/SKILL.md +0 -167
  93. package/catalog/config/configure-throttle/SKILL.md +0 -189
  94. package/catalog/config/configure-transport/SKILL.md +0 -151
  95. package/catalog/deployment/build-for-browser/SKILL.md +0 -95
  96. package/catalog/deployment/build-for-cli/SKILL.md +0 -100
  97. package/catalog/deployment/deploy-to-cloudflare/SKILL.md +0 -192
  98. package/catalog/deployment/deploy-to-vercel/SKILL.md +0 -196
  99. package/catalog/deployment/deploy-to-vercel/references/vercel.json.example +0 -60
  100. package/catalog/development/decorators-guide/SKILL.md +0 -598
  101. package/catalog/plugins/create-plugin/SKILL.md +0 -336
  102. package/catalog/setup/frontmcp-skills-usage/SKILL.md +0 -200
  103. package/catalog/setup/project-structure-nx/SKILL.md +0 -186
  104. package/catalog/setup/project-structure-standalone/SKILL.md +0 -153
@@ -0,0 +1,641 @@
1
+ ---
2
+ name: example-knowledge-base
3
+ description: Multi-app knowledge base with vector storage, semantic search, AI research agent, and audit plugin
4
+ ---
5
+
6
+ # Example: Knowledge Base (Advanced)
7
+
8
+ > Skills used: setup-project, multi-app-composition, create-tool, create-resource, create-provider, create-agent, create-plugin, configure-auth, deploy-to-vercel
9
+
10
+ A multi-app knowledge base MCP server with three composed apps: document ingestion with vector storage, semantic search with resource templates, and an autonomous AI research agent. Includes a custom audit log plugin and demonstrates advanced patterns like multi-app composition, DI across app boundaries, agent inner tools, and plugin hooks.
11
+
12
+ ---
13
+
14
+ ## Server Entry Point
15
+
16
+ ```typescript
17
+ // src/main.ts
18
+ import { FrontMcp } from '@frontmcp/sdk';
19
+ import { IngestionApp } from './ingestion/ingestion.app';
20
+ import { SearchApp } from './search/search.app';
21
+ import { ResearchApp } from './research/research.app';
22
+ import { AuditLogPlugin } from './plugins/audit-log.plugin';
23
+
24
+ @FrontMcp({
25
+ info: { name: 'knowledge-base', version: '1.0.0' },
26
+ apps: [IngestionApp, SearchApp, ResearchApp],
27
+ plugins: [AuditLogPlugin],
28
+ auth: { mode: 'remote', provider: 'https://auth.example.com', clientId: 'my-client-id' },
29
+ redis: { provider: 'redis', host: process.env.REDIS_URL ?? 'localhost' },
30
+ })
31
+ export default class KnowledgeBaseServer {}
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Ingestion App
37
+
38
+ ### App Registration
39
+
40
+ ```typescript
41
+ // src/ingestion/ingestion.app.ts
42
+ import { App } from '@frontmcp/sdk';
43
+ import { VectorStoreProvider } from './providers/vector-store.provider';
44
+ import { IngestDocumentTool } from './tools/ingest-document.tool';
45
+
46
+ @App({
47
+ name: 'Ingestion',
48
+ description: 'Document ingestion and chunking pipeline',
49
+ providers: [VectorStoreProvider],
50
+ tools: [IngestDocumentTool],
51
+ })
52
+ export class IngestionApp {}
53
+ ```
54
+
55
+ ### Provider: Vector Store
56
+
57
+ ```typescript
58
+ // src/ingestion/providers/vector-store.provider.ts
59
+ import { Provider } from '@frontmcp/sdk';
60
+ import type { Token } from '@frontmcp/di';
61
+
62
+ export interface DocumentChunk {
63
+ id: string;
64
+ documentId: string;
65
+ content: string;
66
+ embedding: number[];
67
+ metadata: Record<string, string>;
68
+ }
69
+
70
+ export interface VectorStore {
71
+ upsert(chunks: DocumentChunk[]): Promise<void>;
72
+ search(embedding: number[], topK: number): Promise<DocumentChunk[]>;
73
+ getByDocumentId(documentId: string): Promise<DocumentChunk[]>;
74
+ deleteByDocumentId(documentId: string): Promise<void>;
75
+ }
76
+
77
+ export const VECTOR_STORE: Token<VectorStore> = Symbol('VectorStore');
78
+
79
+ @Provider({ token: VECTOR_STORE })
80
+ export class VectorStoreProvider implements VectorStore {
81
+ private client!: { upsert: Function; query: Function; delete: Function };
82
+
83
+ async onInit(): Promise<void> {
84
+ const apiKey = process.env.VECTOR_DB_API_KEY;
85
+ if (!apiKey) {
86
+ throw new Error('VECTOR_DB_API_KEY environment variable is required');
87
+ }
88
+
89
+ // Initialize your vector DB client (e.g., Pinecone, Weaviate, Qdrant)
90
+ this.client = await this.createVectorClient(apiKey);
91
+ }
92
+
93
+ async upsert(chunks: DocumentChunk[]): Promise<void> {
94
+ await this.client.upsert(
95
+ chunks.map((c) => ({
96
+ id: c.id,
97
+ values: c.embedding,
98
+ metadata: { ...c.metadata, documentId: c.documentId, content: c.content },
99
+ })),
100
+ );
101
+ }
102
+
103
+ async search(embedding: number[], topK: number): Promise<DocumentChunk[]> {
104
+ const results = await this.client.query({ vector: embedding, topK });
105
+ return results.matches.map((m: Record<string, unknown>) => ({
106
+ id: m.id as string,
107
+ documentId: (m.metadata as Record<string, string>).documentId,
108
+ content: (m.metadata as Record<string, string>).content,
109
+ embedding: m.values as number[],
110
+ metadata: m.metadata as Record<string, string>,
111
+ }));
112
+ }
113
+
114
+ async getByDocumentId(documentId: string): Promise<DocumentChunk[]> {
115
+ const results = await this.client.query({
116
+ filter: { documentId },
117
+ topK: 100,
118
+ vector: new Array(1536).fill(0),
119
+ });
120
+ return results.matches.map((m: Record<string, unknown>) => ({
121
+ id: m.id as string,
122
+ documentId,
123
+ content: (m.metadata as Record<string, string>).content,
124
+ embedding: m.values as number[],
125
+ metadata: m.metadata as Record<string, string>,
126
+ }));
127
+ }
128
+
129
+ async deleteByDocumentId(documentId: string): Promise<void> {
130
+ await this.client.delete({ filter: { documentId } });
131
+ }
132
+
133
+ private async createVectorClient(_apiKey: string): Promise<{ upsert: Function; query: Function; delete: Function }> {
134
+ // Stub: replace with your vector DB SDK (e.g., Pinecone, Weaviate, Qdrant)
135
+ // This placeholder focuses on the FrontMCP patterns, not the vector DB integration.
136
+ throw new Error('Implement with your vector DB provider (e.g., Pinecone, Weaviate, Qdrant)');
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### Tool: Ingest Document
142
+
143
+ ```typescript
144
+ // src/ingestion/tools/ingest-document.tool.ts
145
+ import { Tool, ToolContext } from '@frontmcp/sdk';
146
+ import { z } from 'zod';
147
+ import { VECTOR_STORE } from '../providers/vector-store.provider';
148
+ import type { DocumentChunk } from '../providers/vector-store.provider';
149
+
150
+ @Tool({
151
+ name: 'ingest_document',
152
+ description: 'Ingest a document by chunking its content and storing embeddings',
153
+ inputSchema: {
154
+ documentId: z.string().min(1).describe('Unique document identifier'),
155
+ title: z.string().min(1).describe('Document title'),
156
+ content: z.string().min(1).describe('Full document text content'),
157
+ tags: z.array(z.string()).default([]).describe('Optional tags for filtering'),
158
+ },
159
+ outputSchema: {
160
+ documentId: z.string(),
161
+ chunksCreated: z.number(),
162
+ title: z.string(),
163
+ },
164
+ })
165
+ export class IngestDocumentTool extends ToolContext {
166
+ async execute(input: { documentId: string; title: string; content: string; tags: string[] }) {
167
+ const store = this.get(VECTOR_STORE);
168
+
169
+ this.mark('chunking');
170
+ const textChunks = this.chunkText(input.content, 512);
171
+
172
+ this.mark('embedding');
173
+ await this.respondProgress(0, textChunks.length);
174
+
175
+ const chunks: DocumentChunk[] = [];
176
+ for (let i = 0; i < textChunks.length; i++) {
177
+ const embedding = await this.generateEmbedding(textChunks[i]);
178
+ chunks.push({
179
+ id: `${input.documentId}-chunk-${i}`,
180
+ documentId: input.documentId,
181
+ content: textChunks[i],
182
+ embedding,
183
+ metadata: { title: input.title, tags: input.tags.join(','), chunkIndex: String(i) },
184
+ });
185
+ await this.respondProgress(i + 1, textChunks.length);
186
+ }
187
+
188
+ this.mark('storing');
189
+ await store.upsert(chunks);
190
+
191
+ await this.notify(`Ingested "${input.title}" with ${chunks.length} chunks`, 'info');
192
+
193
+ return {
194
+ documentId: input.documentId,
195
+ chunksCreated: chunks.length,
196
+ title: input.title,
197
+ };
198
+ }
199
+
200
+ private chunkText(text: string, maxTokens: number): string[] {
201
+ const sentences = text.split(/(?<=[.!?])\s+/);
202
+ const chunks: string[] = [];
203
+ let current = '';
204
+
205
+ for (const sentence of sentences) {
206
+ if ((current + ' ' + sentence).trim().length > maxTokens * 4) {
207
+ if (current) chunks.push(current.trim());
208
+ current = sentence;
209
+ } else {
210
+ current = current ? current + ' ' + sentence : sentence;
211
+ }
212
+ }
213
+ if (current.trim()) chunks.push(current.trim());
214
+ return chunks;
215
+ }
216
+
217
+ private async generateEmbedding(text: string): Promise<number[]> {
218
+ const response = await this.fetch('https://api.openai.com/v1/embeddings', {
219
+ method: 'POST',
220
+ headers: {
221
+ 'Content-Type': 'application/json',
222
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
223
+ },
224
+ body: JSON.stringify({ input: text, model: 'text-embedding-3-small' }),
225
+ });
226
+ const data = await response.json();
227
+ return data.data[0].embedding;
228
+ }
229
+ }
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Search App
235
+
236
+ ### App Registration
237
+
238
+ ```typescript
239
+ // src/search/search.app.ts
240
+ import { App } from '@frontmcp/sdk';
241
+ import { VectorStoreProvider } from '../ingestion/providers/vector-store.provider';
242
+ import { SearchDocsTool } from './tools/search-docs.tool';
243
+ import { DocResource } from './resources/doc.resource';
244
+
245
+ @App({
246
+ name: 'Search',
247
+ description: 'Semantic search and document retrieval',
248
+ providers: [VectorStoreProvider],
249
+ tools: [SearchDocsTool],
250
+ resources: [DocResource],
251
+ })
252
+ export class SearchApp {}
253
+ ```
254
+
255
+ ### Tool: Search Documents
256
+
257
+ ```typescript
258
+ // src/search/tools/search-docs.tool.ts
259
+ import { Tool, ToolContext } from '@frontmcp/sdk';
260
+ import { z } from 'zod';
261
+ import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
262
+
263
+ @Tool({
264
+ name: 'search_docs',
265
+ description: 'Semantic search across the knowledge base',
266
+ inputSchema: {
267
+ query: z.string().min(1).describe('Natural language search query'),
268
+ topK: z.number().int().min(1).max(20).default(5).describe('Number of results'),
269
+ },
270
+ outputSchema: {
271
+ results: z.array(
272
+ z.object({
273
+ documentId: z.string(),
274
+ content: z.string(),
275
+ score: z.number(),
276
+ title: z.string(),
277
+ }),
278
+ ),
279
+ total: z.number(),
280
+ },
281
+ })
282
+ export class SearchDocsTool extends ToolContext {
283
+ async execute(input: { query: string; topK: number }) {
284
+ const store = this.get(VECTOR_STORE);
285
+
286
+ this.mark('embedding-query');
287
+ const queryEmbedding = await this.generateQueryEmbedding(input.query);
288
+
289
+ this.mark('searching');
290
+ const chunks = await store.search(queryEmbedding, input.topK);
291
+
292
+ const results = chunks.map((chunk) => ({
293
+ documentId: chunk.documentId,
294
+ content: chunk.content,
295
+ score: chunk.metadata.score ? parseFloat(chunk.metadata.score) : 0,
296
+ title: chunk.metadata.title ?? 'Untitled',
297
+ }));
298
+
299
+ return { results, total: results.length };
300
+ }
301
+
302
+ private async generateQueryEmbedding(query: string): Promise<number[]> {
303
+ const response = await this.fetch('https://api.openai.com/v1/embeddings', {
304
+ method: 'POST',
305
+ headers: {
306
+ 'Content-Type': 'application/json',
307
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
308
+ },
309
+ body: JSON.stringify({ input: query, model: 'text-embedding-3-small' }),
310
+ });
311
+ const data = await response.json();
312
+ return data.data[0].embedding;
313
+ }
314
+ }
315
+ ```
316
+
317
+ ### Resource Template: Document by ID
318
+
319
+ ```typescript
320
+ // src/search/resources/doc.resource.ts
321
+ import { ResourceTemplate, ResourceContext } from '@frontmcp/sdk';
322
+ import type { ReadResourceResult } from '@frontmcp/protocol';
323
+ import { VECTOR_STORE } from '../../ingestion/providers/vector-store.provider';
324
+
325
+ @ResourceTemplate({
326
+ name: 'document',
327
+ uriTemplate: 'kb://documents/{documentId}',
328
+ description: 'Retrieve all chunks of a document by its ID',
329
+ mimeType: 'application/json',
330
+ })
331
+ export class DocResource extends ResourceContext<{ documentId: string }> {
332
+ async execute(uri: string, params: { documentId: string }): Promise<ReadResourceResult> {
333
+ const store = this.get(VECTOR_STORE);
334
+ const chunks = await store.getByDocumentId(params.documentId);
335
+
336
+ if (chunks.length === 0) {
337
+ this.fail(new Error(`Document not found: ${params.documentId}`));
338
+ }
339
+
340
+ const document = {
341
+ documentId: params.documentId,
342
+ title: chunks[0].metadata.title ?? 'Untitled',
343
+ chunks: chunks.map((c) => ({
344
+ chunkIndex: c.metadata.chunkIndex,
345
+ content: c.content,
346
+ })),
347
+ };
348
+
349
+ return {
350
+ contents: [
351
+ {
352
+ uri,
353
+ mimeType: 'application/json',
354
+ text: JSON.stringify(document, null, 2),
355
+ },
356
+ ],
357
+ };
358
+ }
359
+ }
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Research App
365
+
366
+ ### App Registration
367
+
368
+ ```typescript
369
+ // src/research/research.app.ts
370
+ import { App } from '@frontmcp/sdk';
371
+ import { ResearcherAgent } from './agents/researcher.agent';
372
+
373
+ @App({
374
+ name: 'Research',
375
+ description: 'AI-powered research agent for knowledge synthesis',
376
+ agents: [ResearcherAgent],
377
+ })
378
+ export class ResearchApp {}
379
+ ```
380
+
381
+ ### Agent: Researcher
382
+
383
+ ```typescript
384
+ // src/research/agents/researcher.agent.ts
385
+ import { Agent, AgentContext } from '@frontmcp/sdk';
386
+ import { z } from 'zod';
387
+ import { SearchDocsTool } from '../../search/tools/search-docs.tool';
388
+ import { IngestDocumentTool } from '../../ingestion/tools/ingest-document.tool';
389
+
390
+ @Agent({
391
+ name: 'research_topic',
392
+ description: 'Research a topic across the knowledge base and synthesize findings into a structured report',
393
+ inputSchema: {
394
+ topic: z.string().min(1).describe('Research topic or question'),
395
+ depth: z.enum(['shallow', 'deep']).default('shallow').describe('Research depth'),
396
+ },
397
+ outputSchema: {
398
+ topic: z.string(),
399
+ summary: z.string(),
400
+ sources: z.array(
401
+ z.object({
402
+ documentId: z.string(),
403
+ title: z.string(),
404
+ relevance: z.string(),
405
+ }),
406
+ ),
407
+ confidence: z.enum(['low', 'medium', 'high']),
408
+ },
409
+ llm: {
410
+ provider: 'anthropic', // Any supported provider — 'anthropic', 'openai', etc.
411
+ model: 'claude-sonnet-4-20250514', // Any supported model for the chosen provider
412
+ apiKey: { env: 'ANTHROPIC_API_KEY' },
413
+ maxTokens: 4096,
414
+ },
415
+ tools: [SearchDocsTool, IngestDocumentTool],
416
+ systemInstructions: `You are a research assistant with access to a knowledge base.
417
+ Your job is to:
418
+ 1. Search the knowledge base for relevant documents using the search_docs tool.
419
+ 2. Analyze the results and identify key themes.
420
+ 3. If depth is "deep", perform multiple searches with refined queries.
421
+ 4. Synthesize findings into a structured summary with source attribution.
422
+ Always cite which documents support your findings.`,
423
+ })
424
+ export class ResearcherAgent extends AgentContext {
425
+ async execute(input: { topic: string; depth: 'shallow' | 'deep' }) {
426
+ const maxIterations = input.depth === 'deep' ? 5 : 2;
427
+ const prompt = [
428
+ `Research the following topic: "${input.topic}"`,
429
+ `Depth: ${input.depth} (max ${maxIterations} search iterations)`,
430
+ 'Search the knowledge base, analyze results, and produce a structured summary.',
431
+ 'Return your findings as JSON matching the output schema.',
432
+ ].join('\n');
433
+
434
+ return this.run(prompt, { maxIterations });
435
+ }
436
+ }
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Plugin: Audit Log
442
+
443
+ ```typescript
444
+ // src/plugins/audit-log.plugin.ts
445
+ import { Plugin } from '@frontmcp/sdk';
446
+ import type { PluginHookContext } from '@frontmcp/sdk';
447
+
448
+ @Plugin({
449
+ name: 'AuditLog',
450
+ description: 'Logs all tool invocations for audit compliance',
451
+ })
452
+ export class AuditLogPlugin {
453
+ private readonly logs: Array<{
454
+ timestamp: string;
455
+ tool: string;
456
+ userId: string | undefined;
457
+ duration: number;
458
+ success: boolean;
459
+ }> = [];
460
+
461
+ async onToolExecuteBefore(ctx: PluginHookContext): Promise<void> {
462
+ ctx.state.set('audit:startTime', Date.now());
463
+ }
464
+
465
+ async onToolExecuteAfter(ctx: PluginHookContext): Promise<void> {
466
+ const startTime = ctx.state.get('audit:startTime') as number;
467
+ const duration = Date.now() - startTime;
468
+
469
+ const entry = {
470
+ timestamp: new Date().toISOString(),
471
+ tool: ctx.toolName,
472
+ userId: ctx.session?.userId,
473
+ duration,
474
+ success: true,
475
+ };
476
+ this.logs.push(entry);
477
+
478
+ // In production, send to an external logging service
479
+ if (process.env.AUDIT_LOG_ENDPOINT) {
480
+ await ctx
481
+ .fetch(process.env.AUDIT_LOG_ENDPOINT, {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify(entry),
485
+ })
486
+ .catch(() => {
487
+ // Audit logging should not block tool execution
488
+ });
489
+ }
490
+ }
491
+
492
+ async onToolExecuteError(ctx: PluginHookContext): Promise<void> {
493
+ const startTime = ctx.state.get('audit:startTime') as number;
494
+ const duration = Date.now() - startTime;
495
+
496
+ this.logs.push({
497
+ timestamp: new Date().toISOString(),
498
+ tool: ctx.toolName,
499
+ userId: ctx.session?.userId,
500
+ duration,
501
+ success: false,
502
+ });
503
+ }
504
+
505
+ getLogs(): typeof this.logs {
506
+ return [...this.logs];
507
+ }
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Test: Researcher Agent
514
+
515
+ ```typescript
516
+ // test/researcher.agent.spec.ts
517
+ import { AgentContext } from '@frontmcp/sdk';
518
+ import { ResearcherAgent } from '../src/research/agents/researcher.agent';
519
+
520
+ describe('ResearcherAgent', () => {
521
+ let agent: ResearcherAgent;
522
+
523
+ beforeEach(() => {
524
+ agent = new ResearcherAgent();
525
+ });
526
+
527
+ it('should configure shallow depth with 2 max iterations', async () => {
528
+ const runFn = jest.fn().mockResolvedValue({
529
+ topic: 'TypeScript patterns',
530
+ summary: 'Key patterns include generics and type guards.',
531
+ sources: [{ documentId: 'doc-1', title: 'TS Handbook', relevance: 'high' }],
532
+ confidence: 'medium',
533
+ });
534
+
535
+ const ctx = {
536
+ run: runFn,
537
+ get: jest.fn(),
538
+ tryGet: jest.fn(),
539
+ fail: jest.fn((err: Error) => {
540
+ throw err;
541
+ }),
542
+ mark: jest.fn(),
543
+ notify: jest.fn(),
544
+ respondProgress: jest.fn(),
545
+ } as unknown as AgentContext;
546
+ Object.assign(agent, ctx);
547
+
548
+ const result = await agent.execute({
549
+ topic: 'TypeScript patterns',
550
+ depth: 'shallow',
551
+ });
552
+
553
+ expect(runFn).toHaveBeenCalledWith(expect.stringContaining('TypeScript patterns'), { maxIterations: 2 });
554
+ expect(result).toHaveProperty('summary');
555
+ expect(result).toHaveProperty('sources');
556
+ expect(result.confidence).toBe('medium');
557
+ });
558
+
559
+ it('should configure deep depth with 5 max iterations', async () => {
560
+ const runFn = jest.fn().mockResolvedValue({
561
+ topic: 'Distributed systems',
562
+ summary: 'Consensus, replication, and partition tolerance.',
563
+ sources: [],
564
+ confidence: 'low',
565
+ });
566
+
567
+ const ctx = {
568
+ run: runFn,
569
+ get: jest.fn(),
570
+ tryGet: jest.fn(),
571
+ fail: jest.fn((err: Error) => {
572
+ throw err;
573
+ }),
574
+ mark: jest.fn(),
575
+ notify: jest.fn(),
576
+ respondProgress: jest.fn(),
577
+ } as unknown as AgentContext;
578
+ Object.assign(agent, ctx);
579
+
580
+ await agent.execute({ topic: 'Distributed systems', depth: 'deep' });
581
+
582
+ expect(runFn).toHaveBeenCalledWith(expect.stringContaining('Distributed systems'), { maxIterations: 5 });
583
+ });
584
+ });
585
+ ```
586
+
587
+ ---
588
+
589
+ ## Test: Audit Log Plugin
590
+
591
+ ```typescript
592
+ // test/audit-log.plugin.spec.ts
593
+ import { AuditLogPlugin } from '../src/plugins/audit-log.plugin';
594
+ import type { PluginHookContext } from '@frontmcp/sdk';
595
+
596
+ describe('AuditLogPlugin', () => {
597
+ let plugin: AuditLogPlugin;
598
+
599
+ beforeEach(() => {
600
+ plugin = new AuditLogPlugin();
601
+ });
602
+
603
+ it('should record a successful tool execution', async () => {
604
+ const state = new Map<string, unknown>();
605
+ const ctx = {
606
+ toolName: 'search_docs',
607
+ session: { userId: 'user-1' },
608
+ state: { set: (k: string, v: unknown) => state.set(k, v), get: (k: string) => state.get(k) },
609
+ fetch: jest.fn(),
610
+ } as unknown as PluginHookContext;
611
+
612
+ await plugin.onToolExecuteBefore(ctx);
613
+ await plugin.onToolExecuteAfter(ctx);
614
+
615
+ const logs = plugin.getLogs();
616
+ expect(logs).toHaveLength(1);
617
+ expect(logs[0].tool).toBe('search_docs');
618
+ expect(logs[0].success).toBe(true);
619
+ expect(logs[0].userId).toBe('user-1');
620
+ expect(logs[0].duration).toBeGreaterThanOrEqual(0);
621
+ });
622
+
623
+ it('should record a failed tool execution', async () => {
624
+ const state = new Map<string, unknown>();
625
+ const ctx = {
626
+ toolName: 'ingest_document',
627
+ session: undefined,
628
+ state: { set: (k: string, v: unknown) => state.set(k, v), get: (k: string) => state.get(k) },
629
+ fetch: jest.fn(),
630
+ } as unknown as PluginHookContext;
631
+
632
+ await plugin.onToolExecuteBefore(ctx);
633
+ await plugin.onToolExecuteError(ctx);
634
+
635
+ const logs = plugin.getLogs();
636
+ expect(logs).toHaveLength(1);
637
+ expect(logs[0].success).toBe(false);
638
+ expect(logs[0].userId).toBeUndefined();
639
+ });
640
+ });
641
+ ```