Guide · AWS Developer Tools · Python / PyPI

MCP Server PyPI Publishing via CodeArtifact — pip.conf, twine, Python CI Publishing

Python MCP servers built with the mcp SDK can be packaged and distributed as pip-installable packages via AWS CodeArtifact — useful for internal tool servers that are consumed by multiple teams or shared via Docker base images. Three pain points catch Python teams on first setup: aws codeartifact login --tool pip and --tool twine write to different config files — pip writes to ~/.config/pip/pip.conf (the index-url), while twine writes to ~/.pypirc (the upload endpoint); you need both login calls if you are both installing dependencies and publishing in the same build; the pip index-url must end with a trailing slash — CodeArtifact's pip endpoint URLs end in /simple/ and omitting the trailing slash produces a 404 during package discovery; and pip's --index-url flag replaces the public PyPI index entirely, meaning that packages not in CodeArtifact (including popular third-party packages not yet cached by the upstream) will fail to install unless you have configured a PyPI upstream connection on the CodeArtifact repository.

TL;DR

Run aws codeartifact login --tool pip to configure pip and aws codeartifact login --tool twine to configure twine (separately — they write different files). The pip endpoint ends in /simple/. Set up a pypi-store upstream connection so pip can fall through to the public PyPI for third-party packages. Use build + twine upload for publishing; flit or uv publish also work with the same credentials.

pyproject.toml for a Python MCP server package

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mcp-server-analytics"
version = "0.1.0"
description = "Analytics MCP server — exposes query_metrics and list_dashboards tools"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
dependencies = [
    "mcp>=1.0.0",
    "httpx>=0.27.0",
    "pydantic>=2.0.0",
]

[project.scripts]
# Allows: uvx mcp-server-analytics or python -m mcp_server_analytics
mcp-server-analytics = "mcp_server_analytics:main"

[tool.hatch.build.targets.wheel]
packages = ["src/mcp_server_analytics"]

# Tool config for publishing — repository name must match [distutils] section in .pypirc
[tool.hatch.publish.index]
repo = "codeartifact"

Using hatchling as the build backend is compatible with CodeArtifact — the same applies to flit-core, setuptools, and pdm-backend. The key requirement is that the built distribution is a standard wheel (.whl) and/or sdist (.tar.gz), which CodeArtifact stores and serves like any other PyPI package.

Setting up the PyPI upstream connection

# Create upstream proxy repository for public PyPI
aws codeartifact create-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository pypi-upstream \
  --description "Upstream proxy for public PyPI"

# Connect the upstream to public PyPI
aws codeartifact associate-external-connection \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository pypi-upstream \
  --external-connection public:pypi

# Create private Python package repository with upstream fallback
aws codeartifact create-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers-python \
  --description "Internal Python MCP server packages"

# Associate the upstream so pip falls through to public PyPI for missing packages
aws codeartifact update-repository \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers-python \
  --upstreams repositoryName=pypi-upstream

Without the upstream connection, pip install httpx fails with "No matching distribution found" even though httpx is published on public PyPI — CodeArtifact will not proxy to PyPI unless explicitly configured.

pip.conf and .pypirc configuration

# pip.conf — written by `aws codeartifact login --tool pip`
# Location: ~/.config/pip/pip.conf (Linux/macOS) or %APPDATA%\pip\pip.ini (Windows)
[global]
index-url = https://aws:<TOKEN>@myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/simple/

# IMPORTANT: The URL must end with /simple/ — pip uses the Simple Repository API
# Omitting the trailing slash causes HTTP 404 during package discovery
# .pypirc — written by `aws codeartifact login --tool twine`
# Location: ~/.pypirc
[distutils]
index-servers = codeartifact

[codeartifact]
repository = https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/
username = aws
password = <TOKEN>

Dual login pattern: CI builds that both install dependencies and publish packages need both login commands:

# Write pip credentials (for: pip install -r requirements.txt)
aws codeartifact login \
  --tool pip \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers-python

# Write twine credentials (for: twine upload dist/*)
aws codeartifact login \
  --tool twine \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers-python

CI buildspec.yml: full Python publish workflow

# buildspec.yml — build and publish Python MCP server package to CodeArtifact
version: 0.2

env:
  variables:
    CODEARTIFACT_DOMAIN: myorg
    CODEARTIFACT_REPO: mcp-servers-python
    AWS_DEFAULT_REGION: us-east-1
  parameter-store:
    CODEARTIFACT_ACCOUNT_ID: /codebuild/codeartifact-account-id

