全Notion利用者のための、Coding Agentに対応したCLIを作った話
by 逆瀬川ちゃん
10 min read
こんにちは!逆瀬川ちゃん (@gyakuse) です!
今日はNotion Remote MCPをCLIでラップした @sakasegawa/ncli(GitHub)を作った話と、その過程で考えた「Agent時代のCLI設計」についてまとめていきたいと思います。
公式のNotion CLIがついにリリース
Notion公式CLI ntn がリリースされました。Jonathan Clem氏のツイートで発表されたもので、Agent向けのSkill(makenotion/skills)も同時公開されています。CLIのリポジトリは現在(2026/03/19)時点ではまだ公開されていません。
New in the Notion CLI,
ntn: The whole Notion API! And a skill so that your agents know how to use it.
待望のNotion CLIです。ワクワクしながらインストールしたのですが、使ってみると少し悲しくなりました(T_T)
ntn は現状では REST API の薄いラッパーです。ntn api コマンドで /v1/ エンドポイントを叩くだけです。認証も NOTION_API_TOKEN 環境変数(Integration Token)が必要で、使いたいページを手動で接続する必要があります。公式も "alpha-y so auth is a little wonky" と認めています。
一方、NotionはRemote MCP (https://mcp.notion.com/mcp) を公開していて、こちらはOAuthでワークスペース全体にアクセスできます。しかも ntn にはない機能がたくさんあります。
| 機能 | Remote MCP | ntn (REST API) |
|---|---|---|
| 認証 | OAuth(全ワークスペース) | Integration Token(手動接続) |
| AI横断検索(Slack, Drive等) | あり | なし(タイトル検索のみ) |
| ビュー作成・更新 | あり | なし |
| ページ複製 | あり | あり(templateパラメータ経由) |
| データベースビュークエリ | あり | なし |
| ミーティングノート | あり(作成・更新) | あり(読み取りのみ) |
| ブロック直接操作 | なし | あり |
| ファイルアップロード | なし | あり |
Remote MCPのほうが明らかにNotionをフル活用するのに向いているのです。
ただ、MCPはContextの問題が大きく、そもそもAgentの命令予算をdisableにしない限り大きく汚染するのが厄介です。以前書いたMCP Lightのような段階的開示アプローチはローカルMCPには有効ですが、Remote MCPサーバーにはプロキシを通すくらいしか処方箋がありません。
てかCLIでサクサク使いたい〜、という気持ちが強かったので作りました!
Agent時代のCLI設計を考えてみる
このCLIを設計するにあたって決めたことは、主なユーザーをCoding Agent(Claude Code, Codexなど)にするということです。ついでにAgent時代のCLI設計を考えたいな〜という気持ちからこうなりました。
人間も利用対象としつつ、最適化の軸はCoding Agentを対象とします。Coding Agentが初めてこのCLIを使うときの導線はこうなります。
ncli --helpを読む(コマンド一覧 + Quick start)- 実行する
- エラーが出たらHintに従って修正する
- 複雑な操作のときだけ
ncli <command> --helpを確認する
エラーのHintが実質的なガイドになります。これを踏まえて4つの設計原則を立ててみます。
1. 出力はAgentが読みやすい形式にする
デフォルト出力はMCPレスポンスのJSONテキストを自動検出して pretty-print します。
人間向けの装飾(色、罫線、スピナー等)はTTY検出で自動制御し、パイプ時は除去されます。デフォルトでもJSONが返るので jq なしで読めます。
--jsonはデフォルトとほぼ同じですが、MCPが非JSONテキストを返した場合も{ "text": "..." }でJSONに包んで返します。Agentがパース失敗しない保証です--rawはMCPレスポンスをそのまま返します(isErrorフラグやcontent配列構造も含む)- エラーも
--json時は{ "error", "why", "hint" }のJSON構造化出力
2. ディスカバリは --help + エラーヒントとする
--help は3レイヤーの段階的開示になっています。
ncli --help→ 全コマンド一覧 + Quick start + ワークフロー例ncli page --help→ サブコマンド一覧ncli db create --help→ フラグ・例・前提条件・次のステップ
ただし前述の通りAgentが実際に頼るのはエラーヒントのほうです。MCPエラーをパターンマッチしてツール固有のヒントを付与する仕組みを入れています。
| エラーパターン | ヒント |
|---|---|
| DB URLでquery | view URLが必要 → fetchかview create |
| page createでDB IDをparentに | data_source_idが必要 → fetchで取得 |
| data_source_idがRequired | fetch <db-id>で collection://... を探す |
| rich_textがRequired | --bodyでコメント内容を指定 |
3. エラーは What + Why + Hint でわかりやすく
すべてのエラーを「何が起きたか」「なぜ起きたか」「次に何をすべきか」の3要素で構造化します。
Error: notion-create-pages failed
Why: Could not find page with ID: abc123...
Hint: If adding to a database, use --parent collection://<ds-id>.
Run "ncli fetch <db-id>" to get the data_source_id
CLI引数パースエラー、MCP isError レスポンス、OAuthエラー、すべてこの形式に統一しています。Agentが同じミスを繰り返さないためにはHintが不可欠です。
4. Escape Hatchで逃げよう
CLIで実装がたいへんなツールや複雑な引数構造には ncli api で対応します。
ncli api notion-search '{"query":"test","page_size":5}'
echo '{"query":"test"}' | ncli api notion-search
AgentはCLIコマンドが不十分なら ncli api にフォールバックできます。
MCP内部のツール名を露出するのは本来避けたいですが、このescape hatchだけは例外です。機能ロックインを防ぐことのほうが重要だからです。
避けるべきパターン
設計時に意識的に避けたパターンもまとめておきます。
| パターン | 問題 |
|---|---|
| MCPツール名をCLIの主要インターフェースにする | noun-verbグルーピングやタブ補完、バリデーションなどCLIのDXが失われる |
専用ディスカバリコマンド(tools等) |
--help で十分。余分なコマンドは認知負荷 |
サブコマンドの --help に重要情報を隠す |
Agentは読まない。エラーヒントのほうが届く |
| 人間向けの装飾表示のみ | パイプ時にパースしにくい |
| エラーで何をすべきかがない | Agentは同じミスを繰り返す |
このCLIにもAgent Skill(skills/notion/SKILL.md)を同梱しています。Search → Fetch → Act のワークフローパターンやID種別(page_id / data_source_id / view_url)の使い分けなど、エラーヒントだけではカバーしきれない体系的なノウハウを入れています。ただしSkillなしでもCLI単体で --help とエラーヒントに従えば使えるようにしているので、Skillはあくまでブースターという扱いです。各自が独自のワークフローを定義するようなSkillを作るときも、「まず ncli --help して使い方を理解してね」という一行を入れればこのツール自体の使い方は教えなくてもだいたいなんとかなる気がします。
実装について
さて、設計原則が決まったところで実装の話に移ります。
アーキテクチャ
Remote MCP (https://mcp.notion.com/mcp) にStreamable HTTP Transportで接続し、CLIコマンドをMCPツール呼び出しに変換する構成です。
ユーザー / Agent
│
▼
CLI (Commander.js)
│ buildXxxCall() でCLI引数→MCP引数マッピング
▼
withConnection()
│ MCPConnection.connect() → callTool() → disconnect()
▼
MCP SDK (StreamableHTTPClientTransport)
│ JSON-RPC over HTTPS
▼
Remote Notion MCP (https://mcp.notion.com/mcp)
全コマンドが同じ3つのパーツで構成されています。
buildXxxCall() は純粋関数で、CLI引数をMCPツール名と引数に変換します。副作用がないのでテストしやすいです。
// src/commands/search.ts
export function buildSearchCall(query: string): {
tool: string;
args: Record<string, unknown>;
} {
return { tool: "notion-search", args: { query } };
}
withConnection() はMCP接続のライフサイクルを管理するヘルパーです。接続→実行→切断を一括で行い、Rate Limit時は自動リトライします。
// src/mcp/with-connection.ts
export async function withConnection<T>(
fn: (conn: MCPConnection) => Promise<T>
): Promise<T> {
const conn = new MCPConnection();
try {
await conn.connect();
return await withRetry(() => fn(conn));
} finally {
await conn.disconnect();
}
}
printOutput() は --json / --raw / デフォルトの出力制御です。
コマンドの実装は毎回このパターンになります。
const { tool, args } = buildSearchCall(query);
await withConnection(async (conn) => {
const result = await conn.callTool(tool, args);
printOutput(result, cmd.optsWithGlobals());
});
--data フラグを全コマンドに持たせていて、JSONを直接渡せばCLIフラグをバイパスしてMCPに投げられるようにしています。これもescape hatchの一種です。
認証: OAuth 2.0 + PKCE
認証はゼロコンフィグを目指しました。初回の ncli search でも ncli login でも、未認証ならブラウザが開いてOAuthフローが始まります。
ncli search "hello"
→ MCPConnection.connect()
→ UnauthorizedError
→ ブラウザでOAuth同意画面を開く
→ CallbackServerでリダイレクト待ち
→ Token Exchange → tokens.json保存 (0o600)
→ 再接続して実行
Dynamic Client Registration、PKCE (S256)、トークンリフレッシュはMCP SDKが面倒を見てくれます。CLIが管理するのはトークンの永続化だけです。トークンは env-paths でOS別の設定ディレクトリに保存されます。
| OS | 保存先 |
|---|---|
| macOS | ~/Library/Preferences/ncli/ |
| Linux | ~/.config/ncli/ |
| Windows | %APPDATA%\ncli\Config\ |
この中に tokens.json(access/refresh token、パーミッション 0o600)と client.json(OAuthクライアント登録情報)が入ります。
エラーヒントシステム
MCPの isError レスポンスを受け取ったら、エラーメッセージを正規表現でパターンマッチしてツール固有のヒントを付与します。
// src/mcp/client.ts
const HINT_RULES: HintRule[] = [
{
pattern: /could not find page with id/i,
tool: "notion-create-pages",
hint: 'If adding to a database, use --parent collection://<ds-id>. '
+ 'Run "ncli fetch <db-id>" to get the data_source_id',
},
{
pattern: /invalid database view url/i,
hint: 'Use a view URL with ?v= parameter. '
+ 'Run "ncli fetch <db-id>" to find view URLs',
},
// ...
];
function mcpErrorToCliError(toolName: string, result): CliError {
const message = extractMcpErrorMessage(result);
const rule = HINT_RULES.find(
r => r.pattern.test(message) && (!r.tool || r.tool === toolName)
);
return new CliError(`${toolName} failed`, message, rule?.hint);
}
ツール固有のルールが先にマッチし、汎用ルール(unauthorized、rate limit 等)がフォールバックになります。Agentが「ページが見つからない」で詰まったとき「ncli fetch で data_source_id を取得しろ」と即座に教えてくれる仕組みです。
テスト戦略
テストは buildXxxCall() の純粋関数テストを優先しています。CLI引数がMCP引数に正しくマッピングされることの検証です。
describe("buildPageCreateCall", () => {
it("maps --title to pages[0].properties.title", () => {
const result = buildPageCreateCall({ title: "My Page" });
expect(result.tool).toBe("notion-create-pages");
const pages = result.args.pages as Record<string, unknown>[];
expect(pages[0].properties).toEqual({ title: "My Page" });
});
});
MCP接続のE2Eテストはしていません。純粋関数テストでCLI→MCPのマッピングを検証し、MCP自体の正しさはNotionに任せる方針です。vitest でビルド・型チェック・lint・テストを一括で回しています。
技術スタック
依存は最小限に抑えるのを目標つぃましたが、@modelcontextprotocol/sdk がクソでかいです。
| ライブラリ | 役割 | サイズ |
|---|---|---|
@modelcontextprotocol/sdk |
MCP Client + OAuth | ~4.2 MB |
commander |
CLIフレームワーク | ~180 KB |
env-paths |
OS別設定ディレクトリ | ~5 KB |
open |
ブラウザ起動(OAuth) | ~50 KB |
Node.js >= 18で動きます。
Notion CLIの紹介
さて、ここからは実際の使い方を紹介していこうと思います。
インストール
npm install -g @sakasegawa/ncli
これで ncli コマンドが入ります。
Quick Start
# ログイン(ブラウザが開く、初回のみ)
ncli login
# 検索
ncli search "project plan"
# ページ取得
ncli fetch <id>
# ページ作成
ncli page create --title "New Page" --parent <page-id>
# プロパティ更新
ncli page update <id> --prop "Status=Done"
コマンド一覧
| コマンド | 説明 |
|---|---|
ncli login / logout / whoami |
OAuth認証管理 |
ncli search <query> |
ワークスペース横断検索 |
ncli fetch <url-or-id> |
ページ・DB取得 |
ncli page create / update / move / duplicate |
ページ操作 |
ncli db create / update / query |
データベース操作 |
ncli view create / update |
ビュー操作 |
ncli comment create / list |
コメント操作 |
ncli user list / team list |
ユーザー・チーム一覧 |
ncli meeting-notes query |
ミーティングノート |
ncli api <tool> [json] |
MCP直接呼び出し(escape hatch) |
代表的なワークフロー
Agentが一番よく使うパターンは Search → Fetch → Act です。
# 1. 検索してIDを取得
ncli search "Tasks DB" --json
# 2. DBの詳細を取得(data_source_idとビューURLを確認)
ncli fetch <db-id> --json
# 3. DBにエントリを追加
ncli page create --parent collection://<ds-id> \
--title "新しいタスク" --prop "Status=Open"
データベースの作成からエントリ追加までの流れはこうなります。
# DB作成(--propでカラム定義)
ncli db create --title "Tasks" --parent <page-id> \
--prop "Name:title" --prop "Status:select=Open,Done"
# レスポンスからdata_source_idを取得して、エントリ追加
ncli page create --parent collection://<ds-id> \
--title "Task 1" --prop "Status=Open"
stdinからのパイプも対応しています。
echo "# Meeting Notes" | ncli page create \
--title "2026-03-18 Weekly" --parent <id> --body -
全コマンドで --json(構造化出力)と --raw(MCP生レスポンス)と --data(JSON直接指定)が使えます。
まとめ
- Remote MCPをCLIでラップすることで、OAuthで全ワークスペースにアクセスしつつターミナルとエージェントの両方からNotionをフル活用できるようになりました
- Agent-first設計(構造化出力、What+Why+Hintエラー、escape hatch)は人間にとっても使いやすいです
- npmからインストールできます。GitHubでソースも公開しています
- よかったら使ってみてください!自分もClaudeに使わせていますが、結構便利です