Guide · AWS Developer Tools · Package Registry

AWS CodeArtifact for MCP Servers — Domain Setup, Upstream Proxying, Token Authentication

AWS CodeArtifact is a fully managed artifact repository that serves as a private npm, PyPI, Maven, or NuGet registry — MCP server teams use it to publish versioned server packages internally and to proxy public registries so CI builds never hit npm/PyPI rate limits. Three subtleties trip nearly every team on first setup: the domain/repository hierarchy is not optional — a domain is the top-level namespace (one per AWS account or shared via cross-account) and a repository lives inside a domain; you cannot create a repository without a domain, and the domain name becomes part of every endpoint URL; the authorization token from get-authorization-token expires after 12 hours, which means CI buildspec.yml files that cache tokens in SSM or environment variables will silently fail on long-running CodeBuild queues; and sts:GetServiceBearerToken is a separate IAM action required alongside codeartifact:GetAuthorizationToken — omitting it from the role policy produces a confusing "User is not authorized to perform: sts:GetServiceBearerToken" error even when the codeartifact action is granted.

TL;DR

Create a CodeArtifact domain first, then repositories inside it. Add an upstream connection to npm-public-registry or pypi-store so private repos proxy public packages. Authenticate with aws codeartifact get-authorization-token --domain <domain> --query authorizationToken --output text — the returned token is valid for 12 hours and used as the password for npm/pip/twine. IAM roles need both codeartifact:GetAuthorizationToken and sts:GetServiceBearerToken to authenticate successfully.

Domain and repository hierarchy

CodeArtifact uses a two-level hierarchy: a domain is the top-level organizational unit (think "company namespace") and repositories live inside it. The domain name is part of the endpoint URL: https://<domain>-<account>.d.codeartifact.<region>.amazonaws.com/npm/<repo>/.

# Create a domain (one per org/account is typical)
aws codeartifact create-domain \
  --domain myorg \
  --region us-east-1

# Create a private npm repository inside the domain
aws codeartifact create-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers \
  --description "Internal MCP server npm packages"

# Create an upstream connection repository that proxies npm public registry
aws codeartifact create-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository npm-upstream \
  --description "Upstream proxy for public npm registry"

# Associate the upstream into your private repository
aws codeartifact update-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers \
  --upstreams repositoryName=npm-upstream

# Connect the upstream proxy to public npm
aws codeartifact associate-external-connection \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository npm-upstream \
  --external-connection public:npmjs

With this setup, an npm install pointed at mcp-servers will:

  1. Check mcp-servers for a local copy of the package.
  2. If not found, traverse the upstream chain to npm-upstream.
  3. If not found there, fetch from registry.npmjs.org and cache it in npm-upstream.

The cached copy stays in npm-upstream — subsequent installs never hit the public registry again, protecting against both rate limits and supply-chain compromises like a package being unpublished.

Token authentication flow

CodeArtifact uses short-lived bearer tokens rather than static API keys. The authentication flow is:

  1. The IAM principal (CI role, developer, ECS task) calls codeartifact:GetAuthorizationToken on the domain ARN.
  2. CodeArtifact calls sts:GetServiceBearerToken internally — this is why both actions must be in the role policy.
  3. A token is returned. It is valid for a configurable duration (default 43,200 seconds = 12 hours; can be set down to 900 seconds = 15 minutes).
  4. The token is used as the HTTP basic-auth password (username is always aws).
# Get a token for the next 6 hours (21600 seconds)
TOKEN=$(aws codeartifact get-authorization-token \
  --domain myorg \
  --domain-owner 123456789012 \
  --duration-seconds 21600 \
  --query authorizationToken \
  --output text)

# Get the npm registry endpoint URL
REGISTRY=$(aws codeartifact get-repository-endpoint \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers \
  --format npm \
  --query repositoryEndpoint \
  --output text)

# Configure npm to use CodeArtifact (writes to project .npmrc)
npm config set registry $REGISTRY
npm config set //${REGISTRY#https://}:_authToken=$TOKEN

# Equivalent: use the aws codeartifact login helper (does the above automatically)
aws codeartifact login \
  --tool npm \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers

Token expiry in CI: CodeBuild builds that queue for more than 12 hours (common in large orgs with concurrency limits) may start with a stale token. Refresh the token at the start of the pre_build phase in every build rather than reading it from SSM where it was pre-populated.

CDK setup

// CDK: CodeArtifact domain + repository + upstream proxy for MCP server packages
import * as codeartifact from "aws-cdk-lib/aws-codeartifact";
import * as iam from "aws-cdk-lib/aws-iam";

