# calmcp - Cloud ALM MCP Server > Upgrading an XSUAA deployment? Read the [OAuth upgrade guide](docs/XSUAA-AUTH-UPGRADE.md) before redeploying. A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that bridges AI assistants (Claude, GitHub Copilot, …) to **SAP Cloud ALM** (aka CALM). It exposes the Cloud ALM read APIs through four consolidated, intent-based tools, runs over **stdio** locally or **Streamable HTTP** remotely, and deploys to **SAP BTP Cloud Foundry**. > calmcp is read-only by default: it never changes or deletes data in SAP Cloud ALM. An operator > can opt in to **creating** documents, features and library entries with > `CALM_WRITE_ENABLED=true` ([Write access](#write-access-opt-in)), and separately to **changing > fields of existing features** with `CALM_UPDATE_ENABLED=true` > ([Update access](#update-access-opt-in)). Nothing is ever deleted. `calmcp` is my second SAP Cloud ALM MCP bridge . It succeeds an earlier **Rust** implementation [sap-cloud-alm-mcp](https://github.com/consetto/sap-cloud-alm-odata-mcp) and reuses the knowledge of the Cloud ALM APIs, while taking a different technical direction. The Architecture is based on [`marianfoo's`](https://github.com/marianfoo) [`arc-1`](https://github.com/marianfoo/arc-1) as reference architecutre. See [docs/PREDECESSOR.md](docs/PREDECESSOR.md) for the project's lineage. ## Tools | Tool | Purpose | | --- | --- | | `calm_list` | List/query any collection — tasks (incl. **requirements**, **user stories** and **defects**), projects, features, documents, test cases, hierarchy nodes, cross-library objects, landscape objects, status events, code lists. OData resources accept `$filter/$select/$expand/$orderby/$top/$skip`; REST resources accept contextual params (`project_id`, or `project_name` resolved to it, `task_id`, `task_type`, `timebox_id`/`timebox_name`, …). `fields` projects the response on any resource; `count_only`/`group_by` return a live count instead of the records. | | `calm_get` | Fetch a single entity by id (a feature also by display id, e.g. `6-123`). | | `calm_analytics` | Query an analytics provider (`Defects`, `Tasks`, `Tests`, …). Providers span the whole tenant, so `count_only`/`group_by` here answer "how many across all projects". It aggregates but does **not** sort: `$orderby` is silently ignored by the service, so it is not offered. | | `calm_resources` | Discovery: the catalog of resources/providers, per-provider analytics dimensions and measures, the task type/status/priority code lists, and worked recipes. With write or update access on, also the fields `calm_create` and `calm_update` accept, per resource by `topic`. | | `calm_create` | **Only when `CALM_WRITE_ENABLED=true`.** Create a new document, feature or library entry (cross-library application, configuration, configuration activity, development, interface). Never changes or deletes. See [Write access](#write-access-opt-in). | | `calm_update` | **Only when `CALM_UPDATE_ENABLED=true`.** Change fields of an existing feature, guarded against overwriting concurrent edits and losing images. See [Update access](#update-access-opt-in). | ### Worked examples - **How many user stories are there in the tenant?** ```json calm_analytics({ "provider": "Tasks", "filter": "type eq 'User Story'", "count_only": true }) ``` - **Open defects per project** ```json calm_analytics({ "provider": "Defects", "filter": "defectStatus eq 'CIPDFCTOPEN'", "group_by": "projectName" }) ``` - **All open defects ordered by priority** (sort the records yourself; nothing in Cloud ALM sorts these, and analytics ignores `$orderby`) ```json calm_list({ "resource": "tasks", "project_id": "", "task_type": "CALMDEF", "status": "CIPDFCTOPEN", "fields": "displayId,title,priority,assigneeName" }) ``` - **Assigned features for defect `Y`** (two steps) ```json calm_list({ "resource": "task_feature_assignments", "task_id": "Y" }) // -> featureIds calm_get({ "resource": "feature", "id": "" }) // -> details ``` - **Open user stories in a sprint** ```json calm_list({ "resource": "tasks", "project_name": "S4 Transformation", "task_type": "CALMUS", "status": "CIPUSOPEN", "timebox_name": "Sprint 5", "fields": "displayId,title,status,assigneeName,dueDate" }) ``` Call `calm_resources` (optionally `{ "topic": "recipes" }`) at any time to discover valid `resource`/`provider` values and required parameters. ### Keeping responses small A task carries 67 attributes, most of them null, and the Tasks REST endpoint supports neither `$select` nor a timebox filter. Unprojected task lists therefore run to hundreds of KB and overflow agent hosts such as Microsoft Copilot Studio. `calm_list` adds two options, both applied by calmcp after fetching: `fields` projects the records, and `timebox_id`/`timebox_name` selects one sprint (paging through the project so the filter is complete). Unknown field names and unknown timebox names are rejected rather than silently ignored. REST resources have no server-side `$filter`, and each reads only a few parameters. A parameter the chosen resource does not read (for example `filter` on a REST resource, or `project_id` on an OData one) is rejected with the list of parameters it does read, rather than left off the request: an unfiltered answer to a filtered question reads as authoritative and is wrong. `calm_resources` reports those `parameters` per resource, derived from how calmcp builds the request. calmcp also caps the response itself. A payload over `CALM_MAX_RESPONSE_BYTES` (default 100 KB) is withheld and replaced by a summary naming how many records matched, which fields they carry, and how to ask again. Handing the payload over instead means the client truncates the JSON mid-record and the model answers from a fragment, which reads as authoritative and is wrong. ### Counting and breakdowns Never answer "how many?" by listing records and counting them. Both query tools take: | Option | Effect | | --- | --- | | `count_only: true` | Returns only the total. One `$count` request on OData and analytics; on REST resources calmcp pages and keeps just the tally. | | `group_by: "status"` | Returns `{ total, groups: [{ value, count }] }` instead of records. Accepts several fields (`"projectName,priority"`). Doubles as a way to discover the values a field actually takes. | | `group_limit` | Caps the number of groups (default 50); the tail folds into `otherCount` rather than being dropped. | | `count: true` | Returns the total *alongside* the records (OData and analytics only). | Which tool you call decides where the number comes from: - **`calm_analytics`** counts tenant-wide, with no `project_id`. It reads a daily snapshot, so the result says so. An analytics row is a data point rather than a record: the service emits one row per combination of a record's dimension values, so one task with several tags and workstreams can produce a hundred rows. Counting rows would overstate the answer badly, so calmcp instead selects the provider's count measure and lets the service aggregate, or counts distinct record ids for a grand total. Results carry `unit: "entities"`. A provider whose identity and measure calmcp does not know is counted by rows and labelled `unit: "rows"` with a warning. - **`calm_list`** counts live, within whatever the resource is scoped to (`resource: "tasks"` needs a `project_id`). The analytics service drops a `$filter` on a field it does not support instead of rejecting it, and then answers with every row, so a wrong field name yields a confident count of the wrong thing. A filtered analytics count therefore also counts the same window unfiltered, and sets `filterVerified: false` when the two totals match. That detects a dropped filter but cannot prove one was applied, so prefer `group_by` on the field you would have filtered: it sends no filter, so nothing can be dropped, and it returns every value with its count in one call. Every counting result reports `method`, the effective `filter`, and `complete`. A walk stopped by the 20 000-record page cap comes back with `complete: false` and says the real total is higher, rather than presenting a floor as the answer. ### Errors A failed call returns an error result whose text is a JSON object, so a client can branch on it without parsing prose: ```json { "error": "FORBIDDEN", "retryable": false, "status": 403, "message": "HTTP error 403: ...", "hint": "The Cloud ALM API service instance behind calmcp probably lacks the scope calm-api.features.read. ..." } ``` Codes: `INVALID_ARGUMENT` (fix the call), `NOT_FOUND`, `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `CONFLICT`, `RATE_LIMITED`, `UPSTREAM_ERROR`, `TIMEOUT`, `NETWORK`, `CANCELLED`, `AUTH`, `CONFIG`, `INTERNAL`. A read answered with 429 or 503 is retried once when Cloud ALM asks for a wait of at most 5 seconds; a create is never retried. A call the client cancels stops issuing requests. A `FORBIDDEN` hint names the scope the service needs (see [Cloud ALM API scopes](#cloud-alm-api-scopes)). SAP limits every Cloud ALM pull API to 500 requests per 5 seconds. calmcp pages one request at a time, so it stays far below that on its own, but the budget is shared with every other client of the tenant. ### Covered services Tasks, Projects (incl. programs and program teams), Features, Documents, Process Hierarchy, Process Scopes, Custom Processes, Test Management (manual + automated), Test Plans *(BETA)*, Analytics, BSM/Status Events, Landscape (objects and SCIM access control lists), and Cross-Library (Applications, Configurations, Developments, Interfaces, with their priority, readiness, usage, clean core level and upgrade impact code lists). In Cloud ALM the Tasks service is not just to-dos: **requirements, user stories, defects, sub-tasks, roadmap and project tasks, quality gates, checklist items and risks are all tasks**, distinguished by a `type` code. So `resource: "tasks"` with `task_type: "CALMREQU"`, `"CALMUS"` or `"CALMDEF"` is how you list requirements, user stories or defects. Call `calm_resources` for the full code list. All services are on API version `v1`. For the spec revision behind each one, and how to refresh them, see [docs/API_VERSIONS.md](docs/API_VERSIONS.md). ### Write access (opt-in) calmcp starts read-only and stays that way unless an operator sets `CALM_WRITE_ENABLED=true` (or turns on **Write Access** in the Claude Desktop extension settings). With the switch on, one extra tool is registered: | Tool | Resources | Cloud ALM call | | --- | --- | --- | | `calm_create` | `document` | `POST /calm-documents/v1/Documents` | | | `feature` | `POST /calm-features/v1/Features` | | | `xlib_application` | `POST /calm-crosslibraryapplications/v1/Applications` | | | `xlib_configuration` | `POST /calm-crosslibraryconfigurations/v1/Configurations` | | | `xlib_configuration_activity` | `POST /calm-crosslibraryconfigurations/v1/ConfigurationActivities` | | | `xlib_development` | `POST /calm-crosslibrarydevelopments/v1/Developments` | | | `xlib_interface` | `POST /calm-crosslibraryinterfaces/v1/Interfaces` | What it does and does not do: - **Create only.** `calm_create` never changes or deletes an existing record. Calling it twice makes two entries. Changing fields of a feature is the separate [`calm_update`](#update-access-opt-in). - **Document images are not copied.** The Documents API returns a document body without its images (see [docs/UPDATES.md](docs/UPDATES.md)); `calm_get` and `calm_list` flag such documents with `imagesOmitted: true`. A new document built from that body would have none of the images. - **Strict payloads.** Each resource's fields are transcribed from the `*-create` schemas in the OpenAPI specs and validated before any request is sent. An unknown field is an error, not something silently dropped. `calm_resources({ topic: "document" })` returns the field list. - **Deep create.** Links (`toURLReferences`) and assignments (`toLibraryAssignments`, `toProcessHierarchyAssignments`, `toTaskAssignments`, …) can be included in the same `data` object and are created together with the entity, exactly as the OData API allows. - **Scopes.** The OAuth2 client (or the BTP destination) needs `calm-api.documents.write` for documents, `calm-api.features.write` for features and `calm-api.lib.write` for library entries, in addition to the read scopes. - **Writer scope on HTTP.** On the HTTP transport the switch alone is not enough: `calm_create` is offered only to callers whose XSUAA token carries the `Writer` scope (role collection `CALMCP_Editor`). Viewers of the same deployment and API-key callers never see the tool. Cloud ALM still records the destination's technical user as the creator. - **Audit log.** calmcp logs every created entity at `info` level with the caller (XSUAA user, else OAuth client, else `local`), the resource, the new uuid and display id, and the names of the fields set, not their values. With Cloud ALM naming only the technical user, this log is where the person is. Example: ```json { "resource": "document", "data": { "title": "Interface design: payment export", "projectId": "11111111-1111-1111-1111-111111111111", "documentTypeCode": "SD", "content": "

