この記事は フリューAdvent Calendar 2025 の12日目の記事となります。
はじめに
多くのエンジニアにとって、もはやコードアシスタントAIを使わない選択肢はない時代になったと思います。
特にエージェントモードでは、自律してタスクが消化されていきます。
タスクが高速で処理されていくだけに、依頼しているこちら側がその場で状況を把握するのは難しいことがあります。
対応結果は説明してくれますが、表面化していない過程を可視化してみようかなと思いそれを記録として残すことにしました。
まだこの記録を活用するというところまでは踏み込めていません。
複数AIでコンテキストを統一したり、圧縮(コンパクション)されて失われたコンテキストの復元なんかに利用できそうだなと検討しています。
今回は、そのためにタスクを可視化する土台作りを紹介させていただきます。
今回の内容
PythonでAsanaにアクセスするMCPサーバーを自作して、Claude Codeから呼び出す。
ターゲット
- 普段からコード生成AIを利用している人
- MCPサーバーを導入してみたい人
なぜMCPサーバーを自作するのか
Asanaには公式のMCPサーバーが存在しています。
わざわざ自作する必要はないのですが、次の点から自作します。
- セキュリティ
手元のサーバーでトークン/権限を最小化し、公式MCPより範囲を絞れる - APIの入出力調整
パラメータやレスポンス項目を限定し、プロジェクト固定やフィールド絞り込みも可能
セキュリティ
過去にAsanaの公式MCPを検証していた時期があったのですが、そのタイミングでインシデントがあったと報告がありました。
外部サービスのMCPサーバーを利用する場合には、このリスクがつきまとうことを考慮しておく必要があります。
今回利用するAPIなら大丈夫なのか、という話もありますがMCPよりは稼働実績があるだろうという想定で、APIを利用するMCPサーバーを自作することで影響範囲を制限するなどリスクを軽減できると考えました。
APIの入出力を調整できる
セキュリティ面でも挙げましたが、APIを呼び出す際のパラメータに制限を掛けたりMCPの入出力に独自ドメインの情報を付与するなど調整が可能となります。
今回は単純な処理のみにするためこのような仕組みは入れませんが、将来的に拡張が可能です。
導入準備
では、実際に動かす環境を作成していきます。
こちらで作成と動作確認を行った環境は以下です。
python venvで仮想環境を利用しています。
| 項目 | バージョン |
|---|---|
| OS | macOS 15.6.1 (Sequoia) |
| Python | 3.11.9 |
| VSCode | 1.106.3 |
Asana MCPサーバーの用意
以下の手順で進めます
- 仮想環境の準備
- MCPローカルサーバーの用意
- Asanaのアクセストークンの準備
- Claude CodeにMCPの設定
- Asanaの利用ルールの設定
環境のファイル構成
作成する環境はこのような形となります。
project/
├── mcp-server/
│ └── server.py # MCPサーバー本体
├── .venv/ # Python仮想環境
├── .env # 環境変数(アクセストークン)
├── .mcp.json # MCPサーバー設定
├── CLAUDE.md # Claude Codeルールファイル
├── requirements.txt # Pythonパッケージ一覧
└── start-mcp-server.sh # サーバー起動スクリプト
仮想環境の準備
venvで仮想環境を作成します。ご自身の環境に合わせて作成してください。
以降はvenvでの環境を想定した手順になります。
python -m venv .venv
仮想環境をアクティブにする
source .venv/bin/activate
依存関係のインストール
asana==5.2.2
python-dotenv==1.2.1
fastmcp==2.13.3
requirements.txtから依存関係をインストールします
pip install -r requirements.txt
MCPローカルサーバーの用意
以下に用意しました。
server.py
import os
import logging
from typing import Optional
from dotenv import load_dotenv
import asana
from fastmcp import FastMCP
# ログ設定
log_level = os.getenv("LOG_LEVEL", "INFO")
logging.basicConfig(
level=getattr(logging, log_level),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# 環境変数の読み込み
load_dotenv()
# Asana クライアントの初期化
ASANA_ACCESS_TOKEN = os.getenv("ASANA_ACCESS_TOKEN")
if not ASANA_ACCESS_TOKEN:
logger.error("ASANA_ACCESS_TOKEN environment variable is not set")
raise ValueError("ASANA_ACCESS_TOKEN environment variable is required")
logger.info("Initializing Asana API client")
# 新しいAsana SDK APIを使用
configuration = asana.Configuration()
configuration.access_token = ASANA_ACCESS_TOKEN
api_client = asana.ApiClient(configuration)
# 各種APIクライアントを初期化
workspaces_api = asana.WorkspacesApi(api_client)
projects_api = asana.ProjectsApi(api_client)
tasks_api = asana.TasksApi(api_client)
# FastMCPサーバーの作成
mcp = FastMCP("asana-mcp")
@mcp.tool()
def list_workspaces() -> str:
"""Asanaのワークスペース一覧を取得します"""
try:
workspaces = list(workspaces_api.get_workspaces({}))
result = "\n".join([f"- {ws.get('name', 'Unknown')} (GID: {ws.get('gid', 'N/A')})" for ws in workspaces])
logger.info(f"Successfully retrieved {len(workspaces)} workspaces")
return f"Asana ワークスペース一覧:\n{result}"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def list_projects(workspace_gid: str) -> str:
"""指定したワークスペースのプロジェクト一覧を取得します
Args:
workspace_gid: ワークスペースのGID
"""
try:
projects = list(projects_api.get_projects({"workspace": workspace_gid}))
result = "\n".join([f"- {proj.get('name', 'Unknown')} (GID: {proj.get('gid', 'N/A')})" for proj in projects])
logger.info(f"Successfully retrieved {len(projects)} projects for workspace {workspace_gid}")
return f"プロジェクト一覧:\n{result}"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def list_tasks(project_gid: str) -> str:
"""指定したプロジェクトのタスク一覧を取得します
Args:
project_gid: プロジェクトのGID
"""
try:
tasks = list(tasks_api.get_tasks({"project": project_gid}))
result = "\n".join([f"- {task.get('name', 'Unknown')} (GID: {task.get('gid', 'N/A')})" for task in tasks])
logger.info(f"Successfully retrieved {len(tasks)} tasks for project {project_gid}")
return f"タスク一覧:\n{result}"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def get_task(task_gid: str) -> str:
"""指定したタスクの詳細情報を取得します
Args:
task_gid: タスクのGID
"""
try:
task = tasks_api.get_task(task_gid, {})
result = f"""タスク詳細:
名前: {task.get('name', 'Unknown')}
GID: {task.get('gid', 'N/A')}
説明: {task.get('notes', 'なし')}
完了状態: {'完了' if task.get('completed') else '未完了'}
作成日: {task.get('created_at', 'N/A')}
担当者: {task.get('assignee', {}).get('name', '未割り当て') if task.get('assignee') else '未割り当て'}
"""
logger.info(f"Successfully retrieved task details for {task_gid}")
return result
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def create_task(
workspace_gid: str,
name: str,
notes: Optional[str] = None,
projects: Optional[list[str]] = None
) -> str:
"""新しいタスクを作成します
Args:
workspace_gid: ワークスペースのGID
name: タスク名
notes: タスクの説明(オプション)
projects: プロジェクトGIDの配列(オプション)
"""
try:
task_data = {
"workspace": workspace_gid,
"name": name,
}
if notes:
task_data["notes"] = notes
if projects:
task_data["projects"] = projects
task_body = {"data": task_data}
task = tasks_api.create_task(task_body, {})
logger.info(f"Successfully created task: {task.get('name')} (GID: {task.get('gid')})")
return f"タスクを作成しました: {task.get('name', 'Unknown')} (GID: {task.get('gid', 'N/A')})"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def update_task(
task_gid: str,
name: Optional[str] = None,
notes: Optional[str] = None,
completed: Optional[bool] = None,
assignee: Optional[str] = None
) -> str:
"""既存のタスクを更新します
Args:
task_gid: タスクのGID
name: 新しいタスク名(オプション)
notes: 新しいタスクの説明(オプション)
completed: 完了状態(オプション)
assignee: 担当者のGIDまたはメールアドレス(オプション)
"""
try:
update_data = {}
if name is not None:
update_data["name"] = name
if notes is not None:
update_data["notes"] = notes
if completed is not None:
update_data["completed"] = completed
if assignee is not None:
update_data["assignee"] = assignee
if not update_data:
raise ValueError("更新する項目を少なくとも1つ指定してください")
update_body = {"data": update_data}
task = tasks_api.update_task(update_body, task_gid, {})
logger.info(f"Successfully updated task: {task.get('name')} (GID: {task.get('gid')})")
return f"タスクを更新しました: {task.get('name', 'Unknown')} (GID: {task.get('gid', 'N/A')})"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def create_subtask(
parent_task_gid: str,
name: str,
notes: Optional[str] = None
) -> str:
"""指定したタスクにサブタスクを作成します
Args:
parent_task_gid: 親タスクのGID
name: サブタスク名
notes: サブタスクの説明(オプション)
"""
try:
subtask_data = {
"name": name,
"parent": parent_task_gid,
}
if notes:
subtask_data["notes"] = notes
subtask_body = {"data": subtask_data}
subtask = tasks_api.create_task(subtask_body, {})
logger.info(f"Successfully created subtask: {subtask.get('name')} (GID: {subtask.get('gid')}) under parent {parent_task_gid}")
return f"サブタスクを作成しました: {subtask.get('name', 'Unknown')} (GID: {subtask.get('gid', 'N/A')})"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
@mcp.tool()
def list_subtasks(parent_task_gid: str) -> str:
"""指定したタスクのサブタスク一覧を取得します
Args:
parent_task_gid: 親タスクのGID
"""
try:
subtasks = list(tasks_api.get_subtasks_for_task(parent_task_gid, {}))
result = "\n".join([f"- {subtask.get('name', 'Unknown')} (GID: {subtask.get('gid', 'N/A')})" for subtask in subtasks])
logger.info(f"Successfully retrieved {len(subtasks)} subtasks for parent task {parent_task_gid}")
return f"サブタスク一覧:\n{result}" if result else "サブタスクはありません"
except asana.rest.ApiException as e:
logger.error(f"Asana API error: {e.status} - {e.reason}")
raise Exception(f"Asana APIエラー: {e.status} - {e.reason}")
if __name__ == "__main__":
mcp.run()
このコードはAsana SDKとFastMCPを使ってAsanaへアクセスするスクリプトとなります。
@mcp.toolデコレータを利用することで、メソッドがLLMに公開されます。
LLMはメソッド名や、メソッドの説明コメントから適切なツールを選択し実行します。
今回のサーバーでできること
- list_workspaces - ワークスペース一覧を取得
- list_projects - プロジェクト一覧を取得
- list_tasks - タスク一覧を取得
- get_task - タスク詳細を取得
- create_task - 新しいタスクを作成
- update_task - 既存のタスクを更新
- create_subtask - サブタスクを作成
- list_subtasks - サブタスク一覧を取得
Asanaのアクセストークンの準備
Asanaへアクセスする際のユーザーのアクセストークンを発行する必要があります。
アクセスするユーザーでAsanaへログインしてユーザー設定を開きます。
設定からアプリを選択

.envファイルを作成して以下を記述
ASANA_ACCESS_TOKEN=コピーしたトークン
トークンのセキュリティに注意
この取得したトークンは外部に漏らさないよう注意してください。
Gitで管理する場合は.gitignoreに.envを入れるなどして共有しないようにしてください。
Claude CodeにMCPの設定
server.pyを起動するスクリプトを作成します。
これは確実に仮想環境からMCPサーバーを実行するためのものになります
#!/bin/bash
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Activate virtual environment and run the MCP server
source "$SCRIPT_DIR/.venv/bin/activate"
exec python "$SCRIPT_DIR/mcp-server/server.py"
start-mcp-server.shに実行権限を付与しておきます
chmod +x start-mcp-server.sh
次にMCPサーバーを起動するプログラムを記載したJsonファイルを用意します
{
"mcpServers": {
"asana": {
"type": "stdio",
"command": "/Users/yourname/project/start-mcp-server.sh",
"args": [],
"env": {}
}
}
}
※ commandにはstart-mcp-server.shへの絶対パスを指定してください。
Claude Codeを起動してスラッシュコマンド(/mから候補表示)でManage MCPsを選択

failedの場合は、shファイルの実行権限不足、ファイル記述ミスがある可能性があります。
動かない場合はターミナルで./start-mcp-server.shを実行してみるとログが確認できると思います。
VSCodeを再起動するとAsanaMCPが使えます。
Asanaの利用ルールの設定
CLAUDE.mdにAsanaを利用する際に、どのように管理するか決めておきましょう。
個人的には生成AIの意思を可視化する形で進めるのが良いかなと考えています。
また、記載テンプレートも決めておくのもよいでしょう。
Asanaのプロジェクトあらかじめ作成してそれを指定しておいてください。
影響範囲を限定させるため今回のMCPサーバーにはプロジェクト作成は実装していません。
以下は一例です、細かくタスクを細分化しすぎるとトークン消費量も上がるので注意しましょう。
### Asana利用ルール
- プロジェクトは〇〇プロジェクトを利用する。
- 感じたことを素直にタスク内に記載する。
- 事実だけではなく、後に必要となりそうな情報を推測し記載することを心がける。
### Asana利用ワークフロー
1. **親タスクの登録**
- まず`create_task`ツールで親タスクをAsanaに登録
- タスク名に全体の実施内容を明記(例: "新機能: サブタスク作成機能の実装")
- `notes`に以下の項目を含めた初期情報を記載:
- **背景**: なぜこのタスクが必要になったか(ユーザーからの要求、発見した問題、改善の動機など)
- **目的**: このタスクで達成したいこと
- **予定作業**: 実施予定の作業内容(サブタスクの一覧を含む)
2. **サブタスクの登録**
- 親タスクの下に、付随するすべての個別の作業をサブタスクとして作成
- 各サブタスクには具体的な作業内容を記載
- サブタスクの`notes`には、その作業の詳細を記録
3. **作業の実行**
- サブタスクごとに作業を進める
- 作業開始時に該当サブタスクを確認
- コードの実装、テスト、デバッグなどを実施
- 作業中の重要な判断や変更内容をメモしておく
4. **サブタスクごとの記録と完了**
- 各サブタスク完了時に`update_task`で作業内容を`notes`に追記
- 以下の項目を記録:
- **実装内容**: 実際に実施した作業の詳細
- **変更ファイル**: 変更したファイルとその行番号
- **技術的判断**: 実装時の重要な判断や選択した理由
- **課題・気づき**: 発見した問題や今後の改善点
- サブタスク完了時に`update_task`で`completed: true`を設定
5. **親タスクの完了**
- すべてのサブタスクが完了したら、親タスクの`notes`に全体のまとめを追記
- 親タスクを`update_task`で`completed: true`を設定
- これにより階層的な作業履歴がAsanaに残る
### 記録例
#### 親タスクテンプレート
タスク名: 新機能: 〇〇機能の実装
notes(タスク登録時のテンプレート):
【背景】
- ユーザーから「タスクにコメントを追加できるようにしてほしい」という要望
- Asana MCPサーバーの機能拡充の一環
- タスク管理の完全性を高めるために必要
【目的】
- タスクへのコメント投稿機能を実装
- Asana APIのcomments機能をMCPツールとして公開
【予定作業】
以下のサブタスクに分割:
1. comments_apiクライアントの追加
2. add_commentツールの実装
3. 動作確認テストの実施
---
notes(全サブタスク完了後に追記するテンプレート):
【完了サマリー】
- 全3サブタスクを完了
- コメント追加機能を正常に実装
- 動作確認済み
【今後の展開】
- コメント編集・削除機能の実装を検討
- 複数コメント取得機能も候補
#### サブタスク1
タスク名: apiクライアントの追加
親タスク: 新機能: 〇〇機能の実装
notes(作業完了時):
【実装内容】
- src/server.pyにcomments_apiクライアントを追加(行26)
- asana.CommentsApi(api_client)で初期化
【変更ファイル】
- src/server.py (行26)
【技術的判断】
- 既存のworkspaces_api、projects_api、tasks_apiと同じパターンで実装
- 新しいAsana SDK APIパターンを踏襲
#### サブタスク2
タスク名: add_commentツールの実装
親タスク: 新機能: 〇〇機能の実装
notes(作業完了時):
【実装内容】
- list_tools()にadd_commentツールの定義を追加
- call_tool()にadd_commentの処理を実装(行145-165)
- パラメータ: task_gid(必須)、text(必須)
【変更ファイル】
- src/server.py (行145-165)
【技術的判断】
- comments_api.create_comment()を使用
- エラーハンドリング: task_gidの存在確認を追加
- レスポンス: コメント追加成功メッセージを返す
【課題・気づき】
- HTMLテキストのサポートは今回見送り(plain textのみ)
以上で準備は完了です。
新しくClaude Codeのセッションを立ち上げることでCLAUDE.mdが読み込まれるはずです。
実際に作業させてみる
シンプルなToDoアプリを作成させてみます。
今回はアプリを作るのが本質ではないのでプロンプトはなんでも良いと思いますが、Asanaでタスク管理するということは明示しましょう。
シンプルで高機能なToDoアプリをPythonで作成してください。
Asanaでタスク管理をしながら設計から進めてください。
ここでは実装内容については確認しませんが、ちゃんと親タスクを作成してからサブタスクを作って実行されたことが確認できます。

これで、CLAUDE.mdにて自分が必要と考えるルールをテンプレートとして記述すれば良いと思います。
終わりに
MCPを導入するところから、Asanaでタスクを記録していく土台を作成しました。
この仕組みにより、AIエージェントの思考プロセスが可視化され、以下のようなメリットが得られます:
- トークンリミットなどによって中断してもタスクの進捗が把握できる
- 実装の意思決定過程が記録として残る
- AIの判断プロセスをレビューできる
今後は、このタスク履歴を活用して複数のAIセッション間でコンテキストを共有したり、コンパクションで失われた情報を復元するなど、さらなる活用方法を検討していきたいと思います。







