取り組みの背景:「仕様書とコードがズレる」問題を根本から解決する
「Notionに仕様書を書いたのに、実装したコードが全然違う方向に進んでいた」「PRレビューで仕様との差異を指摘されるたびに手が止まる」——こうした経験は、多くの開発者にとって日常的な悩みです。
仕様書とコードは本来、常に同期しているべきものです。しかし現実には、開発が進むにつれてこの2つの間にどんどんギャップが生まれてしまいます。その原因は単純で、仕様書を書く作業とコードを書く作業が、ツールレベルで分断されているからです。
ここで扱うのは**Antigravity IDEとNotion APIを連携させたドキュメント駆動開発(Document-Driven Development / DDD)**の実装を完全解説します。Notionの仕様書を直接読み込んでAntigravityがコードを生成し、完成したコードのサマリーを自動でNotionに書き戻す——この双方向ワークフローが実現すると、仕様書とコードの乖離という問題は構造的になくなります。
この記事で構築するシステムの全体像:
- Notion → Antigravity: Notion上の仕様書・タスクをMCP経由でAntigravityが読み込み、コード・テスト・PR説明文を生成
- Antigravity → Notion: 生成されたコードの実装サマリー・進捗状況をNotionに自動書き戻し
- GitHub連携: PRが作成されると仕様書のステータスが自動更新
対象読者は、Notionを日常的に使っていてAntigravityの活用を深めたい中級〜上級の開発者です。
前提環境と準備
必要なもの
- Antigravity IDE(最新版)
- Notionアカウント(無料プランでも動作)
- Node.js 20以上
- GitHub CLI(gh)(オプション)
Notion APIの初期設定
まずNotion側でAPIインテグレーションを作成します。
ステップ1: インテグレーション作成
Notion Developers にアクセスし、「New integration」をクリックします。
設定項目:
- Name:
Antigravity Dev Assistant
- Associated workspace: 使用するワークスペースを選択
- Capabilities: Read content / Update content / Insert content の3つを有効化
「Submit」をクリックすると、Internal Integration Secret(secret_xxx...形式)が表示されます。これを安全な場所に保存してください。
ステップ2: データベースの準備
Notionに以下の構造を持つデータベースを作成します:
📋 開発タスクDB
├── Name(タイトル): タスク名
├── Status(セレクト): Todo / In Progress / Done / Blocked
├── Priority(セレクト): High / Medium / Low
├── Spec(テキスト): 仕様詳細
├── Implementation(テキスト): 実装サマリー(Antigravityが書き込む)
├── PR URL(URL): 対応するPRのURL
└── Assignee(ユーザー): 担当者
作成したデータベースのURLからDatabase IDを抽出します:
https://www.notion.so/[workspace]/[DATABASE_ID]?v=xxx
ステップ3: インテグレーションをデータベースに接続
データベースページの右上「...」→「Connections」→先ほど作成したインテグレーションを選択して接続します。
MCPサーバーの構築:Antigravityとの橋渡し
Antigravity IDEはMCP(Model Context Protocol)を通じて外部ツールと連携できます。Notion用のMCPサーバーを構築することで、Antigravityがノードを読み書きできるようになります。
プロジェクト構造
mkdir notion-mcp-server && cd notion-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk @notionhq/client dotenv
npm install -D typescript @types/node tsx
環境変数の設定
.envファイルを作成します(.gitignoreに必ず追加すること):
NOTION_TOKEN=secret_YOUR_INTEGRATION_SECRET
NOTION_DATABASE_ID=YOUR_DATABASE_ID_HERE
⚠️ 注意: secret_で始まるトークンやYOUR_API_KEY等のプレースホルダーをそのまま使用してください。実際のトークン値はコードに直書きしないでください。
MCPサーバーの実装
src/notion-mcp-server.tsを作成します:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Client } from "@notionhq/client";
import * as dotenv from "dotenv";
dotenv.config();
// Notion クライアント初期化
const notion = new Client({
auth: process.env.NOTION_TOKEN,
});
const DATABASE_ID = process.env.NOTION_DATABASE_ID!;
// MCPサーバー初期化
const server = new Server(
{
name: "notion-dev-assistant",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// ツール一覧の定義
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "get_todo_tasks",
description:
"開発タスクDBからTodoステータスのタスク一覧を取得する",
inputSchema: {
type: "object",
properties: {
priority: {
type: "string",
enum: ["High", "Medium", "Low"],
description: "絞り込む優先度(省略時は全件)",
},
},
},
},
{
name: "get_task_spec",
description: "特定タスクの仕様書詳細を取得する",
inputSchema: {
type: "object",
properties: {
page_id: {
type: "string",
description: "NotionページID",
},
},
required: ["page_id"],
},
},
{
name: "update_task_implementation",
description:
"タスクの実装サマリーとステータスをNotionに書き込む",
inputSchema: {
type: "object",
properties: {
page_id: {
type: "string",
description: "更新するNotionページID",
},
implementation_summary: {
type: "string",
description: "実装内容のサマリー(Markdown形式)",
},
status: {
type: "string",
enum: ["In Progress", "Done", "Blocked"],
},
pr_url: {
type: "string",
description: "PR URL(オプション)",
},
},
required: ["page_id", "implementation_summary", "status"],
},
},
{
name: "create_task",
description: "新しい開発タスクをNotionDBに作成する",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "タスク名" },
spec: { type: "string", description: "仕様詳細" },
priority: {
type: "string",
enum: ["High", "Medium", "Low"],
},
},
required: ["name", "spec"],
},
},
],
};
});
// ツール実行ハンドラー
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "get_todo_tasks": {
const filter: any = {
property: "Status",
select: { equals: "Todo" },
};
// 優先度フィルターがある場合は AND 条件を追加
const queryFilter =
args?.priority
? {
and: [
filter,
{
property: "Priority",
select: { equals: args.priority },
},
],
}
: filter;
const response = await notion.databases.query({
database_id: DATABASE_ID,
filter: queryFilter,
sorts: [
{
property: "Priority",
direction: "ascending",
},
],
});
const tasks = response.results.map((page: any) => ({
id: page.id,
name: page.properties.Name?.title?.[0]?.plain_text ?? "",
priority: page.properties.Priority?.select?.name ?? "",
status: page.properties.Status?.select?.name ?? "",
spec:
page.properties.Spec?.rich_text?.[0]?.plain_text?.slice(
0,
200
) + "..." ?? "",
}));
return {
content: [
{
type: "text",
text: JSON.stringify(tasks, null, 2),
},
],
};
}
case "get_task_spec": {
const page = await notion.pages.retrieve({
page_id: args!.page_id as string,
}) as any;
const spec =
page.properties.Spec?.rich_text
?.map((t: any) => t.plain_text)
.join("") ?? "";
const name =
page.properties.Name?.title?.[0]?.plain_text ?? "";
const priority =
page.properties.Priority?.select?.name ?? "";
return {
content: [
{
type: "text",
text: `# ${name}\n\n**Priority**: ${priority}\n\n## 仕様詳細\n\n${spec}`,
},
],
};
}
case "update_task_implementation": {
const updates: any = {
Status: {
select: { name: args!.status },
},
Implementation: {
rich_text: [
{
text: { content: args!.implementation_summary as string },
},
],
},
};
if (args?.pr_url) {
updates["PR URL"] = {
url: args.pr_url as string,
};
}
await notion.pages.update({
page_id: args!.page_id as string,
properties: updates,
});
return {
content: [
{
type: "text",
text: `✅ タスク ${args!.page_id} を更新しました(Status: ${args!.status})`,
},
],
};
}
case "create_task": {
const newPage = await notion.pages.create({
parent: { database_id: DATABASE_ID },
properties: {
Name: {
title: [{ text: { content: args!.name as string } }],
},
Status: { select: { name: "Todo" } },
Priority: {
select: { name: (args?.priority ?? "Medium") as string },
},
Spec: {
rich_text: [
{ text: { content: args!.spec as string } },
],
},
},
});
return {
content: [
{
type: "text",
text: `✅ タスク作成完了: ${(newPage as any).id}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
// サーバー起動
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Notion MCP Server running on stdio");
}
main().catch(console.error);
ビルドと設定
package.jsonにビルドスクリプトを追加します:
{
"scripts": {
"build": "tsc",
"start": "node dist/notion-mcp-server.js"
}
}
tsconfig.jsonを作成します:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
ビルドと動作確認:
npm run build
# 出力: dist/notion-mcp-server.js が生成される
Antigravity IDEへのMCP登録
AntigravityにNotion MCPサーバーを登録します。
.antigravity/mcp.jsonを作成(プロジェクトルート):
{
"mcpServers": {
"notion-dev-assistant": {
"command": "node",
"args": ["/path/to/notion-mcp-server/dist/notion-mcp-server.js"],
"env": {
"NOTION_TOKEN": "${NOTION_TOKEN}",
"NOTION_DATABASE_ID": "${NOTION_DATABASE_ID}"
}
}
}
}
環境変数の設定(シェル設定ファイルに追加):
export NOTION_TOKEN="secret_YOUR_INTEGRATION_SECRET"
export NOTION_DATABASE_ID="YOUR_DATABASE_ID"
Antigravity IDEを再起動すると、チャットパネルにNotion連携ツールが認識されます。
実践ワークフロー:仕様書からコードを生成する
MCPが動作したら、いよいよドキュメント駆動開発のワークフローを実践します。
ワークフロー1:Todoタスクの確認と自動コード生成
Antigravityのチャットに以下のプロンプトを入力します:
@notion-dev-assistant Todoステータスで優先度Highのタスクを取得して、
一番優先度の高いタスクの仕様書を読み込んでください。
その仕様に基づいてTypeScript + Next.jsのコードを実装してください。
完成したらテストコードも生成し、実装サマリーをNotionに書き戻してください。
AntigravityとNotionのやり取り(実際の動作例):
> get_todo_tasks (priority: "High")
[返答]
[
{
"id": "abc123-...",
"name": "ユーザー認証APIの実装",
"priority": "High",
"status": "Todo",
"spec": "JWT認証を使ったRESTful APIを実装する。..."
}
]
> get_task_spec (page_id: "abc123-...")
[返答]
# ユーザー認証APIの実装
**Priority**: High
## 仕様詳細
エンドポイント:
- POST /api/auth/login: メールアドレスとパスワードで認証
- POST /api/auth/refresh: リフレッシュトークンで新トークン発行
- DELETE /api/auth/logout: セッション無効化
要件:
- JWTトークン(アクセス: 15分, リフレッシュ: 7日)
- bcryptでパスワードハッシュ化
- Prisma + PostgreSQLでユーザーデータ管理
- レートリミット: 10回/分
この仕様を受け取ったAntigravityは、以下のコードを自動生成します:
// src/app/api/auth/login/route.ts
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { prisma } from "@/lib/prisma";
import { rateLimit } from "@/lib/rate-limit";
const ACCESS_TOKEN_EXPIRES = "15m";
const REFRESH_TOKEN_EXPIRES = "7d";
export async function POST(request: NextRequest) {
// レートリミットチェック
const ip = request.ip ?? "unknown";
const { success } = await rateLimit(ip, { limit: 10, window: 60 });
if (!success) {
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
const { email, password } = await request.json();
// バリデーション
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
);
}
// ユーザー取得
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
return NextResponse.json(
{ error: "Invalid credentials" },
{ status: 401 }
);
}
// パスワード検証
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) {
return NextResponse.json(
{ error: "Invalid credentials" },
{ status: 401 }
);
}
// トークン生成
const payload = { userId: user.id, email: user.email };
const accessToken = jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: ACCESS_TOKEN_EXPIRES,
});
const refreshToken = jwt.sign(payload, process.env.JWT_REFRESH_SECRET!, {
expiresIn: REFRESH_TOKEN_EXPIRES,
});
// リフレッシュトークンをDBに保存
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return NextResponse.json({
accessToken,
refreshToken,
user: { id: user.id, email: user.email },
});
}
コード生成後、Antigravityは自動でNotionに書き戻します:
> update_task_implementation (
page_id: "abc123-...",
status: "In Progress",
implementation_summary: "JWT認証APIを実装しました。
- POST /api/auth/login: bcrypt検証 + JWTトークン発行
- POST /api/auth/refresh: リフレッシュトークン検証・更新
- DELETE /api/auth/logout: DBからトークン削除
レートリミット(10回/分)とエラーハンドリングも実装済み。"
)
✅ タスク abc123-... を更新しました(Status: In Progress)
ワークフロー2:PR説明文の自動生成とNotion連携
実装が完了したら、PR作成も自動化できます:
実装が完了しました。GitHub CLIを使ってPRを作成し、
NotionのタスクステータスをDoneに更新してPR URLを記録してください。
Antigravityは以下を実行します:
# PRを作成(gh CLIを使用)
gh pr create \
--title "feat: JWT認証API実装 (#abc123)" \
--body "## 概要
Notionタスク「ユーザー認証APIの実装」の実装PR。
## 変更内容
- POST /api/auth/login: bcrypt + JWT認証
- POST /api/auth/refresh: トークン更新
- DELETE /api/auth/logout: セッション無効化
- レートリミット(10回/分)実装
## テスト
- Unit tests: 15件(全パス)
- Integration tests: 8件(全パス)"
その後、NotionのPR URLとステータスを自動更新します。
双方向同期の自動化:GitHub Actionsとの統合
より高度な自動化として、GitHubのPRマージイベントをトリガーにNotionを更新するGitHub Actionsを構築できます。
.github/workflows/notion-sync.yml
name: Sync Notion on PR Merge
on:
pull_request:
types: [closed]
jobs:
update-notion:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Extract Notion Page ID from PR title
id: extract
run: |
# PRタイトルから Notion Page ID を抽出
# 例: "feat: JWT認証API実装 (#abc123-xxxxx)"
PAGE_ID=$(echo "${{ github.event.pull_request.title }}" \
| grep -oP '\(#\K[a-z0-9-]+(?=\))')
echo "page_id=$PAGE_ID" >> $GITHUB_OUTPUT
- name: Update Notion Task Status
if: steps.extract.outputs.page_id != ''
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
run: |
PAGE_ID="${{ steps.extract.outputs.page_id }}"
PR_URL="${{ github.event.pull_request.html_url }}"
# Notion API で直接更新
curl -s -X PATCH \
"https://api.notion.com/v1/pages/${PAGE_ID}" \
-H "Authorization: Bearer ${NOTION_TOKEN}" \
-H "Content-Type: application/json" \
-H "Notion-Version: 2022-06-28" \
-d "{
\"properties\": {
\"Status\": {\"select\": {\"name\": \"Done\"}},
\"PR URL\": {\"url\": \"${PR_URL}\"}
}
}"
echo "✅ Notion updated: ${PAGE_ID} → Done"
このワークフローにより、PRのマージ後に自動的にNotionのタスクが「Done」に更新されます。
高度な活用:仕様書テンプレートの設計
ドキュメント駆動開発の効果を最大化するには、Notionの仕様書テンプレートを整備する点が肝心です。以下のテンプレートがAntigravityとの相性に優れています。
推奨仕様書フォーマット
## 機能概要
[何を実装するかを1〜2文で説明]
## 対象ユーザー
[この機能を使うのは誰か]
## 入力・出力仕様
### 入力
- パラメータ1: 型・バリデーション条件
- パラメータ2: 型・バリデーション条件
### 出力
- 成功時: レスポンス形式
- エラー時: エラーコードとメッセージ一覧
## ビジネスロジック
1. ステップ1の処理内容
2. ステップ2の処理内容
3. 例外ケースの処理
## 技術スタック
- 言語/フレームワーク: TypeScript / Next.js
- DB: PostgreSQL + Prisma
- 依存ライブラリ: bcryptjs, jsonwebtoken
## テスト要件
- [ ] 正常系: [具体的なテストケース]
- [ ] 異常系: [エラーケース]
- [ ] パフォーマンス: [閾値]
## 参考リンク
- [関連する既存コード]
- [外部APIドキュメント]
このフォーマットで仕様を書くと、Antigravityが構造を正確に解釈してより精度の高いコードを生成できます。
よくあるエラーと対処法
エラー1: MCPサーバーが起動しない
Error: Cannot connect to MCP server "notion-dev-assistant"
原因: パスの設定ミスまたは環境変数未設定
対処法:
# MCPサーバーを単独で起動してテスト
NOTION_TOKEN=secret_xxx NOTION_DATABASE_ID=xxx \
node /path/to/notion-mcp-server/dist/notion-mcp-server.js
# 正常なら "Notion MCP Server running on stdio" と出力される
エラー2: Notion APIが401 Unauthorizedを返す
原因: インテグレーションがデータベースに接続されていない
対処法: Notionのデータベースページで「Connections」→インテグレーションを再接続します。
エラー3: ページIDが見つからない
APIResponseError: Could not find block with ID
原因: Database IDとPage IDの混同、またはIDのフォーマットミス
対処法:
// Notion URLからPage IDを正規化する関数
function normalizePageId(idOrUrl: string): string {
// UUID形式(ハイフン含む)はそのまま使用
if (idOrUrl.match(/^[a-f0-9-]{36}$/)) return idOrUrl;
// URLから抽出(32文字の16進数)
const match = idOrUrl.match(/([a-f0-9]{32})/);
if (match) {
const raw = match[1];
return `${raw.slice(0,8)}-${raw.slice(8,12)}-${raw.slice(12,16)}-${raw.slice(16,20)}-${raw.slice(20)}`;
}
throw new Error(`Invalid Notion page ID: ${idOrUrl}`);
}
運用のベストプラクティス
1. Notionデータベースにインデックスを活用する
大規模プロジェクトでは、Notionの「フィルター済みビュー」を活用してタスクを整理します。Antigravityへのプロンプトでも「ビューID」を渡すことで、特定のスプリントや担当者のタスクだけを対象にできます。
2. 実装サマリーの粒度を統一する
Notionへの書き戻しサマリーは、後から読む人(将来の自分を含む)が内容を理解できる粒度で書く点が肝心です。Antigravityへのプロンプトに「以下の形式でサマリーを書いてください」と明示するのが効果的です。
3. セキュリティ:トークン管理の注意点
Notion Internal Integration Secretはデータベース全体への読み書き権限を持ちます。CI/CDに組み込む場合は、必ずGitHub Secretsや環境変数として管理し、リポジトリにコミットしないようにしてください。
4. Notion APIのレートリミット
Notion APIは1秒3リクエストのレートリミットがあります。大量のタスクを処理する場合は、リクエスト間に適切な遅延を設けるか、バッチ処理を実装してください。
個人開発者の視点から(実体験メモ)
まとめ
ここで扱うのはAntigravityとNotion APIを連携させたドキュメント駆動開発(DDD)の完全な実装手順を解説しました。
ポイントを振り返ります:
- MCPサーバーの構築:
@modelcontextprotocol/sdkと@notionhq/clientでNotion APIをAntigravityのツールとして公開
- 双方向同期: Notionの仕様書を読んでコードを生成し、実装サマリーを自動でNotionに書き戻す
- GitHub Actions連携: PRマージ時にNotionのタスクステータスを自動更新
- 運用のコツ: 仕様書テンプレートの整備、レートリミット対策、セキュリティ管理
仕様書とコードの乖離は、多くの開発現場で慢性的な課題です。このシステムを導入することで、仕様書が常にコードの「真実の情報源(Single Source of Truth)」として機能するようになります。最初のセットアップに1〜2時間かかりますが、その後の開発速度は確実に向上するはずです。
ドキュメント駆動開発の理論的背景をさらに深く