0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zenn CLIをMCPツール化するzenn-mcpを作った・使ってみた

0
Last updated at Posted at 2026-08-16

Claude CodeなどのAIエージェントからZennの記事・本を直接作成させたいとき、選択肢は大きく2つあります。ひとつはエージェントに npx zenn new:article をBashツールで直接叩かせる方法、もうひとつはMCP(Model Context Protocol)サーバー経由でツールとして公開する方法です。今回は後者の実装である zenn-mcp を、導入手順とアーキテクチャの両面から紹介します。

zenn-mcpとは

Zenn CLI の主要コマンドをMCPツールとしてラップした、stdio方式のMCPサーバーです。TypeScript製、MITライセンスで公開されています。

提供されるツールは次の5つで、いずれも対応するzenn-cliコマンドに1:1で対応します。

ツール名 対応するzenn-cliコマンド 主な引数
zenn_init npx zenn init projectDir
zenn_new_article npx zenn new:article projectDir, slug?, title?, type?, emoji?, published?
zenn_new_book npx zenn new:book projectDir, slug?
zenn_list_articles npx zenn list:articles projectDir
zenn_list_books npx zenn list:books projectDir

Zenn CLIには、zenn preview(ローカルプレビューサーバーの起動)の機能がありますが、意図的にスコープ外としました。プレビューサーバーは常駐プロセスであり、MCPのリクエスト・レスポンス型のツール呼び出しモデルとは相性が悪いためです1

このリポジトリ自体は記事や本のデータを一切持ちません。各ツール呼び出し時に引数として渡される projectDir(操作対象のZennプロジェクトの絶対パス)に対して動作する、汎用的なラッパーとして設計されています。

セットアップ

インストールとビルド

git clone https://github.com/kannkyo/zenn-mcp.git
cd zenn-mcp
npm install
npm run build

MCPクライアントへの登録

Claude CodeなどのMCPクライアントの設定ファイルに、以下のように登録します。npx -y github:kannkyo/zenn-mcp の形式なので、事前に手元でclone・buildしていなくても、npxがGitHub上のリポジトリを直接取得して実行してくれます。

mcp.json
{
  "mcpServers": {
    "zenn-mcp": {
      "command": "npx",
      "args": ["-y", "github:kannkyo/zenn-mcp"]
    }
  }
}

登録後、Claude Codeなどのエージェントから zenn_initzenn_new_article といったツール名で呼び出せるようになります。

開発時のコマンド

リポジトリを手元でいじる場合は、以下のコマンドが使えます。

npm run dev        # tsxでソースを直接起動(ビルド不要)
npm test           # vitestでテストを実行
npm run typecheck  # tsc --noEmit

使い方

MCPクライアント側からは、たとえば次のような呼び出しでZennプロジェクトを操作できます。

{
  "tool": "zenn_new_article",
  "arguments": {
    "projectDir": "/home/user/my-zenn-repo",
    "slug": "my-new-article-slug",
    "title": "新しい記事のタイトル",
    "type": "tech",
    "emoji": "🎉",
    "published": false
  }
}

slugtitle などのオプション引数は、指定しなければzenn-cli側の自動生成に委ねられます。実体としては npx zenn new:article --slug my-new-article-slug --title "新しい記事のタイトル" --type tech --emoji "🎉" --published false というコマンドライン引数が組み立てられ、projectDir をカレントディレクトリとして実行される形です。

zenn_list_articles / zenn_list_books は、zenn-cliのコンソール出力をそのままテキストとして返します。構造化してパースし直すことはしていません。これはzenn-cli側の出力フォーマットが変わったときに壊れにくくするための、あえての判断です。

アーキテクチャ

全体構成

ソースコードはコンパクトで、以下の3層に分かれています。

src/
  index.ts    # McpServerのセットアップ + stdio transportの起動
  zennCli.ts  # npx zenn 呼び出しの共通ラッパー(spawn・検証・結果整形)
  tools/      # 各ツールのハンドラ(Zodによる引数スキーマ定義 + zennCli呼び出し)
    init.ts
    newArticle.ts
    newBook.ts
    listArticles.ts
    listBooks.ts

index.ts は各ツールの登録を並べているだけです。

src/index.ts
const server = new McpServer({
  name: "zenn-mcp",
  version: "0.1.0",
});

registerZennInit(server);
registerZennNewArticle(server);
registerZennNewBook(server);
registerZennListArticles(server);
registerZennListBooks(server);

const transport = new StdioServerTransport();
await server.connect(transport);

zenn-cliのロジックには一切手を入れない

設計ドキュメント1で明確に書かれていますが、「zenn-cli自体のロジックには一切手を入れない」という方針で作っています。各ツールは内部で child_process.spawn を使って npx zenn <command> を呼び出すだけの薄いラッパーに徹しています。

src/zennCli.ts(抜粋)
export function runZennCommand(
  projectDir: string,
  args: string[]
): Promise<RunZennResult> {
  return new Promise((resolve, reject) => {
    const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
    const child = spawn(npxBin, ["--yes", "-p", "zenn-cli", "zenn", ...args], {
      cwd: projectDir,
    });
    // ...stdout/stderrを蓄積し、closeイベントでresolve
  });
}

