AGENT.md•12.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向けガイド