Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
name, description
| name | description |
|---|---|
| mcp-builder | 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.
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:
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:
packages/api-stack/api/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
// 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
// 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
// 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:
-- 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;
-- 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:
{
"files": ["dist", "surql"]
}
Hono Route Handlers
// 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:
// packages/api-stack/api/api/src/plugin/plugin-registry.ts
import { myPlugin } from '@suaveplan/api-plugin-my-name';
registry.register(myPlugin);
Testing Patterns
Plugin Lifecycle Tests
// 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
// 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
.tsimplementation file needs a matching.mdfile with 200+ words, simple + advanced examples filesin package.json must include"surql"when migrations exist- Plugin naming:
@suaveplan/api-plugin-<name>for packages,<name>for the pluginnamefield - Migration files must be numbered sequentially:
001-,002-, etc.
Checklist
- Plugin defined with
definePlugin()and proper lifecycle hooks - Zod config schema with sensible defaults
- SurrealDB migrations in
surql/directory - Hono routes for HTTP API
- MCP adapter wrapping plugin operations as tools
- Tests for plugin lifecycle, routes, and MCP tools
- Documentation for every implementation file
- Registered in plugin registry
package.jsonincludes"surql"in files array- Lint clean:
bun run lint --filter=@suaveplan/api-plugin-<name>