mcpField
M
mcp-server-builder

mcp-server-builder

github.com/patricio0312revエージェントワークフロー63256 回インストール42 週間前に更新
リポジトリ

mcp-server-builder について

mcp-server-builderは、カスタムツールやリソースでClaudeを拡張するMCPサーバーを作る開発者向けスキルです。トランスポートの選択から実装、テスト、デプロイまでを扱います。

主な機能

  1. 01stdioとHTTP/SSEの設定
  2. 02ツールスキーマの定義と検証
  3. 03静的・動的リソースの追加
  4. 04再利用可能なプロンプトの作成
  5. 05MCP Inspectorによるテスト

活用シーン

  1. 01Claudeへのカスタムツール追加
  2. 02ファイルやデータリソースの提供
  3. 03MCPサーバーのローカルテストとデプロイ

MCP 対応

MCPクライアントにツール、リソース、プロンプトを提供するMCPサーバーの構築方法を説明します。ローカルのstdioとリモートのHTTP/SSEトランスポートを扱います。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patricio0312rev/skills --skill mcp-server-builder

インストール後、Claude Code で/mcp-server-builderを入力して読み込まれたか確認します。

詳細

MCP サーバービルダー

カスタムツールやリソースを使って Claude の機能を拡張する Model Context Protocol サーバーを作成します。

コアワークフロー

  1. 目的を定義する:追加する機能を特定します
  2. トランスポートを選択する:stdio(ローカル)または HTTP/SSE(リモート)
  3. ツールを設計する:ツールのスキーマとハンドラーを定義します
  4. リソースを追加する:ファイルやデータへのアクセス(任意)
  5. プロンプトを作成する:再利用可能なプロンプト(任意)
  6. ローカルでテストする:MCP インスペクターで検証します
  7. デプロイする:Claude Desktop または API 用に設定します

MCP アーキテクチャの概要

┌─────────────┐     MCP Protocol      ┌─────────────┐
│   Claude    │◄────────────────────►│ MCP Server  │
│   (Host)    │  JSON-RPC over stdio  │  (Your App) │
└─────────────┘                       └─────────────┘
                                            │
                                            ▼
                                      ┌───────────┐
                                      │  Tools    │
                                      │ Resources │
                                      │  Prompts  │
                                      └───────────┘

プロジェクトのセットアップ

TypeScript MCP サーバー
# Create project
mkdir my-mcp-server && cd my-mcp-server
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod

# Dev dependencies
npm install -D typescript @types/node tsx
// package.json
{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "my-mcp-server": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src/**/*"]
}

基本的なサーバー構成

// src/index.ts
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

// Create server instance
const server = new Server(
  {
    name: "my-mcp-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
      prompts: {},
    },
  }
);

// Define tools
const TOOLS = [
  {
    name: "get_weather",
    description: "Get current weather for a location",
    inputSchema: {
      type: "object" as const,
      properties: {
        location: {
          type: "string",
          description: "City name or coordinates",
        },
        units: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
          default: "celsius",
        },
      },
      required: ["location"],
    },
  },
  {
    name: "search_database",
    description: "Search the internal database",
    inputSchema: {
      type: "object" as const,
      properties: {
        query: {
          type: "string",
          description: "Search query",
        },
        limit: {
          type: "number",
          default: 10,
        },
      },
      required: ["query"],
    },
  },
];

// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools: TOOLS };
});

// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "get_weather": {
      const { location, units = "celsius" } = args as {
        location: string;
        units?: string;
      };

      // Implement your logic here
      const weather = await fetchWeather(location, units);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(weather, null, 2),
          },
        ],
      };
    }

    case "search_database": {
      const { query, limit = 10 } = args as { query: string; limit?: number };

      const results = await searchDatabase(query, limit);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(results, null, 2),
          },
        ],
      };
    }

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

// Start server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server running on stdio");
}

main().catch(console.error);

// Helper functions (implement your logic)
async function fetchWeather(location: string, units: string) {
  // Your implementation
  return { location, temperature: 22, units, condition: "sunny" };
}

async function searchDatabase(query: string, limit: number) {
  // Your implementation
  return { query, results: [], total: 0 };
}

ツール

ツールスキーマの定義
// src/tools/index.ts
import { z } from "zod";

// Define input schemas with Zod for validation
export const GetWeatherSchema = z.object({
  location: z.string().describe("City name or coordinates"),
  units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
});

export const SearchSchema = z.object({
  query: z.string().min(1).describe("Search query"),
  filters: z
    .object({
      category: z.string().optional(),
      dateFrom: z.string().optional(),
      dateTo: z.string().optional(),
    })
    .optional(),
  limit: z.number().min(1).max(100).default(10),
});

