cloud-alm-mcp-server 0.0.0-stage → 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/LICENSE +21 -0
- package/README.md +375 -2
- package/dist/approval.d.ts +41 -0
- package/dist/approval.js +225 -0
- package/dist/calm/client.d.ts +139 -0
- package/dist/calm/client.js +852 -0
- package/dist/calm/errors.d.ts +22 -0
- package/dist/calm/errors.js +43 -0
- package/dist/calm/format.d.ts +26 -0
- package/dist/calm/format.js +247 -0
- package/dist/calm/grant.d.ts +32 -0
- package/dist/calm/grant.js +98 -0
- package/dist/calm/index.d.ts +6 -0
- package/dist/calm/index.js +7 -0
- package/dist/calm/model.d.ts +316 -0
- package/dist/calm/model.js +24 -0
- package/dist/calm/transport.d.ts +70 -0
- package/dist/calm/transport.js +238 -0
- package/dist/calm/values.d.ts +18 -0
- package/dist/calm/values.js +94 -0
- package/dist/calm/writer.d.ts +25 -0
- package/dist/calm/writer.js +104 -0
- package/dist/cli/checks.d.ts +26 -0
- package/dist/cli/checks.js +127 -0
- package/dist/cli/clients.d.ts +19 -0
- package/dist/cli/clients.js +62 -0
- package/dist/cli/key.d.ts +47 -0
- package/dist/cli/key.js +97 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +223 -0
- package/dist/cli/scopes.d.ts +25 -0
- package/dist/cli/scopes.js +116 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +51 -0
- package/dist/http.d.ts +25 -0
- package/dist/http.js +71 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/node-http.d.ts +6 -0
- package/dist/node-http.js +44 -0
- package/dist/server.d.ts +48 -0
- package/dist/server.js +405 -0
- package/dist/stdio.d.ts +20 -0
- package/dist/stdio.js +45 -0
- package/dist/views/edit.d.ts +81 -0
- package/dist/views/edit.js +650 -0
- package/dist/views/html.d.ts +1 -0
- package/dist/views/html.js +8 -0
- package/dist/views/items.d.ts +62 -0
- package/dist/views/items.js +151 -0
- package/dist/views/snapshot.d.ts +123 -0
- package/dist/views/snapshot.js +390 -0
- package/dist/views/text.d.ts +41 -0
- package/dist/views/text.js +140 -0
- package/dist/views/tools.d.ts +61 -0
- package/dist/views/tools.js +883 -0
- package/dist/views/write.d.ts +67 -0
- package/dist/views/write.js +373 -0
- package/package.json +72 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this package. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0-beta.0] - 2026-11-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `calm_create_task` takes no description when there is none (Cloud ALM
|
|
14
|
+
does not need one), names its priority codes, and its preview and card
|
|
15
|
+
say the type and priority ("Create a defect", "priority high").
|
|
16
|
+
- The approval ask states when it expires and also travels in
|
|
17
|
+
`structuredContent`, for hosts that show the model only that.
|
|
18
|
+
- `init` says when the key could write and how to turn writes on; for a
|
|
19
|
+
prerelease, its setup lines pin the version.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Seven task-shaped read tools replace the 21 endpoint mirrors:
|
|
24
|
+
`calm_overview`, `calm_find`, `calm_get`, `calm_trace`, `calm_gaps`,
|
|
25
|
+
`calm_analytics`, `calm_landscape`. They accept short display ids
|
|
26
|
+
(`3-17`, `8-1-2`), read a whole project in a few requests, answer in
|
|
27
|
+
compact text, and show the latest test results. The tool list costs about
|
|
28
|
+
1.8k tokens instead of 4.5k.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Approval of writes (`CALM_APPROVAL=prefer|required|off`): every write is
|
|
33
|
+
a proposal the user approves in an approval card in the chat (MCP Apps)
|
|
34
|
+
or, in `prefer`, by saying yes in the chat (`calm_confirm`). The client's
|
|
35
|
+
confirm dialog is opt-in (`CALM_APPROVAL_DIALOG=on`), as some clients
|
|
36
|
+
decline it unseen. Approvals are single-use, expire after an hour (the
|
|
37
|
+
card then shows them as expired), and are bound to
|
|
38
|
+
the caller.
|
|
39
|
+
- `doctor` checks scopes per tool, write mode, approval and the default project.
|
|
40
|
+
|
|
41
|
+
- Write mode, off by default (`CALM_WRITE=on|dry-run`):
|
|
42
|
+
`calm_create_task`, `calm_create_test_case`, `calm_create_feature`,
|
|
43
|
+
`calm_create_document`, `calm_update`, `calm_delete`, `calm_comment`,
|
|
44
|
+
`calm_link` (link and unlink, also documents and requirements),
|
|
45
|
+
`calm_tag`. Duplicates refused; every write tool takes `dryRun` and
|
|
46
|
+
previews each change before → after; a 403 names the missing scope.
|
|
47
|
+
- `CALM_TOOLS` (allow list) and `CALM_PROJECT` (default project).
|
|
48
|
+
- Skills `cloud-alm-explore` and `cloud-alm-test-design`.
|
|
49
|
+
- `cloud-alm-mcp-server init [file|-]`: checks a service key (shape, token,
|
|
50
|
+
`calm-api.*` scopes per area, one read), saves it mode 600 under the
|
|
51
|
+
user's config directory and prints configuration for Claude Code, Codex,
|
|
52
|
+
Claude Desktop and Cursor. `--force` replaces a saved key, `--skip-check`
|
|
53
|
+
saves without contacting SAP.
|
|
54
|
+
- `cloud-alm-mcp-server doctor`: the same checks on an existing setup, plus
|
|
55
|
+
key file permissions and the Node version.
|
|
56
|
+
- `--version` and `--help`.
|
|
57
|
+
- `CALM_SERVICE_KEY` takes the key JSON itself as well as a path; without
|
|
58
|
+
it the stdio server uses the key saved by `init`.
|
|
59
|
+
- Package ready for npm: `bin`, `files` (no tests, no source maps),
|
|
60
|
+
`exports`, `engines`, `prepublishOnly` (typecheck, test, build).
|
|
61
|
+
- MIT licence.
|
|
62
|
+
- CI on Node 22 and 24, with a check of the packed command; a release
|
|
63
|
+
workflow (tag `v*` → `npm publish --provenance`) that stays inert until
|
|
64
|
+
an `NPM_TOKEN` secret exists.
|
|
65
|
+
|
|
66
|
+
## 0.1.0
|
|
67
|
+
|
|
68
|
+
- First version: read-only Cloud ALM reader and MCP server, stdio mode
|
|
69
|
+
with a service key, streamable HTTP mode with a bearer per request.
|
|
70
|
+
Not published.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Didro
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,376 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cloud-alm-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets
|
|
4
|
+
an agent (Claude, Codex, Cursor…) work with **SAP Cloud ALM**: projects,
|
|
5
|
+
requirements, user stories, defects, features, documents, test cases and
|
|
6
|
+
test plans, processes, landscape, cross-library, analytics and service
|
|
7
|
+
status events.
|
|
8
|
+
|
|
9
|
+
**Read-only by default.** The read tools are annotated `readOnlyHint` and
|
|
10
|
+
the reader sends GET requests only, to `*.alm.cloud.sap` hosts only.
|
|
11
|
+
Writing (create, change, link, tag, comment, delete) is opt-in
|
|
12
|
+
with `CALM_WRITE` ([Modes and governance](#modes-and-governance)).
|
|
13
|
+
|
|
14
|
+
> Status: beta (0.1.0-beta.0). MIT licence.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
Requires Node 22 or later.
|
|
19
|
+
|
|
20
|
+
1. Get a Cloud ALM API service key (below) and download its JSON.
|
|
21
|
+
2. Check it and save it:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx -y cloud-alm-mcp-server init ~/Downloads/calm-read.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`init` gets a token to prove the key works, lists which areas of Cloud
|
|
28
|
+
ALM its scopes can read, makes one read, then saves the key readable
|
|
29
|
+
only by you (mode 600) in `~/.config/cloud-alm-mcp-server/` and prints
|
|
30
|
+
the configuration for Claude Code, Codex, Claude Desktop and Cursor.
|
|
31
|
+
Paste the JSON instead of naming a file: `init` alone reads stdin.
|
|
32
|
+
You can delete the downloaded copy afterwards.
|
|
33
|
+
3. Add the server to your client with the printed line, e.g.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
claude mcp add cloud-alm --scope user -e CALM_SERVICE_KEY=$HOME/.config/cloud-alm-mcp-server/service-key.json -- npx -y cloud-alm-mcp-server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Any time later: `npx -y cloud-alm-mcp-server doctor` checks the key,
|
|
40
|
+
token, scopes, API and Node version.
|
|
41
|
+
|
|
42
|
+
| Command | Does |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `cloud-alm-mcp-server` | starts the stdio server (default) |
|
|
45
|
+
| `cloud-alm-mcp-server http` | starts the streamable HTTP server |
|
|
46
|
+
| `cloud-alm-mcp-server init [file\|-] [--force] [--skip-check]` | checks and saves a key, prints client config |
|
|
47
|
+
| `cloud-alm-mcp-server doctor` | checks the setup |
|
|
48
|
+
| `cloud-alm-mcp-server --version` / `--help` | |
|
|
49
|
+
|
|
50
|
+
No command prints the key's secret or a token.
|
|
51
|
+
|
|
52
|
+
> Beta. Until 0.1.0 is out, name the beta: `npx -y cloud-alm-mcp-server@beta`.
|
|
53
|
+
> A packed build installs with `npm install -g ./cloud-alm-mcp-server-<version>.tgz`;
|
|
54
|
+
> then use `cloud-alm-mcp-server` instead of `npx -y cloud-alm-mcp-server`.
|
|
55
|
+
|
|
56
|
+
## Get a service key
|
|
57
|
+
|
|
58
|
+
Cloud ALM's APIs take an OAuth token from a **service key** of the "SAP Cloud
|
|
59
|
+
ALM API" service, created in the BTP subaccount where your Cloud ALM tenant
|
|
60
|
+
lives. Nothing is deployed; the instance only registers an OAuth client.
|
|
61
|
+
|
|
62
|
+
1. In the BTP cockpit, open the Cloud ALM subaccount → *Services* →
|
|
63
|
+
*Instances and Subscriptions* → *Create*.
|
|
64
|
+
2. Service **SAP Cloud ALM API**, plan **standard**, runtime environment
|
|
65
|
+
**Other**. Name it, e.g. `calm-read`.
|
|
66
|
+
3. Parameters: give the client the read scopes it needs (the default
|
|
67
|
+
template has none, which gives a token that can read nothing):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"xs-security": {
|
|
72
|
+
"xsappname": "calm-read",
|
|
73
|
+
"authorities": [
|
|
74
|
+
"$XSMASTERAPPNAME.calm-api.projects.read",
|
|
75
|
+
"$XSMASTERAPPNAME.calm-api.tasks.read",
|
|
76
|
+
"$XSMASTERAPPNAME.calm-api.features.read",
|
|
77
|
+
"$XSMASTERAPPNAME.calm-api.documents.read",
|
|
78
|
+
"$XSMASTERAPPNAME.calm-api.testcases.read",
|
|
79
|
+
"$XSMASTERAPPNAME.calm-api.testplans.read",
|
|
80
|
+
"$XSMASTERAPPNAME.calm-api.processmanagement.read",
|
|
81
|
+
"$XSMASTERAPPNAME.calm-api.processhierarchy.read",
|
|
82
|
+
"$XSMASTERAPPNAME.calm-api.landscape.read",
|
|
83
|
+
"$XSMASTERAPPNAME.calm-api.lib.read",
|
|
84
|
+
"$XSMASTERAPPNAME.calm-api.analytics.read",
|
|
85
|
+
"$XSMASTERAPPNAME.calm-api.analytics.providers.read",
|
|
86
|
+
"$XSMASTERAPPNAME.calm-api.bsm.read"
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Add only what you will read. Analytics datasets also need their area's
|
|
93
|
+
scope (`calm-api.tasks.read` for `Tasks`, `…defects.read`,
|
|
94
|
+
`…requirements.read`, `…tests.read`, …), and operations datasets their
|
|
95
|
+
own (`…metrics.read`, `…jobs.read`, `…exm.read` …). Personal fields
|
|
96
|
+
(assignee names) need `calm-api.tasks.personal.read` and its siblings.
|
|
97
|
+
4. Create a **service binding** (the key) on the instance and download its
|
|
98
|
+
JSON. Run `cloud-alm-mcp-server init <file>` on it: it saves a copy
|
|
99
|
+
readable only by you. Never put it in a repository.
|
|
100
|
+
|
|
101
|
+
The key holds `uaa.url`, `uaa.clientid`, `uaa.clientsecret` and
|
|
102
|
+
`endpoints.Api` (e.g. `https://eu20.alm.cloud.sap/api`, regional; the token
|
|
103
|
+
selects your tenant). The token carries no user: calls run as a machine
|
|
104
|
+
with access to the whole tenant. SAP cannot limit a key to one project.
|
|
105
|
+
|
|
106
|
+
Changed the scopes? The token reflects them at once: rerun
|
|
107
|
+
`cloud-alm-mcp-server doctor`. Rotated the key? `init --force <new file>`.
|
|
108
|
+
|
|
109
|
+
## Tools
|
|
110
|
+
|
|
111
|
+
Seven read tools, each answering a question in one call. Items are named by
|
|
112
|
+
their short **display ids** (`3-17` task, `6-1` feature, `7-1` document,
|
|
113
|
+
`8-1-2` test case, `8-3-1` test plan), which every tool accepts; GUIDs work
|
|
114
|
+
too. Answers are compact text written for a model's context.
|
|
115
|
+
|
|
116
|
+
| Tool | Answers |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `calm_overview` | Start here: the project, its current phase, counts of requirements, user stories, defects, features, test cases and documents by status, latest test results, tags in use, gaps |
|
|
119
|
+
| `calm_find` | Search or list items across types (tasks, features, documents, test cases, test plans, processes, library) by words in titles and tags (`a b \| c`), type, status, tag |
|
|
120
|
+
| `calm_get` | Full content of 1–10 items at once: description, test steps and latest result, document text, feature transports, every link named |
|
|
121
|
+
| `calm_trace` | An item's links both ways: requirement → user stories → features (status, transports), test cases (with results), documents, solution process |
|
|
122
|
+
| `calm_gaps` | What is missing or failing: requirements and user stories without a test case, process or feature; failed test runs; features with untested tasks; test cases linked to nothing or not prepared |
|
|
123
|
+
| `calm_analytics` | Analytics datasets (tasks, defects, test runs, jobs, exceptions, integration messages…), counted with `groupBy` |
|
|
124
|
+
| `calm_landscape` | Systems and cloud services, and SAP service status events, open ones first |
|
|
125
|
+
|
|
126
|
+
With write mode on (below), ten more:
|
|
127
|
+
|
|
128
|
+
| Tool | Does |
|
|
129
|
+
| --- | --- |
|
|
130
|
+
| `calm_create_task` | Creates a requirement, user story (optionally under a requirement), defect or project task; refuses a duplicate title |
|
|
131
|
+
| `calm_create_test_case` | Creates a manual test case with activities and steps, linked to the requirements or user stories it covers; refuses a duplicate title |
|
|
132
|
+
| `calm_create_feature` | Creates a feature, optionally linked to the tasks it delivers; refuses a duplicate title |
|
|
133
|
+
| `calm_create_document` | Creates a document (content, type, status, priority) linked to tasks; refuses a duplicate title |
|
|
134
|
+
| `calm_update` | Changes 1–20 items with the same changes: tasks (title, description, status in plain words, priority, assignee, start and due date, timebox, obsolete), features (title, description, status, priority), manual test cases (title, prepared, priority, `addActivity` with steps). Sends only the fields that differ and answers before → after per field |
|
|
135
|
+
| `calm_delete` | Deletes tasks (`soft: true` marks them obsolete instead, as SAP recommends), manual test cases (`force: true` also deletes their runs and results) and documents |
|
|
136
|
+
| `calm_comment` | Adds a comment to a task |
|
|
137
|
+
| `calm_link` | Links, or with `unlink: true` unlinks, a test case (tests), feature (delivers) or document (documents) to tasks, and user stories or project tasks to their requirement; skips links already in that state |
|
|
138
|
+
| `calm_tag` | Adds or removes tags on tasks, keeping the others, and reports tags Cloud ALM silently dropped |
|
|
139
|
+
|
|
140
|
+
Every write tool takes `dryRun: true` and then answers with a complete
|
|
141
|
+
preview: item ids and titles, each field before → after, what gets created,
|
|
142
|
+
linked or deleted. Display ids work everywhere. Statuses are plain words,
|
|
143
|
+
mapped to Cloud ALM's codes per task type (`open`, `in progress`,
|
|
144
|
+
`blocked`, `in review`, `done`, `not relevant` as the type has them;
|
|
145
|
+
requirements also `to be approved`, `approved`, `in realization`,
|
|
146
|
+
`approved for deployment`, `rejected`; defects also `retest`). A field that
|
|
147
|
+
does not apply to an item is refused before anything is written. A 403
|
|
148
|
+
names the scope the key lacks. Writes are never retried.
|
|
149
|
+
|
|
150
|
+
What Cloud ALM's API itself does not allow (no scope changes it):
|
|
151
|
+
|
|
152
|
+
- Features cannot be deleted, and their status moves only along Cloud
|
|
153
|
+
ALM's path (in specification ↔ not planned; in specification → in
|
|
154
|
+
realization → in testing → successfully tested, back to in realization;
|
|
155
|
+
once in realization, never back to in specification or not planned).
|
|
156
|
+
"Ready for production" cannot be set through the API, so "deployed" is
|
|
157
|
+
not reachable through it either.
|
|
158
|
+
- A feature description holding images cannot be changed through the API.
|
|
159
|
+
- A document's title, content, status, priority and type cannot change
|
|
160
|
+
after create; only its links can.
|
|
161
|
+
- Test cases and features cannot be tagged; automated test cases are
|
|
162
|
+
read-only; a test case's project, scope and process cannot change.
|
|
163
|
+
- No API records manual test execution (runs, step results, pass/fail).
|
|
164
|
+
- A requirement's approval is final (it cannot go back to "to be approved"
|
|
165
|
+
or "rejected"), and an approved requirement cannot be deleted.
|
|
166
|
+
- A task has one requirement: unlink it before linking another.
|
|
167
|
+
- Every write is recorded as the key's technical user.
|
|
168
|
+
|
|
169
|
+
Deleting test cases and documents needs extra scopes on the key
|
|
170
|
+
(`testcases.delete`, `testcases.force-delete`, `documents.delete`).
|
|
171
|
+
|
|
172
|
+
A read the key has no scope for is named in the answer ("not readable with
|
|
173
|
+
this key"); the rest still answers. Errors are short text with a code
|
|
174
|
+
(`unauthorized`, `forbidden`, `not_found`, `rate_limited`, `unavailable`,
|
|
175
|
+
`bad_request`) and never carry a token.
|
|
176
|
+
|
|
177
|
+
### Modes and governance
|
|
178
|
+
|
|
179
|
+
| Variable | Effect |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `CALM_WRITE` | `off` (default): read tools only. `dry-run`: write tools are offered but only preview. `on`: write tools write. The key also needs the write scopes. |
|
|
182
|
+
| `CALM_TOOLS` | Comma-separated tool names to offer, e.g. `calm_overview,calm_find,calm_get`. Default: all (of the current mode). |
|
|
183
|
+
| `CALM_APPROVAL` | With `CALM_WRITE=on`: `prefer` (default) has the user approve each write in the card or the chat, `required` in the card only, `off` writes at once. |
|
|
184
|
+
| `CALM_APPROVAL_DIALOG` | `on` also asks in the client's confirm dialog (MCP elicitation), where it declares one. Off by default: only terminal clients show it reliably. |
|
|
185
|
+
| `CALM_PROJECT` | Project GUID used when a call names none. Without it, a tenant with one project uses that one; with several, tools ask for `projectId`. |
|
|
186
|
+
|
|
187
|
+
In HTTP mode the same variables apply to every request; the bearer token
|
|
188
|
+
decides what Cloud ALM allows.
|
|
189
|
+
|
|
190
|
+
### Approving writes
|
|
191
|
+
|
|
192
|
+
With approval on, a write tool only proposes the change: it runs as a dry
|
|
193
|
+
run and answers with the preview; nothing is sent to Cloud ALM until the
|
|
194
|
+
user approves. Every answer carries the same proposal, whatever the client
|
|
195
|
+
says it supports, and the user approves it in one of these ways:
|
|
196
|
+
|
|
197
|
+
| Where | How | Clients (Nov 2026) |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| the approval card | an MCP Apps card in the chat with Approve / Reject | Claude (web, Desktop chat), ChatGPT, VS Code, Goose |
|
|
200
|
+
| the chat (`prefer` only) | the agent shows the preview and asks; on the user's yes it calls `calm_confirm` | every client |
|
|
201
|
+
| the client's dialog (`CALM_APPROVAL_DIALOG=on`) | the client's own confirm dialog | Claude Code and Codex in a terminal |
|
|
202
|
+
|
|
203
|
+
The card's button calls `calm_approve`, a tool visible to the card only,
|
|
204
|
+
with a one-time token that travels in the result's `_meta`, never in the
|
|
205
|
+
text the model reads. If the host drops `_meta` (some do, e.g. on reload),
|
|
206
|
+
the card falls back to the id in `structuredContent`, which approves only
|
|
207
|
+
in `prefer`. If the host does not pass the card's calls to the server
|
|
208
|
+
(it does not declare `serverTools`; the Claude Code tab is one, see
|
|
209
|
+
anthropics/claude-code#106708), the card shows the change without
|
|
210
|
+
buttons and asks the user to reply yes in the chat; a failed call does
|
|
211
|
+
the same. An approval is single-use, expires after 60
|
|
212
|
+
minutes (the card then shows it as expired) and, over HTTP, must come with the same bearer token as the
|
|
213
|
+
request that proposed it. `dryRun: true` previews without asking.
|
|
214
|
+
|
|
215
|
+
Why the dialog is off by default: some clients declare a confirm dialog
|
|
216
|
+
and answer "decline" without showing it (the Claude desktop app, for one),
|
|
217
|
+
and some draw the card without declaring MCP Apps. With the dialog on, a
|
|
218
|
+
declined or failed dialog keeps the proposal open for the card or chat.
|
|
219
|
+
|
|
220
|
+
Scopes (`calm-api.` …): `projects.read`, `tasks.read`, `features.read`,
|
|
221
|
+
`documents.read`, `testcases.read`, `testplans.read`,
|
|
222
|
+
`processmanagement.read`, `landscape.read`, `lib.read`, `analytics.read`
|
|
223
|
+
with `tests.read` (test results), `bsm.read`; for writing `tasks.write`,
|
|
224
|
+
`testcases.write`, `features.write`, `documents.write` (task links of
|
|
225
|
+
features and documents are written through the Tasks API), and to delete
|
|
226
|
+
test cases and documents (or unlink a test case from a task)
|
|
227
|
+
`testcases.delete`, `testcases.force-delete`, `documents.delete`.
|
|
228
|
+
|
|
229
|
+
## Two ways to run it
|
|
230
|
+
|
|
231
|
+
### stdio, with your own key (default)
|
|
232
|
+
|
|
233
|
+
The server finds the key in `CALM_SERVICE_KEY`, either a file path or the
|
|
234
|
+
JSON itself, else in the file `init` saved
|
|
235
|
+
(`$XDG_CONFIG_HOME/cloud-alm-mcp-server/service-key.json`, by default under
|
|
236
|
+
`~/.config`; `%APPDATA%\cloud-alm-mcp-server\` on Windows;
|
|
237
|
+
`CALM_CONFIG_DIR` overrides the directory). It gets tokens by client
|
|
238
|
+
credentials and keeps them in memory only.
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
npx -y cloud-alm-mcp-server # the key saved by init
|
|
242
|
+
CALM_SERVICE_KEY=~/keys/calm-read.json npx -y cloud-alm-mcp-server
|
|
243
|
+
CALM_SERVICE_KEY="$SECRET_FROM_YOUR_VAULT" npx -y cloud-alm-mcp-server # the JSON itself
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Streamable HTTP, a bearer token per request
|
|
247
|
+
|
|
248
|
+
For hosting behind your own gateway: the server holds **no secret**. Each
|
|
249
|
+
request carries `Authorization: Bearer <token>`, a Cloud ALM token the
|
|
250
|
+
caller obtained. The API address comes from the `X-Calm-Api-Url` header,
|
|
251
|
+
or `CALM_API_URL` when the header is absent; it must be an
|
|
252
|
+
`https://…alm.cloud.sap` address, so a token is never sent anywhere else.
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
PORT=3333 CALM_API_URL=https://eu20.alm.cloud.sap/api npx -y cloud-alm-mcp-server http
|
|
256
|
+
# MCP endpoint: http://127.0.0.1:3333/mcp (HOST to bind elsewhere)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Stateless: one server per POST, JSON answers, no session. Requests without
|
|
260
|
+
a bearer get 401; other methods 405. Put it behind TLS and your own
|
|
261
|
+
authentication of callers: the bearer authorizes Cloud ALM, not the caller.
|
|
262
|
+
|
|
263
|
+
## Use as a library
|
|
264
|
+
|
|
265
|
+
The Cloud ALM client is exported too:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { CloudAlm, serviceKeyGrants, formatTasks, createServer, createHttpHandler } from "cloud-alm-mcp-server";
|
|
269
|
+
|
|
270
|
+
const calm = new CloudAlm(serviceKeyGrants(serviceKey)); // from a service key
|
|
271
|
+
const calm2 = new CloudAlm(async () => myBroker.grant("cloud-alm")); // from your own token source
|
|
272
|
+
console.log(formatTasks(await calm.listTasks({ type: "defect", limit: 20 })));
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
A grant is `{ accessToken, baseUrl, expiresAt? }`; a callback is called once
|
|
276
|
+
per operation. The library never reads environment variables or files.
|
|
277
|
+
|
|
278
|
+
## Cloud ALM quirks it hides
|
|
279
|
+
|
|
280
|
+
- **Path casing differs per service**: REST services use lower case
|
|
281
|
+
(`projects`, `tasks`, `landscapeObjects`, `solutionProcesses`), OData
|
|
282
|
+
services capitalised entity sets (`Features`, `ManualTestCases`). The
|
|
283
|
+
wrong case answers 404. The library holds the right paths.
|
|
284
|
+
- **Tasks need a project.** Cloud ALM answers 400 without `projectId`.
|
|
285
|
+
Without one, `listTasks` reads every project (up to 10) and merges.
|
|
286
|
+
- **GUID filters go unquoted** in OData (`projectId eq 1234…`); a quoted
|
|
287
|
+
GUID answers 400 (`Edm.Guid` vs `Edm.String`). Ids are checked as GUIDs
|
|
288
|
+
before they go into a path or a filter.
|
|
289
|
+
- **Paging differs**: OData services take `$top`/`$skip`/`$count`; tasks,
|
|
290
|
+
landscape and status events take `limit`/`offset`; process management
|
|
291
|
+
takes a literal `$top`/`$skip`. Every list here takes `{ limit, offset }`
|
|
292
|
+
(limit 1–200, default 50) and returns
|
|
293
|
+
`{ items, offset, limit, nextOffset, total }`. `nextOffset` is null on
|
|
294
|
+
the last page (one extra row is fetched to know). `total` is set when
|
|
295
|
+
Cloud ALM counts (OData).
|
|
296
|
+
- **Errors come in four shapes** (`{message}`, `{error:{message}}`,
|
|
297
|
+
`[{message}]`, `{message,status}`); the library reads all of them.
|
|
298
|
+
- **Analytics rows are aggregates**, one per combination of dimension
|
|
299
|
+
values, not records. Unknown or unfilterable dimensions in `filters` are
|
|
300
|
+
ignored by Cloud ALM. Some datasets refuse without a filter (428, e.g.
|
|
301
|
+
`ServiceLevels` needs a business service id).
|
|
302
|
+
- **Links live with one side.** A task's test cases are found from Test
|
|
303
|
+
Management (`toTaskAssignments/any(a:a/taskId eq …)`), its features
|
|
304
|
+
from the task's feature assignments, its requirement from relation type
|
|
305
|
+
10 (`parentId` stays empty), its processes from
|
|
306
|
+
`tasks/solutionProcessAssignments`. A link the key has no scope for is
|
|
307
|
+
listed as not readable instead of failing the task. `calm_gaps`
|
|
308
|
+
counts direct links only: a requirement tested only through its user
|
|
309
|
+
stories counts as uncovered.
|
|
310
|
+
|
|
311
|
+
## Errors
|
|
312
|
+
|
|
313
|
+
Every failure is a `CloudAlmError` with a `code` and a short `reason`:
|
|
314
|
+
|
|
315
|
+
| Code | When |
|
|
316
|
+
| --- | --- |
|
|
317
|
+
| `unauthorized` | no usable token, expired token, 401, key refused by the token endpoint |
|
|
318
|
+
| `forbidden` | 403: the key lacks the scope for this call |
|
|
319
|
+
| `not_found` | 404 (also a wrong path) |
|
|
320
|
+
| `rate_limited` | 429 after two short retries (Cloud ALM allows about 500 requests per 5 s per tenant) |
|
|
321
|
+
| `unavailable` | 5xx, network error, timeout (30 s per operation), non-JSON or oversized answer |
|
|
322
|
+
| `bad_request` | 400, 422, 428, or an argument this library refused (bad id, limit, type) |
|
|
323
|
+
|
|
324
|
+
A reason never carries the token, the secret or a raw response body; long
|
|
325
|
+
opaque strings are redacted.
|
|
326
|
+
|
|
327
|
+
## Safety
|
|
328
|
+
|
|
329
|
+
The reader (`CloudAlm`) sends GET only, to the grant's `baseUrl` only,
|
|
330
|
+
which must be `https://` on a `*.alm.cloud.sap` host (`hostPattern` option
|
|
331
|
+
to change it for a test double). No redirects followed. At most 40 requests
|
|
332
|
+
and 30 seconds per operation; answers above 8 MB are refused.
|
|
333
|
+
|
|
334
|
+
The writer (`CloudAlmWriter`), used only in write mode, sends POST, PATCH
|
|
335
|
+
or DELETE only to a fixed allow-list of (method, path) pairs: create and
|
|
336
|
+
change tasks, features, documents and manual test cases (with their
|
|
337
|
+
activities and steps), comments, tags, links, and the deletes the API
|
|
338
|
+
offers. It never retries. A test case write carries its ETag (Cloud ALM
|
|
339
|
+
answers 428 without one).
|
|
340
|
+
|
|
341
|
+
## Develop
|
|
342
|
+
|
|
343
|
+
Bun for development and tests, plain Node 22+ for the built package.
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
bun install
|
|
347
|
+
bun run typecheck
|
|
348
|
+
bun run test # unit tests on hand-written fixtures, in-process MCP client over both modes, CLI with fakes
|
|
349
|
+
bun run build # dist/ for Node
|
|
350
|
+
node dist/cli.js doctor
|
|
351
|
+
CALM_SERVICE_KEY=~/keys/calm-read.json bun scripts/live-check.ts stdio # against a real tenant
|
|
352
|
+
CALM_SERVICE_KEY=~/keys/calm-read.json bun scripts/live-check.ts http
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The live check calls every tool against a real tenant and prints per tool
|
|
356
|
+
whether it answered and the size of the answer, never its text, the secret
|
|
357
|
+
or a token. Test fixtures are invented values in the shapes a real tenant
|
|
358
|
+
returns.
|
|
359
|
+
|
|
360
|
+
Try the package as users get it:
|
|
361
|
+
|
|
362
|
+
```sh
|
|
363
|
+
npm pack --pack-destination /tmp
|
|
364
|
+
cd "$(mktemp -d)" && npm init -y >/dev/null && npm i /tmp/cloud-alm-mcp-server-*.tgz
|
|
365
|
+
npx --no-install cloud-alm-mcp-server doctor
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
CI runs typecheck, tests, build, a start-up check and the packed command on
|
|
369
|
+
Node 22 and 24. Releases: bump `version` in `package.json`, add a
|
|
370
|
+
`CHANGELOG.md` entry, tag `vX.Y.Z` and push the tag; the release workflow
|
|
371
|
+
checks again and runs `npm publish --provenance`. It publishes nothing
|
|
372
|
+
until the repository has an `NPM_TOKEN` secret.
|
|
373
|
+
|
|
374
|
+
## Licence
|
|
375
|
+
|
|
376
|
+
MIT: see [LICENSE](LICENSE). Use it, change it, ship it.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** off: write at once; prefer: approve in the card or the chat; required: in the card (or an opted-in dialog) only. */
|
|
2
|
+
export type ApprovalMode = "off" | "prefer" | "required";
|
|
3
|
+
export declare const APPROVAL_MODES: ApprovalMode[];
|
|
4
|
+
export declare const CARD_URI = "ui://cloud-alm/approve-v1.html";
|
|
5
|
+
export declare const META_KEY = "cloud-alm/approval";
|
|
6
|
+
/** Long enough for a user who answers a turn later; still single-use and bound to one change. */
|
|
7
|
+
export declare const TTL_MS: number;
|
|
8
|
+
/** A write waiting for the user. Holds the tool name and its arguments, not a client. */
|
|
9
|
+
type Pending = {
|
|
10
|
+
tool: string;
|
|
11
|
+
/** Whether it may be taken without the card's nonce (mode prefer). */
|
|
12
|
+
allowChat: boolean;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
preview: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
nonce: Buffer;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
};
|
|
19
|
+
/** Who may approve: the stdio user, or the holder of the same bearer token. */
|
|
20
|
+
export declare const subjectOf: (token: string | undefined) => string;
|
|
21
|
+
export type Proposal = {
|
|
22
|
+
id: string;
|
|
23
|
+
nonce: string;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
};
|
|
26
|
+
export declare function propose(entry: Omit<Pending, "nonce" | "expiresAt">, now?: number): Proposal;
|
|
27
|
+
export declare class ApprovalError extends Error {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Take a pending write out (single use). With `nonce`, it must match: the
|
|
31
|
+
* card path. Without, only entries that allow the chat (mode prefer) may.
|
|
32
|
+
*/
|
|
33
|
+
export declare function take(id: string, subject: string, nonce: string | undefined, now?: number): Pending;
|
|
34
|
+
/** Forget every pending write (tests). */
|
|
35
|
+
export declare function clearPending(): void;
|
|
36
|
+
/**
|
|
37
|
+
* The approval card. Shows the server's preview (never HTML from the
|
|
38
|
+
* model or the tenant: text only), and Approve / Reject.
|
|
39
|
+
*/
|
|
40
|
+
export declare function cardHtml(): string;
|
|
41
|
+
export {};
|