microCMSブログ

microCMSの最新情報や活用事例などをお届けします。

エンジニアリング

HonoとWorkOSを使ってOAuth対応のmicroCMSリモートMCPサーバーを作る

HonoとWorkOSを使ってOAuth対応のmicroCMSリモートMCPサーバーを作る
りゅーそう りゅーそう

最近の開発ではClaudeCode, Cursor, CodexなどのAIエージェントを使って自然言語で開発するケースが多くなってきました。
それに加えて大きな変化だと感じているのがCodexのiOSアプリを使ってスマホからエージェントを起動して開発するスタイルです。私もプライベートの個人開発では行っています。
これによって生活の中に開発が自然と溶け込むようになっていって、開発スタイルの変化を感じています。

それに加えて私は個人ブログで日記をほぼ毎日書くという習慣があります。以前はスマホからmicroCMSの管理画面を開いて入稿をしていましたが、開発と同様にCodexなどのエージェントのサポートも得ながらできないか?考えました。
その解決策として、個人用のリモートMCPサーバーを作成しました。

CodexのスマホアプリなどからもmicroCMSに投稿できる仕組みを作りましたので、紹介したいと思います。

リモートMCPサーバーとは?

MCP(Model Context Protocol)は、AIエージェントを外部システムに接続するためのオープンな標準プロトコルです。これによって、microCMSや他のデータソースをAIエージェントが操作できるようになります。
https://modelcontextprotocol.io/docs/getting-started/intro

microCMSでは現在公式のOSSとしてmicrocms-mcp-serverを公開しています。
https://document.microcms.io/mcp-server/microcms-mcp-server

これは主に、Claude DesktopやCursorなどのMCPクライアントがローカルで npx 経由でサーバーを起動するstdio方式で使う想定です。APIキーはクライアント側の設定に置く形になります。

一方で今回作ったリモートMCPサーバーは、MCPサーバー自体をWebエンドポイントとして公開し、CodexなどのMCPクライアントからStreamable HTTP(HTTP POSTベースのMCPトランスポート)で呼び出せるようにしたものです。

  • クライアント側でサーバーを起動しなくてよいこと
  • microCMSのAPIキーをサーバー側に閉じられること
  • 認証・レート制限・ログをサーバー側に集約できること

が大きな違いです。スマホのCodexアプリのようにローカルプロセスを起動できないクライアントからも使えるため、今回の要件に合っていました。

なお microcms-mcp-server 自体もHTTPモードでリモート運用は可能です。
今回はブログ執筆用の専用ツールを自作してみたかったこと(下書き作成、画像アップロード、カテゴリー整理など)とよりセキュアな要件としてOAuth認証を試してみたかったため、Vercel上に独自のリモートMCPサーバーを構築しました。

今回作成した仕組み

実際に作ったものを紹介します。実際に個人ブログで活用しているものです。

Codexなどのアプリのターミナルで codex mcp login [mcp名] のようなコマンドを叩くとOAuth対応のリモートMCPサーバーが立ち上がります(mcp名は公開しても問題ないのですが、個人用なため不要なアクセスを避けるため隠しています)。

ChatGPT Desktopを開き、ターミナルで codex mcp login [mcp ツール名] を実行してMCPサーバーへのログインを開始する様子。

発行されたURLに遷移すると、ログイン画面に遷移します。

WorkOS AuthKitのサインイン画面。メールアドレスによるSSOログインまたはGoogleアカウントでのログインを選択できる。

ログイン成功するとCodexとの接続許可を確認できます。

WorkOS AuthKitの認可画面。Codexがユーザーのプロフィール情報などへのアクセス権限を要求しており、ユーザーが内容を確認した上でアクセスを許可する。

これらのアクセスを許可すると、Codex上で独自に作ったmicroCMSのMCPツールを叩けます。

以下はブログの下書きを作成するシンプルなツールです。下書きプレビューも見ながら執筆できるのが便利です。スクリーンショットの関係でデスクトップで紹介していますが、スマホのiOSアプリからでも同様の操作が可能です。