// Convert Zod schema to JSON Schema for MCP
export function zodToJsonSchema(schema: z.ZodObject<any>) {
  // Use zod-to-json-schema package in production
  return schema;
}
ツールハンドラーのパターン
// src/tools/handlers.ts
import { z } from "zod";

type ToolHandler<T extends z.ZodSchema> = (
  args: z.infer<T>
) => Promise<{ content: Array<{ type: string; text: string }> }>;

export function createToolHandler<T extends z.ZodSchema>(
  schema: T,
  handler: (args: z.infer<T>) => Promise<any>
): ToolHandler<T> {
  return async (rawArgs) => {
    // Validate input
    const args = schema.parse(rawArgs);

    // Execute handler
    const result = await handler(args);

    // Format response
    return {
      content: [
        {
          type: "text",
          text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
        },
      ],
    };
  };
}

// Usage
export const handleGetWeather = createToolHandler(
  GetWeatherSchema,
  async ({ location, units }) => {
    const response = await fetch(
      `https://api.weather.com/v1/current?location=${location}&units=${units}`
    );
    return response.json();
  }
);

リソース

静的リソース
// src/resources/index.ts
const RESOURCES = [
  {
    uri: "config://app-settings",
    name: "Application Settings",
    description: "Current application configuration",
    mimeType: "application/json",
  },
  {
    uri: "file://docs/readme",
    name: "Documentation",
    description: "Project documentation",
    mimeType: "text/markdown",
  },
];

// List resources handler
server.setRequestHandler(ListResourcesRequestSchema, async () => {
  return { resources: RESOURCES };
});

// Read resource handler
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  switch (uri) {
    case "config://app-settings":
      return {
        contents: [
          {
            uri,
            mimeType: "application/json",
            text: JSON.stringify(getAppSettings(), null, 2),
          },
        ],
      };

    case "file://docs/readme":
      const content = await fs.readFile("./README.md", "utf-8");
      return {
        contents: [
          {
            uri,
            mimeType: "text/markdown",
            text: content,
          },
        ],
      };

    default:
      throw new Error(`Unknown resource: ${uri}`);
  }
});
動的リソース
// Resources with templates (e.g., database records)
const RESOURCE_TEMPLATES = [
  {
    uriTemplate: "db://users/{userId}",
    name: "User Profile",
    description: "Get user by ID",
    mimeType: "application/json",
  },
];

server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
  return { resourceTemplates: RESOURCE_TEMPLATES };
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  // Parse URI to extract parameters
  const userMatch = uri.match(/^db:\/\/users\/(\w+)$/);
  if (userMatch) {
    const userId = userMatch[1];
    const user = await db.users.findById(userId);

    return {
      contents: [
        {
          uri,
          mimeType: "application/json",
          text: JSON.stringify(user, null, 2),
        },
      ],
    };
  }

  throw new Error(`Unknown resource: ${uri}`);
});

プロンプト

再利用可能なプロンプト
// src/prompts/index.ts
const PROMPTS = [
  {
    name: "code_review",
    description: "Review code for best practices and issues",
    arguments: [
      {
        name: "language",
        description: "Programming language",
        required: true,
      },
      {
        name: "focus",
        description: "What to focus on (security, performance, style)",
        required: false,
      },
    ],
  },
  {
    name: "explain_error",
    description: "Explain an error message and suggest fixes",
    arguments: [
      {
        name: "error",
        description: "The error message",
        required: true,
      },
      {
        name: "context",
        description: "Additional context about what you were doing",
        required: false,
      },
    ],
  },
];

// List prompts handler
server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return { prompts: PROMPTS };
});

// Get prompt handler
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "code_review":
      return {
        description: "Code review prompt",
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `Please review the following ${args?.language || "code"} code.
${args?.focus ? `Focus particularly on: ${args.focus}` : ""}

Look for:
- Bugs and potential issues
- Security vulnerabilities
- Performance improvements
- Code style and best practices
- Suggestions for improvement`,
            },
          },
        ],
      };

    case "explain_error":
      return {
        description: "Error explanation prompt",
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `I encountered this error:

\`\`\`
${args?.error}
\`\`\`

${args?.context ? `Context: ${args.context}` : ""}

Please explain:
1. What this error means
2. Common causes
3. How to fix it
4. How to prevent it in the future`,
            },
          },
        ],
      };

    default:
      throw new Error(`Unknown prompt: ${name}`);
  }
});

エラー処理

// src/utils/errors.ts
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

export class ToolError extends Error {
  constructor(
    message: string,
    public code: ErrorCode = ErrorCode.InternalError
  ) {
    super(message);
    this.name = "ToolError";
  }

  toMcpError(): McpError {
    return new McpError(this.code, this.message);
  }
}

