Initial Commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "suaveplan-mcp-builder",
|
||||
"version": "1.0.0",
|
||||
"description": "Create MCP servers exposing Suaveplan plugin functionality"
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
---
|
||||
name: mcp-builder
|
||||
description: Build MCP (Model Context Protocol) servers that expose Suaveplan plugin functionality using TypeScript, Hono, SurrealDB, and the definePlugin() architecture
|
||||
---
|
||||
|
||||
# MCP Builder for Suaveplan
|
||||
|
||||
Build MCP servers that expose Suaveplan's plugin architecture as tool endpoints for AI agents. This skill bridges the Suaveplan API plugin system with the Model Context Protocol, allowing plugins to serve both HTTP routes and MCP tool interfaces from the same codebase.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Suaveplan's server-side plugin system (`@suaveplan/api`) provides the foundation. An MCP server wraps one or more plugins, exposing their operations as MCP tools, resources, and prompts.
|
||||
|
||||
```text
|
||||
Suaveplan Plugin (definePlugin)
|
||||
|
|
||||
+-- Hono routes (HTTP API)
|
||||
|
|
||||
+-- MCP Server adapter
|
||||
|
|
||||
+-- Tools (operations)
|
||||
+-- Resources (data endpoints)
|
||||
+-- Prompts (templated interactions)
|
||||
```
|
||||
|
||||
### Core Technologies
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Plugin Framework | `definePlugin()` from `@suaveplan/api` |
|
||||
| HTTP Router | Hono |
|
||||
| Database | SurrealDB with `.surql` migration files |
|
||||
| Schema Validation | Zod |
|
||||
| Runtime | Bun |
|
||||
| MCP Protocol | `@modelcontextprotocol/sdk` |
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
Every Suaveplan plugin follows this lifecycle:
|
||||
|
||||
```text
|
||||
onRegister -> onInit -> onReady -> (running) -> onClose
|
||||
```
|
||||
|
||||
- **onRegister**: Plugin is registered in the plugin registry. Validate configuration.
|
||||
- **onInit**: Initialize resources, connect to databases, run migrations.
|
||||
- **onReady**: Mount Hono routes, start serving. The MCP adapter binds here.
|
||||
- **onClose**: Cleanup resources, close connections.
|
||||
|
||||
## File Structure
|
||||
|
||||
An MCP-enabled Suaveplan plugin follows this layout:
|
||||
|
||||
```text
|
||||
packages/server/api-plugin-<name>/
|
||||
src/
|
||||
plugin.ts # Plugin definition with definePlugin()
|
||||
plugin.test.ts # Plugin lifecycle tests
|
||||
plugin.types.ts # Configuration and type definitions
|
||||
plugin.md # Plugin documentation
|
||||
mcp/
|
||||
mcp-adapter.ts # MCP server adapter
|
||||
mcp-adapter.test.ts # MCP adapter tests
|
||||
mcp-adapter.types.ts # MCP-specific types
|
||||
mcp-adapter.md # MCP adapter documentation
|
||||
tools/
|
||||
<tool-name>.ts # Individual tool implementations
|
||||
<tool-name>.test.ts # Tool tests
|
||||
<tool-name>.md # Tool documentation
|
||||
resources/
|
||||
<resource-name>.ts # Resource implementations
|
||||
prompts/
|
||||
<prompt-name>.ts # Prompt templates
|
||||
routes/
|
||||
<route>.ts # Hono route handlers
|
||||
<route>.test.ts # Route tests
|
||||
<route>.md # Route documentation
|
||||
surql/
|
||||
001-initial-schema.surql # SurrealDB migration files
|
||||
002-add-indexes.surql # Sequential migrations
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
```
|
||||
|
||||
## Creating a Plugin with definePlugin()
|
||||
|
||||
### Basic Plugin Definition
|
||||
|
||||
```typescript
|
||||
// src/plugin.ts
|
||||
import { definePlugin } from '@suaveplan/api';
|
||||
import z from 'zod';
|
||||
import type { MyPluginConfig } from './plugin.types';
|
||||
|
||||
const ConfigSchema = z.object({
|
||||
apiKey: z.string().min(1),
|
||||
maxRetries: z.number().int().positive().default(3),
|
||||
enableMcp: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const myPlugin = definePlugin<MyPluginConfig>({
|
||||
name: 'my-plugin',
|
||||
version: '1.0.0',
|
||||
description: 'Description of what this plugin does',
|
||||
dependencies: ['database'], // Required plugins
|
||||
optionalDependencies: ['auth'], // Optional plugins
|
||||
migrationsPath: './surql', // SurrealDB migration directory
|
||||
tags: ['mcp', 'custom'],
|
||||
|
||||
configSchema: {
|
||||
schema: ConfigSchema,
|
||||
defaults: {
|
||||
apiKey: '',
|
||||
maxRetries: 3,
|
||||
enableMcp: true,
|
||||
},
|
||||
},
|
||||
|
||||
async onInit(ctx) {
|
||||
// Run after dependencies are initialized
|
||||
// Access config: ctx.config
|
||||
// Access database: ctx.db
|
||||
// Run migrations automatically from migrationsPath
|
||||
},
|
||||
|
||||
async onReady(ctx) {
|
||||
// Mount Hono routes
|
||||
ctx.app.route('/my-plugin', createRoutes(ctx));
|
||||
|
||||
// Initialize MCP adapter if enabled
|
||||
if (ctx.config.enableMcp) {
|
||||
const mcpServer = createMcpAdapter(ctx);
|
||||
ctx.app.route('/mcp/my-plugin', mcpServer);
|
||||
}
|
||||
},
|
||||
|
||||
async onClose(ctx) {
|
||||
// Cleanup resources
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Types Definition
|
||||
|
||||
```typescript
|
||||
// src/plugin.types.ts
|
||||
import type { z } from 'zod';
|
||||
|
||||
export interface MyPluginConfig {
|
||||
apiKey: string;
|
||||
maxRetries: number;
|
||||
enableMcp: boolean;
|
||||
}
|
||||
|
||||
export interface MyPluginContext {
|
||||
config: MyPluginConfig;
|
||||
db: SurrealDB;
|
||||
app: Hono;
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Adapter Pattern
|
||||
|
||||
### Wrapping a Plugin as an MCP Server
|
||||
|
||||
```typescript
|
||||
// src/mcp/mcp-adapter.ts
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import z from 'zod';
|
||||
import type { MyPluginContext } from '../plugin.types';
|
||||
|
||||
export function createMcpAdapter(ctx: MyPluginContext): McpServer {
|
||||
const server = new McpServer({
|
||||
name: 'suaveplan-my-plugin',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Register tools
|
||||
server.tool(
|
||||
'create_item',
|
||||
'Create a new item in the system',
|
||||
{
|
||||
name: z.string().describe('Item name'),
|
||||
category: z.enum(['a', 'b', 'c']).describe('Item category'),
|
||||
metadata: z.record(z.string()).optional().describe('Optional metadata'),
|
||||
},
|
||||
async ({ name, category, metadata }) => {
|
||||
const result = await ctx.db.create('item', { name, category, metadata });
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.tool(
|
||||
'query_items',
|
||||
'Search items with filters',
|
||||
{
|
||||
category: z.enum(['a', 'b', 'c']).optional(),
|
||||
limit: z.number().int().positive().default(10),
|
||||
},
|
||||
async ({ category, limit }) => {
|
||||
const query = category
|
||||
? `SELECT * FROM item WHERE category = $category LIMIT $limit`
|
||||
: `SELECT * FROM item LIMIT $limit`;
|
||||
const results = await ctx.db.query(query, { category, limit });
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Register resources
|
||||
server.resource(
|
||||
'item_schema',
|
||||
'item://schema',
|
||||
async (uri) => ({
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
category: { type: 'string', enum: ['a', 'b', 'c'] },
|
||||
},
|
||||
}),
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
// Register prompts
|
||||
server.prompt(
|
||||
'summarize_items',
|
||||
'Generate a summary of items by category',
|
||||
{ category: z.string().optional() },
|
||||
({ category }) => ({
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: category
|
||||
? `Summarize all items in category "${category}"`
|
||||
: 'Summarize all items across all categories',
|
||||
},
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
```
|
||||
|
||||
## SurrealDB Migrations
|
||||
|
||||
Migration files live in `surql/` and are executed in alphabetical order during `onInit`:
|
||||
|
||||
```sql
|
||||
-- surql/001-initial-schema.surql
|
||||
DEFINE TABLE item SCHEMAFULL;
|
||||
DEFINE FIELD name ON item TYPE string ASSERT $value != NONE;
|
||||
DEFINE FIELD category ON item TYPE string ASSERT $value INSIDE ['a', 'b', 'c'];
|
||||
DEFINE FIELD metadata ON item FLEXIBLE TYPE option<object>;
|
||||
DEFINE FIELD created_at ON item TYPE datetime DEFAULT time::now();
|
||||
DEFINE FIELD updated_at ON item TYPE datetime DEFAULT time::now();
|
||||
|
||||
DEFINE INDEX idx_item_category ON item FIELDS category;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- surql/002-add-full-text.surql
|
||||
DEFINE ANALYZER item_analyzer TOKENIZERS blank, class FILTERS lowercase, ascii;
|
||||
DEFINE INDEX idx_item_name_search ON item FIELDS name SEARCH ANALYZER item_analyzer BM25;
|
||||
```
|
||||
|
||||
**Critical**: The `package.json` `files` array MUST include `"surql"` for migrations to work when the plugin is published:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": ["dist", "surql"]
|
||||
}
|
||||
```
|
||||
|
||||
## Hono Route Handlers
|
||||
|
||||
```typescript
|
||||
// src/routes/items.ts
|
||||
import { Hono } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import z from 'zod';
|
||||
import type { MyPluginContext } from '../plugin.types';
|
||||
|
||||
const CreateItemSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
category: z.enum(['a', 'b', 'c']),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export function createItemRoutes(ctx: MyPluginContext) {
|
||||
const app = new Hono();
|
||||
|
||||
app.post('/', zValidator('json', CreateItemSchema), async (c) => {
|
||||
const body = c.req.valid('json');
|
||||
const result = await ctx.db.create('item', body);
|
||||
return c.json(result, 201);
|
||||
});
|
||||
|
||||
app.get('/', async (c) => {
|
||||
const items = await ctx.db.select('item');
|
||||
return c.json(items);
|
||||
});
|
||||
|
||||
app.get('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const item = await ctx.db.select(`item:${id}`);
|
||||
return c.json(item);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
Register the plugin in the plugin registry:
|
||||
|
||||
```typescript
|
||||
// packages/server/api/src/plugin/plugin-registry.ts
|
||||
import { myPlugin } from '@suaveplan/api-plugin-my-name';
|
||||
|
||||
registry.register(myPlugin);
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Plugin Lifecycle Tests
|
||||
|
||||
```typescript
|
||||
// src/plugin.test.ts
|
||||
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
|
||||
import { myPlugin } from './plugin';
|
||||
|
||||
describe('MyPlugin', () => {
|
||||
it('has correct metadata', () => {
|
||||
expect(myPlugin.name).toBe('my-plugin');
|
||||
expect(myPlugin.version).toBe('1.0.0');
|
||||
expect(myPlugin.dependencies).toContain('database');
|
||||
});
|
||||
|
||||
it('validates config schema', () => {
|
||||
const result = myPlugin.configSchema!.schema.safeParse({
|
||||
apiKey: 'test-key',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects invalid config', () => {
|
||||
const result = myPlugin.configSchema!.schema.safeParse({
|
||||
apiKey: '',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### MCP Adapter Tests
|
||||
|
||||
```typescript
|
||||
// src/mcp/mcp-adapter.test.ts
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createMcpAdapter } from './mcp-adapter';
|
||||
|
||||
describe('MCP Adapter', () => {
|
||||
it('registers expected tools', () => {
|
||||
const adapter = createMcpAdapter(mockContext);
|
||||
const tools = adapter.listTools();
|
||||
expect(tools).toContainEqual(
|
||||
expect.objectContaining({ name: 'create_item' })
|
||||
);
|
||||
expect(tools).toContainEqual(
|
||||
expect.objectContaining({ name: 'query_items' })
|
||||
);
|
||||
});
|
||||
|
||||
it('create_item tool validates input', async () => {
|
||||
const adapter = createMcpAdapter(mockContext);
|
||||
const result = await adapter.callTool('create_item', {
|
||||
name: 'Test Item',
|
||||
category: 'a',
|
||||
});
|
||||
expect(result.content[0].text).toContain('Test Item');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Max 250 lines** per implementation file
|
||||
- **Max 500 lines** per test file
|
||||
- **95% minimum test coverage** via `bun run test:coverage`
|
||||
- **No mocks** -- use real SurrealDB instances for integration tests
|
||||
- **Zod schemas** for all validation (already in monorepo, do not add Joi or Yup)
|
||||
- **TypeScript only** -- no JavaScript files
|
||||
- **Biome linting** -- not ESLint
|
||||
- **Documentation** -- every `.ts` implementation file needs a matching `.md` file with 200+ words, simple + advanced examples
|
||||
- **`files` in package.json** must include `"surql"` when migrations exist
|
||||
- **Plugin naming**: `@suaveplan/api-plugin-<name>` for packages, `<name>` for the plugin `name` field
|
||||
- Migration files must be numbered sequentially: `001-`, `002-`, etc.
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Plugin defined with `definePlugin()` and proper lifecycle hooks
|
||||
2. Zod config schema with sensible defaults
|
||||
3. SurrealDB migrations in `surql/` directory
|
||||
4. Hono routes for HTTP API
|
||||
5. MCP adapter wrapping plugin operations as tools
|
||||
6. Tests for plugin lifecycle, routes, and MCP tools
|
||||
7. Documentation for every implementation file
|
||||
8. Registered in plugin registry
|
||||
9. `package.json` includes `"surql"` in files array
|
||||
10. Lint clean: `bun run lint --filter=@suaveplan/api-plugin-<name>`
|
||||
Reference in New Issue
Block a user