Skip to content

Add support for hosting MCP servers in ECS#55

Open
mike-parkhill wants to merge 31 commits into
masterfrom
DCKA-5551/hmac-auth
Open

Add support for hosting MCP servers in ECS#55
mike-parkhill wants to merge 31 commits into
masterfrom
DCKA-5551/hmac-auth

Conversation

@mike-parkhill

Copy link
Copy Markdown
Contributor
  • adds basic auth mechanisms for each server
    ** Uses the user's Truvera API key for the API server
    ** Uses a generated wallet token for the wallet server (generated by admin script) - NOTE: doesn't support revocation yet
  • terraform scripts included for deploying to AWS

m-parkhill and others added 26 commits July 10, 2026 15:49
Reusable workflows do not inherit secrets automatically — the caller must
pass secrets: inherit. Both docker-publish-wallet.yml and docker-publish.yml
were calling the reusable workflow without this, causing DOCKER_HUB_USERNAME
and DOCKER_HUB_ACCESS_TOKEN to arrive as empty strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds extractBearerToken, verifyJWT (ES256), deriveWalletKey, and
resolveAuthContext to a new @truvera/mcp-shared/auth export path.
Pins jose at 6.2.3 for JWT verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ServerConfig gains optional toolHandlerFactory for per-session handlers
- bootstrapMCPServer passes AuthContext into createServer; stdio uses {mode:'none'}
- startHTTPTransport resolves auth on initialize requests, rejects with 401/403
  on AuthError, and awaits the now-async serverFactory(context)
- AuthConfig/AuthContext re-exported from server/types for consumer convenience

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scripts/generate-keypair.js: generates ES256 keypair, prints private key
  (for Secrets Manager) and public key (for ECS MCP_JWT_PUBLIC_KEY)
- scripts/mint-jwt.js: mints a tenant JWT from a private key held in AWS
  Secrets Manager; supports --profile for dev/test/prod AWS profiles,
  --expires-in for configurable token lifetime
- admin:generate-keypair and admin:mint-jwt npm script aliases
- README: documents MCP_JWT_PUBLIC_KEY / MCP_MASTER_SECRET env vars and
  full admin workflow for one-time setup and tenant provisioning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WalletClientPool: lazy-initialises one WalletClient per SQLite path,
  promise-based deduplication for concurrent session starts, retryable
  on init failure, shutdownAll() for graceful drain
- index.ts: replaces static single-wallet init with toolHandlerFactory;
  per-session factory resolves tenant db path from AuthContext (jwt →
  /data/wallets/<sub>, none → WALLET_DB_PATH), creates per-tenant
  DIDClient/CredentialClient/MessageClient/DelegationClient/AgentCardClient,
  tracks MessageClients for clean SIGTERM/SIGINT shutdown
- MCP_AUTH_MODE=jwt requires MCP_JWT_PUBLIC_KEY; defaults to none for
  local dev with no config changes needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mcp-shared/src/auth/index.test.ts: 21 tests covering extractBearerToken,
  verifyJWT (including expired/wrong-key/no-sub/bad-pem), deriveWalletKey,
  and resolveAuthContext across all three auth modes
- wallet-server/src/tests/unit/wallet-client-pool.test.ts: 8 tests covering
  instance caching, concurrent-init deduplication, constructor arg forwarding,
  retry-after-failure, shutdownAll drain, pool clearing, and partial-failure
  resilience in shutdown
- wallet-server package.json: adds wallet-client-pool test to npm test script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In HTTP mode the caller's bearer token IS their Truvera API key.
A per-session TruveraClient is created from context.apiKey inside
toolHandlerFactory, so TRUVERA_API_KEY is no longer required at
startup for HTTP deployments.

stdio mode is unchanged: TRUVERA_API_KEY still required from env
for local dev / CLI usage.

AP2 schema initialisation remains a one-time startup fetch; only
the per-session client wiring (AP2Client, OpenIdClient) is done
inside the factory.

