21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Bedrock AgentCore Gateway × Keycloak]MCP 2025-11-25 認可仕様に準拠したKeycloakでCIMDと認可コードフローを利用したMCP認可を試してみた

21
Posted at

はじめに

こんにちは、最近実家の近くにラ・ムーができた株式会社 日立製作所の三本康貴です。
近年、AI Agent、Model Context Protocol(MCP)の普及に伴い、MCP Serverの開発、公開も増える一方で、既存のMCP Serverを統合したり、REST APIをMCP Serverとして管理するゲートウェイ機能の需要や認証認可の重要性が高まっています。
本記事では、MCPゲートウェイサービスである「Amazon Bedrock AgentCore Gateway」と認証認可サーバの「Keycloak」を組み合わせて、最新(2026年6月現在)のMCP 2025-11-25 認可仕様に準拠したMCP認可を検証していきます。

Amazon Bedrock AgentCore Gatewayとは

Amazon Bedrock AgentCore Gateway(以下、AgentCore Gateway) は、AWSが提供するAI Agent が外部ツールや既存システムへ安全に接続するためのマネージドなゲートウェイサービスです。
本検証では、次の機能を利用します。

  • ツール統合
    REST API、MCP Server、Lambda、API Gateway などの既存のサービスを単一のMCPエンドポイントに統合
  • 包括的な認可
    AI AgentやMCP Clientの身元確認や外部ツールへの接続を管理

AgentCore Gatewayの認可機能によって、AI AgentやMCP ClientなどがAgentCore Gatewayを呼び出す際に付与されるJWT、IAM identityを利用して呼び出し元が正当な利用者またはアプリケーションであることを検証することができます。JWT検証では、クレーム、スコープを検証します。AgentCore Gatewayと連携する認証認可サーバは、Amazon Cognito等の特定の認証認可サーバに依存せず、OAuth対応であれば連携可能です。

参考:

KeycloakとMCP認可

Keycloakは、OAuth 2.1 準拠の認証認可サーバのOSSであり、MCP認可サーバとして使用することが可能です。
MCP仕様では、認可サーバに対して複数のOAuth関連標準への対応が要求されています。Keycloakの対応バージョンと対応状況は、次の通りです。

MCPバージョン Keycloak対応状況
2025-03-26 対応
2025-06-18 RFC 8707 Resource Indicators未対応のため部分対応 ※
2025-11-25 RFC 8707 Resource Indicators未対応のため部分対応 ※
標準仕様 MCP側の要求(2025-11-25) Keycloak対応
OAuth 2.1 Authorization Framework MUST 対応
OAuth 2.0 Authorization Server Metadata / RFC 8414 MUST 対応
OAuth 2.0 Resource Indicators / RFC 8707 MUST 未対応
OAuth 2.0 Dynamic Client Registration / RFC 7591 MAY 対応
OAuth Client ID Metadata Document / CIMD SHOULD 対応(ただし実験的)

