calmcp
calmcp bridges AI assistants to SAP Cloud ALM through four read tools (plus optional create-only writes when enabled).
List/query Cloud ALM collections with
calm_list: tasks (requirements, user stories, defects), projects, features, documents, test cases, hierarchy nodes, cross-library objects, landscape objects, status events, code lists, etc.Fetch a single entity with
calm_get, including features by display id like6-123.Run tenant-wide analytics with
calm_analyticsacross providers such as Defects, Tasks, Tests, Features, Projects, Metrics; usecount_onlyandgroup_byfor totals and breakdowns.Discover resources with
calm_resources: valid resource/provider names, required parameters, code lists, analytics dimensions/measures, recipes, and create payload fields.Count and break down data safely without listing records:
count_only,group_by,count,group_limit.Keep responses small via
fieldsprojection, REST tasktimebox_id/timebox_name, and automatic max-response-size summarization.Query with OData/REST parameters where supported; unsupported parameters are rejected instead of silently ignored.
Get structured errors as JSON for branching, with retries for some read 429/503 responses.
Use optional create-only writes when
CALM_WRITE_ENABLED=true: create documents or cross-library entries (applications, configurations, configuration activities, developments, interfaces) with strict validated payloads and deep creates; never updates or deletes.Run locally or remotely: stdio, Streamable HTTP, Claude Desktop
.mcpb, Docker, or SAP BTP Cloud Foundry; HTTP can use XSUAA OAuth or an API key.Scope-controlled access: what can be read/written depends on SAP Cloud ALM API scopes and BTP role collections (
CALMCP_Viewer,CALMCP_Editor).
Provides read-only access to SAP Cloud ALM resources including tasks, projects, features, documents, test cases, analytics, and more through consolidated tools.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@calmcpList open defects ordered by priority"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
calmcp - Cloud ALM MCP Server
A Model Context Protocol (MCP) server that bridges AI assistants (Claude, GitHub Copilot, …) to SAP Cloud ALM (aka CALM). It exposes the Cloud ALM read APIs through four consolidated, intent-based tools, runs over stdio locally or Streamable HTTP remotely, and deploys to SAP BTP Cloud Foundry.
calmcp is read-only by default: it never updates or deletes data in SAP Cloud ALM. An operator can opt in to create-only access for documents and library entries with
CALM_WRITE_ENABLED=true; see Write access.
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 architecutre. See docs/PREDECESSOR.md for the project's lineage.
Tools
Tool | Purpose |
| 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 |
| Fetch a single entity by id (a feature also by display id, e.g. |
| Query an analytics provider ( |
| Discovery: the catalog of resources/providers, per-provider analytics dimensions and measures, the task type/status/priority code lists, and worked recipes. With write access on, also the payload fields of every |
| Only when |
Worked examples
How many user stories are there in the tenant?
calm_analytics({ "provider": "Tasks", "filter": "type eq 'User Story'", "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>" }) // -> detailsOpen user stories in a sprint
calm_list({ "resource": "tasks", "project_id": "<uuid>", "task_type": "CALMUS", "status": "CIPUSOPEN", "timebox_name": "Sprint 5", "fields": "displayId,title,status,assigneeName,dueDate" })
Call calm_resources (optionally { "topic": "recipes" }) at any time to discover valid
resource/provider values and required parameters.
Keeping responses small
A task carries 67 attributes, most of them null, and the Tasks REST endpoint supports neither
$select nor a timebox filter. Unprojected task lists therefore run to hundreds of KB and overflow
agent hosts such as Microsoft Copilot Studio. calm_list adds two options, both applied by calmcp
after fetching: fields projects the records, and timebox_id/timebox_name selects one sprint
(paging through the project so the filter is complete). Unknown field names and unknown timebox
names are rejected rather than silently ignored.
REST resources have no server-side $filter, and each reads only a few parameters. A parameter the
chosen resource does not read (for example filter on a REST resource, or project_id on an OData
one) is rejected with the list of parameters it does read, rather than left off the request: an
unfiltered answer to a filtered question reads as authoritative and is wrong. calm_resources
reports those parameters per resource, derived from how calmcp builds the request.
calmcp also caps the response itself. A payload over CALM_MAX_RESPONSE_BYTES (default 100 KB) is
withheld and replaced by a summary naming how many records matched, which fields they carry, and
how to ask again. Handing the payload over instead means the client truncates the JSON mid-record
and the model answers from a fragment, which reads as authoritative and is wrong.
Counting and breakdowns
Never answer "how many?" by listing records and counting them. Both query tools take:
Option | Effect |
| Returns only the total. One |
| Returns |
| Caps the number of groups (default 50); the tail folds into |
| Returns the total alongside the records (OData and analytics only). |
Which tool you call decides where the number comes from:
calm_analyticscounts tenant-wide, with noproject_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 carryunit: "entities". A provider whose identity and measure calmcp does not know is counted by rows and labelledunit: "rows"with a warning.calm_listcounts live, within whatever the resource is scoped to (resource: "tasks"needs aproject_id).
The analytics service drops a $filter on a field it does not support instead of rejecting it,
and then answers with every row, so a wrong field name yields a confident count of the wrong thing.
A filtered analytics count therefore also counts the same window unfiltered, and sets
filterVerified: false when the two totals match. That detects a dropped filter but cannot prove
one was applied, so prefer group_by on the field you would have filtered: it sends no filter, so
nothing can be dropped, and it returns every value with its count in one call.
Every counting result reports method, the effective filter, and complete. A walk stopped by
the 20 000-record page cap comes back with complete: false and says the real total is higher,
rather than presenting a floor as the answer.
Errors
A failed call returns an error result whose text is a JSON object, so a client can branch on it without parsing prose:
{ "error": "FORBIDDEN", "retryable": false, "status": 403, "message": "HTTP error 403: ...",
"hint": "The Cloud ALM API service instance behind calmcp probably lacks the scope calm-api.features.read. ..." }Codes: INVALID_ARGUMENT (fix the call), NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN,
CONFLICT, RATE_LIMITED, UPSTREAM_ERROR, TIMEOUT, NETWORK, CANCELLED, AUTH, CONFIG,
INTERNAL. A read answered with 429 or 503 is retried once when Cloud ALM asks for a wait of at
most 5 seconds; a create is never retried. A call the client cancels stops issuing requests.
A FORBIDDEN hint names the scope the service needs (see Cloud ALM API scopes).
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 |
|
|
|
|
| |
|
| |
|
| |
|
| |
|
|
What it does and does not do:
Create only. There is no update and no delete, on purpose. A Cloud ALM document stores its body as HTML with embedded images; a round-trip through an AI client would not preserve those, so the one operation that cannot damage an existing record is the only one offered. Calling
calm_createtwice makes two entries.Strict payloads. Each resource's fields are transcribed from the
*-createschemas in the OpenAPI specs and validated before any request is sent. An unknown field is an error, not something silently dropped.calm_resources({ topic: "document" })returns the field list.Deep create. Links (
toURLReferences) and assignments (toLibraryAssignments,toProcessHierarchyAssignments,toTaskAssignments, …) can be included in the samedataobject 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.writefor documents andcalm-api.lib.writefor library entries, in addition to the read scopes.Writer scope on HTTP. On the HTTP transport the switch alone is not enough:
calm_createis offered only to callers whose XSUAA token carries theWriterscope (role collectionCALMCP_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.
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" }]
}
}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 |
|
|
| Sandbox API key (sandbox mode). |
| Tenant subdomain and region (e.g. |
| OAuth2 client-credentials from the service binding. |
| Name of a bound BTP Destination (BTP mode; takes precedence). |
| HTTP transport port, and the exact origins of browser-based clients allowed by CORS (comma-separated, e.g. |
| Verbose tracing and request timeout. |
|
|
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 |
| Tasks, including requirements, user stories and defects; |
| Projects, programs, teams, timeboxes; |
| Features; |
| Documents |
| Process hierarchy |
| Process scopes, custom processes |
| Test cases, test plans |
| Cross-library applications, configurations, developments, interfaces |
| Landscape |
| Status events |
|
|
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.readandcalm-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.readscope per analytics provider:calm-api.tasks.personal.read(processor),calm-api.defects.personal.readandcalm-api.requirements.personal.read(assignee),calm-api.features.personal.read(responsible). Without them, those fields are not readable.
Grant only what the deployment should expose: every caller of calmcp sees what these scopes allow. The write scopes are listed under Write access. 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.
Download the latest
calmcp-<version>.mcpbfrom the Releases page (or build it locally — see below).Double-click it, or open Claude Desktop → Settings → Extensions and drag the file in.
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.
Ask Claude: "Using the SAP Cloud ALM tools, list the open defects ordered by priority." — it should call
calm_analytics.
What the bundle is: a pure-JS, cross-platform (macOS / Windows / Linux) build of the stdio server packaged with its dependencies — calmcp has no native modules, so one bundle runs everywhere. It is read-only unless you switch on Write Access in the extension settings (see Write access). 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 rootThis compiles dist/, installs production dependencies, then validates and packs the bundle with
the pinned @anthropic-ai/mcpb CLI. The form Claude Desktop
shows is defined in mcpb-manifest.json; keep its version in sync with
package.json (the build fails if they differ).
Prefer to hand-edit JSON? Use the
claude_desktop_config.jsonsnippet under Run over stdio — that path also supports sandbox-only setups.
Local development
npm install
npm run build
npm test # unit tests (mocked HTTP)
npm run lint # biomeRun over stdio (local MCP clients)
CALM_SANDBOX=true CALM_API_KEY=<key> node dist/index.jsExample 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/mcpDeploy 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:
Cloud Foundry CLI — install for your OS (macOS, Windows, Linux) per the official guide.
MultiApps plugin and MTA build tool (cross-platform):
cf install-plugin multiapps -f # registers the `cf deploy` command used below npm install --global mbt # MTA build tool cf login -a https://api.cf.<region>.hana.ondemand.com --sso # then pick the org/space
If
cf install-plugin multiappsfails withbad CPU type/ architecture errors (e.g. on arm64 machines), download the matching binary for your platform from the MultiApps releases and install it from file:cf install-plugin <downloaded-binary> -f.
Using the MTA descriptor
mbt build
cf deploy mta_archives/calmcp_<version>.mtar # <version> is the one in package.jsonThis 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
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 pushAfter deploy, assign the CALMCP_Viewer role collection to authorized users (or CALMCP_Editor
for users who may use calm_create when writes are enabled) and create the destination as
described below.
Configure the destination
Two differently-named things are involved — don't confuse them:
calmcp-destination— the destination service instance created and bound by the deploy (mta.yaml / manifest.yml). It is the container that holds destinations; you do not edit it by hand.CALM_DESTINATION_NAME(defaultSAP_CALM) — the name of the destination entry the app looks up at runtime. This is the name you give the destination you create. It must match the value ofCALM_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 |
Type |
|
Proxy Type |
|
URL | your Cloud ALM API base including the |
Authentication |
|
Client ID |
|
Client Secret |
|
Token Service URL | the service key's |
Token Service URL Type |
|
Notes:
The
/apisuffix on the URL is required — calmcp appends per-service paths (e.g./calm-features/v1) directly to this URL.The destination's Check Connection button may report 401/403; that is expected for an unauthenticated probe. The real check is the deployed app calling a tool.
No additional destination properties are needed.
Endpoint authentication (HTTP transport)
The destination above is how calmcp authenticates to Cloud ALM. Separately, the /mcp endpoint
itself is protected so only authorized callers can reach it.
Standard approach: a BTP user signing in from an AI tool. When the calmcp-xsuaa service is
bound, /mcp requires a valid XSUAA token carrying the Viewer scope (granted via the
CALMCP_Viewer role collection). calmcp detects the bound service from VCAP_SERVICES and enables
this automatically. It also exposes MCP-native OAuth (RFC 8414 discovery + RFC 7591 dynamic client
registration, proxied to XSUAA), so an AI tool such as Claude Desktop, Cursor or VS Code signs the
user in interactively with no manual token handling. The OAuth flow is delegated to XSUAA; calmcp
never sees the user's password. This is the recommended path: each user authenticates as themselves
with a BTP user and the CALMCP_Viewer role collection.
Alternative: a static API key for non-interactive, server-to-server callers (for example
Microsoft Copilot Studio). Set CALM_HTTP_API_KEY and the caller sends Authorization: Bearer <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 |
| Public base URL used in OAuth metadata and the callback. Defaults to the first route in |
| Secret for HMAC-signing dynamic client registrations. Set it (e.g. |
|
|
| Shared secret for the alternative API-key path. Generate with |
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>/mcpClaude Desktop: Settings → Connectors → add a custom connector with the
/mcpURL.Cursor / VS Code / others: add an MCP server of type HTTP (Streamable) at the
/mcpURL.
On first connect the client triggers the OAuth login; sign in with your BTP user, which must hold
the CALMCP_Viewer role collection. The four tools (calm_list, calm_get, calm_analytics,
calm_resources) then appear.
For non-interactive server-to-server callers such as Microsoft Copilot Studio, use the API-key path instead: see Connecting calmcp to Microsoft Copilot Studio.
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 HTTPPushes to main and every pull request run npm ci, the version check, lint, unit tests, the
build and the e2e tests on Node 22 and 24 (.github/workflows/ci.yml).
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.jsonLicense
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, ornpm run lint:fixto 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 toolscalm_analyticsQuery SAP Cloud ALM analyticsARead-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).
| Name | Required | Description | Default |
|---|---|---|---|
| top | No | OData $top — maximum number of records | |
| skip | No | OData $skip — records to skip | |
| count | No | Return the total as "@count" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead | |
| filter | No | OData $filter, e.g. "status eq 'CIPDFCTOPEN'" | |
| period | No | 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 | |
| select | No | OData $select — comma-separated field list | |
| group_by | No | 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 | |
| provider | Yes | 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 | |
| count_only | No | 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 | |
| resolution | No | 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" | |
| group_limit | No | Maximum groups returned by group_by (default 50); the rest fold into otherCount |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the readOnlyHint/openWorldHint annotations by disclosing non-obvious behaviors: no sorting, tenant-wide scope, silently ignored type text, default counting windows (C1D/D). These are exactly the traps an agent would otherwise fall into.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Dense but front-loaded: capability, then the exclusion, then the counting/grouping recipes. No filler sentences; each clause carries a usable fact.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With 11 parameters, no output schema, and an analytics service full of sharp edges, the description supplies the operational contract (counting defaults, bucket multiplication, group folding) needed to call it correctly. Nothing essential is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
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 nonetheless adds cross-parameter meaning (count vs count_only distinction, group_by as a value-discovery tool, filter syntax with typeID code) that the schema only partially implies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource (query an SAP Cloud ALM analytics provider) and enumerates the provider domain. It clearly positions itself against siblings by framing itself as the tenant-wide totals/breakdown tool, distinct from calm_get/calm_list/calm_resources.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when/when-not guidance: count_only=true for total, group_by for breakdown, and an explicit negative ($orderby is ignored, sort yourself). It also routes the type-filtering case with a concrete example, so an agent knows exactly which knob to turn.
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 entityARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Entity id (uuid, REST id, or feature display id like "6-123") | |
| expand | No | OData $expand for OData entities; REST entities reject it | |
| fields | No | 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 | |
| resource | Yes | Which single entity to fetch (see calm_resources) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish read-only, open-world behavior. The description adds useful context: features can be fetched by display id, tasks have ~70 fields, and passing fields keeps responses small. It also notes that unknown field names are rejected with available ones, going beyond the structured annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded and compact: purpose first, then resource selection, then field guidance. Every sentence serves a purpose, though the field example duplicates schema text slightly and the description stops short of a fully tight three-sentence package.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a get-by-id tool with a rich enum schema and read-only annotations, the description covers the essential call pattern, resource selection, and large-entity field handling. It does not explain return shape or missing-id behavior, but no output schema exists and the 'fetch a single entity' framing implies the return adequately.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description largely repeats what the schema already provides for id, resource, and fields, adding little syntax or meaning beyond the structured parameter documentation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb (Fetch) and resource (single SAP Cloud ALM entity), and distinguishes itself from listing siblings by emphasizing 'single' and retrieval by id. It also highlights the special display-id case and field selection, making the action unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It tells the agent to choose a resource and pass an id, and points to calm_resources for valid values, which implies usage. However, it never says when to use this tool instead of calm_list or calm_analytics, nor does it state any exclusions or prerequisites beyond the resource lookup.
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 dataARead-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/$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.
| Name | Required | Description | Default |
|---|---|---|---|
| ids | No | Fetch specific tasks by id (resource:tasks); sent as a comma-separated list | |
| top | No | OData $top — maximum number of records | |
| skip | No | OData $skip — records to skip | |
| tags | No | Tag filters (resource:tasks) | |
| count | No | Return the total as "@count" ALONGSIDE the records (OData resources and analytics only). For a total without the records, use count_only instead | |
| limit | No | REST page size (REST resources) | |
| expand | No | OData $expand — comma-separated navigation properties | |
| fields | No | 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 | |
| filter | No | 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 | |
| offset | No | REST page offset (REST resources) | |
| select | No | OData $select — comma-separated field list | |
| status | No | Status code filter (e.g. CIPDFCTOPEN; or deployment plan status) | |
| filters | No | Free-form REST filters for landscape_objects / bsm_events | |
| orderby | No | OData $orderby, e.g. "priority desc". OData resources, and REST resources whose calm_resources entry lists orderby; any other resource rejects it | |
| task_id | No | Task id (required for task sub-resources) | |
| team_id | No | Team id (required for team_roles/program_team_roles) | |
| group_by | No | 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 | |
| resource | Yes | Which collection to list (see calm_resources for the catalog and required params) | |
| task_type | No | Task type filter (resource:tasks). CALMDEF = Defect | |
| count_only | No | 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 | |
| program_id | No | Program id (required for program_teams) | |
| project_id | No | Project id (required for tasks/deliverables/etc.) | |
| sub_status | No | Sub-status code filter (resource:tasks) | |
| timebox_id | No | Timebox (sprint/phase) id filter (resource:tasks). Applied by calmcp after fetching, paging through the project automatically | |
| assignee_id | No | Assignee id filter (resource:tasks) | |
| group_limit | No | Maximum groups returned by group_by (default 50); the rest fold into otherCount | |
| timebox_name | No | 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 | |
| last_changed_date | No | Last-changed date filter (resource:tasks). Prefix with an operator: gt:, eq: or lt:, e.g. "gt:2026-08-01" | |
| solution_process_id | No | Solution process id filter (resource:task_solution_process_assignments) | |
| last_changed_timestamp | No | 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 |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so safety is covered. The description adds genuine behavioral context beyond that: REST resources reject unknown params instead of ignoring them, count_only returns bytes rather than KB, and group_by returns {total, groups:[...]} rather than records. It stops short of describing paging defaults or output envelope shape for non-grouped calls.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the scope, then the OData/REST contract, then the counting guidance, then the pointer to calm_resources. Information-dense with little waste, though the single long paragraph packs several distinct rules together and could be split for faster scanning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 30-parameter, no-output-schema tool the description covers the important operational facts: the resource catalogue pointer, per-provider param acceptance/rejection, counting and grouping semantics, and the group_by result shape. It omits default page sizes and the general response envelope, which an agent would otherwise have to discover empirically.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: it distinguishes count from count_only ('for a total without the records, use count_only'), explains which params only OData vs REST resources accept, and gives the defect recipe that ties resource+task_type together. That raises it above the schema-alone baseline.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb+resource and enumerates the collection families it covers (tasks, projects, features, documents, test cases, hierarchy nodes, cross-library objects, landscape objects, status events, code lists). It also distinguishes itself from siblings by pointing to calm_resources for the catalog, so an agent can route without opening either schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use rules: count_only=true for 'how many?', group_by for breakdowns, and the hard exclusion 'never list records to count them, as a few hundred tasks overflow most clients'. It also states the OData-vs-REST param contract and that other params are rejected rather than ignored, plus a concrete recipe for defects (resource="tasks", task_type="CALMDEF").
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 resourcesARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| topic | No | Optional: a resource/provider name, or "recipes" for worked examples |
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v0.9.2- Changed
calm_analytics3 fields changed- added
Input schema / properties / group_limit / maximumAdded value: +9007199254740991 - added
Input schema / properties / skip / maximumAdded value: +9007199254740991 - added
Input schema / properties / top / maximumAdded value: +9007199254740991
- Changed
calm_list6 fields changed- added
Input schema / properties / filters / propertyNamesAdded value: +{ + "type": "string" +} - added
Input schema / properties / group_limit / maximumAdded value: +9007199254740991 - added
Input schema / properties / limit / maximumAdded value: +9007199254740991 - added
Input schema / properties / offset / maximumAdded value: +9007199254740991 - added
Input schema / properties / skip / maximumAdded value: +9007199254740991 - added
Input schema / properties / top / maximumAdded value: +9007199254740991
2 tool updates
v0.9.1- Changed
calm_get2 fields changed- added
Input schema / properties / fieldsAdded 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" +} - added
Input schema / properties / id / patternAdded value: +"^(?!\\.\\.?$).+$"
- Changed
calm_list4 fields changed- added
Input schema / properties / program_id / patternAdded value: +"^(?!\\.\\.?$).+$" - added
Input schema / properties / project_id / patternAdded value: +"^(?!\\.\\.?$).+$" - added
Input schema / properties / task_id / patternAdded value: +"^(?!\\.\\.?$).+$" - added
Input schema / properties / team_id / patternAdded value: +"^(?!\\.\\.?$).+$"
3 tool updates
v0.9.0- Changed
calm_analytics8 fields changed- added
Input schema / properties / countAdded 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" +} - added
Input schema / properties / count_onlyAdded 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" +} - added
Input schema / properties / group_byAdded 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" +} - added
Input schema / properties / group_limitAdded value: +{ + "description": "Maximum groups returned by group_by (default 50); the rest fold into otherCount", + "exclusiveMinimum": 0, + "type": "integer" +} - removed
Input schema / properties / orderbyRemoved value: -{ - "description": "OData $orderby, e.g. \"priority desc\" (OData resources / analytics only)", - "type": "string" -} - added
Input schema / properties / periodAdded 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" +} - changed
Input schema / properties / provider / descriptionPrevious 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" - added
Input schema / properties / resolutionAdded 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" +}
- Changed
calm_get1 field changed- changed
Input schema / properties / expand / descriptionPrevious value: -"OData $expand for OData entities"New value: +"OData $expand for OData entities; REST entities reject it"
- Changed
calm_list7 fields changed- added
Input schema / properties / countAdded 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" +} - added
Input schema / properties / count_onlyAdded 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" +} - changed
Input schema / properties / filter / descriptionPrevious 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" - added
Input schema / properties / group_byAdded 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" +} - added
Input schema / properties / group_limitAdded value: +{ + "description": "Maximum groups returned by group_by (default 50); the rest fold into otherCount", + "exclusiveMinimum": 0, + "type": "integer" +} - changed
Input schema / properties / orderby / descriptionPrevious 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" - changed
Input schema / properties / resource / enumPrevious 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" +]
2 tool updates
v0.2.0- Changed
calm_get1 field changed- changed
Input schema / properties / resource / enumPrevious 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" +]
- Changed
calm_list10 fields changed- added
Input schema / properties / fieldsAdded 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" +} - added
Input schema / properties / idsAdded value: +{ + "description": "Fetch specific tasks by id (resource:tasks); sent as a comma-separated list", + "items": { + "type": "string" + }, + "type": "array" +} - added
Input schema / properties / last_changed_dateAdded value: +{ + "description": "Last-changed date filter (resource:tasks). Prefix with an operator: gt:, eq: or lt:, e.g. \"gt:2026-08-01\"", + "type": "string" +} - added
Input schema / properties / last_changed_timestampAdded 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" +} - added
Input schema / properties / program_idAdded value: +{ + "description": "Program id (required for program_teams)", + "type": "string" +} - changed
Input schema / properties / resource / enumPrevious 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" +] - added
Input schema / properties / solution_process_idAdded value: +{ + "description": "Solution process id filter (resource:task_solution_process_assignments)", + "type": "string" +} - changed
Input schema / properties / team_id / descriptionPrevious value: -"Team id (required for team_roles)"New value: +"Team id (required for team_roles/program_team_roles)" - added
Input schema / properties / timebox_idAdded value: +{ + "description": "Timebox (sprint/phase) id filter (resource:tasks). Applied by calmcp after fetching, paging through the project automatically", + "type": "string" +} - added
Input schema / properties / timebox_nameAdded 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" +}
4 tool updates
v0.1.0- First observed
calm_analytics - First observed
calm_get - First observed
calm_list - First observed
calm_resources
TDQS
Scored across 4 tools
calm_get (single entity), calm_list (collection query), and calm_analytics (tenant-wide aggregates) have broadly distinct roles, and calm_resources is a clear discovery helper. However, calm_list and calm_analytics overlap meaningfully — both expose count_only and group_by and both can count records — so an agent could reasonably pick either for a counting task.
All four tools share a consistent 'calm_' prefix, which gives a predictable family feel. The suffixes mix verb-style (get, list) with noun-style (analytics, resources), so it is not a strict verb_noun pattern, but it remains readable and predictable.
Four generic meta-tools efficiently cover a very broad SAP Cloud ALM surface (tasks, projects, defects, test cases, etc.) by parameterizing resources. It leans slightly thin — only one tool handles the discovery/catalog role — but each tool earns its place.
For a query/read-oriented surface, coverage looks strong: single fetch, collection listing with full OData params, tenant-wide analytics, and a resource/code-list discovery helper with recipes. The main gap is the absence of any mutation operations (create/update/delete), which is likely intentional but limits lifecycle coverage.
Maintenance
Related MCP Connectors
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
An MCP server that provides read access to your cloud storage providers, bank accounts and more.
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
Related MCP Servers
- FlicenseBqualityNot gradedmaintenanceAn 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-
- AlicenseNot gradedqualityAmaintenanceMCP server for SAP Cloud ALM, providing 54 tools across 9 services to manage features, tasks, test cases, documents, projects, and more via natural language.865 npm5GPL 3.0
- AlicenseNot gradedqualityBmaintenanceA 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.112 npm32MIT
- FlicenseNot gradedqualityBmaintenanceAn 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.-