Skip to main content
Glama
AGENT.md12.4 kB
# AGENT.md This file provides guidance to AI coding agents (GitHub Copilot CLI, Codex, etc.) when working with code in this repository. ## プロジェクト概要 **プロジェクト名**: MCP Notes Connector **言語**: Python 3.10+ **目的**: Evernote APIとModel Context Protocol (MCP) を統合し、Claude DesktopなどのMCPクライアントからEvernoteリソースにアクセス可能にする **現在の実装状況**: - ✅ MCPサーバーの基本構造は完成 - ⚠️ Evernote API連携部分は未実装(スタブ状態) - 🔴 動作確認は未実施 詳細は[docs/STATUS.md](docs/STATUS.md)を参照。 ## 技術スタック ### コア依存関係 - **mcp** (>=0.9.0): Model Context Protocol SDK - **evernote3** (>=1.25.14): Evernote API クライアント - **python-dotenv** (>=1.0.0): 環境変数管理 ### 開発ツール - **pytest**: テストフレームワーク - **black**: コードフォーマッター(line-length: 100) - **ruff**: Linter - **mypy**: 型チェッカー(strict mode) ## プロジェクト構造 ``` src/mcp_notes_connector/ ├── __init__.py # パッケージメタデータ ├── server.py # MCPサーバー本体(EvernoteMCPServerクラス) ├── evernote_client.py # Evernote API ラッパー(EvernoteClientクラス) └── types.py # TypedDict型定義 ``` ### 重要なファイル - **server.py**: - `EvernoteMCPServer`クラス: MCPプロトコル実装 - `@server.list_tools()`: ツール定義 - `@server.call_tool()`: ツール実行ハンドラー - `_handle_*`: 各ツールの実装メソッド(現在はスタブ) - **evernote_client.py**: - `EvernoteClient`クラス: Evernote API呼び出しの抽象化層 - 全メソッドが`async def`で定義されている(未実装) ## コーディング規約 ### Python スタイル ```python # 良い例 async def get_note(self, guid: str) -> dict[str, Any]: """ Get note by GUID. Args: guid: Note GUID Returns: Note object """ # 実装... # 避けるべき例 def get_note(self, guid): # 型アノテーションなし # docstringなし pass ``` ### 型アノテーション - **必須**: すべての関数/メソッドに型アノテーションを付ける - **返り値**: `None`を含めて明示的に記述 - **辞書**: `dict[str, Any]`形式を使用(Python 3.9+) - **TypedDict**: 構造化データには`types.py`のTypedDictを使用 ```python from typing import Any from .types import Note, Notebook async def list_notebooks(self) -> list[Notebook]: """型安全な返り値""" pass async def get_note(self, guid: str) -> Note: """TypedDictを使用""" pass ``` ### 非同期処理 - **すべてのAPI呼び出しは`async/await`**: MCPとEvernote APIの両方 - **同期APIの非同期化**: evernote3は同期APIなので、`asyncio.to_thread()`を使用 ```python import asyncio async def list_notebooks(self) -> list[dict[str, Any]]: # 同期的なEvernote API呼び出しを非同期化 return await asyncio.to_thread( self._sync_client.listNotebooks ) ``` ### エラーハンドリング ```python from mcp.types import TextContent async def _handle_get_note(self, arguments: dict[str, Any]) -> str: try: guid = arguments.get("guid") if not guid: raise ValueError("guid is required") note = await self.evernote_client.get_note(guid) return f"Note: {note['title']}" except Exception as e: # 詳細なエラーメッセージを返す return f"Error retrieving note: {str(e)}" ``` ## 開発ワークフロー ### セットアップ ```bash # 仮想環境作成 python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows # 開発版インストール pip install -e ".[dev]" # 環境変数設定 cp .env.example .env # .envファイルを編集してEVERNOTE_TOKENを設定 ``` ### コード品質チェック ```bash # フォーマット black src/ tests/ # Lint ruff check src/ tests/ # 型チェック mypy src/ # テスト pytest ``` ### 新機能追加の手順 1. **ツール定義を追加** ([src/mcp_notes_connector/server.py](src/mcp_notes_connector/server.py)) ```python @self.server.list_tools() async def list_tools() -> list[Tool]: return [ # 既存のツール... Tool( name="new_tool_name", description="ツールの説明(日本語可)", inputSchema={ "type": "object", "properties": { "param1": { "type": "string", "description": "パラメータ説明", }, }, "required": ["param1"], }, ), ] ``` 2. **ハンドラーを実装** ```python async def _handle_new_tool(self, arguments: dict[str, Any]) -> str: """Handle new_tool tool call.""" param1 = arguments.get("param1") result = await self.evernote_client.new_operation(param1) return f"Result: {result}" ``` 3. **ディスパッチに追加** ```python @self.server.call_tool() async def call_tool(name: str, arguments: Any) -> list[TextContent]: if name == "new_tool_name": result = await self._handle_new_tool(arguments) # ... ``` 4. **EvernoteClientにメソッド追加** ([src/mcp_notes_connector/evernote_client.py](src/mcp_notes_connector/evernote_client.py)) ```python async def new_operation(self, param: str) -> dict[str, Any]: """ New operation implementation. Args: param: Parameter description Returns: Operation result """ # Evernote API呼び出し return await asyncio.to_thread( self._client.someOperation, param ) ``` 5. **必要に応じて型定義追加** ([src/mcp_notes_connector/types.py](src/mcp_notes_connector/types.py)) ```python class NewType(TypedDict): """New type description.""" field1: str field2: int optional_field: NotRequired[str] ``` ## 現在の最優先タスク ### Phase 1: Evernote API統合の実装 **現在のブロッカー**: Evernote APIクライアントの初期化が未実装 #### タスク1: EvernoteClientの初期化 ```python # src/mcp_notes_connector/evernote_client.py from evernote.api.client import EvernoteClient as EvernoteSDK from evernote.edam.notestore import NoteStore class EvernoteClient: def __init__(self, token: str, sandbox: bool = False): self.token = token self.sandbox = sandbox # Evernote SDKクライアントの初期化 self._sdk_client = EvernoteSDK( token=token, sandbox=sandbox ) # NoteStoreの取得 self._note_store = self._sdk_client.get_note_store() ``` #### タスク2: list_notebooks()の実装 ```python async def list_notebooks(self) -> list[dict[str, Any]]: """Get list of notebooks.""" notebooks = await asyncio.to_thread( self._note_store.listNotebooks ) return [ { "guid": nb.guid, "name": nb.name, "stack": nb.stack, "default_notebook": nb.defaultNotebook, } for nb in notebooks ] ``` #### タスク3: get_note()の実装 ```python async def get_note(self, guid: str) -> dict[str, Any]: """Get note by GUID.""" note = await asyncio.to_thread( self._note_store.getNote, guid, True, # withContent False, # withResourcesData False, # withResourcesRecognition False # withResourcesAlternateData ) return { "guid": note.guid, "title": note.title, "content": note.content, # ENML形式 "notebook_guid": note.notebookGuid, "tags": note.tagNames or [], "created": note.created, "updated": note.updated, } ``` ## 技術的な注意事項 ### ENML (Evernote Markup Language) Evernoteのノート内容はENML形式で保存されています。 ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"> <en-note> <div>ノートの内容</div> </en-note> ``` プレーンテキスト抽出が必要な場合は、XMLパースが必要です。 ### エラーハンドリング Evernote APIは以下のような例外を投げます: ```python from evernote.edam.error.ttypes import ( EDAMUserException, EDAMSystemException, EDAMNotFoundException, ) try: note = await self.get_note(guid) except EDAMNotFoundException: raise ValueError(f"Note not found: {guid}") except EDAMUserException as e: raise ValueError(f"User error: {e.parameter} - {e.errorCode}") except EDAMSystemException as e: raise RuntimeError(f"System error: {e.errorCode} - {e.message}") ``` ### レート制限 Evernote APIにはレート制限があります: - 1時間あたり最大10,000リクエスト(開発者トークン) - 429エラーが返された場合は再試行ロジックが必要 ```python import time async def _call_with_retry(self, func, *args, max_retries=3): for attempt in range(max_retries): try: return await asyncio.to_thread(func, *args) except EDAMSystemException as e: if e.errorCode == 19: # RATE_LIMIT_REACHED wait_time = e.rateLimitDuration or 60 await asyncio.sleep(wait_time) else: raise raise RuntimeError("Max retries exceeded") ``` ### 環境変数 - `EVERNOTE_TOKEN`: 必須。Evernote開発者トークン - `EVERNOTE_SANDBOX`: オプション。`"true"`でサンドボックス環境使用 ## テスト方針 ### 単体テスト ```python # tests/test_evernote_client.py import pytest from unittest.mock import Mock, AsyncMock from mcp_notes_connector.evernote_client import EvernoteClient @pytest.mark.asyncio async def test_list_notebooks(): client = EvernoteClient("dummy_token", sandbox=True) client._note_store = Mock() # モックのレスポンス mock_notebook = Mock() mock_notebook.guid = "guid-123" mock_notebook.name = "Test Notebook" mock_notebook.stack = None mock_notebook.defaultNotebook = True client._note_store.listNotebooks = Mock(return_value=[mock_notebook]) result = await client.list_notebooks() assert len(result) == 1 assert result[0]["guid"] == "guid-123" assert result[0]["name"] == "Test Notebook" ``` ### 統合テスト 実際のEvernote APIを呼び出すテストは、サンドボックス環境で実行: ```python @pytest.mark.integration @pytest.mark.asyncio async def test_real_api_connection(): token = os.getenv("EVERNOTE_TOKEN") if not token: pytest.skip("EVERNOTE_TOKEN not set") client = EvernoteClient(token, sandbox=True) notebooks = await client.list_notebooks() assert isinstance(notebooks, list) ``` ## デバッグとログ ```python import logging # サーバー起動時にロギング設定 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger(__name__) # 使用例 logger.debug(f"Fetching note: {guid}") logger.info(f"Successfully retrieved {len(notebooks)} notebooks") logger.error(f"Failed to create note: {str(e)}") ``` **注意**: MCPサーバーはstdioで通信するため、ログは必ず`stderr`に出力してください。 ## 参考リンク - [Evernote API Documentation](https://dev.evernote.com/doc/) - [MCP Specification](https://modelcontextprotocol.io/) - [Python MCP SDK](https://github.com/modelcontextprotocol/python-sdk) - [evernote3 Package](https://pypi.org/project/evernote3/) ## 関連ドキュメント - [docs/STATUS.md](docs/STATUS.md) - 実装状況とロードマップ - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) - アーキテクチャ詳細 - [docs/QUICKSTART.md](docs/QUICKSTART.md) - セットアップガイド - [CLAUDE.md](CLAUDE.md) - Claude Code向けガイド

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AnotherStream/mcp-notes-connector'

If you have feedback or need assistance with the MCP directory API, please join our Discord server