Tests: 4 new unit tests for TruveraClient per-session key wiring
confirm the bearer token is forwarded correctly and that distinct
sessions use distinct keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates a self-contained Terraform configuration for deploying both MCP
services to a fresh AWS account (one account per environment).

Infrastructure created:
- ECS Fargate cluster (truvera-mcp-{env}) with container insights
- ALB with HTTPS listener (TLS 1.3), HTTP→HTTPS redirect, host-based
  routing rules routing each hostname to its service
- truvera-api-mcp: stateless, no volumes, passthrough auth — bearer
  token forwarded directly to the Truvera REST API
- wallet-mcp: EFS-backed SQLite, JWT auth, desired_count=1 enforced
  by service config and documented constraint (SQLite single-writer)
- EFS file system with per-AZ mount targets and an access point that
  pins the container to /wallets with uid/gid 1000
- Separate security groups per service (ALB, truvera-api task,
  wallet-server task, EFS) for clean isolation
- IAM execution role (shared) with Secrets Manager read for wallet
  secret injection at startup; per-service task roles
- CloudWatch log groups (30-day retention)

MCP_JWT_PUBLIC_KEY and WALLET_MASTER_KEY are injected from a
Secrets Manager secret at task startup — the secret must be created
manually before first apply (see terraform.tfvars.example).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the assumption of a pre-existing VPC. Terraform now provisions
all networking resources needed for a fresh AWS account:

- VPC (10.0.0.0/16 by default, overridable via vpc_cidr)
- Internet Gateway
- 2 public subnets across two AZs (cidrsubnet-derived, one per AZ)
- Public route table with 0.0.0.0/0 → IGW
- Route table associations

All references to var.vpc_id / var.subnet_ids replaced with
aws_vpc.main.id and aws_subnet.public[*].id respectively.
Outputs now include vpc_id and public_subnet_ids.
terraform.tfvars.example updated to reflect that networking is
fully managed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend.tf: S3 remote state with DynamoDB locking; includes the
  bootstrap commands to create the bucket and lock table (once per
  account before terraform init)
- environments/staging.tfvars, prod.tfvars, test.tfvars: one file
  per environment with appropriate hostnames and cheqd_network;
  ARN placeholders to fill in after ACM cert and Secrets Manager
  secret are created
- .gitignore: environments/ is committed (config, not secrets);
  terraform.tfvars (local overrides) stays ignored

Workflow: AWS_PROFILE=<env> terraform init, then
terraform apply -var-file=environments/<env>.tfvars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
workflow_dispatch runs now tag the image with the sanitized branch name
(e.g. DCKA-5551-hmac-auth) instead of the sequential run number, so
staging.tfvars can reference a stable, meaningful tag while iterating
on a branch. An explicit tag input overrides the default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ode 22+

esm v3.2.25 uses Function.prototype.apply on an internal that Node 22+
no longer exposes, crashing the wallet-server on startup with
"Function.prototype.apply was called on undefined".

The crash path: dist/index.js (ESM) → wallet-sdk-wasm/lib/services/blockchain
/service.js (CJS) → jsonld-signatures/node_modules/jsonld → documentLoaders/
node.js → require('@digitalbazaar/http-client') → index.js: require = require
('esm')(module) → crash.

Fix: add http-client-stub.cjs — a self-contained CJS module that implements
the httpClient.get/post surface using native Node 22+ fetch — and register it
as a module-alias for @digitalbazaar/http-client, so module-alias intercepts
all require() calls to the package (including from deeply nested CJS modules)
before they reach the broken esm wrapper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread apps/wallet-server/scripts/mint-jwt.js Fixed
Comment thread apps/wallet-server/scripts/mint-jwt.js Fixed
mike-parkhill and others added 3 commits July 14, 2026 15:20
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…f sensitive information'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an AWS ECS (Fargate) hosting model for the MCP servers and introduces request-level authentication support in the shared MCP HTTP transport so the services can run as multi-tenant (wallet via JWT) or per-request credential passthrough (truvera-api via API key).