Keycloakは、OAuth 2.1 に準拠するほか、最新バージョン26.6(2026年6月現在)ではCIMDを実験的にサポートしています。
※ 公式リリースでは未対応になっているRFC 8707 Resource Indicatorsについても、OAuth 2.0の scope を利用したワークアラウンドが示されています。また、初期実装のPR(Initial experimental support for Resource Indicators#46763)は既に取り込まれており、まもなく対応されることが期待されます。

参考:

OAuth 2.1と認可コードフロー

OAuth 2.1については、弊社の黒坂さんが執筆した「OAuth 2.1」の認可コードフローを「Keycloak」で実装しようの説明が分かりやすいので引用します。

(引用開始)---
「OAuth 2.1」は、業界標準の認可プロトコルである「OAuth 2.0」の後継として提案されている仕様です。OAuth 2.0は柔軟性が高い反面、不適切な実装が生じる可能性がありました。OAuth 2.1は、10年以上に及ぶOAuth 2.0の運用から得たセキュリティのベストプラクティスを統合し、開発者が安全な実装をしやすくする目的で提案されています。

OAuth 2.1における主な変更点を以下にまとめます。

  • 認可コードフローにおけるPKCE(Proof Key for Code Exchange)の必須化
  • リダイレクトURIの完全一致の必須化
  • Implicit Grantの廃止
  • Resource Owner Password Credentials Grantの廃止
  • URIのクエリでのBearerトークンの送信の禁止
  • パブリッククライアントに向けたリフレッシュトークンは、送信者制約またはワンタイム使用にすることを必須化

OAuth 2.1はOAuth 2.0のセキュリティを強化したサブセットであり、これにより開発者は安全な実装が可能になります。
(引用終了)---

認可コードフローは、次の流れでAccess Tokenを取得します。

  1. クライアントがcode_verifierを生成し、そこからcode_challengeを作ります。
  2. クライアントはユーザエージェント、通常はブラウザを認可エンドポイントへリダイレクトします。
  3. 認可リクエストにはresponse_type=code、client_id、redirect_uri、scope、state、code_challenge、code_challenge_methodを含めます。
  4. 認可サーバはユーザを認証し、同意・認可判断を行います。
  5. 認可されると、認可サーバはredirect_uricodestateを付けてリダイレクトします。
  6. クライアントはトークンエンドポイントにgrant_type=authorization_code、code、code_verifierを送ります。
  7. 認可サーバは認可コード、クライアント、code_verifierを検証し、問題なければAccess Tokenを返します。

参考:

CIMDとは

CIMDは、OAuthのクライアント登録方法の1つで、クライアントとサーバの間に事前の関係がない、つまりAI AgentなどのクライアントをMCP認可サーバに事前登録していない場合でも、OAuthのフローを開始できるようになります。
MCPは、OAuthのクライアント登録方法について次の3つをサポートします。

  • CIMD
    クライアントとサーバの間に事前の関係がない場合。
  • Pre-registration / 事前登録
    クライアントとサーバの間に既存の関係がある場合。
  • DCR(Dynamic Client Registration)/ 動的クライアント登録
    後方互換性、または特定要件のために使用します。

CIMDは client_id 自体を HTTPS URL にして、そのURLで公開されるJSONメタデータを認可サーバが取得してクライアント情報として扱う方式です。

インフォメーション
本記事では、CIMDについてdraft-ietf-oauth-client-id-metadata-document-01 を前提に説明します。CIMDはInternet-Draft段階の仕様であり、今後の改訂により内容が変更される可能性があります。

クライアント登録を踏まえたMCPの認可コードフローのシーケンス図を示します。

authz-flow-step.png

※ シーケンス図はAuthorization Flow Stepsより引用

参考:Authorization - Model Context Protocol

改めて、本記事では、AgentCore GatewayとKeycloakを組み合わせて、

  • CIMDを利用してクライアントを事前登録することなくOAuth 2.1 準拠の認可コードフローでKeycloakからAccess Tokenを取得できること
  • 取得したAccess TokenでAgentCore Gatewayにアクセスできること

を検証します。


検証構成

AWS構成

今回の検証の主なAWS構成を示します。

aws-system-architecture.drawio.png

※1 Keycloak、CIMD metadata、MCP Serverを同一EC2にホストし、同一internal ALBでルーティングします。可読性を考慮して構成図は分けて図示しています。
※2 Public subnet, Elastic IP, Nat GatewayはEC2のセットアップ時に利用しますが、検証の通信には利用しないため図を省略しています。

AWSサービス一覧

分類 AWSサービス 用途
DNS Route 53 Public Hosted Zone ACM public certificateのDNS検証
DNS Route 53 Private Hosted Zone VPC内でKeycloakcimdmcp-serverのドメイン名解決
TLS AWS Certificate Manager internal ALBにpublic certificateを設定
Network VPC / Private Subnet EC2、ALB、VPC Endpointを配置
Network Internal Application Load Balancer Keycloak、CIMD、MCP ServerのHTTPS入口
Network SSM用VPC Endpoint EC2にSSM経由でログイン、ポートフォワードでRDP接続
Network AgentCore Gateway用VPC Endpoint VPC内のMCP InspectorからAgentCore Gatewayにアクセス
Compute EC2 Linux Keycloak、CIMD nginx、MCP Serverをホスト
Compute EC2 Windows MCP Inspectorを実行する踏み台
Access Systems Manager Session Manager EC2への接続、RDPポートフォワード
MCP Gateway AgentCore Gateway MCP Client向け入口、JWT検証、Target集約

ドメイン設定

今回は次のドメインを登録して使用しました。別のドメインで検証する場合は、置き換えて読み進めてください。

agentcore-keycloak-example.com

サブドメインは次のように使います。

auth.agentcore-keycloak-example.com(Keycloak用)
cimd.agentcore-keycloak-example.com(CIMD用)
mcp-server.agentcore-keycloak-example.com(MCPサーバ用)

ACM public certificateは、次のようなワイルドカード証明書を利用します。

*.agentcore-keycloak-example.com

KeycloakはVPC内private resourceですが、AgentCore IdentityがOIDC Discovery URLをHTTPSで検証する必要があるため、internal ALBにpublic ACM certificateを付与します。

参考: AgentCore Identity private IdP

登場人物

OAuth / MCPのロール 今回の実体 説明
Resource Owner エンドユーザ Keycloakにログインし、MCP Clientにアクセスを許可する
OAuth Client / MCP Client MCP Inspector KeycloakからAccess Tokenを取得し、AgentCore Gatewayを呼び出す
Authorization Server Keycloak 認可コードフローを担当する
Protected MCP Server / Resource Server AgentCore Gateway Keycloak JWTを検証し、MCPエンドポイントを提供する
Gateway Target ALB + EC2上のMCP Server 実際のMCP toolsを提供する

検証

前提条件

ローカルPCに次のソフトウェアをインストールします。

  • AWS CLI
  • Session Manager Plugin

IAMの権限は必要に応じて付与してください。筆者はAdministratorAccess相当の権限で検証しています。

参考:

事前準備

次の事前準備を行います。自分で検証される方は必要に応じて参考にしてください。

  1. ドメイン登録とパブリックホストゾーン作成
  2. Windows踏み台EC2のkey-pair作成
  3. CloudFormationで検証環境を作成(AgentCore Gatewayを除く)
  4. EC2上でKeycloak、CIMD nginx、MCP Serverを起動
  5. Windows踏み台EC2にNode.jsをインストール
事前準備

1. ドメイン登録とパブリックホストゾーン作成

パブリックACM証明書をDNS検証するため、Route53を利用してドメインagentcore-keycloak-example.com(任意のドメイン名)の登録とパブリックホストゾーンを作成します。ドメインをRoute 53に登録する際に、そのドメインのホストゾーンが自動的に作成されます。既存のドメインとホストゾーンを利用する場合は、この手順スキップしてください。

参考:新しいドメインの登録

2. Windows踏み台EC2のkey-pair作成

Windows踏み台EC2のAdministratorパスワード復号のためのkey-pairを作成します。既存のkey-pairを利用する場合は、この手順をスキップしてください。

# コマンド例
# Windows踏み台EC2で使うEC2 Key Pairを作成し、秘密鍵をローカルファイルに保存
aws ec2 create-key-pair \
  --region ap-northeast-1 \
  --key-name mcp-keycloak-agentcore-poc-key \
  --key-type rsa \
  --key-format pem \
  --query 'KeyMaterial' \
  --output text > mcp-keycloak-agentcore-poc-key.pem

参考:Amazon EC2 インスタンスのキーペアを作成する

3. CloudFormationで検証環境を作成(AgentCore Gatewayを除く)

CloudFormationでAgentCore Gatewayを除く検証環境を作成します。CloudFormationテンプレートとデプロイコマンド例を示します。

CloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: >
  AgentCore Gateway foundation for Keycloak + CIMD nginx + EC2-hosted MCP server + Windows bastion MCP client.
  Assumes a public domain and an existing Route 53 public hosted zone for ACM DNS validation.
  Creates a public ACM certificate for the internal ALB, VPC, NAT, SSM endpoints, an AgentCore Gateway VPC endpoint,
  Route 53 private hosted zone, internal ALB routes for auth/cimd/mcp-server, EC2 instances, EC2 IAM role/instance profile, and security groups.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Project
        Parameters:
          - ProjectName
          - EnvironmentName
      - Label:
          default: Network
        Parameters:
          - VpcCidr
          - PublicSubnet1Cidr
          - PrivateSubnet1Cidr
          - PrivateSubnet2Cidr
      - Label:
          default: DNS and TLS
        Parameters:
          - HostedZoneName
          - PublicHostedZoneId
          - AuthHostname
          - CimdHostname
          - McpServerHostname
      - Label:
          default: EC2 access
        Parameters:
          - KeyPairName
      - Label:
          default: AgentCore PrivateLink
        Parameters:
          - AgentCoreGatewayEndpointPolicyResourceArn
    ParameterLabels:
      PublicHostedZoneId:
        default: Existing Route 53 public hosted zone ID for ACM DNS validation
      KeyPairName:
        default: Existing EC2 key pair used to decrypt the Windows Administrator password
      AgentCoreGatewayEndpointPolicyResourceArn:
        default: AgentCore Gateway ARN allowed by the VPC endpoint policy

Parameters:
  ProjectName:
    Type: String
    Default: mcp-keycloak-agentcore
    AllowedPattern: '^[a-zA-Z0-9-]+$'
  EnvironmentName:
    Type: String
    Default: poc
    AllowedPattern: '^[a-zA-Z0-9-]+$'

  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
  PublicSubnet1Cidr:
    Type: String
    Default: 10.0.0.0/24
  PrivateSubnet1Cidr:
    Type: String
    Default: 10.0.10.0/24
  PrivateSubnet2Cidr:
    Type: String
    Default: 10.0.11.0/24

  HostedZoneName:
    Type: String
    Default: agentcore-keycloak-example.com
    Description: DNS zone name used for the VPC private hosted zone and ACM wildcard certificate. Do not include a trailing dot.
  PublicHostedZoneId:
    Type: AWS::Route53::HostedZone::Id
    Description: Existing Route 53 public hosted zone ID used by ACM DNS validation. The zone must cover HostedZoneName.
  AuthHostname:
    Type: String
    Default: auth.agentcore-keycloak-example.com
  CimdHostname:
    Type: String
    Default: cimd.agentcore-keycloak-example.com
  McpServerHostname:
    Type: String
    Default: mcp-server.agentcore-keycloak-example.com

  KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Required to retrieve/decrypt the default Windows Administrator password for RDP.

  AgentCoreGatewayEndpointPolicyResourceArn:
    Type: String
    Default: '*'
    Description: >-
      AgentCore Gateway ARN allowed by the AgentCore Gateway interface VPC endpoint policy.
      Keep '*' until the gateway ARN is known, then narrow it to arn:aws:bedrock-agentcore:region:account-id:gateway/gateway-id.

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpc'

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-igw'

  VpcGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Ref PublicSubnet1Cidr
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-public-a'

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Ref PrivateSubnet1Cidr
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-private-a-primary'

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Ref PrivateSubnet2Cidr
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-private-b-alb-only'

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-public-rt'

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: VpcGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  NatEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-nat-eip'

  NatGateway:
    Type: AWS::EC2::NatGateway
    DependsOn: VpcGatewayAttachment
    Properties:
      AllocationId: !GetAtt NatEip.AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-nat'

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-private-rt'

  PrivateDefaultRouteViaNat:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Internal ALB security group
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-alb-sg'

  KeycloakCimdMcpServerEc2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 security group for Keycloak, CIMD nginx, and lightweight MCP server
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2-sg'

  WinBastionMcpClientEc2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Windows bastion MCP client EC2 security group; no inbound required for SSM port forwarding
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2-sg'

  AgentCorePrivateIdpSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Attach this SG to AgentCore Identity private IdP managed VPC resource. ALB allows HTTPS from this SG.
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-private-idp-sg'

  AgentCoreGatewayMcpServerEgressSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Attach this SG to AgentCore Gateway VPC egress private endpoint for EC2-hosted MCP server target.
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-gateway-egress-sg'

  AlbIngressFromWinBastionMcpClient:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AlbSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
      Description: HTTPS from Windows bastion MCP client

  AlbIngressFromKeycloakCimdMcpServerEc2:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AlbSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
      Description: HTTPS from keycloak-cimd-mcp-server EC2 for local validation

  AlbIngressFromAgentCorePrivateIdp:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AlbSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !Ref AgentCorePrivateIdpSecurityGroup
      Description: HTTPS from AgentCore Identity private IdP managed VPC resource

  AlbIngressFromAgentCoreGatewayMcpServerEgress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AlbSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !Ref AgentCoreGatewayMcpServerEgressSecurityGroup
      Description: HTTPS from AgentCore Gateway VPC egress private endpoint to EC2-hosted MCP target

  KeycloakCimdMcpServerEc2IngressKeycloakFromAlb:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
      IpProtocol: tcp
      FromPort: 8080
      ToPort: 8080
      SourceSecurityGroupId: !Ref AlbSecurityGroup
      Description: Keycloak HTTP from internal ALB

  KeycloakCimdMcpServerEc2IngressCimdFromAlb:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
      IpProtocol: tcp
      FromPort: 4000
      ToPort: 4000
      SourceSecurityGroupId: !Ref AlbSecurityGroup
      Description: CIMD nginx from internal ALB

  KeycloakCimdMcpServerEc2IngressMcpServerFromAlb:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
      IpProtocol: tcp
      FromPort: 8000
      ToPort: 8000
      SourceSecurityGroupId: !Ref AlbSecurityGroup
      Description: Lightweight MCP server from internal ALB

  VpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: VPC endpoint security group for private Systems Manager connectivity
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourceSecurityGroupId: !Ref KeycloakCimdMcpServerEc2SecurityGroup
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-sg'

  AgentCoreGatewayVpcEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: VPC endpoint security group for Amazon Bedrock AgentCore Gateway PrivateLink access
      VpcId: !Ref Vpc
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-agentcore-gateway-vpce-sg'

  AgentCoreGatewayVpceIngressFromWinBastionMcpClient:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref AgentCoreGatewayVpcEndpointSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      SourceSecurityGroupId: !Ref WinBastionMcpClientEc2SecurityGroup
      Description: HTTPS from Windows bastion MCP client to AgentCore Gateway interface VPC endpoint

  SsmEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref Vpc
      VpcEndpointType: Interface
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref VpcEndpointSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ssm'

  SsmMessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref Vpc
      VpcEndpointType: Interface
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref VpcEndpointSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ssmmessages'

  Ec2MessagesEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref Vpc
      VpcEndpointType: Interface
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref VpcEndpointSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-ec2messages'

  AgentCoreGatewayEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref Vpc
      VpcEndpointType: Interface
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.bedrock-agentcore.gateway'
      PrivateDnsEnabled: true
      SubnetIds:
        - !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref AgentCoreGatewayVpcEndpointSecurityGroup
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowInvokeGatewayThroughThisEndpoint
            Effect: Allow
            Principal: '*'
            Action:
              - bedrock-agentcore:InvokeGateway
            Resource: !Ref AgentCoreGatewayEndpointPolicyResourceArn
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-vpce-agentcore-gateway'

  PublicAlbCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Sub '*.${HostedZoneName}'
      ValidationMethod: DNS
      DomainValidationOptions:
        - DomainName: !Sub '*.${HostedZoneName}'
          HostedZoneId: !Ref PublicHostedZoneId
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-alb-public-cert'

  PrivateHostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref HostedZoneName
      VPCs:
        - VPCId: !Ref Vpc
          VPCRegion: !Ref AWS::Region
      HostedZoneConfig:
        Comment: !Sub '${ProjectName}-${EnvironmentName} private hosted zone'
      HostedZoneTags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-private-zone'

  InternalAlb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internal
      Type: application
      IpAddressType: ipv4
      Subnets:
        - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
      SecurityGroups:
        - !Ref AlbSecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-internal-alb'

  KeycloakTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref Vpc
      Protocol: HTTP
      Port: 8080
      TargetType: instance
      HealthCheckProtocol: HTTP
      HealthCheckPath: /
      Matcher:
        HttpCode: '200-399'
      Targets:
        - Id: !Ref KeycloakCimdMcpServerEc2
          Port: 8080
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-tg-keycloak'

  CimdTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref Vpc
      Protocol: HTTP
      Port: 4000
      TargetType: instance
      HealthCheckProtocol: HTTP
      HealthCheckPath: /oauth/client-metadata.json
      Matcher:
        HttpCode: '200'
      Targets:
        - Id: !Ref KeycloakCimdMcpServerEc2
          Port: 4000
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-tg-cimd'

  McpServerTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref Vpc
      Protocol: HTTP
      Port: 8000
      TargetType: instance
      HealthCheckProtocol: HTTP
      HealthCheckPath: /
      Matcher:
        HttpCode: '200-499'
      Targets:
        - Id: !Ref KeycloakCimdMcpServerEc2
          Port: 8000
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-tg-mcp-server'

  HttpsListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref InternalAlb
      Port: 443
      Protocol: HTTPS
      Certificates:
        - CertificateArn: !Ref PublicAlbCertificate
      DefaultActions:
        - Type: fixed-response
          FixedResponseConfig:
            StatusCode: '404'
            ContentType: text/plain
            MessageBody: Not found

  KeycloakListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HttpsListener
      Priority: 10
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref AuthHostname
      Actions:
        - Type: forward
          TargetGroupArn: !Ref KeycloakTargetGroup

  CimdListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HttpsListener
      Priority: 20
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref CimdHostname
      Actions:
        - Type: forward
          TargetGroupArn: !Ref CimdTargetGroup

  McpServerListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HttpsListener
      Priority: 30
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref McpServerHostname
      Actions:
        - Type: forward
          TargetGroupArn: !Ref McpServerTargetGroup

  AuthRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: !Ref AuthHostname
      Type: A
      AliasTarget:
        DNSName: !GetAtt InternalAlb.DNSName
        HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID

  CimdRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: !Ref CimdHostname
      Type: A
      AliasTarget:
        DNSName: !GetAtt InternalAlb.DNSName
        HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID

  McpServerRecord:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneId: !Ref PrivateHostedZone
      Name: !Ref McpServerHostname
      Type: A
      AliasTarget:
        DNSName: !GetAtt InternalAlb.DNSName
        HostedZoneId: !GetAtt InternalAlb.CanonicalHostedZoneID

  Ec2InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-role'

  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Sub '${ProjectName}-${EnvironmentName}-ec2-ssm-profile'
      Roles:
        - !Ref Ec2InstanceRole

  KeycloakCimdMcpServerEc2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
      InstanceType: t3.medium
      IamInstanceProfile: !Ref Ec2InstanceProfile
      SubnetId: !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref KeycloakCimdMcpServerEc2SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2'

  WinBastionMcpClientEc2:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: '{{resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base}}'
      InstanceType: t3.medium
      IamInstanceProfile: !Ref Ec2InstanceProfile
      KeyName: !Ref KeyPairName
      SubnetId: !Ref PrivateSubnet1
      SecurityGroupIds:
        - !Ref WinBastionMcpClientEc2SecurityGroup
      Tags:
        - Key: Name
          Value: !Sub '${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2'

Outputs:
  VpcId:
    Description: VPC ID used for AgentCore private connectivity settings.
    Value: !Ref Vpc
  PrivateSubnet1Id:
    Description: Primary private subnet for EC2, VPC endpoints, and AgentCore private connectivity settings.
    Value: !Ref PrivateSubnet1
  PrivateSubnet2Id:
    Description: Secondary private subnet used by the internal ALB and available for multi-AZ private connectivity settings.
    Value: !Ref PrivateSubnet2
  KeycloakCimdMcpServerEc2Id:
    Description: EC2 instance ID hosting Keycloak, CIMD metadata nginx, and the lightweight MCP server.
    Value: !Ref KeycloakCimdMcpServerEc2
  WinBastionMcpClientEc2Id:
    Description: Windows bastion EC2 instance ID used as the MCP client host.
    Value: !Ref WinBastionMcpClientEc2
  AgentCorePrivateIdpSecurityGroupId:
    Description: Use this SG in the AgentCore Identity private IdP managed VPC resource so it can reach the internal ALB over HTTPS.
    Value: !Ref AgentCorePrivateIdpSecurityGroup
  AgentCoreGatewayMcpServerEgressSecurityGroupId:
    Description: Use this SG in the AgentCore Gateway target VPC egress managed VPC resource for the EC2-hosted MCP server target.
    Value: !Ref AgentCoreGatewayMcpServerEgressSecurityGroup
  AgentCoreGatewayVpcEndpointId:
    Description: Interface VPC endpoint ID for Amazon Bedrock AgentCore Gateway PrivateLink. Use this as aws:SourceVpce in the Gateway resource policy.
    Value: !Ref AgentCoreGatewayEndpoint
  AuthUrl:
    Description: Private URL for Keycloak through the internal ALB.
    Value: !Sub 'https://${AuthHostname}'
  KeycloakIssuerUrl:
    Description: Keycloak issuer URL for the mcp-demo realm.
    Value: !Sub 'https://${AuthHostname}/realms/mcp-demo'
  CimdMetadataUrl:
    Description: CIMD client metadata URL served through the internal ALB.
    Value: !Sub 'https://${CimdHostname}/oauth/client-metadata.json'
  McpServerUrl:
    Description: EC2-hosted MCP server URL served through the internal ALB.
    Value: !Sub 'https://${McpServerHostname}/mcp'
  SsmKeycloakCimdMcpServerEc2SessionCommand:
    Description: Run locally to open a Session Manager shell on the keycloak-cimd-mcp-server EC2 instance.
    Value: !Sub >-
      aws ssm start-session --target ${KeycloakCimdMcpServerEc2}
  SsmRdpPortForwardCommand:
    Description: Run locally, then connect an RDP client to localhost:13389.
    Value: !Sub >-
      aws ssm start-session --target ${WinBastionMcpClientEc2} --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["3389"],"localPortNumber":["13389"]}'

