AAS MCP Server

Un adaptador MCP de AAS que expone APIs de Asset Administration Shell configuradas como herramientas del Model Context Protocol, permitiendo que agentes de LLM interactúen con cualquier backend compatible con AAS.

Documentación

REUSE status

AAS MCP Server

About this project

OpenAPI-to-MCP bridge for Asset Administration Shell (AAS) APIs

An AAS MCP adapter that exposes configured Asset Administration Shell APIs as Model Context Protocol tools, enabling LLM agents to interact with any AAS-compliant backend.

License Python 3.12+

Requirements and Setup

Prerequisites

  1. AAS OpenAPI Specifications - the IDTA Part 2 API specs from the git tags of admin-shell-io/aas-specs-api, bundled into one file per API (needs git, perl and Node.js for npx)
  2. AAS Backend Server - SAP BNAC AAS Server, Eclipse BaSyx, FA³ST Service, etc.
  3. Python 3.12+ OR Docker

Setup

  1. Get AAS Specifications:

    TAG=v3.2.0
    SPEC=AssetAdministrationShellRepositoryServiceSpecification/V3.2_SSP-001.yaml
    
    # 1. Clone the tag (a throwaway copy, step 2 edits it)
    git clone -q --depth 1 --branch "$TAG" \
      https://github.com/admin-shell-io/aas-specs-api "aas-specs-api-$TAG"
    
    # 2. v3.1.x only (no-op for v3.2.0): use the shared schemas of the same tag
    #    instead of fetching them from SwaggerHub
    find "aas-specs-api-$TAG" -name '*.yaml' -exec perl -pi -e \
      's#https://api\.swaggerhub\.com/domains/Plattform_i40/(Part1-MetaModel-Schemas|Part2-API-Schemas)/V[0-9.]+#../$1/openapi.yaml#g' {} +
    
    # 3. Bundle into one self-contained file
    mkdir -p "specs/$TAG/$(dirname "$SPEC")"
    npx -y @redocly/cli@1.25.0 bundle "aas-specs-api-$TAG/$SPEC" -o "specs/$TAG/$SPEC"
    

    Repeat step 3 with SPEC set to the file of each other component, e.g. SubmodelRepositoryServiceSpecification/V3.2_SSP-001.yaml, AssetAdministrationShellRegistryServiceSpecification/V3.2_SSP-001.yaml, SubmodelRegistryServiceSpecification/V3.2_SSP-001.yaml, ConceptDescriptionRepositoryServiceSpecification/V3.2_SSP-001.yaml or DiscoveryServiceSpecification/V3.2_SSP-001.yaml. The file names carry the minor version only (V3.2_SSP-001.yaml for all v3.2.x tags), so the tag goes into the folder name. Available tags: git ls-remote --tags https://github.com/admin-shell-io/aas-specs-api.

  2. Create config.yaml (copy from config.yaml.template):

    components:
      aas-repo:
        official_spec: specs/v3.2.0/AssetAdministrationShellRepositoryServiceSpecification/V3.2_SSP-001.yaml
        curation:
          allowlist:
            - [get, "*"]  # All GET operations (wildcard)
            - [post, /shells]
    
  3. Run:

    # Docker (recommended) — pulls the pre-built image from GitHub Container Registry
    docker run \
      -v $(pwd)/config.yaml:/app/config/config.yaml \
      -v $(pwd)/specs:/app/config/specs \
      -e AAS_COMPONENT=aas-repo \
      -e AAS_BASE_URL=http://your-backend:8080 \
      -i ghcr.io/sap/aas-mcp-server:latest
    
    # Or install locally
    pip install -e .
    aas-mcp-server --component aas-repo --base-url http://localhost:8080 --config config.yaml
    

    The image is published as multi-arch (linux/amd64, linux/arm64) at ghcr.io/sap/aas-mcp-server. Available tags: latest, <major>.<minor> (e.g. 0.1), and <version> (e.g. 0.1.0). No login is needed — the package is public.

Configuration

Basic (Official Spec Only)

components:
  aas-repo:
    official_spec: specs/aas-repo-spec.yaml

Filtered (Implementation-Specific)

Filter to only endpoints your backend supports:

components:
  aas-repo:
    official_spec: specs/aas-repo-official.yaml
    implementation_spec: specs/aas-supported-endpoints.yaml

