Files
skills/plugins/suaveplan-mcp-builder/skills/mcp-builder/SKILL.md
admin 9bf56a806b Move docs/ package mirror into document-navigator plugin
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>
2026-05-30 01:10:00 +01:00

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 .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>