パラメータ 設定値 説明
ProjectName mcp-keycloak-agentcore リソース名のPrefixに使うプロジェクト名 ※1
EnvironmentName poc リソース名のPrefixに使う環境名 ※1
HostedZoneName agentcore-keycloak-example.com Route53HostedZoneのドメイン名 ※2
PublicHostedZoneId ZXXXXXXXXXXXXX ACM証明書のDNS検証に使うRoute53 Public Hosted Zone ID
AuthHostname auth.agentcore-keycloak-example.com Keycloakへアクセスするためのホスト名
CimdHostname cimd.agentcore-keycloak-example.com CIMD metadataを配信するためのホスト名
McpServerHostname mcp-server.agentcore-keycloak-example.com MCP Server用のホスト名
KeyPairName mcp-keycloak-agentcore-poc-key Windows踏み台EC2に設定する既存のEC2 Key Pair名
AgentCoreGatewayEndpointPolicyResourceArn * AgentCore Gateway用Interface VPC Endpoint policyで許可するGateway ARN ※3

※1 ${ProjectName}-${EnvironmentName}-リソース名になります。
※2 Keycloak、CIMD metadata、MCP Server用のDNS名の親ドメインになります。
※3 AgentCore Gateway構築前でARNが未確定のため"*"にします。

