AxiomCode
Verifiable intelligence for AI software engineering. Everything starts with a formally grounded code graph.
Open Source: KI-Assistenten wie Claude und Copilot lesen, zählen und dokumentieren direkt in SAP Cloud ALM. Read-only per Default.
Translation pending
PluginsTypeScript · JavaScriptGitHub @consettoOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Loading comments.
Checking sign-in status.
Cloud ALM MCP Server. Supports Tasks, Projects, Features, Requirements, Documents, Tests, Analytics, and many other SAP CALM APIs, featuring stdio/Streamable HTTP transports, XSUAA/OAuth or API-key auth, and easy SAP BTP Cloud Foundry deployment.
Translation pending
Loading the full README. The saved excerpt is shown below.
A Model Context Protocol (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 updates or deletes data in SAP Cloud ALM. An operator
can opt in to create-only access for documents and library entries with
CALM_WRITE_ENABLED=true; see Write access.Read the full README ↗Upgrading an XSUAA deployment? Read the OAuth upgrade guide before redeploying.
A Model Context Protocol (MCP) server that bridges AI assistants (Claude, GitHub Copilot, …) to SAP Cloud ALM (aka CALM). It exposes the Cloud ALM APIs through four consolidated, intent-based read tools (plus two opt-in write 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, tasks, task comments, library entries and links between existing objects with
CALM_WRITE_ENABLED=true(Write access), and separately to changing fields of existing features withCALM_UPDATE_ENABLED=true(Update access). Nothing is ever deleted.
calmcp is my second SAP Cloud ALM MCP bridge. It succeeds an earlier
Rust implementation sap-cloud-alm-mcp and reuses the knowledge of the Cloud ALM APIs, while taking a different technical direction.
The architecture is based on marianfoo's arc-1 as reference architecture. See docs/PREDECESSOR.md for the project's lineage.
| 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; every resource pages with 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, task (incl. user story, defect, requirement, sub-task, risk), task comment or library entry (cross-library application, configuration, configuration activity, development, interface), or link existing objects. Never changes or deletes. See Write access. |
calm_update | Only when CALM_UPDATE_ENABLED=true. Change fields of an existing feature, guarded against overwriting concurrent edits and losing images. See . |
How many user stories are there in the tenant?
calm_analytics({ "provider": "Tasks", "filter": "typeID eq 'CALMUS'", "count_only": true })
Open defects per project
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)
calm_list({ "resource": "tasks", "project_id": "<uuid>", "task_type": "CALMDEF",
"status": "CIPDFCTOPEN", "fields": "displayId,title,priority,assigneeName" })
Assigned features for defect Y (two steps)
calm_list({ "resource": "task_feature_assignments", "task_id": "Y" }) // -> featureIds
calm_get({ "resource": "feature", "id": "<featureId>" }) // -> details
Open user stories in a sprint
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.
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 services cut a list at their own default page size and never say so, so calmcp always asks
for a page size: top, or 500 when the call names none. A full page then carries nextPage with
the skip of the next one, on every resource and in calm_analytics.
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 (reason: "RESPONSE_TOO_LARGE", complete: false) 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.
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. |
with_count: true | Returns the total alongside the records (OData and analytics only). |
A count returns no records, so top, skip, select and with_count are refused next to
count_only or group_by rather than ignored.
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, or by the 40-second time budget for one count (stoppedBy: "time"),
comes back with complete: false and says the real total is higher, rather than presenting a floor
as the answer.
Some analytics providers insist on a filter before they answer: ServiceLevels needs
businessServiceId, MonitoringEvents at least one dimension filter such as serviceId.
calm_resources({ topic: "<provider>" }) states this as requires.
A failed call returns an error result whose text is a JSON object, so a client can branch on it without parsing prose:
{ "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,
RESPONSE_TOO_LARGE (a Cloud ALM answer over 8 MB, not read), INTERNAL. A read answered with 429
or 503 is retried once, after the wait Cloud ALM asks for (at most 5 seconds; 1 second when it
names none). A create or update is
never retried after Cloud ALM processed it; the one exception is a 401, which Cloud ALM answers
before processing anything, so any request is sent once more with a fresh token. A call the client
cancels stops issuing requests.
A FORBIDDEN hint names the scope the service needs (see 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.
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.
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 | |
task | POST /calm-tasks/v1/tasks | |
task_comment | POST /calm-tasks/v1/tasks/{taskId}/comments | |
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 | |
task_feature_link | POST /calm-tasks/v1/tasks/{taskId}/featureAssignments | |
task_document_link | POST /calm-tasks/v1/tasks/{taskId}/documentAssignments | |
task_process_link | POST /calm-tasks/v1/tasks/{taskId}/hierarchyNodeAssignments | |
task_relation | POST /calm-tasks/v1/tasks/{taskId}/relations | |
task_reference | POST /calm-tasks/v1/tasks/{taskId}/references | |
feature_url_reference | POST /calm-features/v1/Features/{featureId}/toURLReferences | |
test_case_task_link | POST /calm-testmanagement/v1/TaskAssignments |
What it does and does not do:
calm_create never changes or deletes an existing record. Calling it twice
makes two entries. A task_comment adds a comment to an existing task and leaves the task as
it is. Changing fields of a feature is the separate
calm_update.calm_get and calm_list flag such documents with
imagesOmitted: true. A new document built from that body would have none of the images.*-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.CALMST) needs parentId, risk fields apply to risks only, and a
classification and its sub-classifications must fit the task type. calmcp creates project tasks,
user stories, sub-tasks, requirements, defects and risks, but not roadmap tasks, quality gates or
checklist items, which come from templates and the project setup. Cloud ALM sets a new task's
start date to today and, when no timeboxId is given, assigns it to a timebox itself.6-123, or 8-1-1 for a test case), which calmcp resolves first; tasks only by uuid, because
the Tasks API cannot look one up by display id. Cloud ALM refuses a duplicate assignment or
relation itself, but stores a URL twice, so calmcp refuses a task_reference or
feature_url_reference whose URL the object already has. task_relation takes 2 (the
related task is the predecessor), 12 (successor) and 1 (peer, project task and user story
only); child and parent relations are refused, because Cloud ALM would accept them between any
two tasks beside the sub-task hierarchy. A sub-task is a task with .Example:
{
"resource": "document",
"data": {
"title": "Interface design: payment export",
"projectId": "11111111-1111-1111-1111-111111111111",
"documentTypeCode": "SD",
"content": "<h1>Purpose</h1><p>...</p>",
"toURLReferences": [{ "name": "Ticket", "url": "https://example.com/T-42" }]
}
}
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 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:
id takes the uuid or the display id (e.g. 6-123). A display id is
resolved to the uuid first; one that matches no feature, or more than one, is refused.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).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.javascript: or data: URLs, CSS url(...), and named entities other
than &, <, >, ", ' and in style, href or src, where
a browser would decode them into a hidden host or CSS escape. 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.<img> 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.calm_get.Known Cloud ALM limitation: descriptions with images. Cloud ALM refuses any change to the
description of a feature that contains images when it comes through the API: its image service
answers 403 to the API's technical user, and Cloud ALM reports a 500 (updateUsedImages). Nothing is
saved (modifiedAt stays the same), so calmcp reports that the update may not have been applied.
Other fields of such a feature, and descriptions of features without images, can be changed. Change
those descriptions in the Cloud ALM UI. Reported to SAP.
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:
{
"resource": "feature",
"id": "6-123",
"expected_modified_at": "2026-10-01T09:30:00Z",
"changes": { "statusCode": "IN_TESTING", "priorityCode": 20 }
}
Configuration is read from environment variables (see .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 (default 8080; --port/-p overrides), 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 (default 30 s). |
CALM_MAX_RESPONSE_BYTES | Largest tool result handed to the client (default 100 000); a larger one is replaced by a summary. |
CALM_TOKEN_REFRESH_BUFFER_SECONDS | Renew the OAuth token this many seconds before it expires (default 5). |
CALM_TRANSPORT | http starts the HTTP transport, like --http. |
CALM_WRITE_ENABLED | true registers calm_create (create-only). Default false: read-only. See Write access. |
CALM_UPDATE_ENABLED | true registers calm_update (change fields of existing features). Default false. See Update access. |
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 objects |
calm-api.landscape.access-control.admin | Landscape access control lists |
calm-api.bsm.read | Status events |
calm-api.analytics.read, calm-api.analytics.providers.read | calm_analytics, plus each provider's own scope |
calm-api.tasks.read, defects.read, requirements.read, tests.read, features.read, projects.read | Analytics providers Tasks, Defects, Requirements, Tests, Features, Projects |
calm-api.metrics.read, jobs.read, exm.read, iep.read, integration-monitoring.read, rum.read, sum.read, csa.read | The operations providers (Metrics, Jobs, Exceptions, MonitoringEvents, Messages, Requests, ScenarioExecutions, , ...). Tested as a set; the exact scope per provider is SAP's |
Two groups of scopes are easy to leave out, and a missing one does not have to show up as an error:
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.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.The scopes are set as authorities when the instance of the SAP Cloud ALM API service is
created or updated in the BTP cockpit. A service key created later inherits them. A complete set
for every calmcp feature, reads and writes:
{
"xs-security": {
"xsappname": "calmcp-api",
"authorities": [
"$XSMASTERAPPNAME.calm-api.tasks.read", "$XSMASTERAPPNAME.calm-api.tasks.personal.read",
"$XSMASTERAPPNAME.calm-api.projects.read", "$XSMASTERAPPNAME.calm-api.projects.private.read",
"$XSMASTERAPPNAME.calm-api.projects.protected.read",
"$XSMASTERAPPNAME.calm-api.features.read", "$XSMASTERAPPNAME.calm-api.features.personal.read",
"$XSMASTERAPPNAME.calm-api.documents.read", "$XSMASTERAPPNAME.calm-api.processhierarchy.read",
"$XSMASTERAPPNAME.calm-api.processmanagement.read", "$XSMASTERAPPNAME.calm-api.processauthoring.read",
"$XSMASTERAPPNAME.calm-api.testcases.read", "$XSMASTERAPPNAME.calm-api.testplans.read",
"$XSMASTERAPPNAME.calm-api.lib.read", "$XSMASTERAPPNAME.calm-api.landscape.read",
"$XSMASTERAPPNAME.calm-api.bsm.read",
"$XSMASTERAPPNAME.calm-api.analytics.read", "$XSMASTERAPPNAME.calm-api.analytics.providers.read",
"$XSMASTERAPPNAME.calm-api.defects.read", "$XSMASTERAPPNAME.calm-api.defects.personal.read",
"$XSMASTERAPPNAME.calm-api.requirements.read", "$XSMASTERAPPNAME.calm-api.requirements.personal.read",
"$XSMASTERAPPNAME.calm-api.tests.read",
"$XSMASTERAPPNAME.calm-api.documents.write", "$XSMASTERAPPNAME.calm-api.features.write",
"$XSMASTERAPPNAME.calm-api.tasks.write", "$XSMASTERAPPNAME.calm-api.testcases.write",
"$XSMASTERAPPNAME.calm-api.lib.write"
]
}
}
Leave out the .write scopes for a read-only deployment. Add the operations scopes from the table
above for the operations analytics providers, and calm-api.landscape.access-control.admin for
access control lists.
Grant only what the deployment should expose: every caller of calmcp sees what these scopes allow. The write scopes are listed under Write access. The full scope list is in SAP's API Guide for SAP Cloud ALM, section "API Scopes".
.mcpb)The simplest path for a single developer: install calmcp as a Claude Desktop extension.
calmcp-<version>.mcpb from the Releases page (or build it locally — see
below).calm_analytics with group_by.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 or Update Access in the extension settings (see Write access and Update access). For multi-user, HTTP, or BTP deployments, use the Docker image or deploy to Cloud Foundry instead (see below).
npm run build:mcpb # → calmcp-<version>.mcpb in the repo root
This compiles dist/, installs production dependencies, then validates and packs the bundle with
the pinned @anthropic-ai/mcpb CLI. The form Claude Desktop
shows is defined in 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.jsonsnippet under Run over stdio — that path also supports sandbox-only setups.
npm install
npm run build
npm test # unit tests (mocked HTTP)
npm run lint # biome
CALM_SANDBOX=true CALM_API_KEY=<key> node dist/index.js
Example client config (Claude Desktop):
{
"mcpServers": {
"calmcp": {
"command": "node",
"args": ["/absolute/path/to/calmcp/dist/index.js"],
"env": { "CALM_SANDBOX": "true", "CALM_API_KEY": "<key>" }
}
}
}
CALM_SANDBOX=true CALM_API_KEY=<key> PORT=8080 node dist/index.js --http
curl http://localhost:8080/health
# MCP endpoint: POST http://localhost:8080/mcp
calmcp authenticates to Cloud ALM via a bound Destination (type OAuth2 client-credentials,
URL = your Cloud ALM API base, e.g. https://<tenant>.<region>.alm.cloud.sap/api). Set
CALM_DESTINATION_NAME to that destination's name.
The deployment needs the Cloud Foundry CLI, the MultiApps plugin (which provides cf deploy), and
the MTA build tool:
Cloud Foundry CLI — install for your OS (macOS, Windows, Linux) per the official guide.
MultiApps plugin and MTA build tool (cross-platform):
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.<region>.hana.ondemand.com --sso # then pick the org/space
If
cf install-plugin multiappsfails withbad CPU type/ architecture errors (e.g. on arm64 machines), download the matching binary for your platform from the MultiApps releases and install it from file:cf install-plugin <downloaded-binary> -f.
mbt build
cf deploy mta_archives/calmcp_<version>.mtar # <version> 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.
cf pushmanifest.yml names the app calmcp; the MTA deployment names it calmcp-srv. The cf set-env
examples in this README use calmcp-srv: replace it with calmcp after a 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.
Create the services, build, then push (see manifest.yml):
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 and calm_update when writes and updates are enabled) and create the destination as
described below.
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 (SAP_CALM in mta.yaml and manifest.yml) — 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 (SAP_CALM in the descriptors) |
| Type | HTTP |
| Proxy Type | Internet |
| URL | your Cloud ALM API base including the /api suffix: https://<tenant>.<region>.alm.cloud.sap/api (the region-only form https://<region>.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://<tenant>.authentication.<region>.hana.ondemand.com/oauth/token |
| Token Service URL Type | Dedicated |
Notes:
/api suffix on the URL is required — calmcp appends per-service paths (e.g.
/calm-features/v1) directly to this URL.The destination above is how calmcp authenticates to Cloud ALM. Separately, the /mcp endpoint
itself is protected so only authorized callers can reach it.
What sign-in does and does not decide. Every caller reads and writes Cloud ALM with the one
technical identity of the destination or OAuth client. The role collections decide which tools a
caller is offered (Viewer the read tools, Writer also calm_create/calm_update) and who is
named in the audit log; they do not narrow what the read tools can see. Cloud ALM's API accepts
client credentials only, so per-user data access is not possible (see
docs/ROADMAP.md for access levels through separate API instances).
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 <key>.
This authenticates the caller, not a user. Both methods coexist on the one endpoint. See
Connecting calmcp to Microsoft Copilot Studio.
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_OAUTH_REDIRECT_PATTERNS | Extra redirect URIs (comma-separated globs, * within a segment, ** across) an MCP client may receive its OAuth code at. calmcp accepts localhost, Claude, ChatGPT, Cursor, VS Code, Mistral and Copilot Studio; any other https:// redirect is refused at registration, so a link to someone else's app cannot carry a user's code there. |
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. |
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 mcp add --transport http calmcp https://<route>/mcp/mcp URL./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 read tools (calm_list, calm_get,
calm_analytics, calm_resources) then appear; a user with CALMCP_Editor also gets calm_create
and calm_update when the deployment enables them.
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.
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=<uuid> 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 dependency audit, the version check,
lint, the type-check, unit tests, the build and the e2e tests on Node 22 and 24 (.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), and builds the Docker image and
checks that it answers /health. Pull requests also get a
dependency review 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): 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:
node scripts/tool-surface.mjs snapshot dist/index.js after.json
node scripts/tool-surface.mjs diff before.json after.json
calmcp follows semantic versioning from 1.0 on. A breaking change to any of the following waits for the next major version; changes are listed in CHANGELOG.md.
resource and provider names.error, retryable, status, message, hint).total, groups, otherCount, complete, method, unit, stoppedBy)
and the calm_create/calm_update answers.Not covered:
Security issues: see SECURITY.md.
MIT
Contributions are welcome! Please ensure your code:
npm run build)npm test) and the type-check (npm run typecheck)npm run lint, or npm run lint:fix to auto-fix)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 for planned and deferred work.
This software is provided "as is", without warranty of any kind, express or implied.
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:
parentIdtoURLReferences) 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.calm-api.documents.write for
documents, calm-api.features.write for features and their URLs, calm-api.tasks.write for
tasks, task comments and every link from a task, calm-api.testcases.write for test case links,
and calm-api.lib.write for library entries,
in addition to the read scopes.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.uuid (tasks and comments: id), the displayId and
each field the call set, as Cloud ALM stored it: rich text as length and image count, a list as
its number of items. A field stored with another value than was sent is listed in
storedDiffersFromSent. calm_get returns the whole entity.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.retryable: false), and the audit entry carries
outcome: "unknown". Check with calm_list before creating it again.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.ConfigurationItems