.claude/skills/writing-mcp-server/SKILL.md
MCPサーバー実装の作成・レビュー時に適用する規約とベストプラクティス。 MCP実装タスクの実行時に自動で有効化される。
npx skillsauth add blackawa/dotfiles writing-mcp-serverInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
MCPサーバー構築時に参照するスキル。設計原則→実装→運用の順で読む。
MCPサーバーのクライアントはLLMエージェントである。全ツール定義(名前・説明・スキーマ)がコンテキストウィンドウに載るため、ツール数・スキーマサイズがトークンコストと精度に直結する。
ワークフロー単位でツールを設計する。 個々のAPIエンドポイントを1:1でラップしない。ユーザーが達成したいタスク単位で1ツールにまとめる。例: GitHub issue作成時に labels・assignees も同一ツールの入力に含め、1回の呼び出しで完結させる。
冪等に作る。 エージェントはリトライ・並列化する。同じ入力で同じ結果を返すこと。リスト系はページネーショントークン/カーソルで応答を小さく保つ。
search_users, create_project(LLMトークナイザーと相性良)slack_send_message(他MCPサーバーとの併用前提)get_, list_, search_, create_, update_, delete_.、()、[](ツール呼び出しが壊れる)LLMのツール選択精度に最も影響する要素。以下の構造を守る:
[1行] 何をするか
[できること/できないことの明確化]
[Args] 各パラメータの型・制約・例
[Returns] 出力スキーマ
[Examples] 自然言語→パラメータのマッピング 2-3件
[Error cases] 代表的エラーと原因
structuredContent で型付き出力を返す(2025-06 spec の outputSchema 対応)has_more, next_offset/next_cursor, total を常に返す。デフォルト20-50件"Access denied""Error: Permission denied. API_TOKEN needs 'read:items' scope.""Not found""Error: Item 'abc-123' not found. Use myservice_search_items to find valid IDs."| キー | 型 | 意味 |
|---|---|---|
| readOnlyHint | bool | 環境を変更しない |
| destructiveHint | bool | 破壊的変更の可能性 |
| idempotentHint | bool | 再実行が安全 |
| openWorldHint | bool | 外部と通信する |
ビルドステップなしで tsx src/index.ts で直接実行する構成を標準とする。
@modelcontextprotocol/sdk # MCP SDK
zod # 入力バリデーション
express # Streamable HTTP 時のみ
tsx # TypeScript 直接実行(devDependencies)
{service}-mcp-server/
├── src/
│ ├── index.ts # サーバー初期化 + トランスポート
│ ├── tools/ # ツール実装(ドメイン分割)
│ ├── services/ # API クライアント共通化
│ └── constants.ts # API_URL, CHARACTER_LIMIT 等
└── package.json # "start": "tsx src/index.ts"
命名: {service}-mcp-server(ハイフン区切り)。
| ✅ 使う | ❌ 非推奨 |
|---|---|
| server.registerTool() | server.tool() |
| server.registerResource() | server.setRequestHandler(...) |
| server.registerPrompt() | 手動ハンドラ登録 |
server.registerTool(
"myservice_search_items",
{
title: "Search Items",
description: `...`, // セクション1のテンプレートに従う
inputSchema: {
query: z.string().min(1).max(200).describe("検索文字列"),
limit: z.number().int().min(1).max(100).default(20),
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
},
async ({ query, limit }) => {
try {
const data = await apiRequest("/search", { q: query, limit });
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
structuredContent: data, // 型付き出力
};
} catch (error) {
return { isError: true, content: [{ type: "text", text: handleError(error) }] };
}
}
);
守るべき点:
.describe() を必ず付ける(LLMのパラメータ理解に必要)structuredContent で構造化データも返すisError: true + content で返す(プロトコルレベルエラーにしない).strict() で余分なフィールドを拒否するツールとの使い分け: シンプルな URI→値のマッピング → Resource / 複雑な入力・副作用 → Tool
server.registerResource(
{ uri: "config://settings/{key}", name: "Settings", mimeType: "application/json" },
async (uri) => {
const key = uri.match(/config:\/\/settings\/(.+)$/)?.[1]!;
return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(getConfig(key)) }] };
}
);
stdio(ローカル・CLI): StdioServerTransport で接続。stdout は MCP メッセージ専用。ログは必ず stderr へ。
Streamable HTTP(リモート・マルチクライアント): express + StreamableHTTPServerTransport。リクエストごとにトランスポートを生成(ステートレス)。/health エンドポイントを必ず用意。
環境変数 TRANSPORT=stdio|http で切替する。
API呼び出し・エラーハンドリング・レスポンスフォーマットは必ず共通関数化(DRY)。ツールごとにコピペしない。
services/api.ts: 共通 fetch ラッパー(ベースURL、認証ヘッダ、タイムアウト、エラーthrow)services/errors.ts: ステータスコード別にエージェント向けメッセージを返す関数constants.ts: API_URL, CHARACTER_LIMIT, DEFAULT_LIMIT 等SDKを使わず JSON-RPC 2.0 だけで実装するパターン。Go, Rust, Java 等で有用。
id ありメッセージ → レスポンス必須。id なし(通知) → レスポンス不要(HTTP は 202)-32700 Parse error / -32600 Invalid Request / -32601 Method not found / -32602 Invalid params| メソッド | 種別 | 返すもの |
|---|---|---|
| initialize | request | protocolVersion, capabilities, serverInfo |
| notifications/initialized | notification | なし(HTTP: 202 / stdio: 無応答) |
| tools/list | request | { tools: ToolDefinition[] } |
| tools/call | request | { content: ContentBlock[] } or { isError, content } |
/mcp)で POST/GET/DELETE を処理application/json or text/event-stream405)Mcp-Session-Id: サーバーが initialize 時に発行→以降クライアントが付与(ステートレスなら省略可)MCP-Protocol-Version: 2025-06-18: 2025-06 spec 以降のヘッダ| stdio | Streamable HTTP | |---|---| | ローカル・CLI・デスクトップ統合 | リモート・クラウド・マルチクライアント |
SSE 単体トランスポートは非推奨(Streamable HTTP に統合済み)。
127.0.0.1(0.0.0.0 は NG)/health を必ず用意npx @modelcontextprotocol/inspector
MCP Inspector で全ツールの正常系・エラー系(認証失敗、レート制限、不正入力、タイムアウト)を確認。
{service}_{verb}_{resource} の snake_case.strict() + .describe() 全パラメータstructuredContent でオブジェクト返却/health + Origin 検証development
X(Twitter)の特定投稿URLから原文を直接取得するスキル。 fxtwitter API(APIキー不要・無料)を使用し、ロングポスト(記事形式)の全文取得にも対応。 以下のようなリクエストで発動する: 「この投稿を取得」「ツイートの内容」「このURLの投稿を見せて」 「このXの投稿を読んで」「このツイートを取得して」。 X/TwitterのURLが含まれるメッセージで、検索ではなく特定投稿の内容取得が目的の場合に使う。 x-ai-search との棲み分け: - 検索(キーワードで複数投稿を探す)→ x-ai-search - 特定投稿の取得(URLやIDで1件取得)→ x-tweet-fetch
development
TypeScript / JavaScript コードの作成・レビュー時に適用する規約とベストプラクティス。 .ts, .tsx, .js, .jsx ファイルの編集、Node.js/Deno プロジェクトのセットアップ、 vitest/biome/tsc の実行時に自動で有効化される。
business
Slackメッセージの作成・送信時に適用する規約とベストプラクティス。 slack_send_message / slack_send_message_draft の実行時に自動で有効化される。 「Slackで連絡して」「Slackに投稿して」「スレッドに返信して」 といった発言があれば、このスキルを使うこと。
development
Python コードの作成・レビュー時に適用する規約とベストプラクティス。 Pythonファイルの編集、Pythonプロジェクトのセットアップ、 pytest/ruff/mypyの実行時に自動で有効化される。