@frontmcp/skills 0.0.1 → 1.0.0-beta.9

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