CodexがmicroCMSへ接続し、「HonoとWorkOSを使ってOAuth対応個人用のmicroCMSリモートMCPサーバーを作る」というタイトルの記事下書きを作成した状態。作成された記事をプレビュー画面で確認している。

カテゴリーを自動生成して紐づけたり、AIの補助を得ながらブログを執筆できます。

Codexが記事に合わせて「MCP」カテゴリを新規作成し、記事へ自動で紐付けた状態。プレビュー画面では既存の「開発」と新規作成した「MCP」の両方のカテゴリが表示されている。

このようなOAuth対応の認証と、リモートMCPサーバーと便利なツールを作ったのでその作り方を解説します。

今回作成した仕組みの裏側

今回のリモートMCPサーバーは、大きく3つの要素で構成されています。

  • Codex(MCPクライアント) — スマホからMCPサーバーに接続し、ツールを呼び出す側
  • Hono(リソースサーバー) — 「ブログと同一ドメイン」の /mcp パスとして公開するMCP本体。トークン検証・レート制限・ツール実行を担う。Honoで作成したサーバーは今回はVercelにデプロイしています。
  • WorkOS AuthKit(認可サーバー) — ログイン画面とOAuthトークンの発行・検証基盤を担う

microCMSのAPIキーはHono/Vercel側の環境変数にだけ置き、Codexには渡しません。

代わりにCodexはOAuthで操作をしているのは「自分であること」を証明し、証明できたリクエストだけがMCPツールを実行できます。

個人ブログ向けに「自分以外は使えない」状態を保ちつつ、AIクライアントから安全にアクセスできるようにするというのが今回の設計の核です。

またHonoは Vercel のAPI Routeとしてデプロイしています。

ブログ本体と同一ドメインで /mcp/api/mcp として公開できるため、環境変数・デプロイ・ログ確認を既存のWebサイト運用に寄せられます。ディレクトリ構成は以下のようなイメージです。

api/mcp.ts                          # VercelのAPIルートとして呼び出す
mcp-server/src/transport/vercel.ts  # Honoアプリ(認証・transportなど)
mcp-server/src/auth/                # トークン検証・401応答
mcp-server/src/server.ts            # MCPサーバー本体(登録したtoolsを呼び出す)
mcp-server/src/tools/microcms/      # microCMS汎用ツール
mcp-server/src/tools/ryusou/        # ブログ執筆用に独自にカスタマイズしたツール(命名はハンドルネームで適当です)
mcp-server/src/security.ts          # レート制限・エンドポイント制限など
src/                                # ルートにAstroのブログ側の実装

OAuthとは何か、3者はどう関わるか

OAuthは、ざっくり言うと「パスワードやAPIキーを直接渡さずに、第三者サービスへのアクセス権を安全に許可する認可の仕組み」です。

詳しくは以下の記事などがわかりやすいです。

https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be

今回の文脈では、Codexが「このMCPリソースを操作していいですよ」という許可(access token)をWorkOS経由で受け取り、Honoがその許可を検証してからMCPツールを実行する、という流れになります。

Codexで codex mcp login [mcpツール名] を実行すると、おおむね次の流れになります。

MCPサーバーのOAuth 2.1認可フローを示すシーケンス図。登場人物はCodex(MCPクライアント)、Hono(リソースサーバー)、WorkOS AuthKit(認可サーバー)の3者。①CodexがトークンなしでMCPエンドポイントへリクエストし、②Honoが401とリソースメタデータの取得先を返す。③CodexがOAuth Protected Resource Metadataを取得し、④Honoがissuerやresource URLなどのメタデータを返却する。⑤CodexはWorkOSでOAuth 2.1認可(Dynamic Client Registration)を開始し、⑥認可コードを取得する。⑦認可コードをアクセストークンへ交換し、⑧WorkOSがJWTアクセストークンを返却する。⑨CodexはBearerトークン付きで再度MCPへアクセスし、⑩HonoはWorkOSのJWKSを用いてJWTのissuer・aud・subを検証する。⑪検証成功後、HonoがMCP JSON-RPCレスポンスを返す。

このフローについて大まかではありますが、実装方法の解説をします。

401とProtected Resource Metadata(図の1~4)

OAuthフローの起点となるのが、Protected Resource Metadata(RFC 9728の規格)です。