Result: Only endpoints in both specs are exposed (intersection).

With Curation (Wildcards Supported)

Control which operations are exposed using wildcards:

components:
  aas-repo:
    official_spec: specs/aas-repo-spec.yaml
    curation:
      allowlist:
        # Specific operations
        - [get, /shells]
        - [post, /shells]
        
        # Wildcards
        - [get, "*"]          # All GET operations on any path
        - ["*", /shells]      # All methods on /shells path
        - ["*", "*"]          # All methods on all paths (use with caution!)
        
      aliases:
        GetAllAssetAdministrationShells: list_shells
        PostAssetAdministrationShell: create_shell

See config.yaml.template for complete options.

MCP Client Configuration

The same aas-mcp-server binary works with all MCP-compatible clients — the server logic is identical, only the config format differs per client. Full examples for all clients are available in client_config_examples.txt.

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows). See claude_desktop_config.example.json for the full four-component example.

{
  "mcpServers": {
    "aas-repo": {
      "command": "aas-mcp-server",
      "args": [
        "--component", "aas-repo",
        "--base-url", "http://localhost:8080",
        "--config", "/path/to/your/config.yaml"
      ],
      "env": { "LOG_LEVEL": "INFO" }
    }
  }
}

Claude CLI (Claude Code)

claude mcp add aas-repo \
  --env LOG_LEVEL=INFO \
  -- aas-mcp-server \
     --component aas-repo \
     --base-url http://localhost:8080 \
     --config /path/to/your/config.yaml

Scope options: --scope local (default, current project), --scope user (all projects), --scope project (shared with team via .mcp.json).

OpenCode

Add to opencode.json in your project root:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "aas-repo": {
      "type": "local",
      "command": [
        "aas-mcp-server",
        "--component", "aas-repo",
        "--base-url", "http://localhost:8080",
        "--config", "/path/to/your/config.yaml"
      ],
      "enabled": true,
      "environment": { "LOG_LEVEL": "INFO" }
    }
  }
}

See client_config_examples.txt for all four components, authentication setup, and write-mode configuration for every client.

Docker Usage

The pre-built image is published to GitHub Container Registry at ghcr.io/sap/aas-mcp-server (multi-arch linux/amd64 + linux/arm64, SBOM and build-provenance attested). The examples below use :latest; pin to a specific version (:0.1.0) or minor line (:0.1) for reproducible deployments.

Basic (stdio — local use, no auth)

docker run \
  -v $(pwd)/config.yaml:/app/config/config.yaml \
  -v $(pwd)/specs:/app/config/specs \
  -e AAS_COMPONENT=aas-repo \
  -e AAS_BASE_URL=http://your-backend:8080 \
  -i ghcr.io/sap/aas-mcp-server:latest

HTTP Transport with OAuth 2.1

For remote deployments where MCP clients connect over the network:

docker run \
  --network your-docker-network \
  -v $(pwd)/config.yaml:/app/config/config.yaml \
  -v $(pwd)/specs:/app/config/specs \
  -e AAS_COMPONENT=aas-repo \
  -e AAS_BASE_URL=http://your-backend:8080 \
  -e MCP_TRANSPORT=streamable-http \
  -e MCP_HOST=0.0.0.0 \
  -e MCP_PORT=8000 \
  -e OAUTH_ISSUER_URL=https://your-idp/realms/your-realm \
  -e OAUTH_CLIENT_ID=your-client-id \
  -e OAUTH_CLIENT_SECRET=your-client-secret \
  -e OAUTH_SERVER_BASE_URL=http://localhost:8000 \
  -p 8000:8000 \
  ghcr.io/sap/aas-mcp-server:latest

Register with an MCP client (example using Claude CLI):

claude mcp add aas-repo \
  --transport http \
  --scope user \
  --client-id your-oauth-client-id \
  http://localhost:8000/mcp

Custom Config Path

docker run \
  -v $(pwd)/my-config.yaml:/custom/config.yaml \
  -v $(pwd)/specs:/custom/specs \
  -e CONFIG_PATH=/custom/config.yaml \
  -e AAS_COMPONENT=aas-repo \
  -e AAS_BASE_URL=http://your-backend:8080 \
  -i ghcr.io/sap/aas-mcp-server:latest

Outbound AAS Request Headers