phases:
  install:
    runtime-versions:
      python: 3.12
    commands:
      # Install build tools
      - pip install --upgrade build twine hatchling

  pre_build:
    commands:
      # Refresh pip credentials (expires in 12h — always refresh in pre_build)
      - aws codeartifact login
          --tool pip
          --domain $CODEARTIFACT_DOMAIN
          --domain-owner $CODEARTIFACT_ACCOUNT_ID
          --repository $CODEARTIFACT_REPO

      # Refresh twine credentials
      - aws codeartifact login
          --tool twine
          --domain $CODEARTIFACT_DOMAIN
          --domain-owner $CODEARTIFACT_ACCOUNT_ID
          --repository $CODEARTIFACT_REPO

      # Install development dependencies (test runner, linter)
      - pip install -r requirements-dev.txt

  build:
    commands:
      - echo "Running tests..."
      - python -m pytest tests/ -v --tb=short

      - echo "Building distribution..."
      - python -m build  # Creates dist/*.whl and dist/*.tar.gz

  post_build:
    commands:
      - |
        if [ "$CODEBUILD_BUILD_SUCCEEDING" != "1" ]; then
          echo "Build failed — skipping publish"
          exit 0
        fi

      # Bump version using bump2version or manually via sed
      # (CodeArtifact rejects duplicate version numbers)
      - pip install bump2version
      - bump2version patch --no-commit --no-tag

      # Rebuild with the bumped version
      - python -m build

      - echo "Publishing to CodeArtifact..."
      - twine upload --repository codeartifact dist/*

      - echo "Published $(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"

Installing from CodeArtifact in Dockerfiles

# Dockerfile: install Python MCP server package from CodeArtifact
FROM python:3.12-slim AS builder

# Build-time token — passed by CI via --build-arg
ARG CODEARTIFACT_AUTH_TOKEN
ARG CODEARTIFACT_DOMAIN_URL=https://myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/simple/

WORKDIR /app
COPY requirements.txt .

# Install packages from CodeArtifact (falls through to public PyPI via upstream)
RUN pip install \
      --index-url https://aws:${CODEARTIFACT_AUTH_TOKEN}@myorg-123456789012.d.codeartifact.us-east-1.amazonaws.com/pypi/mcp-servers-python/simple/ \
      --no-cache-dir \
      -r requirements.txt

# Remove the token from the build environment before the final stage
FROM python:3.12-slim AS runner
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . .
CMD ["python", "-m", "mcp_server_analytics"]

The multi-stage pattern ensures the CODEARTIFACT_AUTH_TOKEN build arg does not persist in the final image layer. Use BuildKit's --secret flag (RUN --mount=type=secret,id=pip_conf pip install ...) for an even cleaner approach that keeps the token out of layer history entirely.

Using uv with CodeArtifact

uv (Astral's fast Python package manager) supports CodeArtifact via environment variables — no config file changes required:

# Get the CodeArtifact token
TOKEN=$(aws codeartifact get-authorization-token \
  --domain myorg \
  --domain-owner 123456789012 \
  --query authorizationToken \
  --output text)

ENDPOINT=$(aws codeartifact get-repository-endpoint \
  --domain myorg \
  --domain-owner 123456789012 \
  --repository mcp-servers-python \
  --format pypi \
  --query repositoryEndpoint \
  --output text)

# Set environment variables for uv
export UV_INDEX_URL="https://aws:${TOKEN}@${ENDPOINT#https://}simple/"

# Install normally — uv reads UV_INDEX_URL
uv pip install mcp-server-analytics

# Publish with uv (uv 0.4+)
uv publish \
  --publish-url "${ENDPOINT}" \
  --token "${TOKEN}" \
  dist/*.whl

Common failure modes

SymptomCauseFix
"No matching distribution found for mcp-server-analytics"pip is using public PyPI (not CodeArtifact) because pip.conf was not configured or the token expiredRun aws codeartifact login --tool pip before installing; verify pip config get global.index-url points to CodeArtifact
pip returns 404 on all packages including public ones (e.g., httpx)The CodeArtifact repository has no PyPI upstream connection; packages not in CodeArtifact cannot be resolvedAdd pypi-upstream with public:pypi external connection as an upstream to your private repository
"HTTPError: 400 Bad Request — File already exists" on twine uploadThe package version already exists in CodeArtifact; versions are immutableBump the version in pyproject.toml before rebuilding and uploading; use bump2version or hatch version patch
pip succeeds but twine upload gets 403aws codeartifact login --tool pip was called but --tool twine was not — they write different files (pip.conf vs .pypirc)Run both login commands; or manually set TWINE_REPOSITORY_URL and TWINE_PASSWORD environment variables from the CodeArtifact token
pip.conf index-url produces 404 for package discoveryThe URL is missing the trailing /simple/ path — CodeArtifact PyPI endpoints must end in /simple/Ensure the index-url ends with /simple/ (the AWS CLI login helper adds this automatically)
"Invalid distribution file" on twine uploadThe distribution was built with a version containing local metadata (1.0.0+local) which PyPI and CodeArtifact rejectUse only PEP 440-compliant versions; avoid +local suffixes in CI builds
pip install hangs then times out in Docker buildThe CodeArtifact token in the Dockerfile build arg has expired (built the image again more than 12h after the token was fetched)Always fetch a fresh token at Docker build time in CI rather than caching it; token lifetime is a maximum of 12 hours