Claude Code Plugins

Community-maintained marketplace

Feedback

mcp-development

@drillan/note-mcp
0
0

MCPサーバー開発を支援します。プロトコル準拠、Pydanticスキーマ設計、Playwright統合のベストプラクティスを提供します。

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name mcp-development
description MCPサーバー開発を支援します。プロトコル準拠、Pydanticスキーマ設計、Playwright統合のベストプラクティスを提供します。
allowed-tools Read Edit Write Glob Grep Bash

MCP Development スキル

このスキルは、Constitution Article 3: MCP Protocol Compliance を支援し、MCPサーバー開発のベストプラクティスを提供します。

起動条件

以下の状況で起動します:

  1. MCPツール実装時: 新しいMCPツールを追加する際
  2. スキーマ設計時: 入出力スキーマを定義する際
  3. Playwright統合時: ブラウザ自動化を実装する際
  4. セッション管理実装時: 認証状態を管理する際
  5. エラーハンドリング設計時: 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モデル設計

必須ルール:

  1. すべての入力パラメータはPydanticモデルで検証
  2. Fieldに説明を必ず付与
  3. 型アノテーションを明確に指定
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のセキュアストレージを使用
  • 有効期限チェックを実装
  • 期限切れ時のエラーメッセージを明確に
  • 再認証フローを提供

参考リソース

注意事項

  • note.comの非公式APIを使用するため、仕様変更で動作しなくなる可能性あり
  • レート制限を遵守(目安: 10リクエスト/分)
  • 自己責任・無保証での公開を前提