Set AAS_HTTP_HEADERS to a JSON object to add static headers to every request that an MCP tool sends to the AAS backend. For example, configure tenant and routing headers in Docker with shell-safe single quotes:

-e 'AAS_HTTP_HEADERS={"X-Tenant-ID":"tenant-a","X-Route":"internal"}'

Each header name and value must be a JSON string. The server rejects malformed JSON, non-object values, and Authorization or Accept header names (case-insensitively). Authorization remains controlled by the backend authentication strategy, and Accept remains application/json.

Header values can be sensitive. Protect AAS_HTTP_HEADERS as configuration secret material: do not commit values, avoid exposing them in command history, and prefer your deployment platform's secret/config injection mechanism when appropriate.

Forward selected inbound HTTP headers

Set AAS_FORWARD_HEADERS to a JSON array of header names to pass an operator-approved header from each HTTP MCP request to the AAS backend. For example, an HTTP client can supply its own subscription key while the server keeps a static fallback:

-e 'AAS_HTTP_HEADERS={"X-Subscription-Key":"deployment-default"}' \
-e 'AAS_FORWARD_HEADERS=["X-Subscription-Key"]'

Names match case-insensitively. A forwarded value overrides the matching AAS_HTTP_HEADERS value for that request; when the client omits the header, the static value remains in effect. The feature applies only to HTTP MCP requests—stdio and other calls without an HTTP request context forward no headers.

The allowlist rejects server-managed, credential, routing, hop-by-hop, and proxy-identity headers, including Authorization, Accept, Cookie, Host, Content-Length, Forwarded, X-Forwarded-*, and X-Real-IP. Never log or commit user header values. Allowlist only headers appropriate for your access model and protect the MCP HTTP endpoint with authentication.

Claude Code HTTP client with a user subscription key

For a remote HTTP MCP server, keep the user-specific value in the local client environment rather than source control. The ${AAS_SUBSCRIPTION_KEY} reference below is resolved by Claude Code when it connects; use single quotes so the shell does not expand it while registering the server.

export AAS_SUBSCRIPTION_KEY='replace-with-your-user-specific-secret'

claude mcp add aas-repo \
  --transport http \
  --scope user \
  --header 'X-Subscription-Key: ${AAS_SUBSCRIPTION_KEY}' \
  http://localhost:8000/mcp

This is client-to-MCP input. The server forwards it to the backend only when its operator has set AAS_FORWARD_HEADERS=["X-Subscription-Key"]; otherwise it is omitted. Do not commit the secret or this export value.

OAuth 2.1 Authorization

The server supports OAuth 2.1 + PKCE for HTTP transports. When enabled, the server validates inbound Bearer tokens and forwards them to the AAS backend.

Environment variables

VariableRequiredDescription
OAUTH_ISSUER_URLYes (to enable)OIDC provider issuer URL. Auth is disabled when not set.
OAUTH_CLIENT_IDYesClient ID of the application registered at the upstream IdP.
OAUTH_CLIENT_SECRETRecommendedClient secret from the IdP app registration.
OAUTH_SERVER_BASE_URLRequired for DockerPublic URL of the MCP server as seen by clients. Required when MCP_HOST=0.0.0.0.
OAUTH_AUDIENCERecommendedExpected aud claim in tokens.
OAUTH_REQUIRED_SCOPESOptionalComma-separated scopes requested from the IdP during the PKCE flow, e.g. aas:read,aas:write.
OAUTH_SESSION_STORE_URLOptionalExternal store for OAuth session state. Supports redis://, rediss://, postgresql://. Defaults to in-memory (state lost on restart).
MCP_RATE_LIMIT_PER_MINUTEOptionalMax requests per client per minute. Default: 60.
AAS_VALIDATE_OUTPUTOptionalValidate tool outputs against generated OpenAPI schemas. Default: true; set to false to advertise a permissive output schema and avoid validation overhead for large nested AAS responses.

How it works

The server acts as an OAuth 2.1 Authorization Server via FastMCP's OIDCProxy. It exposes a standard PKCE flow that proxies to your upstream IdP. MCP clients (Claude CLI, OpenCode, etc.) register via Dynamic Client Registration and complete a browser-based PKCE flow automatically.

The validated Bearer token is forwarded to the AAS backend on every outbound request. Set BACKEND_AUTH_AUDIENCE if the backend requires a different token audience (token exchange via RFC 8693).