# コマンド例
aws cloudformation deploy \
  --region ap-northeast-1 \
  --stack-name mcp-keycloak-agentcore-poc-foundation \
  --template-file ./aws-mcp-keycloak-foundation.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    ProjectName=mcp-keycloak-agentcore \
    EnvironmentName=poc \
    HostedZoneName=agentcore-keycloak-example.com \
    PublicHostedZoneId=ZXXXXXXXXXXXXX \
    AuthHostname=auth.agentcore-keycloak-example.com \
    CimdHostname=cimd.agentcore-keycloak-example.com \
    McpServerHostname=mcp-server.agentcore-keycloak-example.com \
    KeyPairName=mcp-keycloak-agentcore-poc-key \
    AgentCoreGatewayEndpointPolicyResourceArn="*"

4. EC2上でKeycloak、CIMD nginx、MCP Serverを起動

${ProjectName}-${EnvironmentName}-keycloak-cimd-mcp-server-ec2にSSMで接続し、以下を起動します。

  • Keycloak container: 8080
  • CIMD nginx container: 4000
  • MCP server: 8000

4.1 Dockerをインストール

# コマンド例
# Amazon Linux 2023のパッケージを更新
sudo dnf update -y
# Dockerをインストール
sudo dnf install -y docker
# Dockerデーモンを起動し、自動起動有効化
sudo systemctl enable --now docker
# バージョン確認
sudo docker version