// Domain — one per org; shared via cross-account resource policies if needed
const domain = new codeartifact.CfnDomain(this, "McpDomain", {
  domainName: "myorg",
  // Optional: encrypt with a customer-managed KMS key
  // encryptionKey: myKey.keyArn,
});

// Upstream proxy repo: pulls from public npm when packages are missing
const npmUpstream = new codeartifact.CfnRepository(this, "NpmUpstream", {
  domainName: domain.domainName,
  repositoryName: "npm-upstream",
  externalConnections: ["public:npmjs"],
  description: "Upstream proxy for public npm registry",
});
npmUpstream.addDependency(domain);

// Private repo for internal MCP server packages; falls through to upstream
const mcpServersRepo = new codeartifact.CfnRepository(this, "McpServersRepo", {
  domainName: domain.domainName,
  repositoryName: "mcp-servers",
  upstreams: [npmUpstream.repositoryName],
  description: "Internal MCP server npm packages",
});
mcpServersRepo.addDependency(npmUpstream);

// CI role: publish packages to CodeArtifact
const ciRole = new iam.Role(this, "CiRole", {
  assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"),
});

ciRole.addToPolicy(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  actions: [
    "codeartifact:GetAuthorizationToken",      // Get the short-lived token
    "codeartifact:GetRepositoryEndpoint",       // Resolve the npm/PyPI URL
    "codeartifact:PublishPackageVersion",       // Publish new package versions
    "codeartifact:PutPackageMetadata",          // Update package metadata
    "codeartifact:ReadFromRepository",          // Read (install) packages
    "sts:GetServiceBearerToken",                // Required alongside GetAuthorizationToken
  ],
  resources: ["*"],  // Scope to domain/repo ARNs in production
}));

CDK L1 construct caveat: CodeArtifact CDK support uses L1 (CloudFormation) constructs as of CDK v2.140. There are no L2 constructs. The CfnDomain and CfnRepository resources deploy without issues, but you need addDependency() to sequence domain → upstream → private repo; without it, CloudFormation may attempt to create the repos before the domain exists.

Supported package formats

FormatExternal connection stringLogin toolConfig file modified
npmpublic:npmjs--tool npm~/.npmrc
PyPIpublic:pypi--tool pip or --tool twine~/.config/pip/pip.conf, ~/.pypirc
Mavenpublic:maven-central--tool mvn or --tool gradle~/.m2/settings.xml
NuGetpublic:nuget-org--tool dotnetNuGet.Config
Swiftpublic:swift-indexmanual tokenPackage.resolved
Ruby Gemspublic:ruby-gems-org--tool gem~/.gemrc

AliveMCP probe-worker pattern

AliveMCP's probe worker consumes internal utility packages (retry logic, protocol parsers) distributed via CodeArtifact. The ECS task role for the probe worker has codeartifact:ReadFromRepository and codeartifact:GetAuthorizationToken + sts:GetServiceBearerToken. At container startup, an entrypoint script calls aws codeartifact login --tool npm to refresh the 12-hour token into /root/.npmrc, then runs npm install --prefer-offline to pick up any updated packages before the probe loop starts. The --prefer-offline flag means that if the CodeArtifact endpoint is temporarily unavailable, the container starts with the cached node_modules rather than crashing.

Common failure modes

SymptomCauseFix
"User is not authorized to perform: sts:GetServiceBearerToken"sts:GetServiceBearerToken missing from the IAM role — this action is distinct from codeartifact:GetAuthorizationToken and must be granted separatelyAdd sts:GetServiceBearerToken to the role policy; resource can be "*"
"npm ERR! 401 Unauthorized" after working for daysThe 12-hour token stored in .npmrc expired; CI job queued long enough for the token to age outRefresh the token at the start of every build inside pre_build, never pre-populate it in SSM
npm install pulls the wrong package version (stale cache)The upstream cached an old version; CodeArtifact caches the first-seen version by defaultUse aws codeartifact delete-package-versions to clear the cached version; the next install refetches from the public registry
"ResourceNotFoundException: domain not found" on repository createCDK or CLI is targeting a different account or region than the domainPass --domain-owner <account-id> to every CLI command; in CDK, depend on the domain resource explicitly
Repo endpoint URL returns 403 even with a valid tokenThe repository does not have an upstream connection, so it cannot resolve packages from the public registry; the 403 comes from the upstream lookup failingAssociate the upstream (npm-upstream / pypi-store) to the private repository and ensure the upstream has an external connection
"Package version already exists" when republishing the same versionCodeArtifact enforces immutability on published package versions by defaultBump the version before publishing; if you need to overwrite during development, use aws codeartifact delete-package-versions first
CDK deploy fails with "repository already exists"A manually created repository conflicts with the CDK-managed oneImport the existing resource with CfnRepository.fromCfnRepositoryAttributes() or delete the manual resource before deploying