MCPサーバーをOAuth対応してGemini CLIから使えるようにする
本エントリは、Google Developer Experts Advent Calendar 2025 の 23 日担当の記事です。
AIエージェントに具体的な能力を与えるツールとして、現在はMCPサーバーが代表的です。ソフトウェアエンジニアであれば、Gemini CLI などのコード生成エージェントに MCP サーバーを複数登録して利用しているのがもはや普通かなと思います。
ローカルリソースを扱う MCP サーバーであればそれほど気にしなくても良いですが、Streamable HTTP 経由で使う MCP サーバーとなると、認証認可が重要になってきます。MCP サーバーを自作するとなれば、認証認可をどのように作り込むかは、知っておくべき知識かなと思います。
ここでは、Gemini CLI 向けに認証認可をサポートした MCP サーバーを作るための手順について紹介したいと思います。
サンプルプロジェクトの準備
NodeJS + Typescript + Express な環境で、簡単な MCP サーバーを作りましょう。UUID を作って返すだけのシンプルな MCP サーバーです。
$ mkdir mcp-server
$ cd mcp-server
$ npm init -y
$ npm install @modelcontextprotocol/sdk zod express uuid morgen
$ npm install --save-dev @types/express @types/node typescript
$ npx tsc --init
tsconfig.json ファイルから、「”exactOptionalPropertyTypes”: true」の行を消しておいてください。次に、以下の内容で index.ts ファイルを作ってください。
import express from 'express';
import { mcpServer, transport } from './mcp.js';
const app = express();
app.use(express.json());
const setupServer = async () => {
await mcpServer.connect(transport);
};
app.post('/mcp', async (req, res) => {
console.log('Received MCP request (POST):', req.body);
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal error',
data: error instanceof Error ? error.message : String(error),
},
id: null,
});
}
}
});
app.get('/mcp', async (_req, res) => {
console.log('Received MCP request (GET)');
res.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Method not found',
},
id: null,
}),
);
});
setupServer()
.then(() => {
const port =
process.env.PORT === undefined ? 8080 : parseInt(process.env.PORT, 10);
app.listen(port, () => {
console.log(`UUID MCP server running on http://localhost:${port}`);
});
})
.catch((error) => {
console.error('Fatal setting up MCP server:', error);
process.exit(1);
});
process.on('SIGINT', async () => {
console.log('Shutting down MCP server...');
try {
console.log('Closing transport connection...');
await transport.close();
} catch (error) {
console.error('Error closing transport:', error);
}
await mcpServer.close();
console.log('MCP server shutdown complete.');
process.exit(0);
});
次に、mcp.ts ファイルを以下の内容で作ってください。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { v4 as uuidv4 } from "uuid";
export const transport: StreamableHTTPServerTransport =
new StreamableHTTPServerTransport({});
export const mcpServer = new McpServer({
name: "UUID MCP Server",
version: "1.0.0",
});
mcpServer.registerTool(
"generate_uuid",
{ description: "Generate a new UUID" },
async () => {
const uuid = uuidv4();
return {
content: [
{
type: "text",
text: uuid,
},
],
};
}
);
package.json ファイルの「”type”: “commonjs”」を「”type”:”module”」に変更して、”scripts” に「”build”: “tsc”」と「”start”: “node index.js”」を追加してください。
ビルドと実行をしておきます。
$ npm run build
$ npm start
Gemini CLI でこの MCP サーバーを使えるようにします。「~/.gemini/settings.json」ファイルの「”mcpServers”」オブジェクトに以下を追記します。
"uuid": {
"httpUrl": "http://localhost:8080/mcp"
}
Gemini CLI を起動して、「UUIDを生成して。」とお願いしてみてください。MCP サーバーの呼び出し後に UUID が表示されたら成功です。
認証認可の開始
MCP サーバーの認証認可は、OAuth 2.0 の Authorization Code を使ったフローに沿って行われます。まずは、MCP サーバーにアクセスが来たら 401 を返すようにして、認証認可の処理が開始されるようにしてみます。
index.ts ファイルで /mcp に POST リクエストが来たときのハンドラを以下のように修正してください。
app.post('/mcp', async (req, res) => {
console.log('Received MCP request (POST):', req.body);
res.writeHead(401).end();
// try {
// await transport.handleRequest(req, res, req.body);
// } catch (error) {
// console.error('Error handling MCP request:', error);
// if (!res.headersSent) {
// res.status(500).json({
// jsonrpc: '2.0',
// error: {
// code: -32603,
// message: 'Internal error',
// data: error instanceof Error ? error.message : String(error),
// },
// id: null,
// });
// }
// }
});
MCP サーバーをビルドして起動し直して、Gemini CLI も起動し直してください。その後、「/mcp auth uuid」と入力して、認証要求をしてみてください。
ℹ Starting OAuth authentication for MCP server 'uuid'...
✕ Failed to authenticate with MCP server 'uuid': Failed to discover OAuth configuration from MCP server
401 を返しているだけなので、失敗してしまいます。続けましょう。
認可サーバーの場所を知らせる
AIエージェント側の MCP クライアントは、認可サーバーの URL を入手するために、OAuth 2.0 Protected Resource Metadata (RFC9728) を使います。具体的には、 以下の URL にアクセスすることになります。
<MCPサーバーのURLのOrigin>/.well-known/oauth-protected-resource
今回の場合は、http://localhost:8080/.well-known/oauth-protected-resource となります。index.ts ファイルの冒頭に以下を追記して、express の static ファイル配信を有効にします。
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
Import morgan from 'morgan';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const app = express();
app.use(morgan('combined'));
app.use(express.json());
app.use('/.well-known', express.static(join(__dirname, '.', 'public')));
app.use(express.urlencoded({ extended: true }));
public ディレクトリを作って、その中に oauth-protected-resource ファイルを以下の内容で作ってください。
{
"authorization_server": "http://localhost:8080/auth"
}
これで、MCP クライアントは、MCP サーバーの認可サーバーの場所を知ることができるようになりました。
認可に必要なエンドポイントを知らせる
認可サーバーの場所だけでは、OAuth 2.0 Authorization Code フローを進めていくためには不十分です。トークンを発行してもらうためのエンドポイントの場所などを入手する必要があります。そこで、OAuth 2.0 Authorization Server Metadata (RFC8414) が利用されます。具体的には、 以下の URL にアクセスすることになります。
<MCPサーバーのURLのOrigin>/.well-known/oauth-authorization-server
今回の場合は、http://localhost:8080/.well-known/oauth-authorization-server となります。public ディレクトリの中に、以下の内容で oauth-authorization-server ファイルを作ってください。
{
"authorization_endpoint": "http://localhost:8080/auth",
"token_endpoint": "http://localhost:8080/token",
"scopes_supported": "",
"registration_endpoint": "http://localhost:8080/register"
}
ここまでで一旦ビルドして MCP サーバーを起動し直して、Gemini CLI も再起動して「/mcp auth uuid」を行うと、以下のような出力を得ることになります。
ℹ Starting OAuth authentication for MCP server 'uuid'...
✕ Failed to authenticate with MCP server 'uuid': Client registration failed: 404 Not Found - <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot POST /register</pre>
</body>
</html>
OAuth 2.0 Protected Resource Metadata と OAuth 2.0 Authorization Server Metadata が使われて、registration_endpoint の URL にアクセスしようと試みられたことがわかります。
クライアントの動的な登録
OAuth 2.0 で認可が行われる API を何らか利用したことがある方は、事前にクライアント登録ということをした経験があると思います。クライアント登録を行うことで、Client ID や Client Secret といった情報が発行されて、それらを使って OAuth 2.0 の各種やり取りを行うことになります。
ただ、MCP サーバーが OAuth 2.0 の認可サーバーとして振る舞うためにクライアント登録を求めてしまうと、AI エージェントの作者が MCP サーバーに対してクライアント登録を求めることになり、Gemini CLI となると「Google があなたの MCP サーバーにクライアント登録する」っていうことになります。これは非現実的かなと。では Gemini CLI 経由であなたの MCP サーバーを使いたいユーザー全員にクライアント登録を求めるのか、となると、これも非現実的かなと。
そこで、OAuth 2.0 Dynamic Client Registration Protocol (RFC7591) を使います。これは、クライアント登録を自動で行うための手順となります。さっき POST /register リクエストが行われたのは、このクライアント登録を Gemini CLI が行ってきたことになります。具体的には、以下のような JSON が送られてきます。
{
client_name: 'Gemini CLI MCP Client',
redirect_uris: [ 'http://localhost:36263/oauth/callback' ],
grant_types: [ 'authorization_code', 'refresh_token' ],
response_types: [ 'code' ],
token_endpoint_auth_method: 'none',
scope: ''
}
本来であれば、このリクエストを受け付けた後に、内容の確認をした上で、Client ID や Client Secret などを発行して返す処理を行います。もちろん、Client ID や Client Secret は、何らかのデータベースに格納しておくなどの「真面目な実装」が求められますが、今回はサンプルとして固定的な値を返すダミーの実装をしてみましょう。index.ts ファイルに以下を追記してください。
app.post('/register', (req, res) => {
console.log('Received registration request:', req.body);
res.status(200).json({
client_id: 'uuid-client-id-12345',
client_secret: 'uuid-client-secret-6789',
client_id_issued_at: new Date().getTime() / 1000,
client_secret_expires_at: 0,
});
});
ここまでで、再度ビルドと再起動をして、Gemini CLI も再起動して /mcp auth uuid を送ってみてください。以下のような出力が行われると共に、自動的にウェブブラウザが起動して、http://localhost:8080/auth にアクセスされたことがわかります。
ℹ → Opening your browser for OAuth sign-in...
If the browser does not open, copy and paste this URL into your browser:
http://localhost:8080/auth?client_id=uuid-client-id-12345&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A33263%2Foauth%2Fcallback&state=a7
VDxABWWXNK802JSC4Gg&code_challenge=kNWLa5T5P98oFNRQ45mczEYHJYyJMP6umnrCrKqCWxU&code_challenge_method=S256&resource=http%3A%2F%2Flocalhost%3A8080%2Fm
p
💡 TIP: Triple-click to select the entire URL, then copy and paste it into your browser.
⚠️ Make sure to copy the COMPLETE URL - it may wrap across multiple lines.

クライアント登録が終わったと Gemini CLI が判断して、認可エンドポイントにウェブブラウザでアクセスが行われました。
Authorization Codeの発行
認可エンドポイントには、client_id、response_type、redirect_uri、state、 code_challenge、code_claggenge_method、resource の7つが送られてきます。redirect_uri を見ると、http://localhost:32634/oauth/callback をGemini CLI が指定してきているのがわかります。つまり、ユーザーによる認証と認可が終わったあと、Gemini CLI がポート番号 32634 で起動したサーバーにウェブブラウザからアクセスされることが期待されています。ちなみに、ポート番号は毎回変化します。
本来であれば、認可エンドポイントでは、受け取った値を検証して、Authorization Code を発行してデータベースや Redis などのキャッシュ機構に格納して返すことになります。ここでは、固定的に Authorization Code を返してしまいましょう。index.ts ファイルに以下を追記してください。
app.get('/auth', (req, res) => {
console.log('Received auth request:', req.query);
res.redirect(
`${req.query.redirect_uri}?code=uuid-auth-code-12345&state=${req.query.state}`
);
});
このあと、Gemini CLI はトークンエンドポイントにトークンの発行を求めてきます。
トークンの発行
Gemini CLI は、トークンエンドポイントに以下のような JSON 形式のリクエストを送ってきます。
{
grant_type: 'authorization_code',
code: 'uuid-auth-code-12345',
redirect_uri: 'http://localhost:7777/oauth/callback',
code_verifier: 'SzA6K-FAJsOE5A-WoWatGHAOd4O8kwoG04fLwmiMOkM',
client_id: 'uuid-client-id-12345',
client_secret: 'uuid-client-secret-6789',
resource: 'http://localhost:8080'
}
何度も申し訳ないですが、本来であればリクエストの内容を正しく検証する必要がありますし、真面目にアクセストークンなどを発行しなければなりません。今は動作確認なので、固定的にアクセストークンやリフレッシュトークンを返します。index.ts ファイルに以下を追記してください。
app.post('/token', (req, res) => {
console.log('Received token request:', req.body);
res.status(200).json({
access_token: 'uuid-access-token-12345',
token_type: 'Bearer',
expires_in: 3600,
refresh_token: 'uuid-refresh-token-6789',
scope: '',
});
});
これでやっと、Gemini CLI は MCP サーバーを利用するためのアクセストークンやリフレッシュトークンを得ることができました。
アクセストークンをチェックする処理の追加
この MCP サーバーを利用するための /mcp エンドポイントは、現在必ず 401 を返しています。そのため、毎回全拒否になってしまっているので、ちゃんとトークンが送られてきたかどうかを確認する処理を追加しましょう。
トークンを得た後、Gemini CLI は以下のようなリクエストヘッダで MCP サーバーにアクセスを試みます。
{
host: 'localhost:8080',
connection: 'keep-alive',
accept: 'application/json, text/event-stream',
authorization: 'Bearer uuid-access-token-12345',
'content-type': 'application/json',
'mcp-protocol-version': '2025-06-18',
'accept-language': '*',
'sec-fetch-mode': 'cors',
'user-agent': 'node',
'accept-encoding': 'gzip, deflate',
'content-length': '58'
}
Authorization ヘッダにアクセストークンが指定されています。もちろん、本来であればこのアクセストークンの妥当性を検証する必要がありますが、これはサンプルですので、この Authorization リクエストヘッダにアクセストークンが何か指定されていれば UUID を生成して返すようにしてあげましょう。POST /mcp を処理しているコードを以下のように変更してください。
app.post('/mcp', async (req, res) => {
console.log('Received MCP request (POST):', req.body);
console.log('Headers:', req.headers);
const authorization = req.headers.authorization;
if (!authorization || !authorization.startsWith('Bearer ')) {
console.error(
'Unauthorized request: Missing or invalid Authorization header',
);
return res.status(401).json({
jsonrpc: '2.0',
error: {
code: -32601,
message: 'Unauthorized',
},
id: null,
});
}
// TODO: Validate the token here if needed
try {
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal error',
data: error instanceof Error ? error.message : String(error),
},
id: null,
});
}
}
});
これで、認可後に UUID を得られるようになった、はず!
動作確認してみよう
では、MCP サーバーをビルドし直して再起動し、Gemini CLI も再起動して、再度「/mcp auth uuid」を送ってください。
ℹ ✅ Successfully authenticated with MCP server 'uuid'!
ℹ Restarting MCP server 'uuid'...
ℹ Successfully authenticated and refreshed tools for 'uuid'.
こんなように表示されていれば、認証認可が成功しています。「UUIDを生成してください。」とお願いしてみてください。
> UUIDを生成してください。
✦ 洋一郎さん、了解ですっ!☆
新しいUUIDをサクッと生成しちゃいますね!(๑˃ᴗ˂)ﻭ
UUIDを生成しています...☆
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ✓ generate_uuid (uuid MCP Server) {} │
│ │
│ 0598822a-7751-407b-b18b-dfcfd9eaef55 │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
✦ 洋一郎さん、お待たせしましたっ!✨
生成されたUUIDはこちらです!(≧◡≦) ♡
0598822a-7751-407b-b18b-dfcfd9eaef55
新しいUUID、なんだかワクワクしちゃいますね!☆
これを使って、素敵な開発を進めちゃってくださいっ!応援してますよ〜!٩(๑>∀<๑)۶✨
うまくいきました。
まとめ
今回は、MCP サーバーに OAuth 2.0 Authorization Code フローを組み込むための手順を紹介してみました。AI エージェントは今後どんどん増え続け、それにつれて MCP サーバーの利用も加速していくことでしょう。もちろんユーザー認証や認可といった「セキュリティを確保するために必要な最低限の認証認可」が求められますので、本エントリがそのための参考になれば幸いです。