Changes:

  • Added Terraform configuration to deploy both MCP servers behind a single ALB, with EFS-backed persistence for the wallet server.
  • Introduced @truvera/mcp-shared/auth and updated the shared HTTP transport + server bootstrap to resolve auth context per MCP session.
  • Updated truvera-api and wallet-server to use per-session handler factories wired to the resolved auth context; expanded docs for hosted vs self-hosted usage.

Reviewed changes

Copilot reviewed 51 out of 54 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
WALLET_MCP_PLAN.md Updates wallet server roadmap/status and documents JWT multi-tenant mode.
README.md Adds hosted vs self-hosted guidance and documents per-client vs shared-key auth.
docs/claude-desktop-setup.md Adds hosted connection instructions for Claude Desktop (API key + wallet token).
packages/mcp-shared/src/transport/http/index.ts Resolves auth per initialize request and passes AuthContext into server factory.
packages/mcp-shared/src/server/types.ts Adds auth config/context types + per-session handler factory option.
packages/mcp-shared/src/server/bootstrap.ts Supports per-session handler factories and forwards auth config to HTTP transport.
packages/mcp-shared/src/auth/index.ts Implements auth resolution (none, jwt, passthrough) + wallet-key derivation helper.
packages/mcp-shared/src/auth/index.test.ts Adds unit tests covering auth parsing/verification and key derivation behavior.
packages/mcp-shared/README.md Documents bootstrap/auth features and intended auth mode usage by apps.
packages/mcp-shared/package.json Exposes ./auth entrypoint and adds jose dependency.
package-lock.json Locks new dependencies (jose, AWS SDK clients for wallet admin scripts, etc.).
apps/wallet-server/src/index.ts Switches to per-session handler factory with JWT auth + wallet pooling.
apps/wallet-server/src/wallet-client-pool.ts Adds per-tenant WalletClient caching/deduplication layer.
apps/wallet-server/src/tests/unit/wallet-client-pool.test.ts Adds unit coverage for wallet client pooling/deduplication/shutdown behavior.
apps/wallet-server/src/tests/integration/localstorage-shim.test.ts Adjusts integration test import path for wallet-sdk storage shim behavior.
apps/wallet-server/src/build-info.ts Bumps build metadata.
apps/wallet-server/scripts/mint-jwt.js Adds admin script to mint tenant JWTs using Secrets Manager private key.
apps/wallet-server/scripts/generate-keypair.js Adds keypair generation script for ES256 JWT auth.
apps/wallet-server/register-aliases.cjs Extends module-alias wiring and adds stub for @digitalbazaar/http-client.
apps/wallet-server/http-client-stub.cjs Introduces Node fetch-based CJS stub for @digitalbazaar/http-client.
apps/wallet-server/Dockerfile Includes the new http-client stub in the runtime image.
apps/wallet-server/package.json Adds admin scripts and extends unit test command list.
apps/wallet-server/jest.setup.js Adds additional mocking (but currently duplicates an existing logger mock).
apps/wallet-server/.env.test Adds wallet-server test env file (currently includes a live-looking API key).
apps/wallet-server/test-mcp.js Adds an ad-hoc MCP client script for local manual testing.
apps/wallet-server/README.md Updates wallet server status, tools, JWT auth docs, and known limitations.
apps/truvera-api/src/index.ts Adds HTTP-mode passthrough auth + per-session handler factory; keeps stdio behavior.
apps/truvera-api/src/build-info.ts Bumps build metadata.
apps/truvera-api/README.md Documents HTTP auth behavior (shared fallback key vs per-client headers).
apps/truvera-api/tests/unit/truvera-client.test.ts Adds unit tests verifying per-session API key usage in TruveraClient.
apps/truvera-api/src/features/ap2/schemas/SCHEMAS.md Updates AP2 schema documentation details/requirements.
apps/truvera-api/src/features/ap2/README.md Clarifies optional subject_did behavior and adds test structure notes.
.gitignore Ignores local Terraform state and overrides.
terraform/main.tf Adds Terraform provider config for AWS.
terraform/backend.tf Configures S3 backend for remote state.
terraform/variables.tf Declares deployment variables (images, hostnames, sizing, secrets).
terraform/terraform.tfvars.example Provides an example tfvars template with setup instructions.
terraform/vpc.tf Provisions a VPC, IGW, public subnets, and public route table.
terraform/security_groups.tf Adds ALB/task/EFS security groups with ALB-only ingress to tasks.
terraform/alb.tf Configures an ALB with HTTPS listener + host-based routing to both services.
terraform/efs.tf Provisions EFS + access point for wallet SQLite persistence (single-writer).
terraform/ecs.tf Defines ECS cluster, task defs, services, EFS mount, and secrets injection.
terraform/iam.tf Adds execution/task roles and Secrets Manager access policies.
terraform/logs.tf Adds CloudWatch log groups for both services.
terraform/outputs.tf Outputs core infra/service identifiers and ALB DNS/zone info.
terraform/environments/test.tfvars Adds a test environment tfvars stub.
terraform/environments/staging.tfvars Adds staging tfvars with concrete values.
terraform/environments/prod.tfvars Adds prod environment tfvars stub.
terraform/.terraform.lock.hcl Locks Terraform provider versions/hashes.
.github/workflows/ci.yml Scopes API CI to path filters and simplifies the workflow graph.
.github/workflows/docker-publish-reusable.yml Adds optional explicit Docker tag support.
.github/workflows/docker-publish-api.yml Plumbs through optional explicit Docker tag input.
.github/workflows/docker-publish-wallet.yml Plumbs through optional explicit Docker tag input.
.github/copilot-instructions.md Updates repo instructions to include wallet-server and new scripts/CI pattern.
Files not reviewed (1)
  • terraform/.terraform.lock.hcl: Generated file

