Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"name": "suaveplan-mcp-builder",
"version": "1.0.0",
"description": "Create MCP servers exposing Suaveplan plugin functionality"
}

View File

@@ -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>`