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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues