mcp-server-development

This skill provides comprehensive guidance for building robust MCP servers, with specific focus on the unity-mcp-server architecture and patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "mcp-server-development" with this command: npx skills add akiojin/llmlb/akiojin-llmlb-mcp-server-development

This skill provides comprehensive guidance for building robust MCP servers, with specific focus on the unity-mcp-server architecture and patterns.

Core Philosophy

MCP servers bridge AI assistants to external systems. They must be:

  • Reliable: Handle errors gracefully, never crash unexpectedly

  • Discoverable: Tools should have clear, self-documenting schemas

  • Performant: Minimize latency, especially for stdio transport

  • Protocol-compliant: Follow JSON-RPC 2.0 and MCP spec exactly

Architecture Patterns

Handler-Based Design

ALWAYS use a handler class per tool. Each handler encapsulates:

  • Input validation (Zod schema)

  • Business logic execution

  • Error handling and response formatting

// Pattern: One handler per tool export class SystemPingToolHandler extends BaseToolHandler { constructor(unityConnection) { super({ name: 'system_ping', description: 'Check Unity Editor connectivity', inputSchema: { type: 'object', properties: {} } }); this.unityConnection = unityConnection; }

async execute(params) { const result = await this.unityConnection.send({ command: 'ping' }); return { status: 'ok', ...result }; } }

BaseToolHandler Contract

All handlers MUST:

  • Call super() with tool metadata

  • Implement execute(params) method

  • Return plain objects (framework handles JSON-RPC wrapping)

  • Throw errors with descriptive messages

Input Validation with Zod

ALWAYS validate inputs before processing:

import { z } from 'zod';

const inputSchema = z.object({ name: z.string().min(1).describe('GameObject name'), primitiveType: z.enum(['cube', 'sphere', 'cylinder']).optional() });

validate(input) { return inputSchema.parse(input); }

Transport Layer

Content-Length Framing (Standard)

MCP uses LSP-style Content-Length framing for stdio:

Content-Length: 123\r\n \r\n {"jsonrpc":"2.0","id":1,"method":"tools/call"...}

ALWAYS use Content-Length for output. NEVER mix framing formats in a session.

Hybrid Input (Compatibility)

Accept both Content-Length and NDJSON input for client compatibility:

  • Claude Desktop: Content-Length

  • Some CLI tools: NDJSON (newline-delimited)

Error Handling

Structured Errors

Use MCP error codes from the spec:

const McpError = { ParseError: -32700, InvalidRequest: -32600, MethodNotFound: -32601, InvalidParams: -32602, InternalError: -32603 };

throw new Error(JSON.stringify({ code: McpError.InvalidParams, message: 'primitiveType must be one of: cube, sphere, cylinder' }));

Error Response Format

{ jsonrpc: '2.0', id: requestId, error: { code: -32602, message: 'Invalid params', data: { field: 'name', issue: 'required' } } }

Unity-Specific Patterns

Command Protocol

Unity communication uses a simple request/response pattern:

const result = await this.unityConnection.send({ command: 'gameobject_create', params: { name: 'Cube', primitiveType: 'cube' } });

Workspace Root Resolution

ALWAYS include workspaceRoot in commands requiring file paths:

execute(params) { return this.unityConnection.send({ command: 'screenshot_capture', workspaceRoot: config.workspaceRoot, ...params }); }

Testing Patterns

TDD for Handlers

  • Contract test first: Define expected input/output schema

  • Mock Unity connection: Isolate handler logic

  • Test error paths: Invalid input, connection failures

  • Test happy path last: After contracts are verified

describe('CreateGameObjectHandler', () => { it('validates primitiveType enum', async () => { const handler = new CreateGameObjectHandler(mockConnection); await assert.rejects( () => handler.handle({ name: 'Cube', primitiveType: 'invalid' }), /primitiveType must be one of/ ); }); });

Integration Testing

Test full request/response cycle including JSON-RPC framing:

const stdin = new PassThrough(); const stdout = new PassThrough(); const transport = new HybridStdioServerTransport(stdin, stdout);

stdin.write('Content-Length: 50\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"ping"}'); // Assert stdout contains Content-Length response

Common Mistakes

Mixing framing formats:

  • NEVER: Output NDJSON after receiving Content-Length input

  • ALWAYS: Output Content-Length regardless of input format

Swallowing errors:

  • NEVER: catch (e) { return null; }

  • ALWAYS: Propagate errors with context

Missing validation:

  • NEVER: Trust raw input from clients

  • ALWAYS: Validate with Zod before processing

Blocking stdio:

  • NEVER: Synchronous operations on transport

  • ALWAYS: Use async/await throughout

Handler Registration

Register all handlers in a central index:

// src/handlers/index.js export function createHandlers(unityConnection) { return [ new SystemPingToolHandler(unityConnection), new CreateGameObjectHandler(unityConnection), new ScreenshotHandler(unityConnection), // ... ]; }

Remember

  • Content-Length always: Output framing must be consistent

  • Validate everything: Never trust client input

  • One handler, one tool: Keep handlers focused

  • Test error paths: Most bugs hide in error handling

  • Protocol compliance: Follow JSON-RPC 2.0 exactly

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

llmlb-cli-usage

No summary provided by upstream source.

Repository SourceNeeds Review
General

drawio

No summary provided by upstream source.

Repository SourceNeeds Review
General

writing hookify rules

No summary provided by upstream source.

Repository SourceNeeds Review