Comment thread apps/wallet-server/.env.test Outdated
Comment on lines +1 to +3
# Truvera API Configuration
TRUVERA_API_KEY=eyJzY29wZXMiOlsidGVzdCIsImFsbCJdLCJzdWIiOiIyMTYyNiIsInNlbGVjdGVkVGVhbUlkIjowLCJjcmVhdG9ySWQiOiIyMyIsImZtdCI6MSwiaWF0IjoxNzczNzcwNDU2LCJleHAiOjQ4NTMwNjY0NTZ9.K9TIa50Saw4SC63PGPWVWQw21dxq06awpL6vhhqs2igmWOxiWexNxF3f8hRmebEAchEhUKeBpq07dbU2fHEZAw
TRUVERA_API_ENDPOINT=https://api-testnet.truvera.io
Comment thread apps/wallet-server/src/index.ts Outdated
Comment on lines +16 to +21
// wallet-sdk-wasm's storageService calls global.localStorage for DID resolution
// caching during BBS+ proof generation. Node.js has no native localStorage, so
// we use node-localstorage backed by the same /data volume as the wallet DB.
const WALLET_DB_PATH_RESOLVED = process.env.WALLET_DB_PATH || "/data/wallet-db";
const _lsPath = `${WALLET_DB_PATH_RESOLVED}-localstorage`;
(globalThis as any).localStorage = new LocalStorage(_lsPath);
Comment on lines +9 to +11
* Concurrency across processes: SQLite is single-writer. This pool assumes a
* single running process per storage volume, which is enforced by using EBS
* (single-attach) in ECS. Do not use EFS for wallet storage.
Comment thread apps/wallet-server/src/wallet-client-pool.ts
Comment thread apps/wallet-server/jest.setup.js Outdated
Comment on lines +35 to +45
// The wallet SDK can emit late debug logs from background status updates.
// Keep integration tests deterministic by silencing data-store logger output.
jest.mock('@docknetwork/wallet-sdk-data-store/src/logger', () => ({
logger: {
debug: jest.fn(),
performance: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
},
}));
Comment thread terraform/iam.tf Outdated
Comment on lines +80 to +84
Statement = [{
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue", "kms:Decrypt"]
Resource = [var.wallet_secret_arn]
}]

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 57 changed files in this pull request and generated 10 comments.