4.2 Keycloakを起動

# コマンド例
# イメージをpull
sudo docker pull quay.io/keycloak/keycloak:26.6.1
# 永続化用volume作成
sudo docker volume create keycloak-data

# Keycloak起動(CIMD機能有効化)
# 検証のためHTTP有効化
# CIMDのキャッシュは最小1分、最大60分
sudo docker run -d --name keycloak \
  --restart unless-stopped \
  -p 8080:8080 \
  -v keycloak-data:/opt/keycloak/data \
  -e KC_BOOTSTRAP_ADMIN_USERNAME="admin" \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD="admin" \
  -e KC_HOSTNAME="https://auth.agentcore-keycloak-example.com" \
  -e KC_PROXY_HEADERS=xforwarded \
  -e KC_HTTP_ENABLED=true \
  quay.io/keycloak/keycloak:26.6.1 \
  start-dev --features=cimd \
  --spi-client-policy-executor--client-id-metadata-document--min-cache-time=1 \
  --spi-client-policy-executor--client-id-metadata-document--max-cache-time=60

#応答確認
curl -i http://127.0.0.1:8080/realms/master/.well-known/openid-configuration

4.3 CIMD nginxを起動

CIMDのメタデータを配信するためにJSONを用意します。​Example Metadata Documentをベースに値を設定します。

client-metadata.json
{
  "client_id": "https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json",
  "client_name": "MCP Inspector AWS CIMD Client",
  "grant_types": [
    "authorization_code"
  ],
  "response_types": [
    "code"
  ],
  "token_endpoint_auth_method": "none",
  "redirect_uris": [
    "http://localhost:6274/oauth/callback"
  ]
}
項目 設定値 説明
client_id https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json OAuthクライアントの識別子。CIMDではJSONメタデータをホストするHTTPS URLを指定する。
client_name MCP Inspector AWS CIMD Client エンドユーザに表示するクライアント名
grant_types ["authorization_code"] クライアントが利用するグラント種別。Authorization Code Grantを利用する場合は["authorization_code"]を指定する。
response_types ["code"] 認可エンドポイントで使用するレスポンスタイプ。Authorization Code Grantを利用する場合はcodeを指定する。
token_endpoint_auth_method none トークンエンドポイントでのクライアント認証方式。CIMDでは、クライアントシークレットを持たないpublic clientを使用するため、token_endpoint_auth_method に none を指定する。
redirect_uris ["http://localhost:6274/oauth/callback"] 認可サーバが認可結果を返すリダイレクトURIの一覧。MCP Inspectorがローカルで受け取るコールバックURLを指定する。

