Skip to main content
Glama

calmcp - Cloud ALM MCP Server

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 with CALM_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.

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; 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 Update access.

Worked examples

  • 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.

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 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.

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.

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.

Errors

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.

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.

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

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:

  • Create only. 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.

  • Document images are not copied. The Documents API returns a document body without its images (see 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.

  • Task rules checked first. Rules the Tasks spec states only in prose are checked before sending: a sub-task (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.

  • Links between existing objects. A link is an object of its own; the two objects it connects are left as they are. Features, documents, process hierarchy nodes and manual test cases can be named by display id (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 parentId.

  • 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 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.

  • 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.

  • Short answer. The result is the new 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.

  • 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.

  • Uncertain outcome. A create that times out, gets a 408 or a 5xx is not retried: the result says the entry may or may not exist (retryable: false), and the audit entry carries outcome: "unknown". Check with calm_list before creating it again.

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" }]
  }
}

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 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:

  • Display id or uuid. 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.

  • 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 is checked against an allowlist: only the elements and attributes of formatted text, links to http(s), mailto or Cloud ALM paths, and images from Cloud ALM's image service (an image already in the current text may stay). Everything else is refused: scripts, event handlers, javascript: or data: URLs, CSS url(...), and named entities other than &amp;, &lt;, &gt;, &quot;, &apos; and &nbsp; 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.

  • Image guard. A feature description references its images as <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.

  • PATCH, never PUT, and never retried once Cloud ALM processed it: 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.

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 }
}

Related MCP server: @mcp-abap-adt/calm-server

Configuration

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.

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 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, ConfigurationItems, ...). 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:

  • 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.

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".

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-<version>.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, how many open defects are there per project?" — it should call 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).

Build the bundle locally

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.json snippet under Run over stdio — that path also supports sandbox-only setups.

Local development

npm install
npm run build
npm test          # unit tests (mocked HTTP)
npm run lint      # biome

Run over stdio (local MCP clients)

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>" }
    }
  }
}

Run over HTTP

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

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://<tenant>.<region>.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.

  2. 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 multiapps fails with bad 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.

Using the MTA descriptor

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.

Using cf push

