Skip to main content
Glama

Metadata-Driven SAP Ariba MCP Server (PoC)

A proof-of-concept MCP server that exposes selected SAP Ariba OpenAPI operations as MCP tools. Which operations become tools is configuration, not code: point it at a downloaded OpenAPI file, list the operationIds you want in config/tools.yaml, restart. No per-operation Python.


1. What this PoC does

Client app
  → Entra access token          (who is calling this MCP server)
  → MCP server                  (validates the token, builds the request from OpenAPI)
  → Ariba OAuth token + API key (how this server authenticates to Ariba)
  → SAP Ariba API

Two independent identities are involved, and it matters that you keep them apart:

  • Microsoft Entra ID authenticates the caller to the MCP server. Every MCP request must carry a valid Entra access token for this API, holding the required delegated scope.

  • Ariba OAuth authenticates the MCP application to Ariba. The server holds one set of Ariba client credentials and one API key, and uses them for every call.

  • There is no named-user propagation in this PoC. Ariba sees the same technical identity no matter who called the tool. Ariba has no idea which Entra user triggered a request; the Entra identity appears only in this server's logs.

The server:

  1. Serves MCP over Streamable HTTP.

  2. Validates Entra bearer tokens (signature, issuer, audience, tenant, lifetime, scope).

  3. Loads one local OpenAPI 3.0/3.1 JSON or YAML file at startup.

  4. Exposes only the operations you explicitly enable.

  5. Generates each tool's MCP input schema from the OpenAPI operation.

  6. Validates tool arguments against that schema.

  7. Acquires and caches an Ariba OAuth token.

  8. Calls Ariba with the OAuth token and the API key.

  9. Returns a normalized structured response with a correlation ID.

Related MCP server: swaggbot

2. Architecture

                 Authorization: Bearer <entra-access-token>
   MCP client  ─────────────────────────────────────────────▶  POST /mcp
                                                                   │
   ┌───────────────────────────────────────────────────────────────▼───────────┐
   │ Starlette app (built by the MCP SDK)                                      │
   │                                                                           │
   │   GET /health ──────────────────────────────▶ {"status":"ok"}  (no auth)  │
   │                                                                           │
   │   /mcp ─▶ BearerAuthBackend ─▶ RequireAuthMiddleware ─▶ MCP session        │
   │              │                        │                       │           │
   │              │ 401 invalid token      │ 403 missing scope     │           │
   │              ▼                        ▼                       ▼           │
   │       EntraTokenVerifier                              on_list_tools       │
   │       (OpenID config + JWKS,                          on_call_tool        │
   │        both cached)                                          │           │
   └──────────────────────────────────────────────────────────────┼───────────┘
                                                                  │
      built once at startup                                       ▼
   OpenAPI file ─▶ OpenApiLoader ─▶ RefResolver ─▶ SchemaConverter ─▶ ToolRegistry
   tools.yaml   ─────────────────────────────────────────────────────▶   │
                                                                          ▼
                                                                   ToolExecutor
                                                     (validate ▸ bind ▸ call ▸ normalize)
                                                                          │
                                                   AribaClient ◀──────────┘
                                                        │ Authorization: Bearer <ariba-token>
                                                        │ apiKey: <ariba-api-key>
                                                        ▼
                                                  SAP Ariba API

3. Project structure

Path

Purpose

app/main.py

Startup wiring: builds the registry once, mounts /mcp and /health, runs uvicorn.

app/config.py

Typed environment settings; exits with a readable message when configuration is invalid.

app/errors.py

The AppError model and error codes behind the normalized error envelope.

app/logging_config.py

Key-value logging helpers; only ever receives non-secret fields.

app/auth/entra.py

Validates Entra access tokens and extracts the caller context.

app/auth/ariba_oauth.py

Acquires, caches and refreshes the Ariba application OAuth token.

app/openapi/loader.py

Loads the OpenAPI file and indexes operations by operationId.

app/openapi/models.py

Immutable snapshots of an operation, its parameters and request body.

app/openapi/resolver.py

Resolves local #/... references and refuses external ones.

app/openapi/schema_converter.py

Turns an operation into an MCP input schema plus argument bindings.

app/tools/registry.py

Reads config/tools.yaml and validates it against the OpenAPI document.

app/tools/executor.py

The one execution path shared by every tool.

app/ariba/client.py

The long-lived HTTP client; owns all security-sensitive headers.

config/tools.yaml

Which operations are exposed, under what names, with which parameters.

specs/ariba-api.example.yaml