Supported Components

  • aas-repo - Asset Administration Shell Repository
  • submodel-repo - Submodel Repository
  • aas-registry - AAS Registry
  • submodel-registry - Submodel Registry
  • concept-description-repo - Concept Description Repository
  • aas-discovery - AAS Discovery Service

Testing

Run tests:

# Unit tests only
tests/run_tests.sh

# With integration tests (requires backend on port 8081)
tests/run_tests.sh --integration

Support, Feedback, Contributing

This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.

Security / Disclosure

  • Read-only by default - Write operations disabled unless --enable-writes
  • Allowlist-based - Only explicitly allowed operations exposed
  • Wildcard patterns - [get, "*"], ["*", /path], ["*", "*"]
  • Pagination limits - Max 100 items per request

If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.

How It Works

When the server starts, it processes the OpenAPI specification through a pipeline before handing it to FastMCP:

  1. Load — reads the spec file and applies any overlay (rename, add descriptions, etc.)
  2. Flatten — resolves $ref inheritance chains and merges allOf compositions into flat schemas. This is necessary because the official IDTA AAS spec uses multi-level allOf + $ref inheritance (e.g. AssetAdministrationShell → Identifiable → Referable). Without flattening, FastMCP only sees the properties defined directly on the schema and misses all inherited fields like id, modelType, and assetInformation. Circular references are handled by keeping a $ref pointer at the cycle point instead of recursing infinitely.
  3. Curate — applies the allowlist to filter paths, enforces read-only mode, applies operation ID aliases, and caps pagination limits.
  4. Prune — removes components/schemas entries that are no longer reachable from any remaining path. This prevents FastMCP's schema validator from processing circular schemas that belonged to paths filtered out in the previous step, which would otherwise cause it to hang.
  5. Generate — FastMCP generates MCP tools from the processed spec and wires them to the HTTP client.

Note on allOf merging: When multiple allOf elements define the same non-property keyword (e.g. description, additionalProperties), the first value is kept. This is safe for the standard IDTA AAS spec but may produce weaker validation constraints on specs with conflicting allOf-level keywords.

Troubleshooting

"Configuration file not found"

Provide config via:

  • --config /path/to/config.yaml
  • CONFIG_PATH environment variable
  • Default: /app/config/config.yaml

"official_spec file not found"

Check:

  • Paths in config.yaml are correct
  • Specs are mounted next to the config (Docker): -v $(pwd)/specs:/app/config/specs; relative paths are resolved from the config file's directory

"External or non-local reference not supported"

The spec still references schemas outside the file (SwaggerHub URLs or ../Part2-API-Schemas/openapi.yaml). Bundle it first, see Setup, step 1.

OAuth / HTTP Transport

"Got new credentials, but server rejected them on reconnect"

Token validation is failing inside the container. Check the container logs:

docker logs <container-id> 2>&1 | grep -E "OIDCProxy|401|ERROR"

Common causes:

1. Missing OAUTH_CLIENT_ID or OAUTH_CLIENT_SECRET

The server now uses OIDCProxy, which requires a client registered at your upstream IdP. Both OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET must be set:

-e OAUTH_CLIENT_ID=your-client-id \
-e OAUTH_CLIENT_SECRET=your-client-secret \

2. Hostname not resolvable inside the Docker container

Your IdP hostname (e.g. keycloak.example.localhost) may resolve on your host machine but not inside the Docker container. Verify:

docker exec <container-id> python3 -c \
  "import socket; print(socket.gethostbyname('your-idp-hostname'))"

If it fails, add the hostname with --add-host:

# Find the IP your IdP resolves to on the host
python3 -c "import socket; print(socket.gethostbyname('your-idp-hostname'))"

# Add it to the container
docker run --add-host your-idp-hostname:<ip> ...

If your setup uses an nginx reverse proxy container, use the proxy container's IP — not the IdP container's IP directly. The proxy listens on port 80 and routes by hostname; the IdP container typically only listens on a high port (e.g. 8080) and does not accept plain-hostname requests on port 80.

# Find all container names and IPs on your network
docker network inspect your-network | python3 -c "
import sys, json
data = json.load(sys.stdin)
for c in data[0].get('Containers', {}).values():
    print(c['Name'], c.get('IPv4Address'))
"
# Use the proxy container's IP (e.g. nginx-proxy), not the IdP container's IP

docker run --add-host your-idp-hostname:<proxy-ip> ...

To verify the hostname resolves AND reaches the IdP from inside the container:

docker exec <container-id> python3 -c "
import urllib.request
resp = urllib.request.urlopen('https://your-idp-hostname/.well-known/openid-configuration', timeout=5)
print('OIDC discovery status:', resp.status)
"

3. resource URL mismatch (0.0.0.0 vs localhost)

When MCP_HOST=0.0.0.0, the server's protected resource metadata advertises http://0.0.0.0:8000/mcp as its URL, which doesn't match http://localhost:8000/mcp that the MCP client registered. Set OAUTH_SERVER_BASE_URL to the public-facing URL:

-e OAUTH_SERVER_BASE_URL=http://localhost:8000

Verify the metadata is correct before registering with your MCP client:

curl -s http://localhost:8000/.well-known/oauth-protected-resource/mcp \
  | python3 -m json.tool
# "resource" must exactly match the URL you pass to your MCP client

4. Wrong Docker network

The container must be on the same network as your AAS backend and IdP:

# List container networks
docker ps --format "{{.Names}}\t{{.Networks}}"

# Use the correct network
docker run --network correct-network-name ...

"Not Found" when browser opens for authentication

The MCP client constructed the authorization URL using the wrong endpoint. This happens when the server's /.well-known/oauth-authorization-server returns 404 (expected — this server is a pure resource server) and the client falls back incorrectly. Ensure --client-id is passed when registering the server (example using Claude CLI):

claude mcp add aas-repo \
  --transport http \
  --scope user \
  --client-id your-oauth-client-id \
  http://localhost:8000/mcp

MCP tool succeeds but AAS backend returns 401

The MCP server accepted the token but the backend rejected it. This is a different failure from the MCP server itself returning 401.

Symptom: An MCP tool call returns something like:

Error calling tool 'list_shells': HTTP error 401:

Check the MCP server logs for the outbound request:

docker logs <container-id> 2>&1 | grep -A3 "send_request_headers\|aas-env\|GET /shells"

Look at the headers line. If authorization is absent, token forwarding is not working.

Common causes:

a. FastMCP version changed get_http_headers() behaviour

FastMCP ≥3.3 explicitly excludes authorization from get_http_headers(). The server uses BearerTokenAuth (a custom httpx2.Auth class) to work around this. If you see the header missing, verify you are running the current image:

docker exec <container-id> python3 -c "
from aas_mcp_server.http_client import BearerTokenAuth
print('BearerTokenAuth present — token forwarding is correct')
"

b. Spring Security issuer-uri mismatch

Spring Security validates the iss claim in the token with exact string matching. If the MCP server was configured with OAUTH_ISSUER_URL=http://keycloak.localhost/realms/aas but the AAS backend has issuer-uri: http://keycloak:8080/realms/aas, Spring rejects the token even though it is from the same Keycloak.

Decode the token to check the iss claim:

TOKEN=<your-token>
python3 -c "
import base64, json
payload = '$TOKEN'.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
print('iss:', json.loads(base64.urlsafe_b64decode(payload)).get('iss'))
"

The iss value must exactly match the issuer-uri in the AAS backend's Spring Security config. Set OAUTH_ISSUER_URL to whichever value the AAS backend expects.

Manually testing the full token chain

To reproduce and isolate failures before involving an MCP client:

# 1. Get a token (replace with your provider's token endpoint)
TOKEN=$(curl -s -X POST \
  "https://your-idp/realms/your-realm/protocol/openid-connect/token" \
  -d "grant_type=password&client_id=your-client&username=user&password=pass&scope=openid" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

# 2. Decode the token claims
python3 -c "
import base64, json
payload = '$TOKEN'.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
c = json.loads(base64.urlsafe_b64decode(payload))
print('iss:', c.get('iss'))
print('aud:', c.get('aud'))
print('scope:', c.get('scope'))
"

# 3. Call the MCP server — should return 200
curl -s -o /dev/null -w "%{http_code}" \
  -X POST http://localhost:8000/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1"}},"id":1}'

Code of Conduct

We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.

Licensing

Copyright 2026 SAP SE or an SAP affiliate company and aas-mcp-server contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.