| name | mcp-development |
| description | MCPサーバー開発を支援します。プロトコル準拠、Pydanticスキーマ設計、Playwright統合のベストプラクティスを提供します。 |
| allowed-tools | Read Edit Write Glob Grep Bash |
MCP Development スキル
このスキルは、Constitution Article 3: MCP Protocol Compliance を支援し、MCPサーバー開発のベストプラクティスを提供します。
起動条件
以下の状況で起動します:
- MCPツール実装時: 新しいMCPツールを追加する際
- スキーマ設計時: 入出力スキーマを定義する際
- Playwright統合時: ブラウザ自動化を実装する際
- セッション管理実装時: 認証状態を管理する際
- エラーハンドリング設計時: MCPエラーレスポンスを設計する際
MCPプロトコル要件
ツール定義
MCPツールは以下の構造を持つ必要があります:
from mcp.types import Tool, TextContent
from pydantic import BaseModel, Field
class CreateDraftInput(BaseModel):
"""下書き作成の入力パラメータ"""
title: str = Field(..., description="記事のタイトル")
body: str = Field(..., description="記事の本文(Markdown形式)")
tags: list[str] | None = Field(None, description="記事のタグ一覧")
# ツール定義
create_draft_tool = Tool(
name="create_draft",
description="note.comに記事の下書きを作成します",
inputSchema=CreateDraftInput.model_json_schema(),
)
Pydanticモデル設計
必須ルール:
- すべての入力パラメータはPydanticモデルで検証
- Fieldに説明を必ず付与
- 型アノテーションを明確に指定
from pydantic import BaseModel, Field, field_validator
class ArticleContent(BaseModel):
"""記事コンテンツのスキーマ"""
title: str = Field(..., min_length=1, max_length=200, description="記事タイトル")
body: str = Field(..., min_length=1, description="記事本文")
status: str = Field("draft", pattern="^(draft|published)$", description="記事状態")
@field_validator("title")
@classmethod
def title_not_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("タイトルは空にできません")
return v.strip()
エラーレスポンス
MCPエラーは適切な形式で返す必要があります:
from mcp.types import TextContent, ErrorData
from mcp.shared.exceptions import McpError
class NoteApiError(McpError):
"""note.com API関連のエラー"""
pass
class AuthenticationError(NoteApiError):
"""認証エラー"""
def __init__(self, message: str = "認証が必要です"):
super().__init__(ErrorData(code=-32001, message=message))
class SessionExpiredError(NoteApiError):
"""セッション期限切れエラー"""
def __init__(self):
super().__init__(ErrorData(
code=-32002,
message="セッションの有効期限が切れました。再ログインしてください。"
))
Playwright統合
ブラウザライフサイクル管理
from playwright.async_api import async_playwright, Browser, Page
from contextlib import asynccontextmanager
class BrowserManager:
"""ブラウザインスタンスのライフサイクル管理"""
def __init__(self) -> None:
self._browser: Browser | None = None
self._page: Page | None = None
async def get_page(self) -> Page:
"""既存のページを再利用、なければ新規作成"""
if self._page is not None and not self._page.is_closed():
return self._page
if self._browser is None:
playwright = await async_playwright().start()
self._browser = await playwright.chromium.launch(headless=False)
self._page = await self._browser.new_page()
return self._page
async def close(self) -> None:
"""リソースのクリーンアップ"""
if self._page:
await self._page.close()
if self._browser:
await self._browser.close()
作業ウィンドウの再利用
async def show_preview(self, article_id: str) -> None:
"""プレビューを表示(既存ウィンドウを再利用)"""
page = await self.browser_manager.get_page()
# 既にプレビューページにいる場合はリロード
current_url = page.url
preview_url = f"https://note.com/api/v1/text_notes/{article_id}/preview"
if current_url == preview_url:
await page.reload()
else:
await page.goto(preview_url)
セッション管理
セキュアなセッション保存
OSのキーチェーン/資格情報マネージャーを使用:
import keyring
from dataclasses import dataclass
from datetime import datetime
import json
SERVICE_NAME = "note-mcp"
@dataclass
class Session:
"""セッション情報"""
cookies: dict[str, str]
user_id: str
expires_at: datetime
def is_valid(self) -> bool:
"""セッションが有効かチェック"""
return datetime.now() < self.expires_at
def save_session(session: Session) -> None:
"""セッションをセキュアに保存"""
keyring.set_password(
SERVICE_NAME,
"session",
json.dumps({
"cookies": session.cookies,
"user_id": session.user_id,
"expires_at": session.expires_at.isoformat(),
})
)
def load_session() -> Session | None:
"""保存されたセッションを読み込み"""
data = keyring.get_password(SERVICE_NAME, "session")
if data is None:
return None
parsed = json.loads(data)
session = Session(
cookies=parsed["cookies"],
user_id=parsed["user_id"],
expires_at=datetime.fromisoformat(parsed["expires_at"]),
)
if not session.is_valid():
keyring.delete_password(SERVICE_NAME, "session")
return None
return session
セッション期限切れ処理
async def execute_with_session(self, operation: Callable) -> Any:
"""セッションを確認して操作を実行"""
session = load_session()
if session is None:
raise AuthenticationError("ログインが必要です")
if not session.is_valid():
raise SessionExpiredError()
try:
return await operation()
except SessionExpiredError:
# セッションをクリアして再認証を促す
keyring.delete_password(SERVICE_NAME, "session")
raise
実装チェックリスト
MCPツール作成時
- Pydanticモデルで入力を定義
- Fieldに説明を付与
- ツール定義にdescriptionを記載
- 適切なエラーレスポンスを実装
Playwright統合時
- ブラウザライフサイクルを管理
- 作業ウィンドウを再利用
- クリーンアップ処理を実装
- エラー時のリカバリを考慮
セッション管理時
- OSのセキュアストレージを使用
- 有効期限チェックを実装
- 期限切れ時のエラーメッセージを明確に
- 再認証フローを提供
参考リソース
- MCP Protocol仕様: https://modelcontextprotocol.io/
- Pydantic v2ドキュメント: https://docs.pydantic.dev/
- Playwright Pythonドキュメント: https://playwright.dev/python/
注意事項
- note.comの非公式APIを使用するため、仕様変更で動作しなくなる可能性あり
- レート制限を遵守(目安: 10リクエスト/分)
- 自己責任・無保証での公開を前提