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:
- Check
mcp-serversfor a local copy of the package. - If not found, traverse the upstream chain to
npm-upstream. - If not found there, fetch from
registry.npmjs.organd cache it innpm-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:
- The IAM principal (CI role, developer, ECS task) calls
codeartifact:GetAuthorizationTokenon the domain ARN. - CodeArtifact calls
sts:GetServiceBearerTokeninternally — this is why both actions must be in the role policy. - 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).
- 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
| Format | External connection string | Login tool | Config file modified |
|---|---|---|---|
| npm | public:npmjs | --tool npm | ~/.npmrc |
| PyPI | public:pypi | --tool pip or --tool twine | ~/.config/pip/pip.conf, ~/.pypirc |
| Maven | public:maven-central | --tool mvn or --tool gradle | ~/.m2/settings.xml |
| NuGet | public:nuget-org | --tool dotnet | NuGet.Config |
| Swift | public:swift-index | manual token | Package.resolved |
| Ruby Gems | public: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
| Symptom | Cause | Fix |
|---|---|---|
| "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 separately | Add sts:GetServiceBearerToken to the role policy; resource can be "*" |
| "npm ERR! 401 Unauthorized" after working for days | The 12-hour token stored in .npmrc expired; CI job queued long enough for the token to age out | Refresh 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 default | Use aws codeartifact delete-package-versions to clear the cached version; the next install refetches from the public registry |
| "ResourceNotFoundException: domain not found" on repository create | CDK or CLI is targeting a different account or region than the domain | Pass --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 token | The repository does not have an upstream connection, so it cannot resolve packages from the public registry; the 403 comes from the upstream lookup failing | Associate 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 version | CodeArtifact enforces immutability on published package versions by default | Bump 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 one | Import the existing resource with CfnRepository.fromCfnRepositoryAttributes() or delete the manual resource before deploying |