mc8yp 2.3.3 → 2.4.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/README.md CHANGED
@@ -1,186 +1,203 @@
1
- # mc8yp - Full Cumulocity API Access for AI Agents
1
+ # mc8yp — Cumulocity API access for AI agents
2
2
 
3
3
  ![Version](https://img.shields.io/npm/v/mc8yp)
4
4
  ![License](https://img.shields.io/npm/l/mc8yp)
5
5
  ![Node Version](https://img.shields.io/node/v/mc8yp)
6
6
 
7
- mc8yp is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that gives AI agents access to the **full Cumulocity API surface** through a compact code-mode interface.
7
+ mc8yp is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that gives AI agents access to the **full Cumulocity API surface** through just two code-mode tools instead of a huge fixed tool inventory:
8
8
 
9
- It supports the two bundled Cumulocity API families exposed by this project:
9
+ - **`query`** — inspect the OpenAPI specs available on the current tenant
10
+ - **`execute`** — call the live Cumulocity API
10
11
 
11
- - **Core API**
12
- - **DTM API**
12
+ The agent sees not only the bundled **Core** and **DTM** specs, but **any microservice installed on the tenant** that declares an OpenAPI spec in its manifest — mc8yp discovers those live and exposes them alongside the bundled ones. No code changes or rebuild required to support a new service.
13
13
 
14
- Instead of limiting agents to a small fixed set of prebuilt tools, mc8yp gives them broad access to Cumulocity through two code-mode tools:
14
+ Operators stay in control through per-connection **restrictions** and **allow rules**, so the same broad capability can be deployed as a read-only agent, a non-destructive production agent, or anything in between.
15
15
 
16
- - `query` — inspect the bundled Core + DTM OpenAPI specs
17
- - `execute` — call the live Cumulocity API
18
-
19
- The result is an MCP integration where agents can work across the broader Cumulocity platform, while operators still keep **fine-grained control** over what is actually allowed at runtime.
20
-
21
- mc8yp is available in two modes:
22
-
23
- - **Cumulocity microservice mode** for production use with [AI Agent Manager](https://cumulocity.com/docs/ai/aim-introduction/)
24
- - **CLI mode** for local debugging, testing, and development
16
+ ## How it works
25
17
 
26
- ## Why mc8yp
18
+ 1. mc8yp discovers every microservice installed on the tenant that declares an OpenAPI spec, and exposes those alongside the bundled Core (+ DTM) specs.
19
+ 2. The agent uses `query` to inspect the available specs and pick an endpoint.
20
+ 3. The agent uses `execute` to call the live Cumulocity API.
21
+ 4. mc8yp enforces configured restrictions and allow rules before the request leaves the host.
27
22
 
28
- ### Full API power for agents
23
+ ### Live microservice API discovery
29
24
 
30
- mc8yp is built to give agents access to the **complete Cumulocity API surface available through the bundled Core and DTM specs**, instead of a tiny curated subset of actions.
25
+ When a tenant is active, mc8yp asks Cumulocity which applications the tenant is subscribed to, reads the `openApiSpec` declaration from each application manifest, fetches the spec, prefixes its paths with the service's `contextPath`, and exposes it to the sandbox as `serviceSpecs[contextPath]`. Results are cached per tenant for 30 minutes.
31
26
 
32
- That means agents are not blocked just because a specific endpoint was never wrapped as a custom MCP tool.
27
+ The practical effect: **any Cumulocity microservice that ships an OpenAPI spec is automatically usable by the agent**, whether it is one of the bundled snapshots, a Cumulocity-provided service, or a custom microservice built in-house. The bundled specs are just guaranteed offline coverage; the discovery layer fills in everything else.
33
28
 
34
- ### Built for AI Agent Manager first
29
+ When a new service is subscribed mid-session, CLI agents can call the `status` tool with `refresh: true` to bust the cache without waiting for the 30-minute window. Server mode does not yet expose an in-protocol refresh trigger — use the `POST /refresh-apis` HTTP route from ops/CI scripts that sit outside the MCP protocol.
35
30
 
36
- The primary production deployment model is **Cumulocity microservice mode**.
31
+ ## Two ways to run it
37
32
 
38
- Deploy mc8yp as a Cumulocity microservice and expose `/mcp` to **AI Agent Manager**, so agents can use broad Cumulocity API capabilities inside the platform.
33
+ - **Microservice mode** (recommended for production) — deploy inside Cumulocity IoT, expose `/mcp`, integrate with [AI Agent Manager](https://cumulocity.com/docs/ai/aim-introduction/). Auth comes from the request and the service user.
34
+ - **CLI mode** (local development) — run locally over stdio with an MCP client such as Claude Desktop. Credentials are stored in the OS keyring.
39
35
 
40
- ### Full power, controlled access
36
+ ---
41
37
 
42
- Broad capability does **not** have to mean unrestricted access.
38
+ ## Quick start — Microservice (recommended)
43
39
 
44
- mc8yp lets you constrain live API usage with:
40
+ 1. Download the latest release zip from [GitHub Releases](https://github.com/schplitt/mc8yp/releases).
41
+ 2. Upload the `.zip` in Cumulocity **Application Management**.
42
+ 3. Subscribe the application in your tenant.
43
+ 4. Point your agent at:
45
44
 
46
- - **restrictions** to deny specific methods or paths
47
- - **allow rules** to define an allow-list
48
- - **bundled OpenAPI disablement** for selected API families
49
- - **sandboxed execution** and a tenant-host network boundary
50
- - normal **Cumulocity permissions** from the authenticated user or service user
45
+ ```txt
46
+ https://<tenant>.cumulocity.com/service/mc8yp-server/mcp
47
+ ```
51
48
 
52
- This makes setups like these possible:
49
+ No extra credential setup is required — the microservice uses Cumulocity's deployment environment and request authentication.
53
50
 
54
- - **read-only agents**
55
- - **non-destructive production agents**
56
- - agents limited to **inventory**, **alarms**, or other selected API families
57
- - agents allowed to write only to a small approved set of endpoints
51
+ The microservice manifest declares `exposeMcpServers`, so once the application is subscribed it auto-registers with AI Agent Manager as an MCP server at `/service/mc8yp-server/mcp` (with the user's authentication forwarded). No manual MCP server entry is needed in AI Agent Manager.
58
52
 
59
- ### Token efficiency comes from the small MCP surface
53
+ **Example: read-only production agent** (allow only safe GETs):
60
54
 
61
- The agent gets broad API reach without requiring a huge fixed tool inventory. Instead of many endpoint-specific tools, mc8yp keeps the MCP surface compact and lets the model reason over the bundled OpenAPI specs.
55
+ ```txt
56
+ /mcp?allow=GET:/inventory/**&allow=GET:/alarm/**&allow=GET:/measurement/**
57
+ ```
62
58
 
63
- ## How it works
59
+ Or via headers:
64
60
 
65
- 1. The agent uses `query` to inspect the bundled Cumulocity OpenAPI specs.
66
- 2. The agent decides which Core or DTM endpoint it needs.
67
- 3. The agent uses `execute` to call the live Cumulocity API.
68
- 4. mc8yp enforces configured restrictions and allow rules before sending the request.
61
+ ```http
62
+ POST /mcp HTTP/1.1
63
+ mc8yp-allow: GET:/inventory/**
64
+ mc8yp-allow: GET:/alarm/**
65
+ mc8yp-allow: GET:/measurement/**
66
+ ```
69
67
 
70
- ## Deployment Modes
68
+ See [Access policy](#access-policy) for the full rule syntax.
71
69
 
72
- ### 1. Cumulocity Microservice Mode (recommended)
70
+ ---
73
71
 
74
- Designed for deployment inside **Cumulocity IoT**.
72
+ ## Quick start — Local CLI
75
73
 
76
- In this mode, mc8yp exposes an HTTP MCP endpoint at `/mcp` and is intended for use with **AI Agent Manager**.
74
+ ### Platform support
77
75
 
78
- - deploy through Cumulocity microservice packaging
79
- - integrate with [AI Agent Manager](https://cumulocity.com/docs/ai/aim-introduction/)
80
- - use the service user's permissions automatically
81
- - configure per-connection MCP policy with restrictions, allow rules, and bundled OpenAPI disablement
76
+ | Platform | Supported | Notes |
77
+ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
78
+ | macOS | ✅ native | Keychain is used for credentials |
79
+ | Linux | ✅ native | Secret Service (libsecret) is used for credentials |
80
+ | Windows | ❌ | Use [WSL 2](https://learn.microsoft.com/windows/wsl/) (see [WSL 2 one-time setup](#wsl-2-one-time-setup) below) or the microservice mode instead |
82
81
 
83
- ### 2. CLI Mode (local development)
82
+ The sandboxed V8 runtime ([`@iso4/sandbox`](https://www.npmjs.com/package/@iso4/sandbox)) communicates with a Rust subprocess over Unix domain sockets, which is why native Windows is not supported.
84
83
 
85
- > **Platform requirement:** CLI mode requires **macOS or Linux**. The sandboxed V8 runtime
86
- > (`@iso4/sandbox`) communicates with a Rust subprocess over Unix domain sockets, which are not
87
- > available on Windows. If you are on Windows, use
88
- > [WSL 2](https://learn.microsoft.com/windows/wsl/) or connect to a
89
- > [Cumulocity microservice deployment](#1-cumulocity-microservice-mode-recommended) instead.
84
+ ### Install and run
90
85
 
91
- CLI mode is ideal for:
86
+ ```sh
87
+ # Run directly (recommended)
88
+ pnpm dlx mc8yp
92
89
 
93
- - local debugging
94
- - testing agent prompts and workflows
95
- - validating access-policy setups before deployment
96
- - working with MCP clients such as Claude Desktop
90
+ # Pick a specific bundled core OpenAPI build for `query`
91
+ pnpm dlx mc8yp --spec 2025
97
92
 
98
- Credentials are stored in your operating system's secure credential manager.
93
+ # Or install globally
94
+ npm install -g mc8yp
95
+ mc8yp
96
+ ```
99
97
 
100
- ## Quick Start: AI Agent Manager / Microservice
98
+ ### Add credentials
101
99
 
102
- 1. Download the latest release package from [GitHub Releases](https://github.com/schplitt/mc8yp/releases)
103
- 2. Upload the `.zip` in **Application Management**
104
- 3. Subscribe the application in your tenant
105
- 4. Connect your agent workflow to:
100
+ `mc8yp creds add` prompts for tenant URL, username, and a masked password, and writes them to the OS keyring.
106
101
 
107
- ```txt
108
- https://<tenant>.cumulocity.com/service/mc8yp-server/mcp
102
+ ```sh
103
+ pnpm dlx mc8yp creds add # add credentials (interactive, masked password)
104
+ pnpm dlx mc8yp creds list # list stored credentials
105
+ pnpm dlx mc8yp creds remove # remove stored credentials
109
106
  ```
110
107
 
111
- No extra tenant credential setup is required in microservice mode. The microservice uses Cumulocity's deployment environment and request authentication model.
108
+ On macOS and standard desktop Linux this works out of the box. On **WSL 2** the keyring stack is not wired up by default and needs a one-time bootstrap:
112
109
 
113
- ### Example: production-safe read-only microservice connection
110
+ <details>
111
+ <summary><strong>WSL 2 one-time setup</strong> — required before <code>mc8yp creds add</code> works on WSL</summary>
114
112
 
115
- You can expose broad API knowledge to the agent while allowing only safe read access at runtime.
113
+ A fresh WSL 2 distro has no Secret Service provider, no session D-Bus, and no `login` keyring collection, so `@napi-rs/keyring` (used by `mc8yp creds add`) has nothing to talk to. On a normal desktop Linux all of this is wired up automatically by the display manager and PAM; on WSL you have to do it once manually.
116
114
 
117
- Example MCP endpoint configuration patterns:
115
+ **1. Inside WSL, install the keyring stack:**
118
116
 
119
- ```txt
120
- /mcp?allow=GET:/inventory/**&allow=GET:/alarm/**&allow=GET:/measurement/**
117
+ ```sh
118
+ sudo apt install -y libsecret-tools dbus-x11
119
+ sudo apt install -y libpam-gnome-keyring
121
120
  ```
122
121
 
123
- Or with headers:
122
+ - `libsecret-tools` provides `secret-tool` and pulls in `libsecret` (the client library `@napi-rs/keyring` uses).
123
+ - `dbus-x11` provides `dbus-launch` so a session D-Bus can be started in a headless shell.
124
+ - `libpam-gnome-keyring` installs `gnome-keyring-daemon` (the actual Secret Service provider) and its PAM module.
124
125
 
125
- ```http
126
- POST /mcp HTTP/1.1
127
- mc8yp-allow: GET:/inventory/**
128
- mc8yp-allow: GET:/alarm/**
129
- mc8yp-allow: GET:/measurement/**
126
+ **2. Force the keyring database to initialize:**
127
+
128
+ ```sh
129
+ secret-tool store --label="init" init init
130
130
  ```
131
131
 
132
- ## Quick Start: Local CLI
132
+ A throwaway write so `gnome-keyring-daemon` creates its on-disk store.
133
+
134
+ **3. Wire up PAM so the keyring auto-unlocks at login:**
133
135
 
134
136
  ```sh
135
- # Run directly (recommended)
136
- pnpm dlx mc8yp
137
+ sudo bash -c 'cat >> /etc/pam.d/login <<EOF
138
+ auth optional pam_gnome_keyring.so
139
+ session optional pam_gnome_keyring.so auto_start
140
+ EOF'
141
+ ```
137
142
 
138
- # Pick a specific bundled OpenAPI build for query
139
- pnpm dlx mc8yp --spec 2025
143
+ **4. From PowerShell, fully restart WSL so PAM picks up the new config:**
140
144
 
141
- # Or install globally
142
- npm install -g mc8yp
143
- mc8yp
145
+ ```powershell
146
+ wsl --shutdown
144
147
  ```
145
148
 
146
- ### Credential Storage
147
-
148
- The interactive `mc8yp creds add` flow uses masked password input and stores credentials in your operating system's secure credential manager.
149
+ **5. Back in WSL, start a session D-Bus (WSL doesn't get one by default):**
149
150
 
150
- - **macOS**: Keychain
151
- - **Windows**: Credential Vault
152
- - **Linux**: Secret Service API (libsecret)
151
+ ```sh
152
+ echo $DBUS_SESSION_BUS_ADDRESS # should be empty
153
+ eval $(dbus-launch --sh-syntax)
154
+ ```
153
155
 
154
- ### Managing Credentials
156
+ **6. Create the `login` collection that libsecret writes into.** On a normal desktop this is created by the graphical login session; on WSL it does not exist and credential writes will fail without it:
155
157
 
156
158
  ```sh
157
- # Add credentials (prompts for tenant URL, username, and a masked password)
158
- pnpm dlx mc8yp creds add
159
+ gdbus call --session \
160
+ --dest org.freedesktop.secrets \
161
+ --object-path /org/freedesktop/secrets \
162
+ --method org.freedesktop.Secret.Service.OpenSession \
163
+ "plain" \
164
+ "<''>"
165
+
166
+ gdbus call --session \
167
+ --dest org.freedesktop.secrets \
168
+ --object-path /org/freedesktop/secrets \
169
+ --method org.freedesktop.Secret.Service.CreateCollection \
170
+ "{'org.freedesktop.Secret.Collection.Label': <'login'>}" \
171
+ ""
172
+ ```
159
173
 
160
- # List stored credentials
161
- pnpm dlx mc8yp creds list
174
+ **7. Trigger the keyring passphrase prompt once:**
162
175
 
163
- # Remove stored credentials
164
- pnpm dlx mc8yp creds remove
176
+ ```sh
177
+ secret-tool store --label="test" service myservice username myuser
165
178
  ```
166
179
 
167
- ### Active Tenant Flow
180
+ This opens a prompt to set the keyring passphrase. You can leave it **empty** — the keyring will then auto-unlock without prompting later, which is what you want for headless WSL.
168
181
 
169
- Adding credentials does not automatically activate a tenant. CLI sessions only run `query` against live tenant data and `execute` against the live API once a tenant has been selected via the `set-active-tenant` MCP tool.
182
+ After this, `mc8yp creds add` will work.
170
183
 
171
- First-time setup an agent will perform once the MCP client is connected:
184
+ </details>
172
185
 
173
- 1. Call `cli-status` to see stored credentials and the current active tenant.
174
- 2. Call `set-active-tenant` with one of the tenant URLs from `cli-status`. The selection is written to `~/.config/mc8yp/active-tenant.json` and re-applied automatically on every subsequent CLI start.
175
- 3. Call `query` and `execute` as needed. The query footer and execute marker keep the active tenant visible on every result.
186
+ ### Activate a tenant
176
187
 
177
- To switch tenants mid-session, call `set-active-tenant` again with the new URL. To deliberately stop working against any tenant and just browse the bundled OpenAPI snapshots, call `set-active-tenant` with `tenantUrl: null`. In that state `query` continues to work against every bundled spec and `execute` returns a missing-auth error — so an agent cannot accidentally hit a tenant it has not selected.
188
+ Adding credentials does **not** auto-activate a tenant. `execute` only runs against a live tenant once one has been selected, and the agent does that itself through MCP tools:
178
189
 
179
- If the stored credentials for the active tenant are removed (for example by `mc8yp creds remove`), the next `cli-status` call — or the next CLI restart — detects the drift and automatically resets the active tenant to `(none)`, preventing stale auth headers from going on the wire.
190
+ 1. The agent calls `status` to see stored credentials, the current active tenant, and the specs currently visible to `query`.
191
+ 2. The agent calls `set-active-tenant` with one of the tenant URLs. The selection is written to `~/.config/mc8yp/active-tenant.json` and reused across CLI restarts.
192
+ 3. The agent runs `query` and `execute` as needed. Each result includes a footer or marker line showing which tenant it ran against.
180
193
 
181
- ### Connecting a Local MCP Client
194
+ To switch tenants, call `set-active-tenant` again. To stop targeting any tenant (browse bundled specs only), call it with `tenantUrl: null` — `query` keeps working, `execute` returns a missing-auth error so the agent cannot accidentally hit a tenant.
182
195
 
183
- For Claude Desktop or any MCP client, add:
196
+ If the active tenant's credentials are removed via `mc8yp creds remove`, the next `status` call clears the active tenant automatically.
197
+
198
+ ### Connect a local MCP client
199
+
200
+ For Claude Desktop or any stdio MCP client:
184
201
 
185
202
  ```json
186
203
  {
@@ -194,7 +211,7 @@ For Claude Desktop or any MCP client, add:
194
211
  }
195
212
  ```
196
213
 
197
- Example with read-only access rules:
214
+ With read-only access rules:
198
215
 
199
216
  ```json
200
217
  {
@@ -205,67 +222,33 @@ Example with read-only access rules:
205
222
  "args": [
206
223
  "dlx",
207
224
  "mc8yp",
208
- "-a",
209
- "GET:/inventory/**",
210
- "-a",
211
- "GET:/alarm/**",
212
- "-a",
213
- "GET:/measurement/**"
225
+ "-a", "GET:/inventory/**",
226
+ "-a", "GET:/alarm/**",
227
+ "-a", "GET:/measurement/**"
214
228
  ]
215
229
  }
216
230
  }
217
231
  }
218
232
  ```
219
233
 
220
- ## Bundled OpenAPI Coverage
221
-
222
- The `query` tool exposes the bundled OpenAPI snapshots included by this project:
223
-
224
- - **Core** snapshots: `release`, `2026`, `2025`, and `2024`
225
- - **DTM** snapshot: bundled alongside each supported core build
226
-
227
- In CLI mode, use `--spec` or `-s` to choose which bundled **core** OpenAPI snapshot `query` exposes:
228
-
229
- ```sh
230
- # Default: latest bundled release build
231
- mc8yp
232
-
233
- # Explicitly use the 2025 bundled build
234
- mc8yp --spec 2025
235
-
236
- # Short form
237
- mc8yp -s 2024
238
- ```
239
-
240
- This only changes the bundled OpenAPI data that `query` sees. The `execute` tool still calls the live Cumulocity API of the selected tenant or deployed service environment.
241
-
242
- ## Tools & Prompts
243
-
244
- ### Tools
245
-
246
- | Tool | Description |
247
- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
248
- | `query` | Search and inspect the bundled and discovered OpenAPI specs by running a JavaScript function expression. The sandbox exposes `coreSpec` and `serviceSpecs` (microservice APIs keyed by `contextPath`). |
249
- | `execute` | Execute JavaScript against the live Cumulocity API. Provide an async JavaScript function expression. A top-level `cumulocity` binding provides `cumulocity.request({ method, path, body?, headers? })`. Return the final value from that function. |
250
- | `cli-status` | _(CLI mode only)_ Read the active tenant (or note that none is set) and the list of stored credentials from your system keyring. Auto-clears the active tenant if its credentials have been removed. Call this before `query` / `execute` so you know which tenant they will hit. |
251
- | `set-active-tenant` | _(CLI mode only)_ Select the tenant `query` and `execute` operate against, persisted to `~/.config/mc8yp/active-tenant.json` across CLI restarts. Pass `tenantUrl: null` to clear the selection and fall back to browsing the bundled OpenAPI snapshots. |
252
-
253
- Both code-mode tools run in a sandboxed V8 runtime ([@iso4/sandbox](https://github.com/schplitt/iso4)) hosted in a separate Rust subprocess.
234
+ ---
254
235
 
255
- - `query` returns JSON text for easier inspection of OpenAPI data. Every result ends with a footer line naming the active tenant (or noting there is none) so the agent can verify which tenant the visible specs reflect.
256
- - `execute` returns the successful function result in [Toon format](https://github.com/nicepkg/toon). If execution is blocked or fails, it returns a plain text message instead. In CLI mode every `execute` result is prefixed with an `Executed against tenant: <url>` marker line so the active tenant is always visible — the active tenant is global to a CLI session and can be flipped between calls by `set-active-tenant`.
236
+ ## Tools and prompts
257
237
 
258
- ### Prompts
238
+ | Tool | Description |
239
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `query` | Inspect the bundled and discovered OpenAPI specs by running a JavaScript function expression in a sandbox. Exposes `coreSpec` and `serviceSpecs` (keyed by `contextPath`). Returns JSON text. |
241
+ | `execute` | Run an async JavaScript function expression that calls the live Cumulocity API via `cumulocity.request({ method, path, body?, headers? })`. Returns the function result in [Toon format](https://github.com/nicepkg/toon). |
242
+ | `status` | _(CLI only)_ Show the active tenant, stored credentials, and the specs currently visible to `query`. Auto-clears the active tenant if its credentials are gone. Pass `refresh: true` to bust the 30-minute discovery cache and re-run discovery for the active tenant — useful right after (un)subscribing a microservice. Noop when no tenant is active. |
243
+ | `set-active-tenant` | _(CLI only)_ Select the tenant `query` and `execute` operate against. Pass `tenantUrl: null` to clear. |
259
244
 
260
- | Prompt | Description |
261
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
262
- | `code-mode-guide` | Full reference for the `query` and `execute` tools, including available types, examples, and access-policy info for the current connection. |
245
+ Both code-mode tools run in a sandboxed V8 runtime ([`@iso4/sandbox`](https://github.com/schplitt/iso4)) hosted in a separate Rust subprocess. The sandbox has no `fetch` global — the only path to the tenant is the host-bridged `cumulocity.request` helper, which is DNS-pinned and SSRF-hardened via [`@iso4/fetch`](https://www.npmjs.com/package/@iso4/fetch).
263
246
 
264
- ## Execute Input Shape
247
+ The **`code-mode-guide`** prompt contains the full reference for `query` and `execute`, including types, examples, and the active access policy for the current connection.
265
248
 
266
- The `execute` tool expects an async function expression, not module source with `export default`.
249
+ ### `execute` input shape
267
250
 
268
- Recommended shape:
251
+ `execute` expects an async function expression:
269
252
 
270
253
  ```js
271
254
  async () => {
@@ -276,7 +259,7 @@ async () => {
276
259
  }
277
260
  ```
278
261
 
279
- You can also perform intermediate processing before returning the final value:
262
+ You can do intermediate work before returning:
280
263
 
281
264
  ```js
282
265
  async () => {
@@ -285,63 +268,35 @@ async () => {
285
268
  path: '/inventory/managedObjects?pageSize=20&withTotalPages=true',
286
269
  })
287
270
 
288
- return devices.managedObjects?.map((device) => ({ id: device.id, name: device.name }))
271
+ return devices.managedObjects?.map((d) => ({ id: d.id, name: d.name }))
289
272
  }
290
273
  ```
291
274
 
292
- ## API Access Policy
275
+ ---
293
276
 
294
- mc8yp supports two per-connection rule types:
295
-
296
- - **Restrictions** — deny rules that block matching API operations
297
- - **Allow rules** — allow-list rules that permit matching API operations and block everything else when at least one allow rule is configured
298
-
299
- If both apply to the same operation, **restrictions take priority**.
300
-
301
- This is what makes it possible to expose broad API capability while still keeping an agent in a **read-only** or otherwise **non-destructive** operating mode.
302
-
303
- Example: allowing `/inventory/**` but restricting `/inventory/managedObjects` still blocks `/inventory/managedObjects`.
277
+ ## Access policy
304
278
 
305
- Both rule types use the same syntax.
306
-
307
- ### Restrictions
308
-
309
- Restrictions are deny rules that block specific API operations.
310
-
311
- ### Allow Rules
279
+ mc8yp supports two per-connection rule types:
312
280
 
313
- Allow rules are the inverse of restrictions. They define what is permitted. When one or more allow rules are configured, any operation that does not match at least one allow rule is blocked.
281
+ - **Restrictions** — deny rules that block matching API operations.
282
+ - **Allow rules** — allow-list rules. When at least one allow rule is set, anything not matching is blocked.
314
283
 
315
- ### Rule Format
284
+ If both apply to the same operation, **restrictions win**. This is how you expose broad API knowledge while still running an agent in a read-only or otherwise constrained mode.
316
285
 
317
- A restriction or allow rule can be written in either of these forms:
286
+ ### Rule format
318
287
 
319
288
  ```txt
320
289
  <path-pattern>
321
290
  <method>:<path-pattern>
322
291
  ```
323
292
 
324
- - **Without a method prefix** — matches all HTTP methods for matching paths
325
- - **With a method prefix** — matches only that method (for example `GET:`, `DELETE:`, `POST:`)
326
- - **The `:` separator is only present when a method prefix is provided**
327
- - **Supported methods** — `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, `PUT`, `TRACE`, or `*`
328
- - Method names are case-insensitive when parsed (`get:/inventory/**` becomes `GET:/inventory/**`)
293
+ - No method prefix → matches all HTTP methods.
294
+ - With a method prefix → only that method. Supported: `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, `PUT`, `TRACE`, or `*`. Case-insensitive.
295
+ - Patterns must start with `/`. Query strings and fragments are not allowed in patterns.
296
+ - Wildcards: `*` matches within a single path segment; `**` matches zero or more whole segments and must be its own segment.
329
297
 
330
- ### Path Pattern Syntax
331
-
332
- Patterns are matched against the request **pathname**.
333
-
334
- - Query strings and fragments are **not allowed in rule patterns**
335
- - Incoming request query strings are ignored for matching, so `/inventory/**` also matches requests such as `/inventory?pageSize=5`
336
- - Patterns must start with `/`
337
- - Matching is path-segment aware: `/` separates segments
338
-
339
- Supported wildcards:
340
-
341
- - `*` — wildcard **inside a single path segment**. It matches any characters except `/`
342
- - `**` — recursive wildcard across **zero or more whole path segments**. `**` must be its own complete segment
343
-
344
- ### Path Pattern Examples
298
+ <details>
299
+ <summary><strong>Path pattern examples</strong></summary>
345
300
 
346
301
  | Pattern | Matches | Does Not Match |
347
302
  | --------------------- | ----------------------------------------------------------- | ------------------------------------------ |
@@ -353,160 +308,103 @@ Supported wildcards:
353
308
  | `/inventory/*/child` | `/inventory/device-1/child`, `/inventory/x/child` | `/inventory/child`, `/inventory/a/b/child` |
354
309
  | `/inventory/**/child` | `/inventory/child`, `/inventory/a/b/child` | `/inventory/a/b/sibling` |
355
310
 
356
- ### Common Rule Examples
311
+ Notes:
312
+
313
+ - `/inventory/**` already matches `/inventory` itself.
314
+ - `/i**` is invalid — `**` must be its own segment. Use `/i*/**` instead.
315
+ - Rule patterns may not contain `//`, `.`, `..`, query strings, or fragments.
316
+
317
+ </details>
318
+
319
+ <details>
320
+ <summary><strong>Common rule examples</strong></summary>
357
321
 
358
322
  | Rule | Restriction Effect | Allow-list Effect |
359
323
  | -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
360
- | `/inventory/**` | Block all methods on `/inventory` and everything below it | Permit all methods on `/inventory` and everything below it |
361
- | `DELETE:/inventory/**` | Block only DELETE on `/inventory` and everything below it | Permit only DELETE on `/inventory` and everything below it |
324
+ | `/inventory/**` | Block all methods on `/inventory` and below | Permit all methods on `/inventory` and below |
325
+ | `DELETE:/inventory/**` | Block only DELETE on `/inventory` and below | Permit only DELETE on `/inventory` and below |
362
326
  | `/alarm/alarms` | Block all methods on the exact path `/alarm/alarms` | Permit all methods on the exact path `/alarm/alarms` |
363
327
  | `GET:/measurement/measurements` | Block only GET on the exact path `/measurement/measurements` | Permit only GET on the exact path `/measurement/measurements` |
364
328
  | `POST:/inventory/managedObjects` | Block creating new managed objects | Permit creating new managed objects |
365
- | `/i*/**` | Block all routes whose first path segment starts with `i` | Permit all routes whose first path segment starts with `i` |
366
329
  | `/user/**` | Block all user management paths | Permit all user management paths |
367
330
 
368
- ### Important Notes
331
+ </details>
369
332
 
370
- - `/inventory/**` already matches `/inventory` itself, so you do **not** need both `/inventory` and `/inventory/**`
371
- - `/i**` is **not valid** because `**` must be its own segment. Use `/i*/**` if you want to match a first segment starting with `i` and everything below it
372
- - `*:/inventory/**` is allowed and means the same thing as `/inventory/**`
373
- - Root paths across the bundled Core and DTM specs are intentionally treated as disjoint, so path-based restriction and allow rules are enough for request enforcement
374
- - Rule patterns may not contain empty segments (`//`), `.` or `..` segments, query strings, or fragments
333
+ ### CLI usage
375
334
 
376
- ### CLI Mode
377
-
378
- Pass restrictions and allow rules as CLI arguments.
379
-
380
- Repeat `-r`, `--restrict`, or `--restriction` for deny rules:
335
+ Repeat `-r`, `--restrict`, or `--restriction` for deny rules; `-a`, `--allow`, or `--allowed` for allow rules:
381
336
 
382
337
  ```sh
383
- # Block all inventory access
384
- mc8yp -r "/inventory/**"
385
-
386
- # Block deletes on inventory and all alarm access
338
+ # Block all inventory writes and all alarm access
387
339
  mc8yp -r "DELETE:/inventory/**" -r "/alarm/**"
388
340
 
389
- # Same thing using the long alias
390
- mc8yp --restrict "/user/**"
391
- ```
392
-
393
- Repeat `-a`, `--allow`, or `--allowed` for allow rules:
394
-
395
- ```sh
396
- # Only permit inventory access
397
- mc8yp -a "/inventory/**"
398
-
399
- # Permit GET inventory access and POST alarms
341
+ # Only permit GET inventory + POST alarms
400
342
  mc8yp --allow "GET:/inventory/**" --allowed "POST:/alarm/**"
401
343
 
402
- # Allow inventory broadly, but still block one path with a restriction
344
+ # Allow inventory broadly, but still block one path
403
345
  mc8yp -a "/inventory/**" -r "/inventory/managedObjects"
404
346
  ```
405
347
 
406
- ### Microservice Mode (HTTP)
348
+ ### Microservice usage (HTTP)
407
349
 
408
- Pass restrictions as `restriction`, `restrict`, or `r` query parameters on the MCP endpoint URL.
409
- Pass allow rules as `allowed`, `allow`, or `a` query parameters.
410
- You can also send project-scoped HTTP headers to avoid conflicts with well-known headers:
350
+ Use query parameters or project-scoped headers on the `/mcp` endpoint:
411
351
 
412
- - `mc8yp-restriction` for deny rules
413
- - `mc8yp-allow` for allow-list rules
352
+ - Deny rules: `restriction`, `restrict`, or `r` query params, or `mc8yp-restriction` header.
353
+ - Allow rules: `allowed`, `allow`, or `a` query params, or `mc8yp-allow` header.
414
354
 
415
- Both headers accept either repeated header instances or a comma-separated list of values. Query parameters and headers can be combined on the same connection.
355
+ Both headers accept either repeated header instances or a comma-separated list. Query parameters and headers can be combined.
416
356
 
417
357
  ```txt
418
- /mcp?restriction=/inventory/**&restrict=DELETE:/alarm/**
419
- /mcp?r=/inventory/**&r=DELETE:/alarm/**
420
- /mcp?allow=/inventory/**&allowed=POST:/alarm/**
358
+ /mcp?r=/inventory/**&r=DELETE:/alarm/**&allow=GET:/measurement/**
421
359
  ```
422
360
 
423
361
  ```http
424
362
  POST /mcp HTTP/1.1
425
363
  Authorization: Bearer <token>
426
364
  mc8yp-restriction: /inventory/**
427
- mc8yp-restriction: DELETE:/alarm/**
428
365
  mc8yp-allow: GET:/measurement/**
429
366
  ```
430
367
 
431
- ### How Access Policy Works
432
-
433
- 1. **Query visibility**: The `query` tool exposes resolved OpenAPI specs through `coreSpec` and `serviceSpecs`. With an active tenant, services not installed on that tenant are dropped from the sandbox surface so the agent only sees what is actually reachable. In CLI mode with no active tenant, every bundled snapshot is exposed for reference browsing only — `execute` is unavailable in that state.
434
-
435
- 2. **Request enforcement**: The host-side bridge that backs `cumulocity.request` evaluates restrictions and allow rules before any HTTP request leaves the host. Matching deny rules block first. If any allow rules are configured, requests must also match at least one allow rule. Blocked requests never reach Cumulocity.
436
-
437
- 3. **Network boundary**: The sandbox itself has no `fetch` global. Sandbox code reaches the tenant only through the host-bridged `cumulocity.request` helper, which injects auth, evaluates restriction and allow rules, and issues the live HTTP call via [@iso4/fetch](https://www.npmjs.com/package/@iso4/fetch) (DNS-pinned, SSRF-hardened). Every other network egress is unavailable to the agent.
438
-
439
- When an `execute` request is blocked by MCP connection policy, the tool returns explanatory text stating whether the operation was denied by a restriction or blocked because it is outside the configured allow list, no request was sent to Cumulocity, and retrying through the same connection will not help.
440
-
441
- ## Build And Packaging
442
-
443
- The repository bundles multiple OpenAPI specs for CLI use and builds one microservice server bundle per configured build version.
444
-
445
- ### Build Outputs
446
-
447
- `pnpm build` produces:
368
+ When `execute` is blocked by connection policy, the tool returns explanatory text, no request is sent to Cumulocity, and retrying through the same connection will not help.
448
369
 
449
- - CLI bundle in `dist/`
450
- - Versioned server bundles in `.output/release/`, `.output/2026/`, `.output/2025/`, and `.output/2024/`
370
+ ---
451
371
 
452
- The build matrix is driven by [`openapi-builds.json`](openapi-builds.json). Core snapshots live under `openapi/core/`, DTM snapshots live under `openapi/dtm/`, and each server bundle contains the configured combination for that build.
372
+ ## OpenAPI coverage
453
373
 
454
- ### Release Packaging
374
+ What the agent sees through `query` comes from two layers:
455
375
 
456
- Use the dedicated packaging command after `pnpm build` to create Docker-based Cumulocity release zips:
376
+ 1. **Live-discovered specs** — every microservice subscribed on the active tenant whose manifest declares an `openApiSpec`. Discovered at runtime, cached for 30 minutes per tenant, exposed as `serviceSpecs[contextPath]`. This works for any service, not just the ones bundled here.
377
+ 2. **Bundled snapshots** — shipped with the build so Core and DTM are always available even when discovery hasn't run yet:
378
+ - **Core** snapshots: `release`, `2026`, `2025`, `2024`
379
+ - **DTM** snapshot bundled alongside each supported core build
457
380
 
458
- The packaging step writes a temporary generated Dockerfile under `.c8y/`, copies the selected versioned server bundle into `/app/server/`, and installs production dependencies inside the Linux image with pnpm before copying them into the runtime stage. This avoids cross-platform native optional dependency issues when release artifacts are built on macOS but deployed as `linux/amd64` microservices.
381
+ With an active tenant, services not installed on that tenant are dropped from the sandbox so the agent only sees what is actually reachable.
459
382
 
460
- The deployed HTTP transport uses POST-only streamable HTTP (`GET /mcp` intentionally returns `405`) because some reverse proxies and microservice ingress layers do not keep the optional long-lived SSE notification channel stable enough for reliable MCP tool calls.
383
+ In CLI mode, pick which **core** snapshot `query` exposes:
461
384
 
462
385
  ```sh
463
- pnpm package:microservices
386
+ mc8yp # default: latest bundled release
387
+ mc8yp --spec 2025 # use the 2025 snapshot
388
+ mc8yp -s 2024 # short form
464
389
  ```
465
390
 
466
- That command creates one zip per bundled server variant in the repository root, for example:
467
-
468
- - `mc8yp-core-release-dtm-v1.2.3.zip`
469
- - `mc8yp-core-2026-dtm-v1.2.3.zip`
470
- - `mc8yp-core-2025-dtm-v1.2.3.zip`
471
- - `mc8yp-core-2024-dtm-v1.2.3.zip`
391
+ This only affects the bundled core view. `execute` always hits the live Cumulocity API of the selected tenant or deployed service environment.
472
392
 
473
- The GitHub release workflow uses that packaging command when building tagged releases.
393
+ ---
474
394
 
475
395
  ## Development
476
396
 
477
- ### Prerequisites
478
-
479
- - Node.js ≥24.0.0
480
- - pnpm
481
-
482
- ### Setup
397
+ Requires Node.js ≥ 24 and pnpm.
483
398
 
484
399
  ```sh
485
400
  pnpm install
486
- pnpm lint
487
- pnpm typecheck
488
- pnpm build
489
- ```
490
-
491
- ### Testing
492
-
493
- ```sh
494
- # Run tests
495
- pnpm test:run
496
-
497
- # Run benchmarks
498
- pnpm test:bench
499
- ```
500
-
501
- ### Run Locally From Source
502
-
503
- Build first, then point your MCP client at the compiled CLI:
504
-
505
- ```sh
506
- pnpm build
401
+ pnpm test:run # tests
402
+ pnpm lint:fix # lint with autofix
403
+ pnpm typecheck # tsc --noEmit
404
+ pnpm build # CLI bundle in dist/, server bundles in .output/<version>/
507
405
  ```
508
406
 
509
- Then add to your local MCP client configuration:
407
+ Run locally from source by pointing your MCP client at the built CLI:
510
408
 
511
409
  ```json
512
410
  {
@@ -520,6 +418,26 @@ Then add to your local MCP client configuration:
520
418
  }
521
419
  ```
522
420
 
421
+ <details>
422
+ <summary><strong>Release packaging</strong></summary>
423
+
424
+ ```sh
425
+ pnpm package:microservices
426
+ ```
427
+
428
+ Produces one Docker-based Cumulocity zip per bundled server variant in the repository root, e.g.:
429
+
430
+ - `mc8yp-core-release-dtm-v1.2.3.zip`
431
+ - `mc8yp-core-2026-dtm-v1.2.3.zip`
432
+ - `mc8yp-core-2025-dtm-v1.2.3.zip`
433
+ - `mc8yp-core-2024-dtm-v1.2.3.zip`
434
+
435
+ The packaging step writes a temporary generated Dockerfile under `.c8y/`, copies the selected versioned server bundle into `/app/server/`, and installs production dependencies inside a `linux/amd64` Docker image so the per-platform native binaries of `@iso4/sandbox` resolve correctly. The deployed HTTP transport is POST-only (`GET /mcp` returns `405`) because some reverse proxies and Cumulocity ingress layers do not keep a long-lived SSE channel stable enough for reliable MCP tool calls.
436
+
437
+ The build matrix is driven by [`openapi-builds.json`](openapi-builds.json). Core snapshots live under `openapi/core/`, DTM snapshots under `openapi/dtm/`.
438
+
439
+ </details>
440
+
523
441
  ## License
524
442
 
525
443
  MIT