トークンなしで MCPサーバーに直接アクセスすると、Honoは401を返し、WWW-Authenticate ヘッダーに resource_metadata のURLを載せます。Codexはそこから「どの認可サーバーにログインすればよいか」を自動で知ります。

Honoでは以下のように実装します。

mcp-server/src/transport/vercel.ts
// 一部抜粋
app.route(
  "/",
  simpleMcpAuthRouter({
    issuer: config.workosAuthkitDomain,
    resourceServerUrl: new URL(config.mcpResourceUrl),
    scopesSupported: ["openid", "profile", "email"],
  }),
);

simpleMcpAuthRouter を使って認証なしで公開し、Codexが「どの認可サーバー(WorkOS)にログインすればよいか」を自動で知れるようにします。

トークンなしで /mcp にPOSTすると401が返ります。このとき WWW-Authenticate ヘッダーにRFC 9728に沿ったメタデータURLを載せて返却するようにします。以下のようなヘッダーをオリジンを元に生成する関数を用意します。

export function buildChallengeHeader(): string {
  const config = getConfig();
  const rsUrl = new URL(config.mcpResourceUrl);
  const rsPath = rsUrl.pathname === "/" ? "" : rsUrl.pathname;
  const prmUrl = `${rsUrl.origin}/.well-known/oauth-protected-resource${rsPath}`;

  return `Bearer error="Unauthorized", ..., resource_metadata="${prmUrl}"`;
}

@hono/mcpbearerAuth ミドルウェアを使って、トークンがない場合・無効な場合の両方でこのヘッダーを返すよう設定しています。

mcp-server/src/transport/vercel.ts
const challengeHeader = buildChallengeHeader()

app.use(path, bearerAuth({
  verifyToken: async (token) => (await verifyAccessToken(token)) !== null,
  noAuthenticationHeader: { wwwAuthenticateHeader: challengeHeader },
  invalidToken: { wwwAuthenticateHeader: challengeHeader },
}));

HonoはMCPの認証を作るクライアントが整備されていてとても使い勝手が良いです。

OAuth2.1認可~accessTokenを受け取るところまで(図の5~8)

前のステップでクライアントはどのような方法・URLに認可を受ければ良いのかを知ることができました。次に実際に認可サーバーにリクエストを送り、MCPツールを操作するのに必要なaccessTokenを得る必要があります。

この認可サーバーはWorkOSというSaaSを使い、自身でサーバー運用しない構成としました。

今回はWorkOSは自分自身以外の認証を弾く(第三者が利用できない状態)ために以下の設定を行なっています。

  • Sign upをOFFにして新規登録を禁止とする(WorkOSから直接inviteしたユーザーのみ認証)。
  • Radar allowlistで自分のメールアドレスだけホワイトリスト形式で許可する。
  • Resource Indicatorで該当のMCPのURLのみを許可する。

ちなみに二重防御としてHonoのサーバー側でもこれらの権限などをチェックしています。

Resource IndicatorでURLを許可することによって、ログイン成功時にCodexなどにaccessTokenが付与されます。

MCPのツールにリクエストする。JWTの検証(図の9~11)

accessTokenが付与されたので、これでCodexはMCPを経由してリクエストをする準備ができました。

このリクエストに対してもJWTの署名検証を行うのがこのフェーズです。 issuer (WorkOSのAuthkitのドメインと一致しているか)や sub ユーザーが正しいのか検証するフェーズです。

josejwtVerify を使って検証します。

mcp-server/src/auth/workos.ts
export async function verifyAccessToken(token: string): Promise<JWTPayload | null> {
  const config = getConfig();
  try {
    const { payload } = await jwtVerify(token, getJwks(), {
      issuer: config.workosAuthkitDomain,
      audience: config.mcpResourceUrl,
    });

    const sub = payload.sub
    if (!sub || !config.allowedUserIds.includes(sub)) return null;

    return payload;
  } catch {
    return null;
  }
}

認証を通過したPOSTだけがMCPのツール群を呼び出せるという仕組みです。

サーバーの起動の仕方や、ツールの登録方法などは microcms-mcp-server を参考にすると良いと思います(この記事では省略します)。

