Build production-grade MCP (Model Context Protocol) servers with observability, correlation ID tracing, and dual logging. Use when creating new MCP servers, adding tools to existing servers, implementing file logging, debugging MCP issues, wrapping CLI tools with spawnSyncCollect, or following Side Quest marketplace patterns. Covers @sidequest/core/mcp declarative API, @sidequest/core/spawn CLI wrapper patterns, Zod schemas, Bun runtime, and 9 gold standard patterns validated across Kit plugin (18 tools). Includes error handling, response format switching, MCP annotations, and graceful degradation.
Build production MCP servers with observability, tracing, and dual logging. Use when creating new MCP servers, adding tools, implementing file logging, debugging MCP issues, or wrapping CLI tools.
/plugin marketplace add nathanvale/side-quest-marketplace/plugin install bookmarks@side-quest-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/core-mcp-api.mdreferences/error-handling.mdreferences/gold-standard-patterns.mdreferences/kit-case-study.mdreferences/mcp-protocol.mdBuild production-grade MCP servers using @sidequest/core/mcp and Bun.
/plugin-template:create my-plugin
cd plugins/my-plugin
Create mcp/index.ts:
#!/usr/bin/env bun
import { createCorrelationId, log, startServer, tool, z } from "@sidequest/core/mcp";
tool("hello", {
description: "A simple greeting tool",
inputSchema: {
name: z.string().describe("Name to greet"),
response_format: z.enum(["markdown", "json"]).optional()
.describe("Output format: 'markdown' (default) or 'json'"),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async (args: Record<string, unknown>) => {
const { name, response_format } = args as { name: string; response_format?: string };
const cid = createCorrelationId();
log.info({ cid, tool: "hello", args: { name } }, "greeting");
const greeting = `Hello, ${name}!`;
const text = response_format === "json" ? JSON.stringify({ greeting }) : `# Hello\n\n${greeting}`;
log.info({ cid, tool: "hello", success: true }, "greeting");
return { content: [{ type: "text" as const, text }] };
});
startServer("my-plugin", {
version: "1.0.0",
fileLogging: { enabled: true, subsystems: ["greeting"], level: "debug" },
});
Create .mcp.json:
{
"mcpServers": {
"my-plugin": {
"command": "bun",
"args": ["run", "${CLAUDE_PLUGIN_ROOT}/mcp/index.ts"]
}
}
}
Test: bun run mcp/index.ts
Every handler follows this pattern:
async (args: Record<string, unknown>) => {
const { query, response_format } = args as { query: string; response_format?: string };
// 1. Generate correlation ID
const cid = createCorrelationId();
const startTime = Date.now();
// 2. Log request
log.info({ cid, tool: "my_tool", args: { query } }, "search");
try {
// 3. Execute business logic
const result = await doSomething(query);
// 4. Format output
const text = response_format === "json" ? JSON.stringify(result) : formatAsMarkdown(result);
// 5. Log response
log.info({ cid, tool: "my_tool", success: true, durationMs: Date.now() - startTime }, "search");
// 6. Return MCP response
return { content: [{ type: "text" as const, text }] };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log.error({ cid, tool: "my_tool", error: errorMessage }, "search");
return {
content: [{ type: "text" as const, text: JSON.stringify({ error: errorMessage, hint: "Recovery action", isError: true }) }],
isError: true
};
}
}
9 production-validated patterns from Kit plugin. See @./references/gold-standard-patterns.md for details.
| # | Pattern | Purpose |
|---|---|---|
| 1 | Declarative Tool Registration | Type-safe Zod schemas + MCP annotations |
| 2 | Correlation ID Tracing | Request tracking across logs |
| 3 | Structured Error Responses | { error, hint, isError: true } |
| 4 | Response Format Switching | Markdown (default) + JSON |
| 5 | File Logging Configuration | Subsystem-based hierarchical logs |
| 6 | MCP Annotations | readOnlyHint, destructiveHint, etc. |
| 7 | No Nested Package.json | Avoid MCP discovery failures |
| 8 | Dual Logging | MCP protocol + file persistence |
| 9 | Separation of Concerns | Business logic vs MCP layer |
tool("my_tool", {
description: "What this tool does",
inputSchema: {
query: z.string().describe("Search query"),
limit: z.number().optional().describe("Max results"),
response_format: z.enum(["markdown", "json"]).optional()
.describe("Output format: 'markdown' (default) or 'json'"),
},
annotations: {
readOnlyHint: true, // No side effects
destructiveHint: false, // Doesn't delete data
idempotentHint: true, // Safe to retry
openWorldHint: false, // Local only
},
}, handler);
startServer("my-plugin", {
version: "1.0.0",
fileLogging: {
enabled: true,
subsystems: ["search", "index", "api"],
level: "debug", // "debug" | "info" | "warning" | "error"
maxSize: 10_000_000, // 10MB
maxFiles: 5,
},
});
Logs: ~/.claude/logs/<plugin>.jsonl
View: tail -f ~/.claude/logs/my-plugin.jsonl | jq
Filter: jq 'select(.cid == "abc123")'
For tools wrapping external CLIs:
import { buildEnhancedPath, spawnSyncCollect } from "@sidequest/core/spawn";
const result = spawnSyncCollect(
["bun", "run", `${pluginRoot}/src/cli.ts`, "search", query],
{ env: { PATH: buildEnhancedPath() } }
);
return {
...(result.exitCode !== 0 ? { isError: true } : {}),
content: [{ type: "text" as const, text: result.exitCode === 0 ? result.stdout : result.stderr }],
};
For detailed error patterns, see @./references/error-handling.md.
Quick pattern:
enum ToolError {
InvalidInput = "INVALID_INPUT",
NotFound = "NOT_FOUND",
Timeout = "TIMEOUT",
InternalError = "INTERNAL_ERROR",
}
// Return structured error
return {
content: [{ type: "text", text: JSON.stringify({ error: msg, errorType, hint, isError: true }) }],
isError: true
};
Type guard:
type Result<T> = { ok: true; data: T } | { ok: false; error: string; hint?: string };
const isSuccess = <T>(r: Result<T>): r is { ok: true; data: T } => r.ok;
For complete examples, see @./references/core-mcp-api.md.
Graceful degradation:
try {
return await semanticSearch(query);
} catch (error) {
if (error.message.includes("not available")) {
log.warning({ cid, fallback: "grep" }, "search");
return await grepSearch(query);
}
throw error;
}
Timeout:
const withTimeout = <T>(promise: Promise<T>, ms: number) =>
Promise.race([promise, new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))]);
ResponseFormat enum:
enum ResponseFormat { MARKDOWN = "markdown", JSON = "json" }
const format = response_format === "json" ? ResponseFormat.JSON : ResponseFormat.MARKDOWN;
import { describe, expect, test } from "bun:test";
describe("my-server", () => {
test("executes tool", async () => {
const result = await callTool("mcp__my-plugin_my-server__hello", { name: "Nathan" });
expect(result.content[0].text).toContain("Hello");
});
test("handles errors", async () => {
const result = await callTool("mcp__my-plugin_my-server__search", { query: "" });
expect(result.isError).toBe(true);
});
});
| Problem | Solution |
|---|---|
| Server won't start | bun run mcp/index.ts to check errors |
| Tool not appearing | Check .mcp.json path, verify tool name pattern |
| Handler crashes | Add try/catch, log args before processing |
| Nested package.json | Delete it — breaks MCP discovery |
Essential:
API:
@sidequest/core/mcp + @sidequest/core/spawnCLI:
/plugin-template:create — Generate plugin scaffold/review-mcp — Validate against checklisttool, startServer, log, createCorrelationId, z from @sidequest/core/mcplog.info(data, "subsystem"){ error, hint, isError: true }~/.claude/logs/<plugin>.jsonlProduction example: @../../kit/CLAUDE.md
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.