npx --yes -p zenn-cli zenn ... という呼び出し方をしている点がポイントです。zenn-cliを依存関係として組み込まず、あえてコマンド呼び出しを使うことでCLIから分離して疎結合にしました。

-p zenn-cli でzenn-cliパッケージを明示的に指定しつつ、zenn コマンドを実行するため、事前に zenn-cli をグローバルインストールしていない環境でも動作します。また process.platform === "win32" の分岐で npx.cmd を使うようにしており、Windows環境も考慮されています。

このアプローチのメリットは、zenn-cliのアップデートに対してメンテナンスコストがほぼゼロで追従できる点です。zenn-cli側でサブコマンドの挙動が変わっても、zenn-mcp側の対応するコードは「どの引数をどう組み立てるか」のロジックだけなので影響範囲が小さくなります。

projectDirの検証

コマンドを実行する前に、projectDir が実在し、かつZennプロジェクトらしいディレクトリであることを検証しています。

src/zennCli.ts(抜粋)
export function validateZennProject(projectDir: string): void {
  validateProjectDir(projectDir);
  const hasArticles = existsSync(join(projectDir, "articles"));
  const hasPackageJson = existsSync(join(projectDir, "package.json"));
  if (!hasArticles && !hasPackageJson) {
    throw new ZennCliError(
      `projectDir is not a Zenn project (no articles/ or package.json found): ${projectDir}`
    );
  }
}

articles/ ディレクトリか package.json のどちらかが存在すればZennプロジェクトとみなす、という緩めの判定です。zenn_init だけは「これから初期化するディレクトリ」を扱うため、この検証をスキップして validateProjectDir(存在確認のみ)にとどめている点も理にかなっています。

引数の組み立てとエラーハンドリング

buildNewArticleArgs は、渡されたオプションのうち値が存在するものだけをCLI引数に変換する、素直な実装です。

src/zennCli.ts(抜粋)
export function buildNewArticleArgs(options: NewArticleOptions): string[] {
  const args = ["new:article"];
  if (options.slug) args.push("--slug", options.slug);
  if (options.title) args.push("--title", options.title);
  if (options.type) args.push("--type", options.type);
  if (options.emoji) args.push("--emoji", options.emoji);
  if (options.published !== undefined)
    args.push("--published", options.published ? "true" : "false");
  return args;
}

spawn に配列で引数を渡しているため、シェルを経由しないコマンド実行になり、title に空白や記号が含まれていてもシェルインジェクションのリスクなく安全に渡せます。

エラー処理も一貫しています。projectDir が不正な場合はコマンドを実行せずに ZennCliError を返し、npx 自体の実行に失敗した場合(Node.js/npm未インストール環境など)は spawnerror イベントを拾ってエラーメッセージに変換し、npx zenn が非0終了コードで終わった場合はstderrの内容をそのままMCPのエラーレスポンスとして返します。

src/zennCli.ts(抜粋)
export function formatZennResult(result: RunZennResult): ToolResult {
  if (result.exitCode !== 0) {
    return {
      content: [
        {
          type: "text",
          text:
            result.stderr ||
            result.stdout ||
            `zenn command exited with code ${result.exitCode}`,
        },
      ],
      isError: true,
    };
  }
  // ...
}

テスト戦略

テストは test/tools/ 配下の各ツールのユニットテストと、test/integration/zennCli.integration.test.ts の統合テストに分かれています。ユニットテストでは child_process をモック化して引数組み立てロジックとバリデーションを検証し、統合テストでは実際に一時ディレクトリに対して initnew:article が成功することを確認する、という軽量な構成にとどめています1。ネットワークやzenn-cli本体の内部実装に依存しすぎないテスト設計です。

このリポジトリの docs/superpowers/ 配下には、実装前に書かれた要件定義書(spec)と実装計画(plan)が残っています。「なぜpreviewをスコープ外にしたか」「なぜlist系を構造化パースしないか」といった設計判断の理由が、コードだけでなくドキュメントとしても追えるようになっています。

まとめ

zenn-mcpは、Zenn CLIの機能をそのままMCPツールとして薄くラップした、割り切りの効いた実装としています。

  • npx zenn <command>spawn で呼び出すだけの薄いラッパーに徹し、zenn-cli本体のロジックには手を入れない
  • projectDir の検証と、配列渡しによる安全な引数組み立てで、最低限の堅牢性を確保している
  • zenn preview のような常駐プロセス系のコマンドは、MCPのツール呼び出しモデルと相性が悪いため明示的にスコープ外としている
  • list:* 系は構造化パースをあえてせず、zenn-cli側の出力フォーマット変更への耐性を優先している

MCPクライアントからAIエージェントに記事作成を任せたい場合、npx -y github:kannkyo/zenn-mcp で登録するだけで導入できる手軽さも魅力です。似たようなCLIラッパー型のMCPサーバーを自作する際の設計リファレンスとしても参考になる規模のコードなので、気になった方はソースを覗いてみることをおすすめします。

参考リンク

  1. docs/superpowers/specs/2026-08-15-zenn-cli-mcp-design.md - kannkyo/zenn-mcp 2 3

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?