※CIMDドラフトでは、CIMDドキュメントのメタデータ値は OAuth Dynamic Client Registration Metadata registry に定義された値を使う、と説明されています。

参考:

nginxを起動します。

# コマンド例
# CIMD metadataとnginx設定ファイルの配置先を作成
sudo mkdir -p /opt/cimd/public/oauth /opt/cimd/nginx

# CIMD client metadata JSONを作成
sudo tee /opt/cimd/public/oauth/client-metadata.json >/dev/null <<__CIMD_JSON__
{
  "client_id": "https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json",
  "client_name": "MCP Inspector AWS CIMD Client",
  "grant_types": [
    "authorization_code"
  ],
  "response_types": [
    "code"
  ],
  "token_endpoint_auth_method": "none",
  "redirect_uris": [
    "http://localhost:6274/oauth/callback"
  ]
}
__CIMD_JSON__

# nginx設定を作成
sudo tee /opt/cimd/nginx/default.conf >/dev/null <<'__NGINX_CONF__'
server {
    listen 80;
    server_name _;

    root /usr/share/nginx/html;

    location = /oauth/client-metadata.json {
        default_type application/json;
        add_header Cache-Control "no-store";
        try_files /oauth/client-metadata.json =404;
    }

    location / {
        return 404;
    }
}
__NGINX_CONF__

# イメージをpull
sudo docker pull nginx:1.30.0-alpine3.23

# CIMD metadata配信用nginxを起動
sudo docker run -d --name cimd-nginx \
  --restart unless-stopped \
  -p 4000:80 \
  -v /opt/cimd/public:/usr/share/nginx/html:ro \
  -v /opt/cimd/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro \
  nginx:1.30.0-alpine3.23

# 応答確認
curl -i http://127.0.0.1:4000/oauth/client-metadata.json

4.4 MCPサーバを起動

認証認可不要の軽量のMCP Serverとしてmy_mcp_server.pyを用意します。

from mcp.server.fastmcp import FastMCP
from starlette.responses import JSONResponse

mcp = FastMCP(host="0.0.0.0", port=8000, stateless_http=True)

@mcp.tool()
def add_numbers(a: int, b: int) -> int:
    """Add two numbers together"""
    return a + b

@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
    """Multiply two numbers together"""
    return a * b

@mcp.tool()
def greet_user(name: str) -> str:
    """Greet a user by name"""
    return f"Hello, {name}! Nice to meet you."

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

uvをインストールして、MCP Serverを起動します。

# コマンド例
# uvを現在のOSユーザのホームディレクトリ配下にインストール
curl -LsSf https://astral.sh/uv/install.sh | sh

# このシェルでuvコマンドを使えるようにPATHを追加
export PATH="$HOME/.local/bin:$PATH"

# uvのバージョン確認
uv --version     

# MCP Serverの配置先を作成
sudo mkdir -p /opt/mcp-server

# MCP ServerのPythonファイルを作成
sudo tee /opt/mcp-server/my_mcp_server.py >/dev/null <<'__MCP_PY__'
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(host="0.0.0.0", port=8000, stateless_http=True)

@mcp.tool()
def add_numbers(a: int, b: int) -> int:
    """Add two numbers together"""
    return a + b

@mcp.tool()
def multiply_numbers(a: int, b: int) -> int:
    """Multiply two numbers together"""
    return a * b

@mcp.tool()
def greet_user(name: str) -> str:
    """Greet a user by name"""
    return f"Hello, {name}! Nice to meet you."

if __name__ == "__main__":
    mcp.run(transport="streamable-http")
__MCP_PY__

# systemdサービス定義を作成
sudo tee /etc/systemd/system/mcp-server.service >/dev/null <<'__SYSTEMD__'
[Unit]
Description=FastMCP Server
After=network-online.target
Wants=network-online.target

[Service]
WorkingDirectory=/opt/mcp-server
ExecStart=/usr/local/bin/uv run --with mcp /opt/mcp-server/my_mcp_server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
__SYSTEMD__

# systemd設定を再読み込みし、MCP Serverを起動
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server

# MCP Serverの状態を確認
sudo systemctl status mcp-server

#応答確認
curl -i http://127.0.0.1:8000/mcp
# 応答
#HTTP/1.1 406 Not Acceptable
#date: Wed, 20 May 2026 05:13:19 GMT
#server: uvicorn
#content-type: application/json
#content-length: 126

#{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Not Acceptable: Client must accept text/event-stream"}}

参考:Installing uv

5. Windows踏み台EC2にNode.jsをインストール

