An MCP server providing AI assistants with tools and knowledge for Arm architecture development, migration, and optimization.
If your goal is to migrate an application from x86 to Arm as quickly as possible, start here:
Automate x86-to-Arm application migration using Arm MCP Server
This MCP server equips AI assistants with specialized tools for Arm development:
- Knowledge Base Search: Semantic search across Arm documentation, learning resources, intrinsics, and software compatibility information
- Code Migration Analysis: Scan codebases for Arm compatibility using migrate-ease (supports C++, Python, Go, JavaScript, Java)
- Container Architecture Inspection: Check Docker image architecture support using integrated Skopeo and check-image tools.
- Assembly Performance Analysis: Analyze assembly code performance using LLVM-MCA
- Arm Performix: Run APX recipe workflows against a target device over SSH to capture and analyze workload performance data
- System Information: Instructions for gathering detailed system architecture information via sysreport
If you would prefer to use a pre-built, multi-arch image, the official image can be found in Docker Hub here: armlimited/arm-mcp:latest
- Docker with Buildx support
- An MCP-compatible AI assistant client (e.g. GitHub Copilot, Kiro CLI, Codex CLI, Claude Code, etc)
From the root of this repository:
docker buildx build -f mcp-local/Dockerfile -t armlimited/arm-mcp . --loadThis builds for the Docker host's native architecture. The release workflow is responsible for explicit multi-architecture builds.
Choose the configuration that matches your MCP client:
The examples below include the optional Docker arguments required for Arm Performix. These SSH-related settings are only needed when you want the MCP server to run remote commands on a target device through Arm Performix. If you are not using Arm Performix, you can omit the SSH -v lines.
Add to .mcp.json in your project:
{
"mcpServers": {
"arm-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--pull=always",
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"armlimited/arm-mcp"
]
}
}
}Add to .vscode/mcp.json in your project, or globally at ~/Library/Application Support/Code/User/mcp.json (macOS):
{
"servers": {
"arm-mcp": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--pull=always",
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"armlimited/arm-mcp"
]
}
}
}The easiest way to open this file in VS Code for editing is command+shift+p and search for
MCP: Open User Configuration
Add to ~/.kiro/settings/mcp.json:
{
"mcpServers": {
"arm-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--pull=always",
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"armlimited/arm-mcp"
],
"timeout": 60000
}
}
}It is recommended to use a project-local configuration file to ensure the relevant workspace is mounted.
Add to .gemini/settings.json in your project root:
{
"mcpServers": {
"arm-mcp": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--pull=always",
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"armlimited/arm-mcp"
]
}
}
}[mcp_servers.arm-mcp]
command = "docker"
args = [
"run",
"--rm",
"-i",
"--pull=always",
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"armlimited/arm-mcp"
]Note: Replace /path/to/your/workspace with the actual path to your project directory that you want the MCP server to access. If you are enabling Arm Performix, also replace the /path/to/your/ssh/private_key and /path/to/your/ssh/known_hosts paths with your local files. The MCP container auto-discovers files mounted under /run/keys, as shown in the configs above.
After updating the configuration, restart your MCP client to load the Arm MCP server.
Depending on usage, the server may write two log files under /workspace. With the
configuration examples above, these files appear in the project directory on
your computer:
mcp-traffic.jsonlrecords when tools are used, the inputs provided, and the reason for each tool call. It also records results from knowledge base searches.error_logging.yamlrecords details about errors encountered by the server. This information can help with troubleshooting.
These logs may contain information from your project and tool requests. Review their contents before sharing them.
mcp-local/: The MCP server implementationserver.py: Main FastMCP server with tool definitionsutils/: Helper modules for each tooldata/: Pre-built knowledge base (embeddings and metadata)Dockerfile: Multi-stage Docker build
embedding-generation/: Scripts for regenerating the knowledge base from source documents
- Build the mcp server docker image
- Install the locked test dependencies with
uv sync --locked --only-group testwithin themcp-localdirectory.
- Run the test script with
uv run --locked --only-group test pytest -s tests/test_mcp.py - Check if following 2 docker containers have started - mcp server & testcontainer
- All tests should pass without any errors. Warnings can be ignored.
The final MCP image build does not resolve or download Python packages, Ubuntu
packages, Performix, or migrate-ease. Those inputs are acquired separately by
the manually triggered Build MCP Input Bundle workflow and published as a
private, multi-architecture OCI image at ghcr.io/arm/mcp-build-inputs.
The acquisition workflow runs natively on AMD64 and Arm64. For each architecture it:
- verifies the checked-in uv lock and exports hash-locked pip requirements;
- downloads the exact Python wheels allowed by those hashes;
- downloads the complete
.debclosures from the recorded Ubuntu snapshot and checks every package againstmcp-local/build-inputs.lock.json; - downloads and verifies the architecture-specific Performix archive and the pinned migrate-ease source archive; and
- publishes those bytes and their lock metadata in a scratch image, then combines both architecture images into one private OCI image index.
mcp-local/Dockerfile selects the matching architecture from that index and
copies the files from it. Python and apt installation use only those local
files with network access disabled. The Ubuntu base image, input bundle, and
embedding vector-store image are all selected by immutable OCI digest. The
currently approved input bundle is recorded in
mcp-local/build-inputs.lock.json and defaults to:
ghcr.io/arm/mcp-build-inputs@sha256:8db95af8e7d819b82adbed0bd1c9eadcd1d0f2afdd144c52618b58bccfbf07cf
Each Dockerfile represents a different trust or network boundary:
mcp-local/Dockerfile.inputshas noRUNcommands and starts fromscratch. It packages the verified wheels,.debfiles, archives, and lock metadata into the multi-architecture OCI artifact that GHCR can store.mcp-local/Dockerfileis the final application build. It consumes the approved MCP-input and embedding artifacts by digest and installs their contents with networking disabled.embedding-generation/Dockerfile.toolchaincreates the pinned Python and model environment used by the embedding pipeline.embedding-generation/Dockerfile.acquireis the controlled network phase that collects source material and publishes the resulting chunks.embedding-generation/Dockerfile.vectorstoreconsumes the pinned toolchain and chunks, generates the model index and metadata offline, and packages the output in ascratchartifact for the MCP image.
These could technically be stages in one large Dockerfile, but keeping the
artifacts separate lets the workflows publish, inspect, cache, approve, and
pin each boundary independently. It also makes it difficult for a supposedly
offline phase to acquire dependencies accidentally. Dockerfile.inputs is
the small adapter needed because GHCR stores OCI images rather than arbitrary
directories; using a scratch image adds no runtime operating system.
Authenticate Docker to GHCR before building because the build-input and embedding images are private:
docker login ghcr.io
docker buildx build \
--network none \
--file mcp-local/Dockerfile \
--tag arm-mcp:local \
--load \
.The local build defaults to the Docker host's native architecture. GitHub Actions performs the same GHCR login and explicitly builds both release architectures without running the acquisition script.
The Dockerfile installs the application but does not run the application test
suite during image assembly. After building arm-mcp:local, run a lightweight
container smoke test explicitly:
docker run --rm \
--entrypoint sh \
arm-mcp:local \
-c 'set -eu
skopeo --version
llvm-mca --version
git --version
test -x "$APX_BIN"
migrate-ease-cpp --help >/dev/null
python -c "import magic, requests; from utils.docker_utils import check_docker_image_architectures"'The packaged embedding model can be checked separately without network access:
docker run --rm \
--network none \
--entrypoint python \
arm-mcp:local \
-c "from sentence_transformers import SentenceTransformer; SentenceTransformer('/app/embedding-model', local_files_only=True, trust_remote_code=False)"For the full MCP protocol and tool integration suite, tag the local image as
arm-mcp:latest or set MCP_IMAGE=arm-mcp:local, then follow the repository's
integration-test setup. The APX integration cases additionally require the SSH
target, key mounts, and Java workload configured in
.github/workflows/integration-tests.yml.
The index and its platform manifests can be inspected without unpacking it:
MCP_INPUTS="ghcr.io/arm/mcp-build-inputs@sha256:8db95af8e7d819b82adbed0bd1c9eadcd1d0f2afdd144c52618b58bccfbf07cf"
docker buildx imagetools inspect "$MCP_INPUTS"
docker buildx imagetools inspect "$MCP_INPUTS" --format '{{json .Manifest}}' | jqTo inspect the actual files for the Docker host's native architecture:
docker pull "$MCP_INPUTS"
container_id="$(docker create "$MCP_INPUTS")"
docker cp "$container_id:/mcp-build-inputs" ./mcp-build-inputs-inspect
docker rm "$container_id"
find ./mcp-build-inputs-inspect -maxdepth 3 -type f | sortThe copied metadata/ directory contains the checked-in dependency metadata
and the generated pip-compatible lock used during acquisition. The checked-in
manifest additionally records the published index digest, per-architecture
manifest digests, source commit, workflow run, and verification method.
Refreshing is deliberately a reviewed two-commit process because an OCI artifact cannot contain its own not-yet-known digest:
mcp-local/pyproject.toml is the only direct dependency declaration, and
mcp-local/uv.lock is the only checked-in transitive dependency lock. Use the
uv version recorded in mcp-local/build-inputs.lock.json to update the lock:
uv --version
uv lock --directory mcp-local --upgrade-package PACKAGE_NAMEReview and commit the pyproject.toml and uv.lock changes. During input
acquisition, pinned uv exports a pip-compatible hashed lock from uv.lock.
That generated file is used to download the AMD64 and Arm64 wheelhouses and is
preserved as metadata/requirements.lock in the immutable GHCR input artifact;
it is not checked into the repository.
An interpreter upgrade changes the wheel ABI and the Ubuntu package closure. Update all of the following together:
mcp-local/.python-version;project.requires-pythoninmcp-local/pyproject.toml;generated_with.pythoninmcp-local/build-inputs.lock.json;python-versionin.github/workflows/build-mcp-inputs.yml;- the
--python-versionand--abiarguments inmcp-local/scripts/stage-build-inputs.py; and - the builder and runtime Python packages in the Ubuntu package lock, if the
selected Ubuntu base does not provide the new interpreter through the
existing
python3package.
Regenerate the uv lock, refresh the Ubuntu package manifests as described below, and build both architectures before accepting the upgrade.
Change the snapshot timestamp and/or requested package roles in
mcp-local/build-inputs.lock.json. On Linux with Docker configured for both
target architectures, regenerate the exact .deb closures and hashes with:
python3 mcp-local/scripts/stage-build-inputs.py \
--arch all \
--skip-wheels \
--refresh-os-lockReview every package addition, removal, version change, and checksum change in the manifest before committing it. The normal publication workflow does not rewrite this lock; it fails if the snapshot produces different bytes.
Update the versioned URL or source revision and the expected SHA256 in
mcp-local/build-inputs.lock.json. Prefer an upstream-published checksum when
one is available. Performix has separate AMD64 and Arm64 artifacts;
migrate-ease is one pinned source archive used by both architectures. The
publication workflow fails before publishing if any archive differs from its
recorded checksum.
Record immutable @sha256: references for new Ubuntu or MCP-input images. For
a multi-architecture image, also record its AMD64 and Arm64 platform manifest
digests. Keep the corresponding UBUNTU_IMAGE or MCP_BUILD_INPUTS_IMAGE
default in mcp-local/Dockerfile synchronized with the checked-in manifest.
Production must never consume a mutable tag.
Embedding updates use an automated promotion PR instead of being copied into the MCP release directly:
- Let Build Offline Embedding Pipeline run from
mainevery Sunday at 09:00 UTC, or start it manually for an out-of-band update. - The workflow publishes an immutable candidate vector-store image and opens
or updates
automation/pin-embedding-vectorstore. - The promotion branch updates both
container_images.embeddingsinmcp-local/build-inputs.lock.jsonandEMBEDDINGS_IMAGEinmcp-local/Dockerfile. It also records the embedding source commit and workflow run. - Review the source revision and digest change before merging.
- Merging the promotion PR to
maintriggers a minor MCP release. Merely generating an embedding candidate does not release or alter the MCP image.
The promotion workflow never merges its own PR. This preserves the reviewed, checked-in digest as the release boundary and keeps MCP releases independent from unsuccessful or unwanted embedding candidates.
After updating any source lock, use the same common publication process:
-
Commit and push the updated source locks to a branch.
-
Start the workflow on that branch:
gh workflow run build-mcp-inputs.yml --ref YOUR_BRANCH
-
Confirm that both native architecture jobs pass and that the workflow reports the package as private.
-
Inspect the published index, then copy its immutable index digest, per-architecture manifest digests, source commit, and workflow run into the
container_images.mcp_build_inputsentry inmcp-local/build-inputs.lock.json. -
Update the
MCP_BUILD_INPUTS_IMAGEdefault inmcp-local/Dockerfileto the same index digest and submit that pin as a reviewed follow-up change. -
Run the integration workflow. The release and integration builds must pull the digest and must never invoke
stage-build-inputs.py.
The publication workflow also creates a tag containing the source commit, workflow run ID, and attempt. That tag is only a discovery aid; production builds always use the digest.
Rollback is a reviewed pin change. Restore the last approved image references
and metadata in mcp-local/build-inputs.lock.json, and keep the corresponding
image defaults in mcp-local/Dockerfile synchronized. Submit the rollback
through the normal pull-request process and run the AMD64 and Arm64 integration
builds before release. Do not delete, overwrite, or retag the immutable GHCR
artifacts.
To debug or explore the container environment:
docker run --rm -it --entrypoint /bin/bash armlimited/arm-mcp- Timeout errors during migration scans: Increase the
timeoutvalue in your MCP client configuration (e.g.,"timeout": 120000for 2 minutes) - Empty workspace: Ensure your volume mount path is correct and the directory exists
- Architecture mismatches: Confirm that the local image matches the Docker host's native architecture; use the release workflow for explicit cross-platform builds.
Contributions are welcome! Please feel free to submit issues or pull requests.
When contributing:
- Follow PEP 8 style guidelines for Python code
- Update documentation for any new features or changes
- Ensure the Docker image builds successfully before submitting
Note:
Images tagged latest and semantic version tags (e.g., 2.3.0) should be treated as the prod environment, while dated tags (YYYY-MM-DD-<run_number>, e.g., 2026-05-31-123) should be treated as the stage environment. The dev environment refers only to locally built images created by individual developers.
Copyright © 2026, Arm Limited and Contributors. All rights reserved.
Licensed under the Apache License, Version 2.0. See LICENSE for details.