// Usage in handlers
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;
    // ... handle tool
  } catch (error) {
    if (error instanceof ToolError) {
      throw error.toMcpError();
    }
    if (error instanceof z.ZodError) {
      throw new McpError(
        ErrorCode.InvalidParams,
        `Invalid parameters: ${error.errors.map((e) => e.message).join(", ")}`
      );
    }
    throw new McpError(
      ErrorCode.InternalError,
      error instanceof Error ? error.message : "Unknown error"
    );
  }
});

Claude Desktop の設定

// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/dist/index.js"],
      "env": {
        "API_KEY": "your-api-key"
      }
    },
    "npx-server": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
    }
  }
}

HTTP/SSE トランスポート

// src/http-server.ts
import express from "express";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const app = express();
const server = new Server(/* ... */);

// SSE endpoint for MCP
app.get("/mcp", async (req, res) => {
  const transport = new SSEServerTransport("/mcp/messages", res);
  await server.connect(transport);
});

// Message endpoint
app.post("/mcp/messages", express.json(), async (req, res) => {
  // Handle incoming messages
});

app.listen(3000, () => {
  console.log("MCP HTTP server running on port 3000");
});

テスト

MCP インスペクター
# Install MCP inspector
npx @modelcontextprotocol/inspector

# Run your server with inspector
npx @modelcontextprotocol/inspector node dist/index.js
単体テスト
// tests/tools.test.ts
import { describe, it, expect } from "vitest";
import { handleGetWeather } from "../src/tools/handlers";

describe("get_weather tool", () => {
  it("returns weather data for valid location", async () => {
    const result = await handleGetWeather({
      location: "New York",
      units: "celsius",
    });

    expect(result.content[0].type).toBe("text");
    const data = JSON.parse(result.content[0].text);
    expect(data).toHaveProperty("temperature");
  });

  it("throws error for invalid location", async () => {
    await expect(
      handleGetWeather({ location: "", units: "celsius" })
    ).rejects.toThrow();
  });
});

完全な例:GitHub MCP サーバー

// src/index.ts
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

const server = new Server(
  { name: "github-mcp", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

const TOOLS = [
  {
    name: "list_issues",
    description: "List issues in a GitHub repository",
    inputSchema: {
      type: "object" as const,
      properties: {
        owner: { type: "string", description: "Repository owner" },
        repo: { type: "string", description: "Repository name" },
        state: { type: "string", enum: ["open", "closed", "all"], default: "open" },
      },
      required: ["owner", "repo"],
    },
  },
  {
    name: "create_issue",
    description: "Create a new issue",
    inputSchema: {
      type: "object" as const,
      properties: {
        owner: { type: "string" },
        repo: { type: "string" },
        title: { type: "string" },
        body: { type: "string" },
        labels: { type: "array", items: { type: "string" } },
      },
      required: ["owner", "repo", "title"],
    },
  },
];

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "list_issues": {
      const { owner, repo, state = "open" } = args as any;
      const { data } = await octokit.issues.listForRepo({ owner, repo, state });

      return {
        content: [{
          type: "text",
          text: JSON.stringify(
            data.map((i) => ({ number: i.number, title: i.title, state: i.state })),
            null,
            2
          ),
        }],
      };
    }

    case "create_issue": {
      const { owner, repo, title, body, labels } = args as any;
      const { data } = await octokit.issues.create({ owner, repo, title, body, labels });

      return {
        content: [{
          type: "text",
          text: `Created issue #${data.number}: ${data.html_url}`,
        }],
      };
    }

    default:
      throw new Error(`Unknown tool: ${name}`);
  }
});

const transport = new StdioServerTransport();
server.connect(transport);

ベストプラクティス

  1. 入力を検証する:スキーマ検証には Zod などを使用します
  2. エラーを適切に処理する:意味のあるエラーメッセージを返します
  3. 環境変数を使用する:シークレットをハードコードしないでください
  4. 外部呼び出しをレート制限する:API の制限を守ります
  5. 適切にログを記録する:ログには stderr を使用します(stdout は MCP 用です)
  6. ツールを適切に文書化する:明確な説明によって Claude が正しくツールを使用できます
  7. ツールの目的を絞る:各ツールの責務を単一にします
  8. インスペクターでテストする:デプロイ前に動作を検証します

出力チェックリスト

すべての MCP サーバーに含める項目:

  • 名前とバージョンを含むサーバー
  • ケイパビリティの宣言(ツール/リソース/プロンプト)
  • 説明付きのツールスキーマ
  • 明確なエラーメッセージを含む入力検証
  • 適切なエラー処理と MCP エラーコード
  • 環境変数の設定
  • Claude Desktop の設定例
  • MCP インスペクターによるテスト
  • ハンドラーの単体テスト
  • セットアップ手順を記載した README

コメント