manifest.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.

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 (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:

  • 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.

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.

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://<route>/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 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.

Testing

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

Versioning and stability

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.

  • Tool names, parameter names and their meaning; resource and provider names.

  • Error codes and the error result shape (error, retryable, status, message, hint).

  • The counting result (total, groups, otherCount, complete, method, unit, stoppedBy) and the calm_create/calm_update answers.

  • Environment variables, CLI flags, XSUAA scope and role collection names.

Not covered:

  • The wording of descriptions, server instructions, hints and messages, which is tuned for models and may change in any release.

  • Resources SAP marks as beta (Test Plans).

  • Changes SAP makes to Cloud ALM itself: a field or code SAP removes (for example the Fit/Gap sub-classifications) disappears from calmcp too, in a minor release, with a note in the changelog.

Security issues: see SECURITY.md.

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 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.

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

Available Tools

4 tools
calm_analyticsQuery SAP Cloud ALM analyticsA
Read-only

Query an SAP Cloud ALM analytics provider (Defects, Tasks, Tests, Features, Projects, Metrics, ...). Supports $filter, and aggregates: it is the tool for tenant-wide totals and breakdowns. It does NOT sort — $orderby is ignored, so sort the records yourself. Every provider spans the whole tenant, so this is how you count without naming a project: count_only=true for a total, group_by="status" for a breakdown (Defects: "defectStatus"). Tasks covers user stories, defects and requirements; filter them by type CODE, e.g. filter="typeID eq 'CALMUS'" (the type text is silently ignored).

NameRequiredDescriptionDefault
topNoMaximum number of records (page size)
skipNoRecords to skip (page offset)
filterNoOData $filter, e.g. "status eq 'CIPDFCTOPEN'"
periodNoAnalytics time window, sent inside $filter. Format <L|C><n><H|D|W|M|Y>, e.g. "L1D" (last day) or "C1M" (current month). Counting defaults to "C1D" so each record is counted once
selectNoOData $select — comma-separated field list
group_byNoComma-separated field name(s) to break the count down by, e.g. "status" or "projectName,status". Returns {total, groups:[{value,count}]} instead of records. Also the quickest way to discover which values a field actually takes
providerYesAnalytics provider (e.g. Defects, Tasks, Tests). Every provider spans the whole tenant, so this is the only way to count without naming a project. It aggregates but does not sort: the service ignores $orderby, so never present its output as sorted
count_onlyNoReturn ONLY the total number of matching records, no records at all. Use this for every "how many ...?" question — the answer is a few hundred bytes instead of hundreds of KB. Works for every resource and provider
resolutionNoAnalytics bucket size, sent inside $filter: D, W, M or Y. A record appears once per bucket, so a wide window with a small bucket multiplies the count. Counting defaults to "D"
with_countNoReturn the total as "@count" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead
group_limitNoMaximum groups returned by group_by (default 50); the rest fold into otherCount

TDQS

A4.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the read-only profile, and the description adds genuinely non-obvious behavior: it does not sort, every provider spans the whole tenant, the type text filter is silently ignored (a silent-failure trap), and bucket/resolution multiplies counts. Missing only a note on default page size / cost of large top values.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core purpose, then the counting/breakdown workflow, then caveats. Dense but every sentence carries an operational fact; only mild duplication with the provider enum description's tenant-wide/aggregation note.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With 11 parameters and no output schema, the description still tells the agent what output shapes to expect (records vs total vs grouped counts) and flags the counting pitfalls (counting defaults to C1D, bucket multiplication, group_limit folding into otherCount). Enough to invoke correctly for the common analytics tasks.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3; the description earns above that by explaining what group_by returns ({total, groups:[{value,count}]}) and mapping intent to parameters (count_only for "how many", group_by for value discovery, provider-specific field names like "defectStatus"). It leaves top/skip/select/period/resolution to the schema, which documents them adequately.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Names a specific verb and resource ("Query an SAP Cloud ALM analytics provider") and enumerates the provider families (Defects, Tasks, Tests, Features, Projects, Metrics). Crucially it carves out its role versus the record-oriented siblings: "it is the tool for tenant-wide totals and breakdowns", so an agent can tell it apart from calm_list/calm_get without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit routing rules: count_only=true for totals, group_by="status" for breakdowns, count_only instead of with_count when records aren't needed, and filter by type CODE ("typeID eq 'CALMUS'") for Tasks. It also states a negative: $orderby is ignored, so the agent must sort client-side.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

calm_getGet one SAP Cloud ALM entityA
Read-only

Fetch a single SAP Cloud ALM entity by id (a feature can also be fetched by display id like "6-123"). Choose a "resource" and pass its "id". See calm_resources for valid ones. A task has ~70 fields: pass fields="displayId,title,status,..." to keep the answer small.

NameRequiredDescriptionDefault
idYesEntity id (uuid, REST id, or feature display id like "6-123")
expandNoOData $expand for OData entities; REST entities reject it
fieldsNoComma-separated fields to keep, e.g. "displayId,title,status". Use it for large entities such as tasks; an unknown name is rejected with the available ones
resourceYesWhich single entity to fetch (see calm_resources)

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. The description usefully adds that a feature can be fetched by display id and that tasks have ~70 fields, making fields= important for keeping responses small.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core action and scope, then adds only practical details about resource selection and field projection. It is appropriately sized with no wasted sentences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a read-only entity fetch with a rich 100%-covered schema and annotations, the description covers the essential retrieval model, display-id exception, and large-entity field tip. Minor gaps remain around expand behavior and response shape, but no output schema exists to require return-value documentation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents resource, id, fields, and expand. The description reinforces the id and fields usage but largely repeats schema content and does not add information about the expand parameter.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific verb and resource: 'Fetch a single SAP Cloud ALM entity by id.' The word 'single' and the id-based retrieval imply the distinction from list-style siblings, but the description does not explicitly name calm_list or another alternative.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains the retrieval pattern: choose a resource and pass its id, and it points to calm_resources for valid resources. It also gives concrete field-projection advice for large task entities, though it does not explicitly state when to prefer calm_list or another sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

calm_listList SAP Cloud ALM dataA
Read-only

List or query any SAP Cloud ALM collection (tasks, projects, features, documents, test cases, hierarchy nodes, cross-library objects, landscape objects, status events, code lists). Choose a "resource"; OData resources accept filter/select/expand/orderby, every resource pages with top/skip; REST resources read only the params calm_resources lists for them, and any other param (including $filter) is rejected rather than ignored. Defects: resource="tasks", task_type="CALMDEF". To answer "how many?" pass count_only=true, or group_by="status" for a breakdown — never list records to count them, as a few hundred tasks overflow most clients. See calm_resources for the full catalog.

NameRequiredDescriptionDefault
idsNoFetch specific tasks by id (resource:tasks); sent as a comma-separated list
topNoMaximum number of records (page size)
skipNoRecords to skip (page offset)
tagsNoTag filters (resource:tasks)
expandNoOData $expand — comma-separated navigation properties
fieldsNoComma-separated field projection applied by calmcp to the returned records (any resource). Use it to keep responses small, e.g. "displayId,title,status,assigneeName,timeboxId". Unlike $select this works for REST resources too. Unknown names are rejected
filterNoOData $filter, e.g. "status eq 'CIPDFCTOPEN'". OData resources only: REST resources have no server-side filter and reject it. calm_resources lists the parameters each resource reads
selectNoOData $select — comma-separated field list
statusNoStatus code filter (e.g. CIPDFCTOPEN; or deployment plan status)
orderbyNoOData $orderby, e.g. "priority desc". OData resources, and REST resources whose calm_resources entry lists orderby; any other resource rejects it
task_idNoTask id (required for task sub-resources)
team_idNoTeam id (required for team_roles/program_team_roles)
group_byNoComma-separated field name(s) to break the count down by, e.g. "status" or "projectName,status". Returns {total, groups:[{value,count}]} instead of records. Also the quickest way to discover which values a field actually takes
resourceYesWhich collection to list (see calm_resources for the catalog and required params)
task_typeNoTask type filter (resource:tasks). CALMDEF = Defect
count_onlyNoReturn ONLY the total number of matching records, no records at all. Use this for every "how many ...?" question — the answer is a few hundred bytes instead of hundreds of KB. Works for every resource and provider
program_idNoProgram id (required for program_teams)
project_idNoProject id (required for tasks/deliverables/etc.)
sub_statusNoSub-status code filter (resource:tasks)
timebox_idNoTimebox (sprint/phase) id filter (resource:tasks). Applied by calmcp after fetching, paging through the project automatically
with_countNoReturn the total as "@count" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead
assignee_idNoAssignee id filter (resource:tasks)
group_limitNoMaximum groups returned by group_by (default 50); the rest fold into otherCount
project_nameNoProject name instead of project_id, e.g. "S4 Transformation" (case and spacing ignored, otherwise exact). Works wherever project_id does; an ambiguous or unknown name is an error naming the candidates
rest_filtersNoQuery parameters of the REST resources that take them (landscape_objects, landscape_access_control_lists, bsm_events); not OData, see filter for that
timebox_nameNoTimebox name filter, e.g. "Sprint 5" (resource:tasks). Resolved against the project's timeboxes; errors listing the known names when it does not match
last_changed_dateNoLast-changed date filter (resource:tasks). Prefix with an operator: gt:, eq: or lt:, e.g. "gt:2026-08-01"
solution_process_idNoSolution process id filter (resource:task_solution_process_assignments)
last_changed_timestampNoLast-changed timestamp filter (resource:tasks). Prefix with gt:, eq: or lt: and use ISO 8601, e.g. "gt:2026-08-01T00:00:00Z". Use this for incremental "what changed since" queries

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Despite annotations already covering the safety profile (readOnlyHint/openWorldHint), the description adds real behavioral detail: REST resources reject unsupported params rather than silently ignoring them, both providers page with top/skip, only OData resources accept filter, and large result sets overflow clients. It also discloses the shape change with group_by ({total, groups}) and the point of count_only (bytes vs hundreds of KB).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Purpose is front-loaded and the remaining clauses are dense but all load-bearing (OData vs REST rules, paging, count/group shortcuts, pointer to calm_resources). It runs long as continuous prose, which makes individual rules harder to scan than they need to be, but nothing is filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 29-parameter, multi-provider tool with no output schema, the description covers the decisions an agent must make: which resource, which filtering dialect, how to page, how to count or group, and where the authoritative per-resource parameter list lives. The group_by payload shape is spelled out, which is the one return contract an agent needs up front.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the schema already carries the per-parameter explanations; the description's contribution is guidance-level (count_only for counts, group_by for value discovery, task_type=CALMDEF for defects) rather than adding meaning the schema lacks. That is additive but modest against a fully documented 29-param schema, so the baseline applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (list/query) and a precisely scoped resource (any SAP Cloud ALM collection), immediately enumerating the collection families so an agent knows the breadth without opening the enum. The scope line makes it implicitly distinct from calm_get (single record) and calm_analytics, and it points at calm_resources for the catalog.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives concrete routing rules — use count_only for "how many?", group_by for breakdowns, never list-then-count, see calm_resources for per-resource params — plus a worked example (Defects via resource=tasks, task_type=CALMDEF). It does not explicitly contrast with calm_get or calm_analytics, so sibling selection is left partly implicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

calm_resourcesDiscover SAP Cloud ALM resourcesA
Read-only

Discovery helper: lists every resource/provider the other tools accept, their required parameters, the task type/status/priority code lists, and worked recipes. Pass topic="recipes" for multi-step examples, or a resource/provider name to focus.

NameRequiredDescriptionDefault
topicNoOptional: a resource/provider name, or "recipes" for worked examples

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish the safe read-only, closed-world profile, so the description's job is to add content-level context — and it does, enumerating exactly what a call returns (resource/provider inventory, required parameters, task type/status/priority code lists, worked recipes). It adds no auth, rate-limit or freshness detail, but for a static reference tool that is a minor gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One dense, front-loaded sentence plus a short usage clause; every phrase carries information (what it returns, the special 'recipes' value, the focusing behavior). Nothing is padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, so the description must describe the return payload — and it does so comprehensively, covering the resource inventory, required parameters, code lists and recipes. An agent has everything needed to decide to call it and how to interpret the result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the single parameter is fully documented there, so the schema does the heavy lifting. The description essentially restates the schema's semantics (a resource/provider name, or "recipes" for examples) without adding new format or constraint detail, which is the baseline case.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb and resource: a discovery helper that lists every resource/provider the other tools accept, plus code lists and recipes. The 'Discovery helper' framing cleanly separates it from the data-operating siblings calm_list, calm_get and calm_analytics.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains how to drive the tool ('Pass topic="recipes"... or a resource/provider name to focus'), which implies it is used to look up valid inputs before calling the other tools. However, it never states explicitly when to reach for this versus calm_list/calm_get, nor any exclusions or prerequisites.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv1.0.0
    • Changedcalm_analytics4 fields changed
      • removedInput schema / properties / count
        Removed value: -{
        -  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        -  "type": "boolean"
        -}
      • changedInput schema / properties / skip / description
        Previous value: -"OData $skip — records to skip"New value: +"Records to skip (page offset)"
      • changedInput schema / properties / top / description
        Previous value: -"OData $top — maximum number of records"New value: +"Maximum number of records (page size)"
      • addedInput schema / properties / with_count
        Added value: +{
        +  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        +  "type": "boolean"
        +}
    • Changedcalm_list9 fields changed
      • removedInput schema / properties / count
        Removed value: -{
        -  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        -  "type": "boolean"
        -}
      • removedInput schema / properties / filters
        Removed value: -{
        -  "additionalProperties": {
        -    "type": "string"
        -  },
        -  "description": "Free-form REST filters for landscape_objects / bsm_events",
        -  "propertyNames": {
        -    "type": "string"
        -  },
        -  "type": "object"
        -}
      • removedInput schema / properties / limit
        Removed value: -{
        -  "description": "REST page size (REST resources)",
        -  "exclusiveMinimum": 0,
        -  "maximum": 9007199254740991,
        -  "type": "integer"
        -}
      • removedInput schema / properties / offset
        Removed value: -{
        -  "description": "REST page offset (REST resources)",
        -  "maximum": 9007199254740991,
        -  "minimum": 0,
        -  "type": "integer"
        -}
      • changedInput schema / properties / resource / enum
        Previous value: -[
        -  "features",
        -  "feature_external_references",
        -  "feature_url_references",
        -  "feature_task_assignments",
        -  "feature_priorities",
        -  "feature_statuses",
        -  "documents",
        -  "document_types",
        -  "document_statuses",
        -  "document_sources",
        -  "document_priorities",
        -  "document_approval_states",
        -  "hierarchy_nodes",
        -  "manual_test_cases",
        -  "automated_test_cases",
        -  "test_activities",
        -  "test_actions",
        -  "xlib_applications",
        -  "xlib_configurations",
        -  "xlib_developments",
        -  "xlib_interfaces",
        -  "xlib_application_url_references",
        -  "xlib_configuration_url_references",
        -  "xlib_development_url_references",
        -  "xlib_interface_url_references",
        -  "xlib_configuration_activities",
        -  "xlib_configuration_activity_types",
        -  "xlib_configuration_assignments",
        -  "xlib_application_priorities",
        -  "xlib_application_readiness",
        -  "xlib_application_usage_statuses",
        -  "xlib_application_clean_core_levels",
        -  "xlib_application_upgrade_impacts",
        -  "xlib_configuration_priorities",
        -  "xlib_configuration_readiness",
        -  "xlib_development_priorities",
        -  "xlib_development_readiness",
        -  "xlib_development_usage_statuses",
        -  "xlib_development_clean_core_levels",
        -  "xlib_development_upgrade_impacts",
        -  "xlib_interface_priorities",
        -  "xlib_interface_readiness",
        -  "xlib_interface_usage_statuses",
        -  "xlib_interface_clean_core_levels",
        -  "xlib_interface_upgrade_impacts",
        -  "scopes",
        -  "solution_scenario_versions",
        -  "scope_solution_processes",
        -  "business_processes",
        -  "solution_processes",
        -  "solution_process_flows",
        -  "solution_activities",
        -  "process_assets",
        -  "test_plans",
        -  "test_case_assignments",
        -  "test_plan_tag_assignments",
        -  "tasks",
        -  "task_solution_process_assignments",
        -  "task_subtasks",
        -  "task_comments",
        -  "task_references",
        -  "task_relations",
        -  "task_feature_assignments",
        -  "task_document_assignments",
        -  "task_hierarchy_assignments",
        -  "deliverables",
        -  "workstreams",
        -  "projects",
        -  "project_timeboxes",
        -  "project_teams",
        -  "team_roles",
        -  "programs",
        -  "program_teams",
        -  "program_team_roles",
        -  "system_groups",
        -  "deployment_plans",
        -  "landscape_objects",
        -  "landscape_access_control_lists",
        -  "bsm_events"
        -]New value: +[
        +  "features",
        +  "feature_external_references",
        +  "feature_url_references",
        +  "feature_task_assignments",
        +  "feature_transports",
        +  "feature_transport_references",
        +  "feature_priorities",
        +  "feature_statuses",
        +  "documents",
        +  "document_types",
        +  "document_statuses",
        +  "document_sources",
        +  "document_priorities",
        +  "document_approval_states",
        +  "hierarchy_nodes",
        +  "manual_test_cases",
        +  "automated_test_cases",
        +  "test_activities",
        +  "test_actions",
        +  "xlib_applications",
        +  "xlib_configurations",
        +  "xlib_developments",
        +  "xlib_interfaces",
        +  "xlib_application_url_references",
        +  "xlib_configuration_url_references",
        +  "xlib_development_url_references",
        +  "xlib_interface_url_references",
        +  "xlib_configuration_activities",
        +  "xlib_configuration_activity_types",
        +  "xlib_configuration_assignments",
        +  "xlib_application_priorities",
        +  "xlib_application_readiness",
        +  "xlib_application_usage_statuses",
        +  "xlib_application_clean_core_levels",
        +  "xlib_application_upgrade_impacts",
        +  "xlib_configuration_priorities",
        +  "xlib_configuration_readiness",
        +  "xlib_development_priorities",
        +  "xlib_development_readiness",
        +  "xlib_development_usage_statuses",
        +  "xlib_development_clean_core_levels",
        +  "xlib_development_upgrade_impacts",
        +  "xlib_interface_priorities",
        +  "xlib_interface_readiness",
        +  "xlib_interface_usage_statuses",
        +  "xlib_interface_clean_core_levels",
        +  "xlib_interface_upgrade_impacts",
        +  "scopes",
        +  "solution_scenario_versions",
        +  "scope_solution_processes",
        +  "business_processes",
        +  "solution_processes",
        +  "solution_process_flows",
        +  "solution_activities",
        +  "process_assets",
        +  "test_plans",
        +  "test_case_assignments",
        +  "test_plan_tag_assignments",
        +  "tasks",
        +  "task_solution_process_assignments",
        +  "task_subtasks",
        +  "task_comments",
        +  "task_references",
        +  "task_relations",
        +  "task_feature_assignments",
        +  "task_document_assignments",
        +  "task_hierarchy_assignments",
        +  "deliverables",
        +  "workstreams",
        +  "projects",
        +  "project_timeboxes",
        +  "project_teams",
        +  "team_roles",
        +  "programs",
        +  "program_teams",
        +  "program_team_roles",
        +  "system_groups",
        +  "deployment_plans",
        +  "landscape_objects",
        +  "landscape_access_control_lists",
        +  "bsm_events"
        +]
      • addedInput schema / properties / rest_filters
        Added value: +{
        +  "additionalProperties": {
        +    "type": "string"
        +  },
        +  "description": "Query parameters of the REST resources that take them (landscape_objects, landscape_access_control_lists, bsm_events); not OData, see filter for that",
        +  "propertyNames": {
        +    "type": "string"
        +  },
        +  "type": "object"
        +}
      • changedInput schema / properties / skip / description
        Previous value: -"OData $skip — records to skip"New value: +"Records to skip (page offset)"
      • changedInput schema / properties / top / description
        Previous value: -"OData $top — maximum number of records"New value: +"Maximum number of records (page size)"
      • addedInput schema / properties / with_count
        Added value: +{
        +  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        +  "type": "boolean"
        +}
  2. 2 tool updatesv0.9.4
    • Changedcalm_get2 fields changed
      • addedInput schema / properties / id / minLength
        Added value: +1
      • removedInput schema / properties / id / pattern
        Removed value: -"^(?!\\.\\.?$).+$"
    • Changedcalm_list9 fields changed
      • addedInput schema / properties / program_id / minLength
        Added value: +1
      • removedInput schema / properties / program_id / pattern
        Removed value: -"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / project_id / minLength
        Added value: +1
      • removedInput schema / properties / project_id / pattern
        Removed value: -"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / project_name
        Added value: +{
        +  "description": "Project name instead of project_id, e.g. \"S4 Transformation\" (case and spacing ignored, otherwise exact). Works wherever project_id does; an ambiguous or unknown name is an error naming the candidates",
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / task_id / minLength
        Added value: +1
      • removedInput schema / properties / task_id / pattern
        Removed value: -"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / team_id / minLength
        Added value: +1
      • removedInput schema / properties / team_id / pattern
        Removed value: -"^(?!\\.\\.?$).+$"
  3. 2 tool updatesv0.9.2
    • Changedcalm_analytics3 fields changed
      • addedInput schema / properties / group_limit / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / skip / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / top / maximum
        Added value: +9007199254740991
    • Changedcalm_list6 fields changed
      • addedInput schema / properties / filters / propertyNames
        Added value: +{
        +  "type": "string"
        +}
      • addedInput schema / properties / group_limit / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / limit / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / offset / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / skip / maximum
        Added value: +9007199254740991
      • addedInput schema / properties / top / maximum
        Added value: +9007199254740991
  4. 2 tool updatesv0.9.1
    • Changedcalm_get2 fields changed
      • addedInput schema / properties / fields
        Added value: +{
        +  "description": "Comma-separated fields to keep, e.g. \"displayId,title,status\". Use it for large entities such as tasks; an unknown name is rejected with the available ones",
        +  "type": "string"
        +}
      • addedInput schema / properties / id / pattern
        Added value: +"^(?!\\.\\.?$).+$"
    • Changedcalm_list4 fields changed
      • addedInput schema / properties / program_id / pattern
        Added value: +"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / project_id / pattern
        Added value: +"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / task_id / pattern
        Added value: +"^(?!\\.\\.?$).+$"
      • addedInput schema / properties / team_id / pattern
        Added value: +"^(?!\\.\\.?$).+$"
  5. 3 tool updatesv0.9.0
    • Changedcalm_analytics8 fields changed
      • addedInput schema / properties / count
        Added value: +{
        +  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / count_only
        Added value: +{
        +  "description": "Return ONLY the total number of matching records, no records at all. Use this for every \"how many ...?\" question — the answer is a few hundred bytes instead of hundreds of KB. Works for every resource and provider",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / group_by
        Added value: +{
        +  "description": "Comma-separated field name(s) to break the count down by, e.g. \"status\" or \"projectName,status\". Returns {total, groups:[{value,count}]} instead of records. Also the quickest way to discover which values a field actually takes",
        +  "type": "string"
        +}
      • addedInput schema / properties / group_limit
        Added value: +{
        +  "description": "Maximum groups returned by group_by (default 50); the rest fold into otherCount",
        +  "exclusiveMinimum": 0,
        +  "type": "integer"
        +}
      • removedInput schema / properties / orderby
        Removed value: -{
        -  "description": "OData $orderby, e.g. \"priority desc\" (OData resources / analytics only)",
        -  "type": "string"
        -}
      • addedInput schema / properties / period
        Added value: +{
        +  "description": "Analytics time window, sent inside $filter. Format <L|C><n><H|D|W|M|Y>, e.g. \"L1D\" (last day) or \"C1M\" (current month). Counting defaults to \"C1D\" so each record is counted once",
        +  "type": "string"
        +}
      • changedInput schema / properties / provider / description
        Previous value: -"Analytics provider (e.g. Defects, Tasks, Tests). Supports $orderby."New value: +"Analytics provider (e.g. Defects, Tasks, Tests). Every provider spans the whole tenant, so this is the only way to count without naming a project. It aggregates but does not sort: the service ignores $orderby, so never present its output as sorted"
      • addedInput schema / properties / resolution
        Added value: +{
        +  "description": "Analytics bucket size, sent inside $filter: D, W, M or Y. A record appears once per bucket, so a wide window with a small bucket multiplies the count. Counting defaults to \"D\"",
        +  "type": "string"
        +}
    • Changedcalm_get1 field changed
      • changedInput schema / properties / expand / description
        Previous value: -"OData $expand for OData entities"New value: +"OData $expand for OData entities; REST entities reject it"
    • Changedcalm_list7 fields changed
      • addedInput schema / properties / count
        Added value: +{
        +  "description": "Return the total as \"@count\" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / count_only
        Added value: +{
        +  "description": "Return ONLY the total number of matching records, no records at all. Use this for every \"how many ...?\" question — the answer is a few hundred bytes instead of hundreds of KB. Works for every resource and provider",
        +  "type": "boolean"
        +}
      • changedInput schema / properties / filter / description
        Previous value: -"OData $filter, e.g. \"status eq 'CIPDFCTOPEN'\""New value: +"OData $filter, e.g. \"status eq 'CIPDFCTOPEN'\". OData resources only: REST resources have no server-side filter and reject it. calm_resources lists the parameters each resource reads"
      • addedInput schema / properties / group_by
        Added value: +{
        +  "description": "Comma-separated field name(s) to break the count down by, e.g. \"status\" or \"projectName,status\". Returns {total, groups:[{value,count}]} instead of records. Also the quickest way to discover which values a field actually takes",
        +  "type": "string"
        +}
      • addedInput schema / properties / group_limit
        Added value: +{
        +  "description": "Maximum groups returned by group_by (default 50); the rest fold into otherCount",
        +  "exclusiveMinimum": 0,
        +  "type": "integer"
        +}
      • changedInput schema / properties / orderby / description
        Previous value: -"OData $orderby, e.g. \"priority desc\" (OData resources / analytics only)"New value: +"OData $orderby, e.g. \"priority desc\". OData resources, and REST resources whose calm_resources entry lists orderby; any other resource rejects it"
      • changedInput schema / properties / resource / enum
        Previous value: -[
        -  "features",
        -  "feature_external_references",
        -  "feature_url_references",
        -  "feature_task_assignments",
        -  "feature_priorities",
        -  "feature_statuses",
        -  "documents",
        -  "document_types",
        -  "document_statuses",
        -  "document_sources",
        -  "document_priorities",
        -  "document_approval_states",
        -  "hierarchy_nodes",
        -  "manual_test_cases",
        -  "automated_test_cases",
        -  "test_activities",
        -  "test_actions",
        -  "xlib_applications",
        -  "xlib_configurations",
        -  "xlib_developments",
        -  "xlib_interfaces",
        -  "xlib_application_url_references",
        -  "xlib_configuration_url_references",
        -  "xlib_development_url_references",
        -  "xlib_interface_url_references",
        -  "xlib_configuration_activities",
        -  "xlib_configuration_activity_types",
        -  "xlib_configuration_assignments",
        -  "scopes",
        -  "solution_scenario_versions",
        -  "scope_solution_processes",
        -  "business_processes",
        -  "solution_processes",
        -  "solution_process_flows",
        -  "solution_activities",
        -  "process_assets",
        -  "test_plans",
        -  "test_case_assignments",
        -  "test_plan_tag_assignments",
        -  "tasks",
        -  "task_solution_process_assignments",
        -  "task_subtasks",
        -  "task_comments",
        -  "task_references",
        -  "task_relations",
        -  "task_feature_assignments",
        -  "task_document_assignments",
        -  "task_hierarchy_assignments",
        -  "deliverables",
        -  "workstreams",
        -  "projects",
        -  "project_timeboxes",
        -  "project_teams",
        -  "team_roles",
        -  "programs",
        -  "program_teams",
        -  "program_team_roles",
        -  "system_groups",
        -  "deployment_plans",
        -  "landscape_objects",
        -  "bsm_events"
        -]New value: +[
        +  "features",
        +  "feature_external_references",
        +  "feature_url_references",
        +  "feature_task_assignments",
        +  "feature_priorities",
        +  "feature_statuses",
        +  "documents",
        +  "document_types",
        +  "document_statuses",
        +  "document_sources",
        +  "document_priorities",
        +  "document_approval_states",
        +  "hierarchy_nodes",
        +  "manual_test_cases",
        +  "automated_test_cases",
        +  "test_activities",
        +  "test_actions",
        +  "xlib_applications",
        +  "xlib_configurations",
        +  "xlib_developments",
        +  "xlib_interfaces",
        +  "xlib_application_url_references",
        +  "xlib_configuration_url_references",
        +  "xlib_development_url_references",
        +  "xlib_interface_url_references",
        +  "xlib_configuration_activities",
        +  "xlib_configuration_activity_types",
        +  "xlib_configuration_assignments",
        +  "xlib_application_priorities",
        +  "xlib_application_readiness",
        +  "xlib_application_usage_statuses",
        +  "xlib_application_clean_core_levels",
        +  "xlib_application_upgrade_impacts",
        +  "xlib_configuration_priorities",
        +  "xlib_configuration_readiness",
        +  "xlib_development_priorities",
        +  "xlib_development_readiness",
        +  "xlib_development_usage_statuses",
        +  "xlib_development_clean_core_levels",
        +  "xlib_development_upgrade_impacts",
        +  "xlib_interface_priorities",
        +  "xlib_interface_readiness",
        +  "xlib_interface_usage_statuses",
        +  "xlib_interface_clean_core_levels",
        +  "xlib_interface_upgrade_impacts",
        +  "scopes",
        +  "solution_scenario_versions",
        +  "scope_solution_processes",
        +  "business_processes",
        +  "solution_processes",
        +  "solution_process_flows",
        +  "solution_activities",
        +  "process_assets",
        +  "test_plans",
        +  "test_case_assignments",
        +  "test_plan_tag_assignments",
        +  "tasks",
        +  "task_solution_process_assignments",
        +  "task_subtasks",
        +  "task_comments",
        +  "task_references",
        +  "task_relations",
        +  "task_feature_assignments",
        +  "task_document_assignments",
        +  "task_hierarchy_assignments",
        +  "deliverables",
        +  "workstreams",
        +  "projects",
        +  "project_timeboxes",
        +  "project_teams",
        +  "team_roles",
        +  "programs",
        +  "program_teams",
        +  "program_team_roles",
        +  "system_groups",
        +  "deployment_plans",
        +  "landscape_objects",
        +  "landscape_access_control_lists",
        +  "bsm_events"
        +]
  6. 2 tool updatesv0.2.0
    • Changedcalm_get1 field changed
      • changedInput schema / properties / resource / enum
        Previous value: -[
        -  "feature",
        -  "document",
        -  "hierarchy_node",
        -  "manual_test_case",
        -  "automated_test_case",
        -  "xlib_application",
        -  "xlib_configuration",
        -  "xlib_development",
        -  "xlib_interface",
        -  "task",
        -  "deliverable",
        -  "project",
        -  "program",
        -  "timebox",
        -  "team",
        -  "deployment_plan",
        -  "system_group"
        -]New value: +[
        +  "feature",
        +  "document",
        +  "hierarchy_node",
        +  "manual_test_case",
        +  "automated_test_case",
        +  "xlib_application",
        +  "xlib_configuration",
        +  "xlib_development",
        +  "xlib_interface",
        +  "task",
        +  "deliverable",
        +  "project",
        +  "program",
        +  "timebox",
        +  "team",
        +  "deployment_plan",
        +  "system_group",
        +  "program_team",
        +  "scope",
        +  "solution_scenario_version",
        +  "business_process",
        +  "solution_process",
        +  "solution_activity",
        +  "process_asset",
        +  "test_plan",
        +  "test_case_assignment"
        +]
    • Changedcalm_list10 fields changed
      • addedInput schema / properties / fields
        Added value: +{
        +  "description": "Comma-separated field projection applied by calmcp to the returned records (any resource). Use it to keep responses small, e.g. \"displayId,title,status,assigneeName,timeboxId\". Unlike $select this works for REST resources too. Unknown names are rejected",
        +  "type": "string"
        +}
      • addedInput schema / properties / ids
        Added value: +{
        +  "description": "Fetch specific tasks by id (resource:tasks); sent as a comma-separated list",
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedInput schema / properties / last_changed_date
        Added value: +{
        +  "description": "Last-changed date filter (resource:tasks). Prefix with an operator: gt:, eq: or lt:, e.g. \"gt:2026-08-01\"",
        +  "type": "string"
        +}
      • addedInput schema / properties / last_changed_timestamp
        Added value: +{
        +  "description": "Last-changed timestamp filter (resource:tasks). Prefix with gt:, eq: or lt: and use ISO 8601, e.g. \"gt:2026-08-01T00:00:00Z\". Use this for incremental \"what changed since\" queries",
        +  "type": "string"
        +}
      • addedInput schema / properties / program_id
        Added value: +{
        +  "description": "Program id (required for program_teams)",
        +  "type": "string"
        +}
      • changedInput schema / properties / resource / enum
        Previous value: -[
        -  "features",
        -  "feature_external_references",
        -  "feature_url_references",
        -  "feature_task_assignments",
        -  "feature_priorities",
        -  "feature_statuses",
        -  "documents",
        -  "document_types",
        -  "document_statuses",
        -  "document_sources",
        -  "document_priorities",
        -  "document_approval_states",
        -  "hierarchy_nodes",
        -  "manual_test_cases",
        -  "automated_test_cases",
        -  "test_activities",
        -  "test_actions",
        -  "xlib_applications",
        -  "xlib_configurations",
        -  "xlib_developments",
        -  "xlib_interfaces",
        -  "tasks",
        -  "task_subtasks",
        -  "task_comments",
        -  "task_references",
        -  "task_relations",
        -  "task_feature_assignments",
        -  "task_document_assignments",
        -  "task_hierarchy_assignments",
        -  "deliverables",
        -  "workstreams",
        -  "projects",
        -  "project_timeboxes",
        -  "project_teams",
        -  "team_roles",
        -  "programs",
        -  "system_groups",
        -  "deployment_plans",
        -  "landscape_objects",
        -  "bsm_events"
        -]New value: +[
        +  "features",
        +  "feature_external_references",
        +  "feature_url_references",
        +  "feature_task_assignments",
        +  "feature_priorities",
        +  "feature_statuses",
        +  "documents",
        +  "document_types",
        +  "document_statuses",
        +  "document_sources",
        +  "document_priorities",
        +  "document_approval_states",
        +  "hierarchy_nodes",
        +  "manual_test_cases",
        +  "automated_test_cases",
        +  "test_activities",
        +  "test_actions",
        +  "xlib_applications",
        +  "xlib_configurations",
        +  "xlib_developments",
        +  "xlib_interfaces",
        +  "xlib_application_url_references",
        +  "xlib_configuration_url_references",
        +  "xlib_development_url_references",
        +  "xlib_interface_url_references",
        +  "xlib_configuration_activities",
        +  "xlib_configuration_activity_types",
        +  "xlib_configuration_assignments",
        +  "scopes",
        +  "solution_scenario_versions",
        +  "scope_solution_processes",
        +  "business_processes",
        +  "solution_processes",
        +  "solution_process_flows",
        +  "solution_activities",
        +  "process_assets",
        +  "test_plans",
        +  "test_case_assignments",
        +  "test_plan_tag_assignments",
        +  "tasks",
        +  "task_solution_process_assignments",
        +  "task_subtasks",
        +  "task_comments",
        +  "task_references",
        +  "task_relations",
        +  "task_feature_assignments",
        +  "task_document_assignments",
        +  "task_hierarchy_assignments",
        +  "deliverables",
        +  "workstreams",
        +  "projects",
        +  "project_timeboxes",
        +  "project_teams",
        +  "team_roles",
        +  "programs",
        +  "program_teams",
        +  "program_team_roles",
        +  "system_groups",
        +  "deployment_plans",
        +  "landscape_objects",
        +  "bsm_events"
        +]
      • addedInput schema / properties / solution_process_id
        Added value: +{
        +  "description": "Solution process id filter (resource:task_solution_process_assignments)",
        +  "type": "string"
        +}
      • changedInput schema / properties / team_id / description
        Previous value: -"Team id (required for team_roles)"New value: +"Team id (required for team_roles/program_team_roles)"
      • addedInput schema / properties / timebox_id
        Added value: +{
        +  "description": "Timebox (sprint/phase) id filter (resource:tasks). Applied by calmcp after fetching, paging through the project automatically",
        +  "type": "string"
        +}
      • addedInput schema / properties / timebox_name
        Added value: +{
        +  "description": "Timebox name filter, e.g. \"Sprint 5\" (resource:tasks). Resolved against the project's timeboxes; errors listing the known names when it does not match",
        +  "type": "string"
        +}
  7. 4 tool updatesv0.1.0
    • First observedcalm_analytics
    • First observedcalm_get
    • First observedcalm_list
    • First observedcalm_resources

TDQS

A4/5.0

Scored across 4 tools

Disambiguation3/5

calm_list, calm_analytics, and calm_get are largely distinguishable by cardinality (collection vs single entity), but calm_list and calm_analytics both accept count_only and group_by and both cover tasks/defects, so 'how many defects?' can plausibly route to either. The descriptions do add guidance (project-scoped OData vs tenant-wide provider, sorting ignored), which keeps it workable but not frictionless. calm_resources is clearly separate as a discovery helper.

Naming Consistency4/5

All four tools share the calm_ prefix and snake_case, giving a predictable namespace. Three names imply an action (list/get/analytics) while calm_resources is a plain noun, a minor deviation but still readable and consistent with the rest.

Tool Count4/5

Four parameterized tools cover a very broad domain (tasks, projects, features, tests, landscape, hierarchy) via a resource argument, which is a sensible, compact design rather than an explosion of per-resource tools. The count is on the lean side for such a wide surface, but each tool earns its place.

Completeness3/5

Read coverage is strong—list, filtered query, aggregation, single-entity fetch, and a discovery catalog—but the surface is entirely read-only, with no create, update, or delete operations for tasks, defects, features, or documents. Agents can inspect CALM thoroughly but cannot act on it, and there is no cross-resource text search.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    Not graded
    maintenance
    An MCP server that enables AI assistants to interact with SAP systems via the ABAP Development Tools (ADT) REST API. It allows users to read ABAP source code, inspect DDIC objects, and execute SQL queries directly.
    66
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    A config-driven MCP server that exposes OData and REST APIs as MCP tools, enabling AI assistants to query, manage, and monitor SAP backends through natural language.
    153 npm
    32
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    An MCP server for AI assistants to create and manage SAP Solution Manager Focused Build Requirements and navigate the Solution Documentation process hierarchy via SAP OData API.
    -