${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。

参考:踏み台ホストを使用せずに、Systems Manager Session Manager のポート転送を使用して RDP 経由で EC2 インスタンスに接続する方法を教えてください

MCP Inspectorを使用するには、Node.jsがWindows踏み台EC2にインストールされている必要があります。powershellでwinget経由でインストールするか、公式サイトからNode.js MSIをインストールします。

# コマンド例
winget install OpenJS.NodeJS

Keycloak設定

${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。

1. Realm作成

ブラウザからhttps://auth.agentcore-keycloak-example.com/adminでKeycloak管理コンソールにアクセスします。

User name:
  admin
Password:
  admin

keycloak-admin-login.png

Realmを作成します。

keycloak-realm-1.png

Realm name:
  mcp-demo

keycloak-realm-2.png

Issuer URLは次です。

https://auth.agentcore-keycloak-example.com/realms/mcp-demo

Discovery URLは次です。

https://auth.agentcore-keycloak-example.com/realms/mcp-demo/.well-known/openid-configuration

このURLをAgentCore GatewayのInbound Identity detailsに設定します。

2. 検証ユーザ作成

「Users」から、Userを作成します。

Username:
  alice

keycloak-user-2.png

ユーザ作成後に上タブの「Credentials」からパスワードを設定します。

Password:
  alice

Temporary:
  OFF

keycloak-user-3.png

3. Client Scope mcp:tools 作成

「Client scopes」からclient scopeを作成します。

Name:
  mcp:tools

Protocol:
  OpenID Connect

Client scope type:
  Optional

keycloak-client-scopes-3.png

作成後、Include in token scopeをONにします。これをONにしないと、Access Tokenのscope claimにmcp:toolsが入りません。

4. Audience mapper作成

MCP仕様では、Access Tokenが特定のMCPサーバ向けに発行されたものであることを保証するため、Audience Binding が求められます。

MCP側の要求は次の通りです。

  • MCPクライアントは、認可リクエストおよびトークンリクエストで RFC 8707 の resource パラメータ を含める必要がある
  • resource の値は、そのトークンを使う対象のMCP Serverを識別する必要がある
  • MCP Serverは、提示されたトークンが自分自身のために発行されたものか検証する必要がある

ただし、Keycloakは現時点で resource パラメータを認識できません。そのため、Keycloakで同等の効果を得る暫定策として、resource の代わりにOAuth 2.0の scope を利用し、スコープに紐づくAudience MapperでAccess Tokenの aud クレームを設定します。

参考:Token Audience Binding and Validation

「Client scopes」→「mcp:tools」→「Mappers」から「Configure a new mapper」を押下します。
keycloak-client-scopes-mapper-1.png

「Audience」を選択します。
keycloak-client-scopes-mapper-2.png

設定例:

Name:
  aud-agentcore-gateway

Included Custom Audience:
  mcp-gateway-demo

Add to access token:
  ON

Add to token introspection:
  ON

keycloak-client-scopes-mapper-3.png

「mcp:tools」スコープが要求された時に、MCP Serverの識別子としてaudクレーム「mcp-gateway-demo」を付与します。
Included Custom Audienceの値は、AgentCore Gateway側のAllowed audiencesの設定と一致させます。

5. CIMD Client Policy / Profile作成

Keycloakを--features=cimd付きで起動している前提です。

「Realm settings」→「Client Policies」→「Profiles」からCIMD用のClient Profileを作成します。この Client Policy / Profileを作成することでCIMDを利用することができます。

keycloak-client-profile-2.png

Client Profileを作成後、client-id-metadata-document executorを追加します。

Executor:

client-id-metadata-document

keycloak-client-profile-3.png

設定項目の内容は次の通りです。

設定 意味
Allow http scheme ONの場合、Client ID URLやメタデータ中のURLでHTTPを許可する。開発環境のみONにすべきで、本番ではOFF
Trusted domains Client ID URLやメタデータURLとして許可するドメインパターン
Restrict same domain ONの場合、Client ID URL、Redirect URI、メタデータ内URLが同じ信頼ドメイン配下にあることを確認する
Required properties Client ID Metadata Documentに必須とするメタデータ項目
Only Allow Confidential Client ONの場合、confidential clientのみ許可する。その場合、jwks または jwks_uri が必要で、認証方式は private_key_jwt または tls_client_auth が必要

設定例:

Allow http scheme:
  OFF

Trusted domains:
  cimd.agentcore-keycloak-example.com
  localhost

Restrict same domain:
  OFF

Only Allow Confidential Client:
  OFF

Required properties:
  client_id
  client_name
  redirect_uris

keycloak-client-profile-4.png

「Realm settings」→「Client Policies」→「Policies」からClient Policyを作成します。

Client Policyを作成し、client-id-uri conditionを追加します。このconditionでは、認可リクエストの client_id が、指定したURIスキームや信頼ドメインに一致するかを判定します。

設定 意味
URI scheme client_id に許可するURIスキーム。本番では通常 https のみ
Trusted domains client_id URLのホスト部として許可するドメイン

設定例:

Condition:
  client-id-uri

URI scheme:
  https

Trusted domains:
  cimd.agentcore-keycloak-example.com

keycloak-client-policies-2.png

Client Policyを作成後、Client Profileに追加します。

keycloak-client-policies-3.png

参考:Client Registration

6. OAuth 2.1 Client Policy / Profile作成

「Realm settings」→「Client Policies」→「Profiles」から OAuth 2.1 用のClient Profileを作成します。このClient Policy / Profileを作成することで OAuth 2.1 準拠の認可コードフローを強制します。

Client Profileを作成後、各Executorを追加します。
ビルトインのoauth-2-1-for-public-clientプロファイルをベースにoauth-2-1-for-public-client-no-dpopを作成します。OAuth 2.1 ではDPoP(Demonstration of Proof-of-Possession)は推奨ですが、本検証では利用しないため削除します。

Executor:

pkce-enforcer
reject-implicit-grant
reject-ropc-grant
secure-redirect-uris-enforcer

keycloak-client-profile-oauth-1.png

secure-redirect-uris-enforcerExecutorは、検証のためloopback addressやhttpを許可します。
他のExecutorの設定は、oauth-2-1-for-public-clientプロファイルと同様なので省略します。

設定例:

Allow IPv4 loopback address:
  ON

Allow IPv6 loopback address:
  ON

Allow private use URI:
  ON

Allow http scheme:
  ON

Allow wildcard in context-path:
  OFF

OAuth 2.1 Compliant:
  ON

Allow open redirect:
  OFF

keycloak-client-profile-oauth-2.png

続いて、Client Policyを作成し、client-access-type conditionを追加します。このconditionでは、クライアントのアクセス種別がpublic clientかどうかを判定します。

設定例:

Condition:
  client-access-type

Client Access Type:
  public

keycloak-client-policy-oauth-2.png

Client Profileにoauth-2-1-for-public-client-no-dpopを追加します。

keycloak-client-policy-oauth-1.png

インフォメーション
OAuth 2.1 用のClient Profileで全てのExecutorが有効になることが正常な動きですが、検証してみたところsecure-redirect-uris-enforcerが有効化されないようです(2026年6月時点)。回避策として暫定的にCIMD用のClient Policyに OAuth 2.1用のClient Profileを追加することで、secure-redirect-uris-enforcerが有効化され、CIMDの無効なリダイレクトURIがバリデーションエラーになることが確認できたので、この対応方法で検証を進めています。本件については、KeycloakのGithub Issue作成含めkeycloakコミュニティーへのフィードバックを実施済みです。
[CIMD] secure-redirect-uris-enforcer is not enforced for CIMD public clients during authorization requests
#49456


Bedrock AgentCore Gateway設定

AWSマネジメントコンソールからBedrock AgentCore Gatewayの作成、設定を行います。

1. Gatewayを作成

AgentCore Gatewayを作成します。

Define gateway details:

Gateway name:
  mcp-gateway-keycloak-demo

IAM permissions:
  Create default role

JWT Authorization Configurationでaudscopeで検証するよう設定します。

Inbound Identity details:

Inbound Auth type:
  Use JSON Web Tokens (JWT)

JWT schema configuration:
  Use existing Identity provider configurations

Discovery URL:
  https://auth.agentcore-keycloak-example.com/realms/mcp-demo/.well-known/openid-configuration

JWT Authorization Configuration:
Allowed audiences:
  mcp-gateway-demo

Allowed scopes:
  mcp:tools

Allowed clients:
  none

Custom claims:
  none

VPC SecurityでAgentCore GatewayがKeycloak(Private Idp)が配置されるVPCにアクセスできるよう設定します。
ここで指定するSecurity GroupはKeycloakへのアクセスが許可されているものとします(CloudFormationで既に作成済み)。

VPC Security:
  Managed

VPC:
  ${ProjectName}-${EnvironmentName}-vpc

Subnets:
  ${ProjectName}-${EnvironmentName}-private-b-alb-only

Security Group:
  ${ProjectName}-${EnvironmentName}-AgentCorePrivateIdpSecurityGroup

agentcore-gateway-2.jpeg

2. MCP Server Targetを作成

Select a target protocol:
  MCP target

Target name:
  my-mcp-server

Target type:
  MCP server

MCP endpoint:
  https://mcp-server.agentcore-keycloak-example.com/mcp

MCP listing mode:
  Default

Outbound Auth configurations:
  No auth

VPC SecurityでAgentCore GatewayがMCP Serverが配置されるVPCにアクセスできるよう設定します。
ここで指定するSecurity GroupはMCP Serverへのアクセスが許可されているものとします(CloudFormationで既に作成済み)。

Additional configurations:

VPC Security:
  Managed

VPC:
  ${ProjectName}-${EnvironmentName}-vpc

Subnets:
  ${ProjectName}-${EnvironmentName}-private-b-alb-only

Security Group:
  ${ProjectName}-${EnvironmentName}-AgentCoreGatewayMcpServerEgressSecurityGroup

Endpoint IP address type:
  IPv4

agentcore-gateway-3.jpeg

マネジメントコンソールでは、Gateway作成時にTargetも指定する画面フローになります。Gateway本体がまだCreatingの間にTarget作成が走り、次のようなエラーになることがあります。

Gateway ... is in Creating state, there was an error in creating the target

その場合、GatewayのステータスがReadyになった後、Targetを再作成してください。

Resource-based policyの設定(Option)

指定したVPC Endpoint以外からInvokeGatewayを拒否するResource-based policyの設定をします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowInvokeGatewayOnlyFromVpce",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "bedrock-agentcore:InvokeGateway",
      "Resource": "arn:aws:bedrock-agentcore:ap-northeast-1:<account-id>:gateway/<gateway-id>",
      "Condition": {
        "StringEquals": {
          "aws:SourceVpce": "<vpce-id>"
        }
      }
    },
    {
      "Sid": "DenyInvokeGatewayNotFromVpce",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "bedrock-agentcore:InvokeGateway",
      "Resource": "arn:aws:bedrock-agentcore:ap-northeast-1:<account-id>:gateway/<gateway-id>",
      "Condition": {
        "StringNotEquals": {
          "aws:SourceVpce": "<vpce-id>"
        }
      }
    }
  ]
}