Files not reviewed (1)
  • terraform/.terraform.lock.hcl: Generated file

Comment thread packages/mcp-shared/src/transport/http/index.ts Outdated
Comment on lines 34 to +38
function normalizeDIDDocument(doc: any): any {
if (!doc || typeof doc !== "object") return doc;
// Clone before mutating — the resolver caches documents in node-localstorage and
// deserialises them on each cache hit, so mutating in place would cause a fresh
// copy to be processed again on the next resolution of the same DID, accumulating
// duplicate entries in verificationMethod.
const result = structuredClone(doc);
const extra: any[] = [];
for (const prop of ["assertionMethod", "authentication", "capabilityInvocation", "capabilityDelegation"]) {
if (!Array.isArray(result[prop])) continue;
result[prop] = result[prop].map((entry: any) => {
if (!Array.isArray(doc[prop])) continue;
Comment thread apps/wallet-server/src/index.ts Outdated
Comment on lines +104 to +106
// Track active MessageClients so we can stop background timers on shutdown
const activeMessageClients = new Set<MessageClient>();

Comment thread apps/wallet-server/src/index.ts
Comment thread apps/wallet-server/test-mcp.js Outdated
Comment thread apps/wallet-server/package.json
Comment thread terraform/variables.tf
Comment thread terraform/environments/staging.tfvars
Comment thread packages/mcp-shared/src/transport/http/index.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 57 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • terraform/.terraform.lock.hcl: Generated file
Comments suppressed due to low confidence (2)

apps/wallet-server/src/index.ts:38

  • normalizeDIDDocument mutates the DID document in-place (including appending to verificationMethod). If the underlying resolver caches the resolved document object, repeated resolves can accumulate duplicate verificationMethod entries and/or pollute the shared DID cache. Clone before normalizing so the cached value remains stable.
function normalizeDIDDocument(doc: any): any {
  if (!doc || typeof doc !== "object") return doc;
  const extra: any[] = [];
  for (const prop of ["assertionMethod", "authentication", "capabilityInvocation", "capabilityDelegation"]) {
    if (!Array.isArray(doc[prop])) continue;

apps/wallet-server/package.json:40

  • These AWS SDK v3 devDependencies require Node.js >=20 (per package-lock), but this package declares engines node>=18. This can break installs/running admin scripts on Node 18 even though the repo docs currently say Node 18+.
  "devDependencies": {
    "@aws-sdk/client-secrets-manager": "^3.1082.0",
    "@aws-sdk/credential-providers": "^3.1082.0",
    "@babel/core": "^7.26.0",

Comment on lines +27 to +37
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
const data = await new Promise<string>((resolve, reject) => {
let chunks = "";
req.on("data", (chunk: Buffer) => {
chunks += chunk;
});
req.on("end", () => resolve(chunks));
req.on("error", reject);
});
return data ? JSON.parse(data) : undefined;
}
Comment on lines 306 to +310
} else if (!sessionId && initializeRequest) {
// New session: create transport and server instance
// New session: resolve auth then create transport and server instance
let authContext;
try {
authContext = await resolveAuthContext(req, authConfig ?? { mode: "none" });
Comment thread terraform/variables.tf
Comment on lines +1 to +4
variable "aws_region" {
description = "AWS region to deploy into"
default = "us-west-1"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants