Skip to main content
Glama
pcolazurdo

blog-zero-secrets-mcp

by pcolazurdo

AgentCore + Cognito Public Client MCP PoC

End-to-end proof of concept demonstrating two deployment modes for an MCP server on AgentCore:

  1. Standalone — Runtime with Cognito JWT auth directly (no gateway)

  2. Gateway — Runtime behind an AgentCore Gateway with Cognito PKCE inbound auth and IAM outbound auth

Both modes use a public Cognito client (no client_secret) with PKCE for user authentication.

Architecture

Mode A: Standalone (Runtime with direct JWT auth)

Claude Code / Kiro
    │ PKCE → Cognito Hosted UI → browser
    │ Bearer JWT
    ▼
AgentCore Runtime (CUSTOM_JWT validates token)
    │
    ▼
MCP Server (FastMCP, Python)
Claude Code / Kiro
    │ PKCE → Cognito Hosted UI → browser
    │ Bearer JWT
    ▼
AgentCore Gateway (CUSTOM_JWT validates token)
    │ SigV4 (gateway IAM role)
    ▼
AgentCore Runtime (AWS_IAM auth)
    │
    ▼
MCP Server (FastMCP, Python)

The Gateway mode provides:

  • Centralized authentication (gateway handles all JWT validation)

  • Tool discovery and semantic search across multiple targets

  • Protocol-level MCP routing

  • Separation of concerns (runtime doesn't need to know about user auth)

Related MCP server: local-kms-mcp-server

Project Structure

.
├── server/
│   ├── cognitopocmcp/              # Runtime deployed via agentcore CLI
│   │   ├── app/cognito_poc_mcp/
│   │   │   └── main.py            # FastMCP server with sample tools
│   │   └── agentcore/             # agentcore CLI config
│   ├── mcp_server.py              # MCP server source (standalone mode)
│   └── requirements.txt
├── src/
│   ├── config.mjs                 # Shared config (project name, region, helpers)
│   ├── auth.mjs                   # PKCE auth module (no secrets!)
│   ├── mcp-server.mjs             # Stdio MCP server (proxy mode)
│   └── test-auth.mjs              # Standalone auth flow test
├── scripts/
│   ├── setup-cognito.mjs          # Creates Cognito pool + public client + user
│   ├── deploy.sh                  # Deploys runtime (standalone mode, with JWT auth)
│   ├── deploy-infrastructure.mjs  # Creates gateway + IAM role + target (gateway mode)
│   ├── test-gateway.mjs           # Tests gateway end-to-end
│   ├── test-deployed.mjs          # Tests standalone runtime end-to-end
│   └── teardown-cognito.mjs       # Deletes all infrastructure
├── .env                           # Generated by setup (Cognito config)
├── .mcp.json                      # Generated by deploy-infra (gateway URL + OAuth)
├── claude-mcp-config.json         # Same as .mcp.json (for copying to Claude/Kiro)
└── package.json

Prerequisites

# AWS CLI + credentials configured
aws sts get-caller-identity

# Node.js 20+
node --version

# AgentCore CLI
npm install -g @aws/agentcore

# Python 3.10+ (for the MCP server)
python3 --version

Step 1: Install dependencies

npm install

Step 2: Create Cognito infrastructure

npm run setup

Creates a Cognito User Pool with a public app client (no secret), a hosted UI domain, and a test user (testuser / TestPass123!). Config is saved to .env.

Step 3: Deploy the runtime

npm run deploy-runtime

Deploys the MCP server to AgentCore Runtime using the agentcore CLI. The runtime uses default IAM auth (the gateway will authenticate users).

Step 4: Deploy the gateway

npm run deploy-infra

Creates:

  • An IAM role for the gateway (with permission to invoke the runtime)

  • An AgentCore Gateway with CUSTOM_JWT inbound auth (Cognito PKCE)

  • A gateway target pointing at the runtime via GATEWAY_IAM_ROLE (SigV4)

Updates .mcp.json and claude-mcp-config.json with the gateway URL.

Step 5: Test

npm run test-gateway

Authenticates via Cognito (non-interactive using test user), then:

  • Verifies unauthenticated requests are rejected (401)

  • Initializes MCP session

  • Lists discovered tools

  • Calls tools (greet_user, add_numbers, get_server_info)

Step 6: Connect Claude Code / Kiro

Copy the generated config:

# For Kiro — .mcp.json is already in the project root
# For Claude Code
cp claude-mcp-config.json ~/.claude/mcp.json

The config looks like:

{
  "mcpServers": {
    "cognito-poc": {
      "type": "http",
      "url": "https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp",
      "oauth": {
        "clientId": "<public-client-id>",
        "callbackPort": 8976
      }
    }
  }
}

On first tool invocation, Claude/Kiro opens your browser for Cognito login. After that, tokens are cached and refreshed automatically.

Quick Start: Standalone Mode

If you don't need a gateway and want the runtime to handle JWT auth directly:

npm run setup         # Create Cognito pool
npm run deploy        # Deploy runtime with CUSTOM_JWT auth
npm run test-deployed # Test via PKCE (opens browser)

npm Scripts

Script

Description

npm run setup

Create Cognito User Pool + public client + test user

npm run deploy-runtime

Deploy MCP runtime via agentcore CLI (IAM auth, for gateway)

npm run deploy-infra

Create gateway + IAM role + target via Control Plane API

npm run deploy

Deploy runtime with direct JWT auth (standalone, no gateway)

npm run test-gateway

Test gateway end-to-end (non-interactive)

npm run test-gateway -- --pkce

Test gateway with browser-based PKCE login

npm run test-deployed

Test standalone runtime via PKCE

npm run test-auth

Test PKCE auth flow only (opens browser)

npm run test-local

Run MCP server locally for development

npm run teardown

Delete all infrastructure (gateway, IAM role, Cognito pools)

MCP Tools Available

The sample MCP server exposes:

Tool

Description

add_numbers

Add two numbers together

multiply_numbers

Multiply two numbers together

greet_user

Greet a user by name

get_server_info

Return deployment and version info

analyze_text

Analyze text and return basic statistics

When accessed through the gateway, tool names are prefixed with the target name: mcp-runtime___add_numbers.

Cleanup

npm run teardown

This deletes:

  • AgentCore Gateway (targets + gateway)

  • Gateway IAM role

  • Cognito User Pool(s)

  • Local files (.env, .mcp.json, claude-mcp-config.json)

The AgentCore Runtime is NOT deleted (managed separately by agentcore CLI). To remove it:

cd server/cognitopocmcp && agentcore destroy

Key Concepts

Zero-secrets authentication

  • Cognito public client: GenerateSecret: false — no client secret exists

  • PKCE (code_challenge + code_verifier) proves the requester without a shared secret

  • Only the client_id is stored locally (a public identifier, not a credential)

  • Tokens are in-memory with 1-hour expiry + auto-refresh

Gateway outbound auth

The gateway authenticates to the runtime using its own IAM role (SigV4). This avoids the complexity of OAuth machine-to-machine flows between the gateway and runtime. The IAM role has bedrock-agentcore:* permission scoped to the runtime ARN.

Portability

All environment-specific values are derived at runtime:

  • AWS Account ID: resolved via STS.GetCallerIdentity

  • Gateway URL: read from .mcp.json (generated by deploy-infra)

  • Runtime ARN: read from agentcore deployed state

  • Project constants: centralized in src/config.mjs

To deploy in a different account/region, just configure AWS credentials and re-run the setup steps.

Security

See CONTRIBUTING for information on reporting security issues.

License

This library is licensed under the MIT-0 License. See the LICENSE file.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers