IBM Verify SaaS の MCP サーバーをローカルに構築してみた
はじめに
IBM Verify SaaS は、IBM が提供するクラウドベースの IAM(Identity and Access Management)プラットフォームです。SCIM 2.0 準拠の REST API を持っており、ユーザー管理やアクセス制御を自動化できます。
もちろん公式ドキュメントをBobに入れて勝手にMCPサーバーを構築してもらうという手もありますが、今回はあえてドキュメントベースにMCPサーバー構築時のステップを解説します。
本記事では、IBM Verify SaaS の SCIM API を操作する MCP(Model Context Protocol)サーバー をローカル PC 上に構築する手順を解説します。MCP サーバーを作ることで、Claude Desktop や IBM Bob などの AI エージェントから自然言語で Verify のユーザー管理操作(一覧取得・作成・更新・削除・Excel 一括登録)ができるようになります。
参考公式ドキュメント
完成イメージ
AI エージェント(Claude / IBM Bob)
↓ MCP protocol (stdio)
mcp-verify-users(ローカル Node.js プロセス)
↓ SCIM 2.0 REST API + OAuth 2.0
IBM Verify SaaS テナント
構築後は以下のような自然言語命令が使えるようになります。
- 「Verify のユーザー一覧を取得して」
- 「yamada@example.com というユーザーを作成して」
- 「この Excel ファイルのユーザーを一括登録して」
前提条件
| 項目 | 内容 |
|---|---|
| OS | Windows 10/11、macOS、Linux |
| Node.js | v18 以上(node --version で確認) |
| npm | v9 以上(Node.js に同梱) |
| IBM Verify SaaS | テナント管理者権限があるアカウント |
Step 1: IBM Verify SaaS の API クライアントを作成する
MCP サーバーが IBM Verify API を呼び出すためには、OAuth 2.0 クライアントクレデンシャルが必要です。
1-1. Verify 管理コンソールにログイン
https://<your-tenant>.verify.ibm.com にアクセスし、管理者アカウントでログインします。
1-2. API クライアントの作成
- 左メニューから 「セキュリティ」→「API アクセス」 を選択
- 「API クライアントの追加」 ボタンをクリック
- 以下を設定して保存
| 設定項目 | 値 |
|---|---|
| 名前 |
mcp-verify-users(任意) |
| クライアント ID | 自動生成(後でコピー) |
| クライアントシークレット | 自動生成(後でコピー) |
| 権限 |
manageUsers、readUsers にチェック |
⚠️ クライアントシークレットは作成時にしか表示されません。 必ずコピーして安全な場所に保存してください。
1-3. 取得した情報をメモ
以下の 3 つの値を控えておきます。
VERIFY_TENANT_URL = https://<your-tenant>.verify.ibm.com
VERIFY_CLIENT_ID = <クライアント ID>
VERIFY_CLIENT_SECRET = <クライアントシークレット>
Step 2: プロジェクトを作成する
2-1. ディレクトリ作成と初期化
mkdir mcp-verify-users
cd mcp-verify-users
npm init -y
2-2. 依存パッケージのインストール
npm install @modelcontextprotocol/sdk zod xlsx
npm install -D typescript @types/node
| パッケージ | 用途 |
|---|---|
@modelcontextprotocol/sdk |
MCP サーバー実装の SDK |
zod |
ツールの入力スキーマ定義・バリデーション |
xlsx |
Excel ファイルの読み込み(一括登録機能) |
typescript |
TypeScript コンパイラ |
2-3. TypeScript の設定
tsconfig.json をプロジェクトルートに作成します。
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
2-4. package.json を編集
package.json を以下のように修正します。
{
"name": "mcp-verify-users",
"version": "1.0.0",
"description": "IBM Verify User Management MCP Server",
"type": "module",
"main": "build/index.js",
"scripts": {
"build": "tsc"
},
"bin": {
"mcp-verify-users": "./build/index.js"
}
}
"type": "module" は ES Modules を使用するために必須です。
Step 3: MCP サーバーの実装
src/index.ts を作成し、以下のコードを記述します。
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as XLSX from "xlsx";
import * as fs from "fs";
import * as path from "path";
// ── 環境変数から認証情報を読み込む ──────────────────────────
const TENANT_URL = process.env.VERIFY_TENANT_URL;
const CLIENT_ID = process.env.VERIFY_CLIENT_ID;
const CLIENT_SECRET = process.env.VERIFY_CLIENT_SECRET;
if (!TENANT_URL || !CLIENT_ID || !CLIENT_SECRET) {
console.error(
"Error: VERIFY_TENANT_URL, VERIFY_CLIENT_ID, VERIFY_CLIENT_SECRET are required"
);
process.exit(1);
}
const server = new McpServer({ name: "mcp-verify-users", version: "1.0.0" });
// ── OAuth 2.0 アクセストークン取得 ──────────────────────────
async function getAccessToken(): Promise<string> {
const url = `${TENANT_URL}/oauth2/token`;
const body = new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID!,
client_secret: CLIENT_SECRET!,
});
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token request failed: ${res.status} ${text}`);
}
const data = (await res.json()) as { access_token: string };
return data.access_token;
}
// ── SCIM API 共通リクエスト ──────────────────────────────────
async function verifyRequest(
method: string,
path: string,
body?: unknown
): Promise<unknown> {
const token = await getAccessToken();
const url = `${TENANT_URL}${path}`;
const res = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/scim+json",
Accept: "application/scim+json",
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (!res.ok) {
throw new Error(`Verify API error: ${res.status} ${text}`);
}
return text ? JSON.parse(text) : {};
}
ツール登録
続いて、AI エージェントが呼び出せる各ツールを registerTool で登録します。
① ユーザー一覧取得
server.registerTool(
"list_users",
{
description: "IBM Verify テナントのユーザー一覧を取得します",
inputSchema: z.object({
count: z.number().optional().describe("取得件数(デフォルト: 20)"),
startIndex: z.number().optional().describe("開始インデックス(デフォルト: 1)"),
filter: z.string().optional().describe('SCIM フィルター例: "userName eq \\"john\\""'),
}),
},
async ({ count = 20, startIndex = 1, filter }) => {
const params = new URLSearchParams({
count: String(count),
startIndex: String(startIndex),
});
if (filter) params.set("filter", filter);
const data = await verifyRequest("GET", `/v2.0/Users?${params}`);
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
}
);
② ユーザー作成
server.registerTool(
"create_user",
{
description: "IBM Verify に新しいユーザーを作成します",
inputSchema: z.object({
userName: z.string().describe("ユーザー名(メールアドレス推奨)"),
givenName: z.string().describe("名(First name)"),
familyName: z.string().describe("姓(Last name)"),
email: z.string().describe("メールアドレス"),
userPassword: z.string().optional().describe("初期パスワード(省略時は招待メール送信)"),
}),
},
async ({ userName, givenName, familyName, email, userPassword }) => {
const body: Record<string, unknown> = {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
userName,
name: { givenName, familyName },
emails: [{ value: email, type: "work", primary: true }],
};
if (userPassword) body["password"] = userPassword;
const data = await verifyRequest("POST", "/v2.0/Users", body);
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
}
);
③ ユーザー更新(SCIM PATCH)
server.registerTool(
"update_user",
{
description: "既存ユーザーの情報を更新します(SCIM PATCH)",
inputSchema: z.object({
userId: z.string().describe("更新対象のユーザー ID"),
givenName: z.string().optional().describe("新しい名(First name)"),
familyName: z.string().optional().describe("新しい姓(Last name)"),
email: z.string().optional().describe("新しいメールアドレス"),
active: z.boolean().optional().describe("アカウントの有効/無効"),
}),
},
async ({ userId, givenName, familyName, email, active }) => {
const operations: unknown[] = [];
if (givenName !== undefined)
operations.push({ op: "replace", path: "name.givenName", value: givenName });
if (familyName !== undefined)
operations.push({ op: "replace", path: "name.familyName", value: familyName });
if (email !== undefined)
operations.push({ op: "replace", path: 'emails[type eq "work"].value', value: email });
if (active !== undefined)
operations.push({ op: "replace", path: "active", value: active });
const body = {
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
Operations: operations,
};
const data = await verifyRequest("PATCH", `/v2.0/Users/${userId}`, body);
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
}
);
④ Excel 一括ユーザー登録
Excel ファイルの列構成(1 行目はヘッダー):
| userName | givenName | familyName | userPassword | |
|---|---|---|---|---|
| john@example.com | John | Doe | john@example.com | Pass1234! |
server.registerTool(
"bulk_create_users",
{
description: "ExcelファイルからIBM Verifyにユーザーを一括登録します",
inputSchema: z.object({
filePath: z.string().describe("Excelファイルのパス(絶対パスまたは相対パス)"),
}),
},
async ({ filePath }) => {
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`ファイルが見つかりません: ${resolvedPath}`);
}
const workbook = XLSX.readFile(resolvedPath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: "" });
// ... 各行をループして verifyRequest("POST", "/v2.0/Users", body) を呼び出す
}
);
⑤ ユーザー削除
server.registerTool(
"delete_user",
{
description: "指定した ID のユーザーを削除します",
inputSchema: z.object({
userId: z.string().describe("削除対象のユーザー ID"),
}),
},
async ({ userId }) => {
await verifyRequest("DELETE", `/v2.0/Users/${userId}`);
return { content: [{ type: "text" as const, text: `ユーザー ${userId} を削除しました` }] };
}
);
サーバー起動処理
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("mcp-verify-users running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});
Step 4: ビルドする
npm run build
build/index.js が生成されれば成功です。
mcp-verify-users/
├── build/
│ └── index.js ← コンパイル済みファイル
├── src/
│ └── index.ts
├── package.json
└── tsconfig.json
Step 5: AI エージェントに MCP サーバーを登録する
IBM Bob の場合(.bob/mcp.json)
プロジェクトルートの .bob/mcp.json、またはグローバル設定 ~/.bob/mcp.json に以下を追記します。
{
"mcpServers": {
"mcp-verify-users": {
"command": "node",
"args": ["/absolute/path/to/mcp-verify-users/build/index.js"],
"env": {
"VERIFY_TENANT_URL": "https://<your-tenant>.verify.ibm.com",
"VERIFY_CLIENT_ID": "<クライアント ID>",
"VERIFY_CLIENT_SECRET": "<クライアントシークレット>"
}
}
}
}
ポイント:
argsのパスは 絶対パス を指定してください。Windows ではC:\\Users\\...のようにバックスラッシュをエスケープするか、フォワードスラッシュC:/Users/...を使用します。
Claude Desktop の場合(claude_desktop_config.json)
{
"mcpServers": {
"mcp-verify-users": {
"command": "node",
"args": ["C:/path/to/mcp-verify-users/build/index.js"],
"env": {
"VERIFY_TENANT_URL": "https://<your-tenant>.verify.ibm.com",
"VERIFY_CLIENT_ID": "<クライアント ID>",
"VERIFY_CLIENT_SECRET": "<クライアントシークレット>"
}
}
}
}
設定ファイルの場所:
-
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json -
Windows:
%APPDATA%\Claude\claude_desktop_config.json
Step 6: 動作確認
AI エージェントに以下のように話しかけてみましょう。
Verify のユーザー一覧を 5 件取得して
正常に動作していれば、SCIM API のレスポンスが JSON 形式で返ってきます。
{
"totalResults": 42,
"itemsPerPage": 5,
"startIndex": 1,
"Resources": [
{
"id": "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"userName": "yamada@example.com",
"name": {
"givenName": "太郎",
"familyName": "山田"
},
...
}
]
}
実装のポイント解説
なぜ stdio トランスポートを使うのか
MCP には stdio(標準入出力)と HTTP の 2 つのトランスポートがあります。ローカル PC での構築には stdio が推奨されます。
| stdio | HTTP | |
|---|---|---|
| セットアップ | 簡単(HTTP サーバー不要) | HTTP サーバーが必要 |
| セキュリティ | 高い(ネットワーク露出なし) | 別途 認証が必要 |
| 遅延 | 低い | ネットワーク分のオーバーヘッド |
OAuth 2.0 クライアントクレデンシャルフロー
IBM Verify SaaS の API 認証には OAuth 2.0 の Client Credentials フロー を使用します。ユーザー介在なしにサービス間認証ができるため、自動化・バックエンド連携に適しています。
MCP サーバー → POST /oauth2/token (client_id + client_secret)
← access_token(有効期限あり)
MCP サーバー → GET /v2.0/Users (Authorization: Bearer <token>)
← SCIM レスポンス
アクセストークンには有効期限があります。本実装では各 API 呼び出しの都度トークンを取得しているため、期限切れの心配は不要ですが、高頻度な呼び出しではトークンをキャッシュする最適化も検討してください。
SCIM 2.0 のエンドポイント
IBM Verify は SCIM 2.0 標準に準拠しており、以下のエンドポイントを利用しています。
| 操作 | メソッド | エンドポイント |
|---|---|---|
| 一覧取得 | GET | /v2.0/Users |
| 詳細取得 | GET | /v2.0/Users/{id} |
| 作成 | POST | /v2.0/Users |
| 更新 | PATCH | /v2.0/Users/{id} |
| 削除 | DELETE | /v2.0/Users/{id} |
トラブルシューティング
Error: VERIFY_TENANT_URL, VERIFY_CLIENT_ID, VERIFY_CLIENT_SECRET are required
環境変数が設定されていません。MCP 設定ファイルの env セクションを確認してください。
Token request failed: 401
クライアント ID またはクライアントシークレットが誤っています。Verify 管理コンソールで再確認してください。
Verify API error: 403
API クライアントに必要な権限(manageUsers、readUsers)が付与されていません。Step 1-2 を再確認してください。
build/index.js が見つからない
npm run build を実行してビルドを完了させてください。tsconfig.json の outDir が ./build になっているか確認してください。
まとめ
IBM Verify SaaS の SCIM API を操作する MCP サーバーをローカル PC 上に構築することがそこまで難しいことではないですが、MCP側のツールインストールや、Verify API側の権限設計が重要です。
VerifyにおけるMCPサーバーの権限管理について次回の記事に開設したいと思います。