Purpose

...

", "toURLReferences": [{ "name": "Ticket", "url": "https://example.com/T-42" }] } } ``` ### Update access (opt-in) Changing an existing object can lose what creating one cannot: a newer edit made in the Cloud ALM UI, or images in a rich-text description. calmcp therefore offers updates only behind their own switch, `CALM_UPDATE_ENABLED=true` (or **Update Access** in the Claude Desktop extension), separate from write access, and only for features so far. [docs/UPDATES.md](docs/UPDATES.md) has the analysis behind this: which objects the Cloud ALM APIs can change at all (documents cannot), and where images live. | Tool | Resource | Fields it may change | Cloud ALM call | | --- | --- | --- | --- | | `calm_update` | `feature` | `title`, `description` (HTML), `statusCode`, `priorityCode`, `scopeId`, `responsibleId`, `releaseId`, `workstreamId` | `PATCH /calm-features/v1/Features/{uuid}` | Every call goes through the same checks, in order: - **Allowlist.** Only the fields above; anything else, such as `projectId`, is an error. No link or assignment lists, whose update semantics the API does not state. `null` clears `scopeId`, `responsibleId`, `releaseId` or `workstreamId` (not yet verified against a tenant). - **Change check.** The call carries `expected_modified_at`, the `modifiedAt` the model read. calmcp reads the feature again and refuses with `CONFLICT` if it changed since. Cloud ALM has no ETag or `If-Match`, so this is the only protection against overwriting someone else's edit; it narrows the window to the moment between the check and the write. - **Only real changes.** Fields that already hold the requested value are not sent. - **No active content.** Rich text may not contain scripts, event handlers (`onerror=`, ...), `javascript:`/`vbscript:`/`data:` URLs, CSS `url(...)`, or images from anywhere but Cloud ALM's image service (an image already in the current text may stay). calmcp reads text other people wrote, so an instruction planted in one object could otherwise make a model write a tracking pixel or a script into another. The same check applies to `calm_create`. - **Image guard.** A feature description references its images as `` tags pointing at Cloud ALM's image service. A new description that drops one is refused, naming the image, unless the call lists exactly that image in `remove_images` (e.g. `["imageId:4e4b…"]`) after the user confirmed it. Any other dropped image is still refused. - **PATCH, never PUT,** and never retried: a throttled or failed update is reported, not repeated. When Cloud ALM gives no answer (timeout, 5xx), the result says the update may or may not have been applied, and how to find out with `calm_get`. - **Result and audit.** The answer shows each changed field before and after (a description as its length and image count). Once Cloud ALM accepted the change, it is reported as done even if reading it back fails. calmcp logs every update at `info` level with the caller, the outcome (`applied`, or `unknown` when there was no answer), each field's old and new value, and the modification timestamps, so a change can be undone by hand; Cloud ALM records only the technical user. Like `calm_create`, `calm_update` is offered over HTTP only to callers with the `Writer` scope (role collection `CALMCP_Editor`), and the OAuth2 client needs `calm-api.features.write`. Example: ```json { "resource": "feature", "id": "5a4d46a3-13c4-492e-9bc4-5512ff56ef5c", "expected_modified_at": "2026-09-14T11:20:58.396Z", "changes": { "statusCode": "IN_TESTING", "priorityCode": 20 } } ``` ## Configuration Configuration is read from environment variables (see [`.env.example`](.env.example)). Two local auth modes, plus a BTP destination mode: | Variable | Description | | --- | --- | | `CALM_SANDBOX` | `true` to use the SAP Business Accelerator Hub sandbox with `CALM_API_KEY`. | | `CALM_API_KEY` | Sandbox API key (sandbox mode). | | `CALM_TENANT`, `CALM_REGION` | Tenant subdomain and region (e.g. `eu10`) for OAuth2 mode. | | `CALM_CLIENT_ID`, `CALM_CLIENT_SECRET` | OAuth2 client-credentials from the service binding. | | `CALM_DESTINATION_NAME` | Name of a bound BTP Destination (BTP mode; takes precedence). | | `PORT`, `CALM_CORS_ORIGINS` | HTTP transport port, and the exact origins of browser-based clients allowed by CORS (comma-separated, e.g. `http://localhost:6274`). Unset sends no CORS headers, which is right for MCP clients such as Claude Desktop. A wildcard (`*`) is refused at startup: on the local unauthenticated endpoint it would let any website read Cloud ALM data through the developer's browser. | | `CALM_DEBUG`, `CALM_TIMEOUT_SECONDS` | Verbose tracing and request timeout. | | `CALM_WRITE_ENABLED` | `true` registers `calm_create` (create-only). Default `false`: read-only. See [Write access](#write-access-opt-in). | | `CALM_UPDATE_ENABLED` | `true` registers `calm_update` (change fields of existing features). Default `false`. See [Update access](#update-access-opt-in). | ### Cloud ALM API scopes What calmcp can read is decided by the scopes of the SAP Cloud ALM API service instance behind the OAuth2 client or the BTP destination. They are `authorities` of that instance, set in the SAP BTP cockpit (update the instance), not of any person. calmcp uses this instance for every caller. | Scope | Needed for | | --- | --- | | `calm-api.tasks.read` | Tasks, including requirements, user stories and defects; `Tasks` analytics | | `calm-api.projects.read` | Projects, programs, teams, timeboxes; `Projects` analytics | | `calm-api.features.read` | Features; `Features` analytics | | `calm-api.documents.read` | Documents | | `calm-api.processhierarchy.read` | Process hierarchy | | `calm-api.processmanagement.read`, `calm-api.processauthoring.read` | Process scopes, custom processes | | `calm-api.testcases.read`, `calm-api.testplans.read` | Test cases, test plans | | `calm-api.lib.read` | Cross-library applications, configurations, developments, interfaces | | `calm-api.landscape.read` | Landscape | | `calm-api.bsm.read` | Status events | | `calm-api.analytics.read`, `calm-api.analytics.providers.read` | `calm_analytics`, plus each provider's own scope (e.g. `calm-api.defects.read`, `calm-api.requirements.read`, `calm-api.tests.read`) | Two groups of scopes are easy to leave out, and a missing one does not have to show up as an error: - **Private and protected projects** need `calm-api.projects.private.read` and `calm-api.projects.protected.read` (Projects API and analytics). Without them, totals can come back lower than the Cloud ALM UI shows. - **Personal data** needs a `*.personal.read` scope per analytics provider: `calm-api.tasks.personal.read` (`processor`), `calm-api.defects.personal.read` and `calm-api.requirements.personal.read` (`assignee`), `calm-api.features.personal.read` (`responsible`). Without them, those fields are not readable. Grant only what the deployment should expose: every caller of calmcp sees what these scopes allow. The write scopes are listed under [Write access](#write-access-opt-in). The full scope list is in SAP's *API Guide for SAP Cloud ALM*, section "API Scopes". ## Install in Claude Desktop — one-click (`.mcpb`) The simplest path for a single developer: install calmcp as a Claude Desktop extension. 1. Download the latest `calmcp-.mcpb` from the Releases page (or build it locally — see below). 2. Double-click it, or open **Claude Desktop → Settings → Extensions** and drag the file in. 3. Claude prompts for your Cloud ALM connection. **Tenant**, **Region**, **Client ID** and **Client Secret** are required (the secret is stored in your OS keychain). To use the SAP Business Accelerator Hub sandbox instead, turn on **Use Sandbox** and supply a **Sandbox API Key**. **Debug Logging** and **Request Timeout** are optional. Fill them in and enable the extension. 4. Ask Claude: *"Using the SAP Cloud ALM tools, list the open defects ordered by priority."* — it should call `calm_analytics`. **What the bundle is:** a pure-JS, cross-platform (macOS / Windows / Linux) build of the stdio server packaged with its dependencies — calmcp has no native modules, so one bundle runs everywhere. It is **read-only** unless you switch on **Write Access** in the extension settings (see [Write access](#write-access-opt-in)). For multi-user, HTTP, or BTP deployments, use the Docker image or deploy to Cloud Foundry instead (see below). ### Build the bundle locally ```bash npm run build:mcpb # → calmcp-.mcpb in the repo root ``` This compiles `dist/`, installs production dependencies, then validates and packs the bundle with the pinned [`@anthropic-ai/mcpb`](https://github.com/anthropics/mcpb) CLI. The form Claude Desktop shows is defined in [`mcpb-manifest.json`](mcpb-manifest.json); keep its `version` in sync with `package.json` (the build fails if they differ). > Prefer to hand-edit JSON? Use the `claude_desktop_config.json` snippet under > [Run over stdio](#run-over-stdio-local-mcp-clients) — that path also supports sandbox-only setups. ## Local development ```bash npm install npm run build npm test # unit tests (mocked HTTP) npm run lint # biome ``` ### Run over stdio (local MCP clients) ```bash CALM_SANDBOX=true CALM_API_KEY= node dist/index.js ``` Example client config (Claude Desktop): ```json { "mcpServers": { "calmcp": { "command": "node", "args": ["/absolute/path/to/calmcp/dist/index.js"], "env": { "CALM_SANDBOX": "true", "CALM_API_KEY": "" } } } } ``` ### Run over HTTP ```bash CALM_SANDBOX=true CALM_API_KEY= PORT=8080 node dist/index.js --http curl http://localhost:8080/health # MCP endpoint: POST http://localhost:8080/mcp ``` ## Deploy to SAP BTP Cloud Foundry calmcp authenticates to Cloud ALM via a bound **Destination** (type OAuth2 client-credentials, URL = your Cloud ALM API base, e.g. `https://..alm.cloud.sap/api`). Set `CALM_DESTINATION_NAME` to that destination's name. ### Prerequisites The deployment needs the Cloud Foundry CLI, the MultiApps plugin (which provides `cf deploy`), and the MTA build tool: 1. **Cloud Foundry CLI** — install for your OS (macOS, Windows, Linux) per the [official guide](https://docs.cloudfoundry.org/cf-cli/install-go-cli.html). 2. **MultiApps plugin and MTA build tool** (cross-platform): ```bash cf install-plugin multiapps -f # registers the `cf deploy` command used below npm install --global mbt # MTA build tool cf login -a https://api.cf..hana.ondemand.com --sso # then pick the org/space ``` > If `cf install-plugin multiapps` fails with `bad CPU type` / architecture errors (e.g. on > arm64 machines), download the matching binary for your platform from the > [MultiApps releases](https://github.com/cloudfoundry/multiapps-cli-plugin/releases) and install it > from file: `cf install-plugin -f`. ### Using the MTA descriptor ```bash mbt build cf deploy mta_archives/calmcp_.mtar # is the one in package.json ``` This creates and binds `calmcp-xsuaa` (XSUAA), `calmcp-destination` (Destination) and `calmcp-logs` (Application Logs), and runs the HTTP transport with a `/health` check. ### Using `cf push` Before creating or updating XSUAA, configure the exact public `/oauth/callback` in `xs-security.json`; the checked-in localhost example is not a production route. See the [OAuth upgrade guide](docs/XSUAA-AUTH-UPGRADE.md). Create the services, build, then push (see [`manifest.yml`](manifest.yml)): ```bash cf create-service xsuaa application calmcp-xsuaa -c xs-security.json cf create-service destination lite calmcp-destination cf create-service application-logs lite calmcp-logs npm run build cf push ``` After deploy, assign the `CALMCP_Viewer` role collection to authorized users (or `CALMCP_Editor` for users who may use `calm_create` when writes are enabled) and create the **destination** as described below. ### Configure the destination Two differently-named things are involved — don't confuse them: - **`calmcp-destination`** — the destination *service instance* created and bound by the deploy (mta.yaml / manifest.yml). It is the container that holds destinations; you do not edit it by hand. - **`CALM_DESTINATION_NAME`** (default `SAP_CALM`) — the name of the destination *entry* the app looks up at runtime. **This is the name you give the destination you create.** It must match the value of `CALM_DESTINATION_NAME`; change one and change the other. Create the entry either at the **subaccount level** (Connectivity → Destinations) or inside the `calmcp-destination` service instance — the Cloud SDK checks both. Fill it in from your Cloud ALM API service key (an OAuth2 client-credentials key for the Cloud ALM API): | Field | Value | | --- | --- | | **Name** | the value of `CALM_DESTINATION_NAME` (default `SAP_CALM`) | | **Type** | `HTTP` | | **Proxy Type** | `Internet` | | **URL** | your Cloud ALM API base **including the `/api` suffix**: `https://..alm.cloud.sap/api` (the region-only form `https://.alm.cloud.sap/api` works too) | | **Authentication** | `OAuth2ClientCredentials` | | **Client ID** | `clientid` from the service key | | **Client Secret** | `clientsecret` from the service key | | **Token Service URL** | the service key's `url` plus `/oauth/token`: `https://.authentication..hana.ondemand.com/oauth/token` | | **Token Service URL Type** | `Dedicated` | Notes: - The **`/api` suffix on the URL is required** — calmcp appends per-service paths (e.g. `/calm-features/v1`) directly to this URL. - The destination's **Check Connection** button may report 401/403; that is expected for an unauthenticated probe. The real check is the deployed app calling a tool. - No additional destination properties are needed. ### Endpoint authentication (HTTP transport) The destination above is how calmcp authenticates **to** Cloud ALM. Separately, the `/mcp` endpoint itself is protected so only authorized callers can reach it. **Standard approach: a BTP user signing in from an AI tool.** When the `calmcp-xsuaa` service is bound, `/mcp` requires a valid XSUAA token carrying the `Viewer` scope (granted via the `CALMCP_Viewer` role collection). calmcp detects the bound service from `VCAP_SERVICES` and enables this automatically. It also exposes MCP-native OAuth (RFC 8414 discovery + RFC 7591 dynamic client registration, proxied to XSUAA), so an AI tool such as Claude Desktop, Cursor or VS Code **signs the user in interactively** with no manual token handling. The OAuth flow is delegated to XSUAA; calmcp never sees the user's password. This is the recommended path: each user authenticates as themselves with a BTP user and the `CALMCP_Viewer` role collection. **Alternative: a static API key** for non-interactive, server-to-server callers (for example Microsoft Copilot Studio). Set `CALM_HTTP_API_KEY` and the caller sends `Authorization: Bearer `. This authenticates the caller, not a user. Both methods coexist on the one endpoint. See [Connecting calmcp to Microsoft Copilot Studio](docs/copilot-studio.md). **With neither configured, the HTTP transport refuses to start.** For local development only, `CALM_HTTP_ALLOW_UNAUTHENTICATED=true` serves an open `/mcp` bound to `127.0.0.1` that accepts only loopback `Host` headers. The switch is ignored on Cloud Foundry, and a bound but unreadable XSUAA service is a startup error, so a broken binding never exposes Cloud ALM data publicly. Relevant environment variables (HTTP transport): | Variable | Description | | --- | --- | | `CALM_PUBLIC_URL` | Public base URL used in OAuth metadata and the callback. Defaults to the first route in `VCAP_APPLICATION`, so it's normally not needed. | | `CALM_DCR_SIGNING_SECRET` | Secret for HMAC-signing dynamic client registrations. Set it (e.g. `cf set-env calmcp-srv CALM_DCR_SIGNING_SECRET "$(openssl rand -base64 48)"`) so registered clients survive a `cf deploy` (which rotates the XSUAA `clientsecret`). Defaults to the XSUAA `clientsecret`. | | `CALM_HTTP_ALLOW_UNAUTHENTICATED` | `true` allows an open `/mcp` on `127.0.0.1` when no auth is configured. Local development only; ignored on Cloud Foundry. | | `CALM_HTTP_API_KEY` | Shared secret for the alternative API-key path. Generate with `openssl rand -base64 48`. Leave empty to rely on XSUAA only. See the [Copilot Studio guide](docs/copilot-studio.md). | ### Consuming the deployed server from an AI tool This is the standard way to use the deployed server. `/mcp` speaks the **Streamable HTTP** MCP transport, so point a remote-MCP-capable client at it: - **Claude Code:** `claude mcp add --transport http calmcp https:///mcp` - **Claude Desktop:** Settings → Connectors → add a custom connector with the `/mcp` URL. - **Cursor / VS Code / others:** add an MCP server of type HTTP (Streamable) at the `/mcp` URL. On first connect the client triggers the OAuth login; **sign in with your BTP user**, which must hold the `CALMCP_Viewer` role collection. The four tools (`calm_list`, `calm_get`, `calm_analytics`, `calm_resources`) then appear. For non-interactive server-to-server callers such as Microsoft Copilot Studio, use the API-key path instead: see [Connecting calmcp to Microsoft Copilot Studio](docs/copilot-studio.md). ## Testing ```bash npm test # unit (mocked HTTP via undici MockAgent) npm run typecheck # type-check src and tests (Vitest itself does not) npm run test:integration # live backend from .env; skipped without credentials # CALM_TEST_PROJECT_ID= adds the project-scoped checks npm run build && npm run test:e2e # real MCP calls over stdio and HTTP ``` Pushes to `main` and every pull request run `npm ci`, the version check, lint, unit tests, the build and the e2e tests on Node 22 and 24 ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)). `npm ci` installs strictly from the lockfile, so a stale local `node_modules` can never be mistaken for a real failure again. The same workflow validates `mta.yaml` with `mbt`, checks that `xs-security.json` still defines the scopes the code requires ([`scripts/check-xs-security.mjs`](scripts/check-xs-security.mjs)), and builds the Docker image and checks that it answers `/health`. Pull requests also get a [dependency review](.github/workflows/dependency-review.yml) that fails on a new dependency with a high-severity vulnerability or a GPL-family license. None of these needs credentials. Dependabot opens grouped update PRs for npm and GitHub Actions weekly. Every pull request also gets a **tool surface** comment ([`.github/workflows/tool-surface.yml`](.github/workflows/tool-surface.yml)): the server instructions and each tool's description, annotations and input schema as a model sees them, diffed against `main` sentence by sentence, with sizes. A PR that changes none of it gets no comment. To compare two builds locally: ```bash node scripts/tool-surface.mjs snapshot dist/index.js after.json node scripts/tool-surface.mjs diff before.json after.json ``` ## License MIT ## Contributing Contributions are welcome! Please ensure your code: - Builds without errors (`npm run build`) - Passes all tests (`npm test`) and the type-check (`npm run typecheck`) - Passes linting and formatting (`npm run lint`, or `npm run lint:fix` to auto-fix) [AGENTS.md](AGENTS.md) explains how calmcp is built and which files change together; it is also what coding agents read. Open a pull request as a draft until it is finished, fill in the template (writes need a test against a real tenant), and check [docs/ROADMAP.md](docs/ROADMAP.md) for planned and deferred work. ## Disclaimer This software is provided "as is", without warranty of any kind, express or implied. ### No Responsibility The author(s) and contributor(s) of this tool assume no responsibility or liability for any damages, losses, or consequences that may result from the use or misuse of this software. This includes, but is not limited to: - Any kind of data loss - Any damage to systems, networks, or data - Any legal consequences resulting from unauthorized or improper use - Any business losses or operational disruptions - Any security incidents or breaches