Skip to main content
Glama
pcolazurdo

blog-zero-secrets-mcp

by pcolazurdo
README.md
# 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)
```

### Mode B: Gateway (recommended)

```
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)

## 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

```bash
# 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
```

## Quick Start: Gateway Mode (recommended)

### Step 1: Install dependencies

```bash
npm install
```

### Step 2: Create Cognito infrastructure

```bash
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

```bash
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

```bash
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

```bash
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:

```bash
# 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:

```json
{
  "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:

```bash
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

```bash
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:

```bash
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](CONTRIBUTING.md#security-issue-notifications) for information on reporting security issues.

## License

This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file.

Maintenance

ActivityMaintained
ResponsivenessNo issues