Mock spec so the PoC runs without the real SAP file.

4. Prerequisites

  • Python 3.12

  • A Microsoft Entra tenant where you can register applications

  • An app registration that exposes this MCP API (the "API app registration")

  • An app registration for the client that will call the MCP server

  • An SAP Ariba Developer Portal application, with access approved for the API you want

  • The downloaded OpenAPI JSON/YAML for that API

  • Ariba OAuth client ID and secret, and the application (API) key

5. Microsoft Entra setup

  1. Register the MCP API. Entra admin centre → App registrations → New registration. Note its Application (client) ID — this is ENTRA_API_CLIENT_ID, and the Directory (tenant) ID — this is ENTRA_TENANT_ID.

  2. Expose the API. On the API registration → Expose an API → set the Application ID URI (api://<client-id> is the default).

  3. Create the delegated scope ariba.access (Add a scope → admin/user consent text → enabled).

  4. Register the client application that will call the MCP server.

  5. On the client → API permissionsMy APIs → select the MCP API → Delegated permissions → tick ariba.access.

  6. Grant admin consent if your tenant requires it.

  7. Configure a redirect URI on the client that matches its type (for a desktop/CLI test client, http://localhost as a Mobile and desktop platform).

  8. Obtain an access token for the scope api://<ENTRA_API_CLIENT_ID>/ariba.access (see §10).

The server expects an access token, not an ID token, and checks these claims:

Claim

Expected

aud

ENTRA_API_CLIENT_ID, or api://<ENTRA_API_CLIENT_ID> — both are accepted

iss

The issuer published by your tenant's OpenID configuration

tid

Exactly ENTRA_TENANT_ID

oid

Required; tid + oid is the audit identity

scp

Space-separated; must contain ENTRA_REQUIRED_SCOPE as a whole entry

exp / nbf

Must be currently valid (60s leeway by default)

A token with no scp claim is rejected outright, which is what keeps an ID token from being accepted even if its audience happens to match.

6. SAP Ariba setup

  1. Create or reuse an application in the SAP Ariba Developer Portal.

  2. Request access to the API you need and wait for approval.

  3. Copy the application key — this is ARIBA_API_KEY.

  4. Generate the OAuth credentialsARIBA_CLIENT_ID and ARIBA_CLIENT_SECRET.

  5. Note the token URL (ARIBA_TOKEN_URL) and the API base URL (ARIBA_BASE_URL) for your region and environment.

  6. Download the OpenAPI JSON/YAML for that API.

  7. Put the file under specs/.

  8. Set ARIBA_OPENAPI_FILE to its path.

The exact token parameters and the API-key header name differ between Ariba APIs and between application configurations. Check your API's documentation and adjust ARIBA_API_KEY_HEADER (commonly apiKey) and ARIBA_TOKEN_AUTH_STYLE. See §15 for what this PoC assumes.

7. Local setup

python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env               # Windows: copy .env.example .env

Then edit .env:

  • ENTRA_TENANT_ID / ENTRA_API_CLIENT_ID from §5.

  • ARIBA_* values from §6.

  • Leave ARIBA_OPENAPI_FILE as the example spec for a first run.

  • Set MCP_ALLOWED_HOSTS if you bind to anything other than localhost (see §9).

.env is in .gitignore. Do not commit it.

8. Configure tools

config/tools.yaml maps operation_id (an operationId from the OpenAPI document) to an MCP tool:

tools:
  - operation_id: searchCatalogItems      # must exist in the OpenAPI file
    tool_name: search_catalog_items       # the MCP tool name; must be unique
    enabled: true                         # only enabled operations are exposed
    read_only: true                       # advertised as an MCP read-only hint
    description: >
      Search SAP Ariba catalog items using approved structured search
      parameters. Use this when a requester is looking for products.
    allowed_parameters:                   # only these parameters are exposed
      - query
      - supplierId
      - commodityCode
      - limit
    defaults:                             # applied when the caller omits the argument
      limit: 10
    limits:                               # may only tighten the OpenAPI schema
      limit:
        maximum: 25

Rules:

  • Only operations with enabled: true become tools.

  • Every operation_id must exist in the OpenAPI document — including disabled ones, so a typo fails at startup rather than the day somebody enables it.

  • allowed_parameters is an allowlist. Anything not listed is invisible to callers. Path parameters are always exposed, since the URL cannot be built without them.

  • defaults and limits must refer to parameters that are actually exposed.

  • limits can only make a constraint stricter. Configuring maximum: 500 against a spec that says maximum: 50 leaves 50 in place.

  • description overrides the OpenAPI description. If both are absent, startup fails.

  • aliases renames an awkward parameter ({"$top": "top"}) while still sending the original name.

  • Restart the server after any change — the configuration is read once at startup.

Optional key not shown above: aliases.

9. Run the server

python -m app.main

Expected routes:

Route

Auth

Purpose

GET /health

none

Liveness. Returns {"status": "ok"}. Never contacts Entra or Ariba.

POST /mcp

Bearer

MCP Streamable HTTP requests.

GET /mcp

Bearer

MCP Streamable HTTP server-to-client stream.

GET /.well-known/oauth-protected-resource/mcp

none

Resource metadata added by the SDK.

On a successful start you will see:

INFO ariba_mcp event=server_started tools=search_catalog_items,get_catalog_item openapi_file=specs/ariba-api.example.yaml
INFO     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

A note on MCP_ALLOWED_HOSTS. The MCP SDK's DNS-rebinding protection is enabled automatically only when the server binds to localhost. If you bind to 0.0.0.0 (the default, and what the container does) that protection is off unless you list the hostnames clients use:

MCP_ALLOWED_HOSTS=ariba-mcp.example.com,localhost:*

Requests whose Host header is not on the list are then rejected with HTTP 421.

10. Obtain a test Entra token

Use your registered client application with MSAL and an interactive sign-in. Save this as get_token.py outside the repository, or run it in a scratch directory:

# pip install msal
import msal

TENANT_ID = "<your-tenant-id>"
CLIENT_ID = "<your-CLIENT-app-client-id>"      # the client, not the API
SCOPE = ["api://<your-API-app-client-id>/ariba.access"]

app = msal.PublicClientApplication(
    CLIENT_ID, authority=f"https://login.microsoftonline.com/{TENANT_ID}"
)
result = app.acquire_token_interactive(scopes=SCOPE)
print(result["access_token"])

Keep the token out of your shell history — assign it in a way your shell does not record (a leading space works in bash/zsh with HISTCONTROL=ignorespace), or have your MCP client fetch it directly. Paste it into jwt.ms to confirm aud, tid and scp match what §5 describes before you blame the server.

11. Test the MCP server

This is an MCP endpoint, not a REST API — talk to it with an MCP client that supports Streamable HTTP and a bearer token, not with plain curl calls to tool URLs (there are none). Configure your client with:

  • Endpoint: http://localhost:8000/mcp

  • Header: Authorization: Bearer <entra-access-token>

With the official Python SDK:

import asyncio, httpx2
from mcp.client.client import Client
from mcp.client.streamable_http import streamable_http_client

TOKEN = "<entra-access-token>"

async def main():
    async with httpx2.AsyncClient(headers={"Authorization": f"Bearer {TOKEN}"}) as http:
        transport = streamable_http_client("http://localhost:8000/mcp", http_client=http)
        async with Client(transport, raise_exceptions=True) as client:
            tools = await client.list_tools()
            for tool in tools.tools:
                print(tool.name, tool.input_schema)

            result = await client.call_tool(
                "search_catalog_items", {"query": "laptop", "limit": 5}
            )
            print(result.structured_content)

asyncio.run(main())

tools/list returns the enabled tools with their OpenAPI-derived schemas:

{
  "name": "search_catalog_items",
  "description": "Search SAP Ariba catalog items using approved structured search parameters. ...",
  "inputSchema": {
    "type": "object",
    "properties": {
      "limit": {"type": "integer", "default": 10, "minimum": 1, "maximum": 25},
      "query": {"type": "string", "minLength": 2, "description": "Free-text search term."},
      "supplierId": {"type": "string"},
      "commodityCode": {"type": "string"}
    },
    "additionalProperties": false,
    "required": ["query"]
  }
}

A successful tools/call returns:

{
  "data": {"items": [{"itemId": "I1", "description": "Laptop"}]},
  "pagination": {"next_cursor": null, "has_more": false},
  "metadata": {
    "tool": "search_catalog_items",
    "operation_id": "searchCatalogItems",
    "http_status": 200,
    "correlation_id": "3f2a..."
  },
  "warnings": []
}

pagination appears only when the backend actually reported pagination. A failure returns is_error: true with:

{
  "error": {
    "code": "ARIBA_API_ERROR",
    "message": "SAP Ariba rejected the request.",
    "retryable": false,
    "correlation_id": "3f2a...",
    "details": {"http_status": 400}
  }
}

You can still check liveness and auth with curl:

curl -i http://localhost:8000/health                  # 200 {"status":"ok"}
curl -i -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'  # 401, no token

12. Run tests

pytest
pytest -q

Tests never contact Microsoft or SAP: Entra discovery/JWKS and every Ariba call are mocked with respx, and test tokens are signed with an RSA key generated in-process.

13. Docker

docker build -t ariba-mcp-poc .
docker run --rm -p 8000:8000 --env-file .env ariba-mcp-poc

Configuration arrives through environment variables at run time. The image contains no secrets and no .env (both are excluded by .dockerignore), and runs as a non-root user. For a single override, add -e ARIBA_VERIFY_SSL=false and so on. To use a spec that is not baked into the image, mount it: -v "$PWD/specs:/app/specs:ro".

14. Replace the example OpenAPI file

  1. Copy your downloaded file into specs/, e.g. specs/ariba-catalog.yaml.

  2. Set ARIBA_OPENAPI_FILE=specs/ariba-catalog.yaml in .env.

  3. Inspect the available operation IDs:

    python -c "from pathlib import Path; from app.openapi.loader import load_openapi_document; \
    print('\n'.join(load_openapi_document(Path('specs/ariba-catalog.yaml')).operation_ids()))"
  4. Add the operations you want to config/tools.yaml with enabled: true.

  5. Start the server.

  6. Resolve any OPENAPI_SCHEMA_UNSUPPORTED errors. Each one names the operation, the location and the construct, e.g. "Operation 'createRequisitionDraft' uses the unsupported construct 'oneOf' at request body." Either pick a different operation, or narrow the exposed parameters with allowed_parameters so the unsupported part is not reachable.

Adding a compatible operation this way exposes a new MCP tool with no Python changes.

Unsupported OpenAPI constructs

These fail at startup for an enabled tool rather than being silently widened:

  • oneOf, anyOf, not, discriminator

  • Recursive schemas (a $ref that reaches itself)

  • Request bodies without an application/json content type

  • allOf that composes anything other than objects

  • Empty/untyped schemas ({})

  • Cookie parameters, and HTTP methods other than GET/POST/PUT/PATCH

  • Types outside string, integer, number, boolean, array, object, null

nullable: true (OpenAPI 3.0) is normalized to a JSON Schema type union ["string", "null"], which matches how OpenAPI 3.1 expresses it.

Objects that declare properties get additionalProperties: false unless the spec says otherwise, so unknown fields are rejected before they reach Ariba. A free-form object (no declared properties) stays open.

15. Assumptions about the Ariba OAuth request

Verified against SAP's documented shape but not against a live Ariba tenant:

  • POST to ARIBA_TOKEN_URL with grant_type=client_credentials, form-encoded.

  • Credentials as an HTTP Basic header, Authorization: Basic base64(client_id:client_secret). Set ARIBA_TOKEN_AUTH_STYLE=body to send client_id/client_secret as form fields instead.

  • expires_in is honoured when present; otherwise a 10-minute lifetime is assumed. The token is refreshed ARIBA_TOKEN_REFRESH_MARGIN_SECONDS (default 60) before it expires.

  • Refresh tokens are not used. A client-credentials token is cheap to re-acquire, so the provider simply requests a new one. If your Ariba application requires a refresh-token flow, AribaTokenProvider._build_token_request is the single method to change.

  • On a backend 401 the cached token is dropped and the request is retried once. A second 401 is returned as an error. Write requests are never retried after a 5xx.

16. Troubleshooting

Symptom

Cause and fix

401 with error="invalid_token"

No Authorization: Bearer header, or the token failed validation. Check the server log for event=entra_token_rejected, which states the reason.

401, token looks fine

Wrong audience. aud must be ENTRA_API_CLIENT_ID or api://<that-id>. Requesting a token for Microsoft Graph instead of your API is the usual cause.

401, "Token was issued by a different tenant"

tidENTRA_TENANT_ID.

401, "no 'scp' claim"

You sent an ID token, not an access token. Request the api://.../ariba.access scope.

403 insufficient_scope

Valid token, but scp lacks ariba.access. Add the delegated permission and grant consent. Matching is exact — ariba.accessible does not count.

421 Misdirected Request

The Host header is not in MCP_ALLOWED_HOSTS. See §9.

ARIBA_AUTHENTICATION_FAILED

Ariba rejected the client credentials. Verify ARIBA_CLIENT_ID/ARIBA_CLIENT_SECRET and ARIBA_TOKEN_URL; try ARIBA_TOKEN_AUTH_STYLE=body.

ARIBA_API_ERROR with 401/403 after a token was obtained

Usually the API key: check ARIBA_API_KEY and that ARIBA_API_KEY_HEADER matches your API's documented header name.

ARIBA_API_ERROR with 404 on every call

ARIBA_BASE_URL is wrong, or already includes a path the OpenAPI templates repeat.

OPENAPI_OPERATION_NOT_FOUND at startup

An operation_id in tools.yaml is not in the spec. Use the command in §14 to list valid IDs.

OPENAPI_SCHEMA_UNSUPPORTED at startup

See §14. The message names the operation, location and construct.

SSL / certificate errors

A TLS-intercepting proxy. Point SSL_CERT_FILE at your CA bundle. ARIBA_VERIFY_SSL=false exists for local debugging only — never use it against production.

TIMEOUT errors

Raise ARIBA_HTTP_TIMEOUT_SECONDS, or check network reachability to ARIBA_BASE_URL. The error is marked retryable: true.

Every tool call logs one line with the correlation ID that also appears in the response metadata, so you can match a client-side failure to a server-side log:

event=tool_call correlation_id=3f2a... tenant_id=... object_id=... tool=search_catalog_items \
operation_id=searchCatalogItems method=GET path=/catalog/items backend_status=200 \
duration_ms=142 status=ok

Note that path is the OpenAPI template, not the substituted URL, so caller-supplied identifiers stay out of the logs.

17. Security limitations

This is a proof of concept, not a production service:

  • A single technical Ariba identity performs every backend call.

  • No named-user propagation. Ariba cannot attribute an action to the Entra user who caused it.

  • No production RBAC. Any caller holding ariba.access can invoke every enabled tool; there is no per-tool authorization.

  • In-memory token cache, so one server instance is assumed. Multiple replicas each hold their own Ariba token.

  • No persistent audit store. Audit information exists only in the process log.

  • Write tools should stay disabled unless you are deliberately testing them. createRequisitionDraft ships as enabled: false for this reason.

  • Secrets come from environment variables, with no secret-manager integration and no rotation.

18. Future enhancements

  • Entra-to-Ariba identity resolution and named-user requester/preparer enforcement

  • App roles and tool-level RBAC

  • Dynamic metadata refresh instead of a restart

  • Azure Key Vault for secrets, and a distributed token cache for multiple replicas

  • Persistent audit storage

  • API-specific response normalization

  • Approval/confirmation controls in front of write operations


Note on the MCP SDK

The original brief called for FastMCP. Built against mcp==2.0.0, this uses the SDK's low-level Server instead, for one blocking reason:

mcp.server.fastmcp no longer exists in 2.0 (it was renamed MCPServer), and neither that class nor 1.x's FastMCP can advertise a hand-built input schema — both derive a tool's inputSchema from its Python function signature via func_metadata, with no override. That is incompatible with the core requirement that schemas come from OpenAPI.

mcp.server.lowlevel.Server takes on_list_tools / on_call_tool callables and passes each types.Tool(input_schema=...) through untouched, and its streamable_http_app(...) supplies the Streamable HTTP transport, the bearer-auth middleware and the unauthenticated /health route in one supported call. Everything else in the brief is unchanged.

Two consequences worth knowing:

  • The low-level server does not validate tools/call arguments against the advertised schema, so ToolExecutor validates them with jsonschema before anything is sent to Ariba.

  • The SDK depends on httpx2. This project's own Ariba client uses httpx 0.28 (so tests can mock it with respx); both libraries coexist in one environment.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    -
    quality
    D
    maintenance
    Transforms SAP S/4HANA or ECC systems into conversational AI interfaces by exposing OData services as dynamic MCP tools. Enables natural language interactions with ERP data for querying, creating, updating, and deleting business entities.
    Last updated
    35
    1
    MIT
  • A
    license
    -
    quality
    D
    maintenance
    Transforms Swagger/OpenAPI documented APIs into conversational interfaces, enabling natural language interaction with APIs through an MCP server for use with AI assistants.
    Last updated
    4
    MIT

View all related MCP servers

Related MCP Connectors

  • MCP server for AI access to SmartBear tools, including BugSnag, Reflect, Swagger, PactFlow, QTM4J.

  • Official Microsoft MCP Server to query Microsoft Entra data using natural language

  • Hosted Amazon Seller and Vendor MCP server for Claude, ChatGPT, Cursor, Codex, Gemini, Copilot.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shafquat-quadar/ariba-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server