動作確認

${ProjectName}-${EnvironmentName}-win-bastion-mcp-client-ec2にSSM経由でRDP接続します。

参考:踏み台ホストを使用せずに、Systems Manager Session Manager のポート転送を使用して RDP 経由で EC2 インスタンスに接続する方法を教えてください

1. MCP Inspectorを起動

事前準備でNode.jsがインストールされている前提です。
powershellでホストと許可オリジンを指定してMCP Inspectorを起動します。
ホストを指定するのは Keycloak の OAuth 2.1 Client Policy / Profile によってlocalhostが拒否されるためです。

# コマンド例
$env:HOST = "127.0.0.1"
$env:ALLOWED_ORIGINS = "http://127.0.0.1:6274"

npx -y @modelcontextprotocol/inspector@0.21.2

参考:

2. MCP InspectorのOAuth設定

MCP Inspectorの接続先をAgentCore Gatewayにします。接続先はGateway resource URLを指定します。

Transport Type:
  Streamable HTTP

URL:
  https://<gateway-endpoint>/mcp

mcp-inspector-setup-1.png

MCP InspectorのOAuth設定で、CIMD URLをClient IDとして指定します。

Client ID:
  https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json

Scope:
  mcp:tools

mcp-inspector-setup-2.png

Connectionを押下して、MCP Serverへ接続を試みると、Keycloakのログイン画面に遷移します。

mcp-inspector-alice-login.png

UsernameとPasswordを入力してログインすると、CIMDによってクライアント登録することを確認されます。

mcp-inspector-cimd-access.png

Keycloakログイン後、MCP Inspectorの上タブのAuthからAccess Tokenを取得、デコードして以下が含まれることを確認します。

{
  "iss": "https://auth.agentcore-keycloak-example.com/realms/mcp-demo",
  "aud": [
    "mcp-gateway-demo"
  ],
  "scope": "openid profile email mcp:tools"
}

3. MCP InspectorからAgentCore Gatewayへ接続(正常系)

OAuthで取得したAccess Tokenが付与された状態で接続します。

mcp-inspector-tools-1.png

期待するtools:

add_numbers
multiply_numbers
greet_user

mcp-inspector-tools-2.png

tools/callで次を実行します。

{
  "a": 2,
  "b": 3
}

mcp-inspector-tools-3.png

add_numbersなら5multiply_numbersなら6が返れば成功です。

mcp-inspector-tools-4.png

4. MCP InspectorからAgentCore Gatewayへ接続(異常系)

誤ったスコープを指定して、再度MCP Serverに接続します。

Client ID:
  https://cimd.agentcore-keycloak-example.com/oauth/client-metadata.json

Scope:
  mcp:fail

mcp-inspector-tools-fail-1.png

予期しないスコープのためアクセスが拒否されることを確認します。

mcp-inspector-tools-fail-2.png


まとめ

本記事では、AgentCore GatewayとKeycloakを組み合わせて、

  • CIMDを利用してクライアントを事前登録することなくOAuth 2.1 準拠の認可コードフローでKeycloakからAccess Tokenを取得できること
  • 取得したAccess TokenでAgentCore Gatewayにアクセスできること

を検証しました。また機会があれば、AgentCore Gatewayから外部ツールへの接続(Outbound Authorization)や詳細なアクセス制御の検証を行っていきたいと思います。

21
4
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
21
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?