https://github.com/microcmsio/microcms-mcp-server/blob/main/src/server.ts

Vercelのサーバーレス環境でリモートMCPサーバーを建てる時のポイントは StreamableHTTPTransport を使うことです。

サーバーレス環境ではプロセスを常時起動できないため、リクエストごとにMCPサーバーとtransportを生成して呼び出す構成にしています。

async function handleMcpRequest(c: Context) {
  const server = createMcpServer();
  const transport = new StreamableHTTPTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });

  await server.connect(transport);
  const parsedBody = await c.req.json().catch(() => undefined);
  const response = await transport.handleRequest(c, parsedBody);
  await transport.close();
  await server.close();

  return response ?? c.text("Internal Server Error", 500);
}

これで基本的なOAuthフローの仕組みと処理のフローの解説は以上です。これらの流れを理解してプロンプトにすれば大まかに実装できるかと思います。

これらのOAuthワークフローを守ればセキュアに認証できますが、追加で以下のような対策も検討すると良いと思います。

  • レート制限を入れる。
  • 監査ログを入れる。
  • APIキーはVercelの環境変数のみに置く(基本的に上記の手順でやれば問題ないかと思います)
  • APIキーは分離して、MCPキー用のキーを発行する。不必要な書き込み権限を付与しない。

独自実装のブログ執筆ツールの例

最後に私がどのようなツールを登録しているのか一例を紹介します。

便利なのが下書きを作成するツールです。

mcp-server/src/tools/ryusou/create-blog-draft.ts
export async function handleCreateBlogDraft(params: CreateBlogDraftParams) {
  const htmlBody = bodyFormat === "html" ? body : markdownToHtml(body);
  const created = await createBlogDraft(client, { title, body: htmlBody });

  const draftKey = await getManagementContentDraftKey("blogs", created.id);

  return {
    id: created.id,
    draftKey,
    previewUrl: buildPreviewUrl(config.siteOrigin, created.id, draftKey),
  };
}

レスポンスにpreview用のURLを含めることで、CodexなどのAIクライアント上で下書きプレビューを確認できます。

その他にも画像をアップロードするツールを少しカスタマイズして、画像データのまま投稿したり圧縮するツールなどを登録しています。

またAIクライアントを活用したツールとしてはカテゴリーを付与するツールもあります。

自分のブログでは、microCMSで

{
    "id": "18q267gniw",
    "createdAt": "2026-05-27T14:19:25.602Z",
    "updatedAt": "2026-05-27T14:19:25.602Z",
    "publishedAt": "2026-05-27T14:19:25.602Z",
    "revisedAt": "2026-05-27T14:19:25.602Z",
    "name": "読書",
    "bg": "#F5F1E8",
    "border": "#8D6E63"
}

のようにカラーコードを付与したデータを持っているのですが、これを自動で既存のカテゴリーのカラーを見て自動作成して記事に付与するツールなどを作成しています。

このように通常のAPIだけでは組めないようなワークフローをAIクライアント上で組めるのもリモートMCPサーバーのメリットなのかなと思います。

ただし、公開まではMCPで自動化していません。下書き作成と更新まではAIに任せ、最終的な公開はmicroCMSの管理画面で人間が確認して行う方針にしています。ここは自分のブログでも、公式ブログに載せる記事でも大事な線引きだと思っています。

まとめ

今回のリモートMCPサーバーは、microCMSをAIエージェントから操作するための入口です。ただ、作ってみると単なるAPIラッパーではなく、ブログを書く、画像を入れる、カテゴリーを整える、プレビューで確認する、という運用そのものをツール化する感覚に近いと感じました。

Hono、WorkOS、Vercelを組み合わせることで、個人ブログでも現実的に運用できるリモートMCPサーバーになりました。

AIに何でも任せるというより、人間が確認すべき場所を残したまま、繰り返しの作業をツールに寄せる。

そのためのインターフェースとして、リモートMCPサーバーはかなり使いやすい選択肢だと思います。

参考

まずは、無料で
試してみましょう。

microCMSは無料ではじめられます。
ご不明な点はお気軽にお問い合わせください。