build-custom-mcp-server
について
このスキルは、開発者がドメイン固有のツールをAIアシスタントに公開するためのカスタムMCPサーバー構築をガイドします。Node.jsまたはRでの実装を網羅し、ツール定義、トランスポート設定、Claude Codeを用いたテスト方法を解説します。標準ツールを超えた専門的な統合が必要な場合や、既存のAPIをMCPサービスとしてラップする際にご活用ください。
クイックインストール
Claude Code
推奨npx skills add pjt222/agent-almanac -a claude-code/plugin add https://github.com/pjt222/agent-almanacgit clone https://github.com/pjt222/agent-almanac.git ~/.claude/skills/build-custom-mcp-serverこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストールします
ドキュメント
Build Custom MCP Server
Create custom MCP server exposing domain-specific tools to AI assistants.
When Use
- Need to expose custom functionality to Claude Code or Claude Desktop
- Building specialized tools beyond what mcptools provides
- Creating domain-specific AI assistant integration
- Wrapping existing APIs or services as MCP tools
Inputs
- Required: List of tools to expose (name, description, parameters, behavior)
- Required: Implementation language (Node.js or R)
- Required: Transport type (stdio or HTTP)
- Optional: Authentication requirements
- Optional: Docker packaging needs
Steps
Step 1: Define Tool Specifications
Before writing code, define each tool:
tools:
- name: query_database
description: Execute a read-only SQL query against the analysis database
parameters:
query:
type: string
description: SQL SELECT query to execute
required: true
limit:
type: integer
description: Maximum rows to return
default: 100
returns: JSON array of result rows
- name: run_analysis
description: Execute a predefined statistical analysis by name
parameters:
analysis_name:
type: string
description: Name of the analysis to run
enum: [descriptive, regression, survival]
dataset:
type: string
description: Dataset identifier
required: true
Got: YAML or markdown spec for each tool with name, description, parameters (types, defaults, required flags), return type documented before writing code.
If fail: Tool specifications unclear? Interview domain expert or review existing API documentation for parameter types and return formats.
Step 2: Implement in Node.js (Using MCP SDK)
// server.js
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "my-analysis-server",
version: "1.0.0",
});
// Define tools
server.tool(
"query_database",
"Execute a read-only SQL query against the analysis database",
{
query: z.string().describe("SQL SELECT query"),
limit: z.number().default(100).describe("Max rows to return"),
},
async ({ query, limit }) => {
// Validate read-only
if (!/^\s*SELECT/i.test(query)) {
return {
content: [{ type: "text", text: "Error: Only SELECT queries allowed" }],
isError: true,
};
}
const results = await executeQuery(query, limit);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
server.tool(
"run_analysis",
"Execute a predefined statistical analysis",
{
analysis_name: z.enum(["descriptive", "regression", "survival"]),
dataset: z.string().describe("Dataset identifier"),
},
async ({ analysis_name, dataset }) => {
const result = await runAnalysis(analysis_name, dataset);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
}
);
// Start server with stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
Got: Working server.js imports MCP SDK, defines tools with Zod schemas, connects via stdio transport. Running node server.js starts server without errors.
If fail: Verify @modelcontextprotocol/sdk and zod installed (npm install). Check import paths match SDK version (SDK reorganized exports between versions).
Step 3: Implement in R (Using mcptools)
# server.R
library(mcptools)
# Register custom tools
mcp_tool(
name = "query_database",
description = "Execute a read-only SQL query",
parameters = list(
query = list(type = "string", description = "SQL SELECT query"),
limit = list(type = "integer", description = "Max rows", default = 100)
),
handler = function(query, limit = 100) {
if (!grepl("^\\s*SELECT", query, ignore.case = TRUE)) {
stop("Only SELECT queries allowed")
}
result <- DBI::dbGetQuery(con, paste(query, "LIMIT", limit))
jsonlite::toJSON(result, auto_unbox = TRUE)
}
)
# Start server
mcptools::mcp_server()
Got: Working server.R registers custom tools with mcp_tool(), starts server with mcp_server(). Running Rscript server.R starts MCP server.
If fail: Ensure mcptools installed from GitHub (remotes::install_github("posit-dev/mcptools")). Check handler function signatures match parameter definitions.
Step 4: Set Up Project Structure
my-mcp-server/
├── package.json # Node.js dependencies
├── server.js # Server implementation
├── tools/ # Tool implementations
│ ├── database.js
│ └── analysis.js
├── test/ # Tests
│ └── tools.test.js
├── Dockerfile # Container packaging
└── README.md # Setup instructions
Got: Project directory created with server.js (or server.R), package.json, tools/ directory for modular tool implementations, test/ for tests.
If fail: Directory structure doesn't match implementation language? Adjust accordingly. R servers may use R/ instead of tools/ and tests/testthat/ instead of test/.
Step 5: Test the Server
Manual testing with stdio:
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node server.js
Register with Claude Code:
claude mcp add my-server stdio "node" "/path/to/server.js"
Verify tools appear:
Start Claude Code session, check custom tools listed and functional.
Got: tools/list JSON-RPC call returns all defined tools with correct names and schemas. claude mcp list shows server registered. Tools callable from Claude Code session.
If fail: tools/list returns empty array? Tools were not registered before server.connect(). Claude Code cannot find server? Verify command path in claude mcp add is absolute, binary is executable.
Step 6: Add Error Handling
server.tool("risky_operation", "...", schema, async (params) => {
try {
const result = await performOperation(params);
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
Got: Each tool handler wrapped in try/catch. Invalid inputs return isError: true with descriptive message instead of crashing server process.
If fail: Server still crashes on bad input? Check try/catch wraps entire handler body including async operations. Ensure promises awaited within try block.
Step 7: Package for Distribution
Create package.json with bin entry:
{
"name": "my-mcp-server",
"version": "1.0.0",
"bin": {
"my-mcp-server": "./server.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
}
}
Users install and configure:
npm install -g my-mcp-server
claude mcp add my-server stdio "my-mcp-server"
Got: package.json with bin entry pointing to server entry point. Users install globally with npm install -g, register with claude mcp add.
If fail: Bin entry doesn't work after global install? Ensure server.js has shebang line (#!/usr/bin/env node), is marked executable. Verify package name doesn't conflict with existing npm packages.
Checks
- Server starts without errors
-
tools/listreturns all defined tools with correct schemas - Each tool executes correctly with valid input
- Tools return appropriate errors for invalid input
- Server works with Claude Code via stdio transport
- Tools discoverable and usable in Claude sessions
Pitfalls
- Blocking operations: MCP servers should handle requests asynchronously. Long-running operations block other tool calls.
- Missing error handling: Unhandled exceptions crash server. Always wrap tool handlers in try/catch.
- Schema mismatches: Tool parameter schemas must exactly match what handler expects
- stdio buffering: Using stdio transport? Ensure output flushed. Node.js buffers stdout by default.
- Security: MCP servers have same access as process. Validate inputs carefully, especially for shell commands or database queries.
See Also
configure-mcp-server- connect built server to clientstroubleshoot-mcp-connection- debug connectivity issuescontainerize-mcp-server- package server in Docker
GitHub リポジトリ
関連スキル
content-collections
メタこのスキルは、Content Collections(Markdown/MDXファイルを型安全なデータコレクションに変換するTypeScriptファーストのツール)の本番環境でテストされた設定を提供します。Zodバリデーションによる型安全性を実現し、ブログ、ドキュメントサイト、コンテンツ重視のVite + Reactアプリケーション構築時にご利用ください。Viteプラグインの設定、MDXコンパイルから、デプロイ最適化、スキーマバリデーションまで、すべてを網羅しています。
polymarket
メタこのスキルは、開発者がPolymarket予測市場プラットフォームを活用したアプリケーション構築を可能にします。API統合による取引や市場データの取得に加え、WebSocketを介したリアルタイムデータストリーミングにより、ライブ取引や市場活動を監視できます。取引戦略の実装や、ライブ市場更新を処理するツールの作成にご利用ください。
creating-opencode-plugins
メタこのスキルは、開発者がコマンド、ファイル、LSP操作など25種類以上のイベントタイプにフックするOpenCodeプラグインを作成することを支援します。JavaScript/TypeScriptモジュール向けに、プラグイン構造、イベントAPI仕様、および実装パターンを提供します。カスタムイベント駆動ロジックでOpenCode AIアシスタントのライフサイクルをインターセプト、監視、または拡張する必要がある場合にご利用ください。
sglang
メタSGLangは、高性能なLLMサービングフレームワークであり、RadixAttentionプレフィックスキャッシュを活用したJSON、正規表現、エージェントワークフロー向けの高速で構造化された生成を特長とします。特にプレフィックスが繰り返されるタスクにおいて、大幅に高速な推論を実現し、複雑な構造化出力やマルチターン対話に最適です。制約付きデコードが必要な場合や、広範なプレフィックス共有を伴うアプリケーションを構築する場合は、vLLMなどの代替案ではなくSGLangを選択してください。
