mc8yp 2.0.0 → 2.1.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 +109 -28
- package/dist/cli.mjs +471 -257
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -154,39 +154,96 @@ async () => {
|
|
|
154
154
|
|
|
155
155
|
### Prompts
|
|
156
156
|
|
|
157
|
-
| Prompt | Description
|
|
158
|
-
| ----------------- |
|
|
159
|
-
| `code-mode-guide` | Full reference for the `query` and `execute` tools, including available types, examples, and
|
|
157
|
+
| Prompt | Description |
|
|
158
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
159
|
+
| `code-mode-guide` | Full reference for the `query` and `execute` tools, including available types, examples, and access-policy info for the current connection. |
|
|
160
160
|
|
|
161
|
-
## API
|
|
161
|
+
## API Access Policy
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
mc8yp supports two per-connection rule types:
|
|
164
|
+
|
|
165
|
+
- **Restrictions** — deny rules that block matching API operations
|
|
166
|
+
- **Allow rules** — allow-list rules that permit matching API operations and block everything else when at least one allow rule is configured
|
|
167
|
+
|
|
168
|
+
If both apply to the same operation, **restrictions take priority**.
|
|
169
|
+
|
|
170
|
+
Example: allowing `/inventory/**` but restricting `/inventory/managedObjects` still blocks `/inventory/managedObjects`.
|
|
171
|
+
|
|
172
|
+
Both rule types use the same syntax.
|
|
173
|
+
|
|
174
|
+
### Restrictions
|
|
175
|
+
|
|
176
|
+
Restrictions are deny rules that block specific API operations.
|
|
177
|
+
|
|
178
|
+
### Allow Rules
|
|
179
|
+
|
|
180
|
+
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.
|
|
164
181
|
|
|
165
182
|
### Rule Format
|
|
166
183
|
|
|
184
|
+
A restriction or allow rule can be written in either of these forms:
|
|
185
|
+
|
|
186
|
+
```txt
|
|
187
|
+
<path-pattern>
|
|
188
|
+
<method>:<path-pattern>
|
|
167
189
|
```
|
|
168
|
-
[METHOD:]<path-pattern>
|
|
169
|
-
```
|
|
170
190
|
|
|
171
|
-
- **Without a method prefix** —
|
|
172
|
-
- **With a method prefix** —
|
|
173
|
-
- **
|
|
174
|
-
-
|
|
191
|
+
- **Without a method prefix** — matches all HTTP methods for matching paths
|
|
192
|
+
- **With a method prefix** — matches only that method (for example `GET:`, `DELETE:`, `POST:`)
|
|
193
|
+
- **The `:` separator is only present when a method prefix is provided**
|
|
194
|
+
- **Supported methods** — `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST`, `PUT`, `TRACE`, or `*`
|
|
195
|
+
- Method names are case-insensitive when parsed (`get:/inventory/**` becomes `GET:/inventory/**`)
|
|
196
|
+
|
|
197
|
+
### Path Pattern Syntax
|
|
198
|
+
|
|
199
|
+
Patterns are matched against the request **pathname**.
|
|
200
|
+
|
|
201
|
+
- Query strings and fragments are **not allowed in rule patterns**
|
|
202
|
+
- Incoming request query strings are ignored for matching, so `/inventory/**` also matches requests such as `/inventory?pageSize=5`
|
|
203
|
+
- Patterns must start with `/`
|
|
204
|
+
- Matching is path-segment aware: `/` separates segments
|
|
205
|
+
|
|
206
|
+
Supported wildcards:
|
|
207
|
+
|
|
208
|
+
- `*` — wildcard **inside a single path segment**. It matches any characters except `/`
|
|
209
|
+
- `**` — recursive wildcard across **zero or more whole path segments**. `**` must be its own complete segment
|
|
175
210
|
|
|
176
|
-
### Examples
|
|
211
|
+
### Path Pattern Examples
|
|
177
212
|
|
|
178
|
-
|
|
|
179
|
-
|
|
|
180
|
-
| `/inventory
|
|
181
|
-
|
|
|
182
|
-
| `/
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
| `/
|
|
213
|
+
| Pattern | Matches | Does Not Match |
|
|
214
|
+
| --------------------- | ----------------------------------------------------------- | ------------------------------------------ |
|
|
215
|
+
| `/inventory` | `/inventory` | `/inventory/managedObjects` |
|
|
216
|
+
| `/inventory/**` | `/inventory`, `/inventory/managedObjects`, `/inventory/x/y` | `/alarm/alarms` |
|
|
217
|
+
| `/i*` | `/inventory`, `/identity`, `/i` | `/inventory/managedObjects` |
|
|
218
|
+
| `/i*/**` | `/inventory`, `/inventory/managedObjects`, `/identity/x` | `/alarm/alarms` |
|
|
219
|
+
| `/inventory/m*` | `/inventory/managedObjects`, `/inventory/measurements` | `/inventory/events`, `/inventory/m/x` |
|
|
220
|
+
| `/inventory/*/child` | `/inventory/device-1/child`, `/inventory/x/child` | `/inventory/child`, `/inventory/a/b/child` |
|
|
221
|
+
| `/inventory/**/child` | `/inventory/child`, `/inventory/a/b/child` | `/inventory/a/b/sibling` |
|
|
222
|
+
|
|
223
|
+
### Common Rule Examples
|
|
224
|
+
|
|
225
|
+
| Rule | Restriction Effect | Allow-list Effect |
|
|
226
|
+
| -------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
|
|
227
|
+
| `/inventory/**` | Block all methods on `/inventory` and everything below it | Permit all methods on `/inventory` and everything below it |
|
|
228
|
+
| `DELETE:/inventory/**` | Block only DELETE on `/inventory` and everything below it | Permit only DELETE on `/inventory` and everything below it |
|
|
229
|
+
| `/alarm/alarms` | Block all methods on the exact path `/alarm/alarms` | Permit all methods on the exact path `/alarm/alarms` |
|
|
230
|
+
| `GET:/measurement/measurements` | Block only GET on the exact path `/measurement/measurements` | Permit only GET on the exact path `/measurement/measurements` |
|
|
231
|
+
| `POST:/inventory/managedObjects` | Block creating new managed objects | Permit creating new managed objects |
|
|
232
|
+
| `/i*/**` | Block all routes whose first path segment starts with `i` | Permit all routes whose first path segment starts with `i` |
|
|
233
|
+
| `/user/**` | Block all user management paths | Permit all user management paths |
|
|
234
|
+
|
|
235
|
+
### Important Notes
|
|
236
|
+
|
|
237
|
+
- `/inventory/**` already matches `/inventory` itself, so you do **not** need both `/inventory` and `/inventory/**`
|
|
238
|
+
- `/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
|
|
239
|
+
- `*:/inventory/**` is allowed and means the same thing as `/inventory/**`
|
|
240
|
+
- Rule patterns may not contain empty segments (`//`), `.` or `..` segments, query strings, or fragments
|
|
186
241
|
|
|
187
242
|
### CLI Mode
|
|
188
243
|
|
|
189
|
-
Pass restrictions as CLI arguments.
|
|
244
|
+
Pass restrictions and allow rules as CLI arguments.
|
|
245
|
+
|
|
246
|
+
Repeat `-r`, `--restrict`, or `--restriction` for deny rules:
|
|
190
247
|
|
|
191
248
|
```sh
|
|
192
249
|
# Block all inventory access
|
|
@@ -197,25 +254,45 @@ mc8yp -r "DELETE:/inventory/**" -r "/alarm/**"
|
|
|
197
254
|
|
|
198
255
|
# Block everything under user management
|
|
199
256
|
mc8yp --restriction "/user/**"
|
|
257
|
+
|
|
258
|
+
# Same thing using the long alias
|
|
259
|
+
mc8yp --restrict "/user/**"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Repeat `-a`, `--allow`, or `--allowed` for allow rules:
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
# Only permit inventory access
|
|
266
|
+
mc8yp -a "/inventory/**"
|
|
267
|
+
|
|
268
|
+
# Permit GET inventory access and POST alarms
|
|
269
|
+
mc8yp --allow "GET:/inventory/**" --allowed "POST:/alarm/**"
|
|
270
|
+
|
|
271
|
+
# Allow inventory broadly, but still block one path with a restriction
|
|
272
|
+
mc8yp -a "/inventory/**" -r "/inventory/managedObjects"
|
|
200
273
|
```
|
|
201
274
|
|
|
202
275
|
### Microservice Mode (HTTP)
|
|
203
276
|
|
|
204
|
-
Pass restrictions as `restriction` query parameters on the MCP endpoint URL
|
|
277
|
+
Pass restrictions as `restriction`, `restrict`, or `r` query parameters on the MCP endpoint URL.
|
|
278
|
+
Pass allow rules as `allowed`, `allow`, or `a` query parameters.
|
|
205
279
|
|
|
206
280
|
```
|
|
207
|
-
/mcp?restriction=/inventory/**&
|
|
281
|
+
/mcp?restriction=/inventory/**&restrict=DELETE:/alarm/**
|
|
282
|
+
/mcp?r=/inventory/**&r=DELETE:/alarm/**
|
|
283
|
+
/mcp?allow=/inventory/**&allowed=POST:/alarm/**
|
|
284
|
+
/mcp?a=/inventory/**&r=/inventory/managedObjects
|
|
208
285
|
```
|
|
209
286
|
|
|
210
|
-
### How
|
|
287
|
+
### How Access Policy Works
|
|
211
288
|
|
|
212
|
-
1. **
|
|
289
|
+
1. **Query visibility**: The `query` tool exposes the raw bundled OpenAPI snapshot for the current MCP connection. It does not annotate or filter operations based on restrictions or allow rules.
|
|
213
290
|
|
|
214
|
-
2. **Sandbox request enforcement**: The `execute` tool checks restrictions inside the generated sandbox request helper, where the actual HTTP method and normalized path are both available. Matching
|
|
291
|
+
2. **Sandbox request enforcement**: The `execute` tool checks restrictions and allow rules inside the generated sandbox request helper, where the actual HTTP method and normalized path are both available. Matching deny rules block first. If any allow rules are configured, requests must also match at least one allow rule.
|
|
215
292
|
|
|
216
293
|
3. **Network boundary**: The secure-exec permission layer independently restricts network access to the configured tenant host. Other network operations are denied.
|
|
217
294
|
|
|
218
|
-
When an `execute` request is blocked by MCP
|
|
295
|
+
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.
|
|
219
296
|
|
|
220
297
|
## Build And Packaging
|
|
221
298
|
|
|
@@ -234,6 +311,10 @@ The versions built are driven by [`openapi-versions.json`](openapi-versions.json
|
|
|
234
311
|
|
|
235
312
|
Use the dedicated packaging command after `pnpm build` to create Docker-based Cumulocity release zips:
|
|
236
313
|
|
|
314
|
+
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.
|
|
315
|
+
|
|
316
|
+
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.
|
|
317
|
+
|
|
237
318
|
```sh
|
|
238
319
|
pnpm package:microservices
|
|
239
320
|
```
|
|
@@ -251,7 +332,7 @@ The GitHub release workflow uses that packaging command when building tagged rel
|
|
|
251
332
|
|
|
252
333
|
### Prerequisites
|
|
253
334
|
|
|
254
|
-
- Node.js ≥
|
|
335
|
+
- Node.js ≥24.0.0
|
|
255
336
|
- pnpm
|
|
256
337
|
|
|
257
338
|
### Setup
|
package/dist/cli.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import { defineTool } from "tmcp/tool";
|
|
|
10
10
|
import * as v from "valibot";
|
|
11
11
|
import { encode } from "@toon-format/toon";
|
|
12
12
|
import { NodeRuntime, createNodeDriver, createNodeRuntimeDriverFactory } from "secure-exec";
|
|
13
|
-
import "ufo";
|
|
14
13
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
15
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
16
15
|
import { useContext } from "unctx";
|
|
@@ -41,7 +40,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
41
40
|
//#endregion
|
|
42
41
|
//#region package.json
|
|
43
42
|
var name = "mc8yp";
|
|
44
|
-
var version = "2.
|
|
43
|
+
var version = "2.1.0";
|
|
45
44
|
var description = "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management";
|
|
46
45
|
//#endregion
|
|
47
46
|
//#region \0virtual:core-openapi
|
|
@@ -85234,7 +85233,9 @@ function createCodeModeGuidePrompt(server) {
|
|
|
85234
85233
|
description: "Guide for the two code-mode tools: query and execute, including available shapes and examples."
|
|
85235
85234
|
}, () => {
|
|
85236
85235
|
const restrictions = server.ctx.custom?.restrictions ?? [];
|
|
85237
|
-
const
|
|
85236
|
+
const allowRules = server.ctx.custom?.allowRules ?? [];
|
|
85237
|
+
const policyLines = [...restrictions.map((rule) => `- deny: \`${rule.source}\``), ...allowRules.map((rule) => `- allow: \`${rule.source}\``)];
|
|
85238
|
+
const restrictionSection = policyLines.length > 0 ? `\n## Current Connection Access Policy\n${policyLines.join("\n")}\n\nThe \`query\` tool still shows the raw bundled OpenAPI spec. These rules are enforced when requests are executed, not by rewriting the spec.\n` : "";
|
|
85238
85239
|
return prompt.message(`# Cumulocity Code Mode
|
|
85239
85240
|
|
|
85240
85241
|
You have exactly two MCP tools available.
|
|
@@ -85247,8 +85248,8 @@ Use \`query\` when you need to inspect the core OpenAPI spec.
|
|
|
85247
85248
|
- Return the exact value you want back from that function
|
|
85248
85249
|
- Sync and async functions are both supported
|
|
85249
85250
|
- Strings are returned as-is; other results are returned as JSON text
|
|
85250
|
-
-
|
|
85251
|
-
-
|
|
85251
|
+
- The \`query\` tool shows the raw bundled OpenAPI spec for the selected snapshot
|
|
85252
|
+
- The current MCP connection may still block \`execute\` requests through deny rules and/or an allow list even when an operation exists in the spec
|
|
85252
85253
|
|
|
85253
85254
|
### Available Shape
|
|
85254
85255
|
\`\`\`ts
|
|
@@ -85296,8 +85297,8 @@ Use \`execute\` when you want to call the real Cumulocity API.
|
|
|
85296
85297
|
- Return the value you want from that function; async functions are usually the right choice here
|
|
85297
85298
|
- On success, the returned value is sent back in Toon format
|
|
85298
85299
|
- If execution is blocked or fails, execute returns a plain text error message
|
|
85299
|
-
- The current MCP connection may reject restricted method/path combinations before network access
|
|
85300
|
-
- If a request is blocked by MCP connection policy, \`execute\` returns an explanatory text message. That is an intentional connection-level restriction, not a Cumulocity API failure, and retrying through the same connection will not help
|
|
85300
|
+
- The current MCP connection may reject restricted method/path combinations before network access, and it may also reject requests that are outside a configured allow list
|
|
85301
|
+
- If a request is blocked by MCP connection policy, \`execute\` returns an explanatory text message. That is an intentional connection-level access restriction, not a Cumulocity API failure, and retrying through the same connection will not help
|
|
85301
85302
|
|
|
85302
85303
|
### Available Shape
|
|
85303
85304
|
\`\`\`ts
|
|
@@ -85363,60 +85364,34 @@ function createPrompts(server) {
|
|
|
85363
85364
|
return [createCodeModeGuidePrompt(server)];
|
|
85364
85365
|
}
|
|
85365
85366
|
//#endregion
|
|
85366
|
-
//#region src/
|
|
85367
|
-
|
|
85368
|
-
|
|
85369
|
-
|
|
85370
|
-
|
|
85371
|
-
|
|
85372
|
-
|
|
85373
|
-
|
|
85374
|
-
"PUT",
|
|
85375
|
-
"TRACE"
|
|
85376
|
-
];
|
|
85377
|
-
const HTTP_METHOD_SET$1 = new Set(HTTP_METHODS);
|
|
85378
|
-
function normalizeRestrictionMatchPath(value) {
|
|
85379
|
-
let raw = value.trim();
|
|
85380
|
-
if (!raw) return "/";
|
|
85381
|
-
raw = raw.startsWith("/") ? raw : `/${raw}`;
|
|
85382
|
-
raw = raw.replace(/\/{2,}/g, "/");
|
|
85383
|
-
return raw.length > 1 && raw.endsWith("/") ? raw.slice(0, -1) : raw || "/";
|
|
85384
|
-
}
|
|
85385
|
-
function normalizeAndValidateRestrictionPath(rawPath) {
|
|
85386
|
-
const segmentPattern = /^[A-Za-z0-9._~*-]+$/;
|
|
85387
|
-
if (rawPath.includes("?") || rawPath.includes("#")) throw new Error(`Restriction pattern "${rawPath}" must not include query strings or fragments.`);
|
|
85388
|
-
if (!rawPath.startsWith("/")) throw new Error("Restriction path pattern must start with \"/\".");
|
|
85389
|
-
const pathPattern = normalizeRestrictionMatchPath(rawPath);
|
|
85390
|
-
const segments = pathPattern === "/" ? [] : pathPattern.slice(1).split("/");
|
|
85391
|
-
for (const segment of segments) {
|
|
85392
|
-
if (segment === "." || segment === "..") throw new Error(`Restriction segment "${segment}" is not allowed.`);
|
|
85393
|
-
if (segment === "**") continue;
|
|
85394
|
-
if (segment.includes("**")) throw new Error(`Invalid restriction segment "${segment}". "**" must be its own path segment.`);
|
|
85395
|
-
if (!segmentPattern.test(segment)) throw new Error(`Restriction segment "${segment}" contains unsupported characters.`);
|
|
85367
|
+
//#region src/codemode/semaphore.ts
|
|
85368
|
+
var AsyncSemaphore = class {
|
|
85369
|
+
#maxConcurrency;
|
|
85370
|
+
#activeCount = 0;
|
|
85371
|
+
#waiters = [];
|
|
85372
|
+
constructor(maxConcurrency) {
|
|
85373
|
+
if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) throw new Error("maxConcurrency must be a positive integer.");
|
|
85374
|
+
this.#maxConcurrency = maxConcurrency;
|
|
85396
85375
|
}
|
|
85397
|
-
|
|
85398
|
-
|
|
85399
|
-
|
|
85400
|
-
|
|
85401
|
-
|
|
85402
|
-
|
|
85403
|
-
|
|
85404
|
-
|
|
85405
|
-
|
|
85406
|
-
|
|
85407
|
-
|
|
85408
|
-
|
|
85409
|
-
|
|
85410
|
-
|
|
85411
|
-
source
|
|
85376
|
+
get activeCount() {
|
|
85377
|
+
return this.#activeCount;
|
|
85378
|
+
}
|
|
85379
|
+
async acquire() {
|
|
85380
|
+
if (this.#activeCount >= this.#maxConcurrency) await new Promise((resolve) => {
|
|
85381
|
+
this.#waiters.push(resolve);
|
|
85382
|
+
});
|
|
85383
|
+
this.#activeCount += 1;
|
|
85384
|
+
let released = false;
|
|
85385
|
+
return () => {
|
|
85386
|
+
if (released) return;
|
|
85387
|
+
released = true;
|
|
85388
|
+
this.#activeCount -= 1;
|
|
85389
|
+
this.#waiters.shift()?.();
|
|
85412
85390
|
};
|
|
85413
85391
|
}
|
|
85414
|
-
|
|
85415
|
-
|
|
85416
|
-
|
|
85417
|
-
source
|
|
85418
|
-
};
|
|
85419
|
-
}
|
|
85392
|
+
};
|
|
85393
|
+
//#endregion
|
|
85394
|
+
//#region src/utils/restriction-matcher.ts
|
|
85420
85395
|
function escapeRestrictionRegex(value) {
|
|
85421
85396
|
let escaped = "";
|
|
85422
85397
|
for (const char of value) escaped += "\\^$.*+?()[]{}|".includes(char) ? `\\${char}` : char;
|
|
@@ -85429,13 +85404,13 @@ function compileRestrictionSegment(segment) {
|
|
|
85429
85404
|
}
|
|
85430
85405
|
function matchCompiledSegments(pattern, path, pi = 0, si = 0) {
|
|
85431
85406
|
while (pi < pattern.length) {
|
|
85432
|
-
const
|
|
85433
|
-
if (
|
|
85407
|
+
const segment = pattern[pi];
|
|
85408
|
+
if (segment === "**") {
|
|
85434
85409
|
if (pi === pattern.length - 1) return true;
|
|
85435
85410
|
for (let i = si; i <= path.length; i++) if (matchCompiledSegments(pattern, path, pi + 1, i)) return true;
|
|
85436
85411
|
return false;
|
|
85437
85412
|
}
|
|
85438
|
-
if (si >= path.length || !(
|
|
85413
|
+
if (si >= path.length || !(segment instanceof RegExp) || !segment.test(path[si])) return false;
|
|
85439
85414
|
pi++;
|
|
85440
85415
|
si++;
|
|
85441
85416
|
}
|
|
@@ -85450,51 +85425,35 @@ function compileRestrictionRule(rule) {
|
|
|
85450
85425
|
function matchesCompiledRule(rule, method, pathSegments) {
|
|
85451
85426
|
return (rule.method === "*" || rule.method === method) && matchCompiledSegments(rule.segments, pathSegments);
|
|
85452
85427
|
}
|
|
85453
|
-
function
|
|
85454
|
-
|
|
85455
|
-
|
|
85456
|
-
|
|
85457
|
-
|
|
85458
|
-
|
|
85459
|
-
|
|
85460
|
-
|
|
85461
|
-
}
|
|
85462
|
-
//#endregion
|
|
85463
|
-
//#region src/utils/restrictions.ts
|
|
85464
|
-
const RESTRICTION_EXTENSION_KEY = "x-mc8yp-restrictions";
|
|
85465
|
-
const RESTRICTED_OPERATION_FLAG = "x-mc8yp-restricted";
|
|
85466
|
-
const RESTRICTED_OPERATION_MESSAGE = "x-mc8yp-restrictionMessage";
|
|
85467
|
-
const RESTRICTED_OPERATION_RULES = "x-mc8yp-restrictionRules";
|
|
85468
|
-
const RESTRICTED_OPERATION_TYPE = "x-mc8yp-restrictionType";
|
|
85469
|
-
const RESTRICTED_AGENT_NOTE = "x-mc8yp-agentNote";
|
|
85470
|
-
const HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
85471
|
-
function normalizePath(value) {
|
|
85472
|
-
let raw = value.trim();
|
|
85473
|
-
if (!raw) return "/";
|
|
85474
|
-
try {
|
|
85475
|
-
if (/^[A-Za-z][A-Za-z\d+\-.]*:\/\//.test(raw)) raw = new URL(raw).pathname || "/";
|
|
85476
|
-
} catch {}
|
|
85477
|
-
raw = (raw.split("#", 1)[0] ?? raw).split("?", 1)[0] ?? raw;
|
|
85478
|
-
raw = raw.startsWith("/") ? raw : `/${raw}`;
|
|
85479
|
-
raw = raw.replace(/\/{2,}/g, "/");
|
|
85480
|
-
return raw.length > 1 && raw.endsWith("/") ? raw.slice(0, -1) : raw || "/";
|
|
85428
|
+
function findMatchingRules(rules, method, pathname) {
|
|
85429
|
+
const normalizedMethod = typeof method === "string" ? method.trim().toUpperCase() : "";
|
|
85430
|
+
const pathSegments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
85431
|
+
return rules.filter((rule) => {
|
|
85432
|
+
const compiledRule = compileRestrictionRule(rule);
|
|
85433
|
+
if (!normalizedMethod) return compiledRule.method === "*" && matchCompiledSegments(compiledRule.segments, pathSegments);
|
|
85434
|
+
return matchesCompiledRule(compiledRule, normalizedMethod, pathSegments);
|
|
85435
|
+
});
|
|
85481
85436
|
}
|
|
85482
|
-
function
|
|
85483
|
-
|
|
85484
|
-
if (HTTP_METHOD_SET.has(upper)) return upper;
|
|
85485
|
-
throw new Error(`Unsupported HTTP method "${value}".`);
|
|
85437
|
+
function findBlockingRestrictions(rules, method, pathname) {
|
|
85438
|
+
return findMatchingRules(rules, method, pathname);
|
|
85486
85439
|
}
|
|
85487
|
-
function
|
|
85488
|
-
const
|
|
85489
|
-
|
|
85490
|
-
|
|
85440
|
+
function evaluateAccessPolicy(restrictions, allowRules, method, pathname) {
|
|
85441
|
+
const matchingRestrictions = findMatchingRules(restrictions, method, pathname);
|
|
85442
|
+
if (matchingRestrictions.length > 0) return {
|
|
85443
|
+
blocked: true,
|
|
85444
|
+
blockedBy: "restriction",
|
|
85445
|
+
matchingRestrictions
|
|
85446
|
+
};
|
|
85447
|
+
if (allowRules.length === 0) return { blocked: false };
|
|
85448
|
+
if (findMatchingRules(allowRules, method, pathname).length > 0) return { blocked: false };
|
|
85491
85449
|
return {
|
|
85492
|
-
|
|
85493
|
-
|
|
85494
|
-
matchingRules: rules.filter((rule) => matchesCompiledRule(compileRestrictionRule(rule), m, segs))
|
|
85450
|
+
blocked: true,
|
|
85451
|
+
blockedBy: "allow"
|
|
85495
85452
|
};
|
|
85496
85453
|
}
|
|
85497
|
-
|
|
85454
|
+
//#endregion
|
|
85455
|
+
//#region src/codemode/network-permissions.ts
|
|
85456
|
+
function createNetworkPermissionDecision(tenantUrl, request, restrictions = [], allowRules = []) {
|
|
85498
85457
|
const tenantHostname = new URL(tenantUrl).hostname;
|
|
85499
85458
|
if (request.op !== "connect") return {
|
|
85500
85459
|
allow: false,
|
|
@@ -85504,96 +85463,33 @@ function createNetworkPermissionDecision(tenantUrl, request, rules = []) {
|
|
|
85504
85463
|
allow: false,
|
|
85505
85464
|
reason: `Network connect blocked: only ${tenantHostname} is allowed in execute mode.`
|
|
85506
85465
|
};
|
|
85507
|
-
if (typeof request.
|
|
85508
|
-
const
|
|
85509
|
-
const
|
|
85510
|
-
|
|
85511
|
-
|
|
85512
|
-
|
|
85513
|
-
|
|
85514
|
-
|
|
85515
|
-
|
|
85516
|
-
}
|
|
85517
|
-
|
|
85518
|
-
|
|
85519
|
-
|
|
85520
|
-
|
|
85521
|
-
|
|
85522
|
-
|
|
85523
|
-
|
|
85524
|
-
|
|
85525
|
-
|
|
85526
|
-
|
|
85527
|
-
|
|
85528
|
-
|
|
85529
|
-
function annotateRestrictedOperation(operation, matchingRules) {
|
|
85530
|
-
return {
|
|
85531
|
-
...operation,
|
|
85532
|
-
[RESTRICTED_OPERATION_FLAG]: true,
|
|
85533
|
-
[RESTRICTED_OPERATION_TYPE]: "deny",
|
|
85534
|
-
[RESTRICTED_OPERATION_RULES]: matchingRules.map((rule) => rule.source),
|
|
85535
|
-
[RESTRICTED_OPERATION_MESSAGE]: "This operation is blocked by the current MCP connection restrictions.",
|
|
85536
|
-
[RESTRICTED_AGENT_NOTE]: "The route exists, but it is intentionally restricted for this MCP connection."
|
|
85537
|
-
};
|
|
85538
|
-
}
|
|
85539
|
-
function applyRestrictionsToOpenApiSpec(spec, rules) {
|
|
85540
|
-
if (!spec.paths || rules.length === 0) return spec;
|
|
85541
|
-
const compiledRules = rules.map(compileRestrictionRule);
|
|
85542
|
-
let nextPaths;
|
|
85543
|
-
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
85544
|
-
let nextPathItem;
|
|
85545
|
-
const pathSegments = path === "/" ? [] : path.slice(1).split("/");
|
|
85546
|
-
for (const method of OPENAPI_OPERATION_METHODS) {
|
|
85547
|
-
const operation = pathItem[method];
|
|
85548
|
-
if (!operation || typeof operation !== "object") continue;
|
|
85549
|
-
const matchingRules = compiledRules.filter((rule) => matchesCompiledRule(rule, method.toUpperCase(), pathSegments));
|
|
85550
|
-
if (matchingRules.length === 0) continue;
|
|
85551
|
-
nextPathItem ??= { ...pathItem };
|
|
85552
|
-
nextPathItem[method] = annotateRestrictedOperation(operation, matchingRules);
|
|
85553
|
-
}
|
|
85554
|
-
if (nextPathItem) {
|
|
85555
|
-
nextPaths ??= { ...spec.paths };
|
|
85556
|
-
nextPaths[path] = nextPathItem;
|
|
85466
|
+
if (typeof request.url === "string") {
|
|
85467
|
+
const pathname = new URL(request.url).pathname;
|
|
85468
|
+
const requestMethod = typeof request.method === "string" ? request.method.trim() : void 0;
|
|
85469
|
+
const normalizedMethod = requestMethod?.toUpperCase() ?? "";
|
|
85470
|
+
if (normalizedMethod) {
|
|
85471
|
+
const decision = evaluateAccessPolicy(restrictions, allowRules, normalizedMethod, pathname);
|
|
85472
|
+
if (decision.blocked) {
|
|
85473
|
+
if (decision.blockedBy === "restriction") return {
|
|
85474
|
+
allow: false,
|
|
85475
|
+
reason: `Network connect blocked by MCP restrictions: ${decision.matchingRestrictions.map((rule) => rule.source).join(", ")}`
|
|
85476
|
+
};
|
|
85477
|
+
return {
|
|
85478
|
+
allow: false,
|
|
85479
|
+
reason: `Network connect blocked by MCP allow list: no allow rule matched ${normalizedMethod} ${pathname}. Configured allow rules: ${allowRules.map((rule) => rule.source).join(", ")}`
|
|
85480
|
+
};
|
|
85481
|
+
}
|
|
85482
|
+
} else {
|
|
85483
|
+
const blockingRules = findBlockingRestrictions(restrictions, requestMethod, pathname);
|
|
85484
|
+
if (blockingRules.length > 0) return {
|
|
85485
|
+
allow: false,
|
|
85486
|
+
reason: `Network connect blocked by MCP restrictions: ${blockingRules.map((rule) => rule.source).join(", ")}`
|
|
85487
|
+
};
|
|
85557
85488
|
}
|
|
85558
85489
|
}
|
|
85559
|
-
return {
|
|
85560
|
-
...spec,
|
|
85561
|
-
paths: nextPaths ?? spec.paths,
|
|
85562
|
-
[RESTRICTION_EXTENSION_KEY]: {
|
|
85563
|
-
mode: "deny",
|
|
85564
|
-
rules: rules.map((rule) => rule.source),
|
|
85565
|
-
message: "Operations marked with x-mc8yp-restricted are intentionally blocked for the current MCP connection."
|
|
85566
|
-
}
|
|
85567
|
-
};
|
|
85490
|
+
return { allow: true };
|
|
85568
85491
|
}
|
|
85569
85492
|
//#endregion
|
|
85570
|
-
//#region src/codemode/semaphore.ts
|
|
85571
|
-
var AsyncSemaphore = class {
|
|
85572
|
-
#maxConcurrency;
|
|
85573
|
-
#activeCount = 0;
|
|
85574
|
-
#waiters = [];
|
|
85575
|
-
constructor(maxConcurrency) {
|
|
85576
|
-
if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) throw new Error("maxConcurrency must be a positive integer.");
|
|
85577
|
-
this.#maxConcurrency = maxConcurrency;
|
|
85578
|
-
}
|
|
85579
|
-
get activeCount() {
|
|
85580
|
-
return this.#activeCount;
|
|
85581
|
-
}
|
|
85582
|
-
async acquire() {
|
|
85583
|
-
if (this.#activeCount >= this.#maxConcurrency) await new Promise((resolve) => {
|
|
85584
|
-
this.#waiters.push(resolve);
|
|
85585
|
-
});
|
|
85586
|
-
this.#activeCount += 1;
|
|
85587
|
-
let released = false;
|
|
85588
|
-
return () => {
|
|
85589
|
-
if (released) return;
|
|
85590
|
-
released = true;
|
|
85591
|
-
this.#activeCount -= 1;
|
|
85592
|
-
this.#waiters.shift()?.();
|
|
85593
|
-
};
|
|
85594
|
-
}
|
|
85595
|
-
};
|
|
85596
|
-
//#endregion
|
|
85597
85493
|
//#region node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
|
|
85598
85494
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
85599
85495
|
function adopt(value) {
|
|
@@ -142365,24 +142261,277 @@ function createC8yAuthHeaders(auth) {
|
|
|
142365
142261
|
throw new Error("Invalid authentication credentials");
|
|
142366
142262
|
}
|
|
142367
142263
|
//#endregion
|
|
142264
|
+
//#region src/utils/restrictions.ts
|
|
142265
|
+
const HTTP_METHODS = [
|
|
142266
|
+
"DELETE",
|
|
142267
|
+
"GET",
|
|
142268
|
+
"HEAD",
|
|
142269
|
+
"OPTIONS",
|
|
142270
|
+
"PATCH",
|
|
142271
|
+
"POST",
|
|
142272
|
+
"PUT",
|
|
142273
|
+
"TRACE"
|
|
142274
|
+
];
|
|
142275
|
+
const HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
142276
|
+
const SEGMENT_PATTERN = /^[A-Za-z0-9._~*-]+$/;
|
|
142277
|
+
function parseRestrictionRule(input) {
|
|
142278
|
+
const inputs = typeof input === "string" ? [input] : input;
|
|
142279
|
+
const parsedRules = [];
|
|
142280
|
+
const failedRules = [];
|
|
142281
|
+
for (const source of inputs) {
|
|
142282
|
+
if (!source) {
|
|
142283
|
+
failedRules.push({
|
|
142284
|
+
rule: source,
|
|
142285
|
+
reason: "Restriction value must not be empty."
|
|
142286
|
+
});
|
|
142287
|
+
continue;
|
|
142288
|
+
}
|
|
142289
|
+
const sep = source.indexOf(":");
|
|
142290
|
+
if (sep > 0 && !source.startsWith("/")) {
|
|
142291
|
+
const rawMethod = source.slice(0, sep).toUpperCase();
|
|
142292
|
+
const rawPath = source.slice(sep + 1);
|
|
142293
|
+
if (!rawPath) {
|
|
142294
|
+
failedRules.push({
|
|
142295
|
+
rule: source,
|
|
142296
|
+
reason: "Restriction path pattern must not be empty."
|
|
142297
|
+
});
|
|
142298
|
+
continue;
|
|
142299
|
+
}
|
|
142300
|
+
if (rawMethod && rawMethod !== "*" && !HTTP_METHOD_SET.has(rawMethod)) {
|
|
142301
|
+
failedRules.push({
|
|
142302
|
+
rule: source,
|
|
142303
|
+
reason: `Unsupported restriction method "${source.slice(0, sep)}".`
|
|
142304
|
+
});
|
|
142305
|
+
continue;
|
|
142306
|
+
}
|
|
142307
|
+
if (rawPath.includes("?") || rawPath.includes("#")) {
|
|
142308
|
+
failedRules.push({
|
|
142309
|
+
rule: source,
|
|
142310
|
+
reason: `Restriction pattern "${rawPath}" must not include query strings or fragments.`
|
|
142311
|
+
});
|
|
142312
|
+
continue;
|
|
142313
|
+
}
|
|
142314
|
+
if (!rawPath.startsWith("/")) {
|
|
142315
|
+
failedRules.push({
|
|
142316
|
+
rule: source,
|
|
142317
|
+
reason: "Restriction path pattern must start with \"/\"."
|
|
142318
|
+
});
|
|
142319
|
+
continue;
|
|
142320
|
+
}
|
|
142321
|
+
const rawPathSegments = rawPath === "/" ? [] : rawPath.slice(1).split("/");
|
|
142322
|
+
if (rawPathSegments.some((segment) => segment.length === 0)) {
|
|
142323
|
+
failedRules.push({
|
|
142324
|
+
rule: source,
|
|
142325
|
+
reason: "Restriction path pattern must not contain empty segments."
|
|
142326
|
+
});
|
|
142327
|
+
continue;
|
|
142328
|
+
}
|
|
142329
|
+
const invalidRawPathSegment = rawPathSegments.find((segment) => {
|
|
142330
|
+
if (segment === "**") return false;
|
|
142331
|
+
return segment.includes("**") || segment === "." || segment === ".." || !SEGMENT_PATTERN.test(segment);
|
|
142332
|
+
});
|
|
142333
|
+
if (invalidRawPathSegment) {
|
|
142334
|
+
failedRules.push({
|
|
142335
|
+
rule: source,
|
|
142336
|
+
reason: invalidRawPathSegment === "." || invalidRawPathSegment === ".." ? `Restriction segment "${invalidRawPathSegment}" is not allowed.` : invalidRawPathSegment.includes("**") ? `Invalid restriction segment "${invalidRawPathSegment}". "**" must be its own path segment.` : `Restriction segment "${invalidRawPathSegment}" contains unsupported characters.`
|
|
142337
|
+
});
|
|
142338
|
+
continue;
|
|
142339
|
+
}
|
|
142340
|
+
parsedRules.push({
|
|
142341
|
+
type: "deny",
|
|
142342
|
+
method: !rawMethod || rawMethod === "*" ? "*" : rawMethod,
|
|
142343
|
+
pathPattern: rawPath,
|
|
142344
|
+
source
|
|
142345
|
+
});
|
|
142346
|
+
continue;
|
|
142347
|
+
}
|
|
142348
|
+
if (source.includes("?") || source.includes("#")) {
|
|
142349
|
+
failedRules.push({
|
|
142350
|
+
rule: source,
|
|
142351
|
+
reason: `Restriction pattern "${source}" must not include query strings or fragments.`
|
|
142352
|
+
});
|
|
142353
|
+
continue;
|
|
142354
|
+
}
|
|
142355
|
+
if (!source.startsWith("/")) {
|
|
142356
|
+
failedRules.push({
|
|
142357
|
+
rule: source,
|
|
142358
|
+
reason: "Restriction path pattern must start with \"/\"."
|
|
142359
|
+
});
|
|
142360
|
+
continue;
|
|
142361
|
+
}
|
|
142362
|
+
const sourceSegments = source === "/" ? [] : source.slice(1).split("/");
|
|
142363
|
+
if (sourceSegments.some((segment) => segment.length === 0)) {
|
|
142364
|
+
failedRules.push({
|
|
142365
|
+
rule: source,
|
|
142366
|
+
reason: "Restriction path pattern must not contain empty segments."
|
|
142367
|
+
});
|
|
142368
|
+
continue;
|
|
142369
|
+
}
|
|
142370
|
+
const invalidSourceSegment = sourceSegments.find((segment) => {
|
|
142371
|
+
if (segment === "**") return false;
|
|
142372
|
+
return segment.includes("**") || segment === "." || segment === ".." || !SEGMENT_PATTERN.test(segment);
|
|
142373
|
+
});
|
|
142374
|
+
if (invalidSourceSegment) {
|
|
142375
|
+
failedRules.push({
|
|
142376
|
+
rule: source,
|
|
142377
|
+
reason: invalidSourceSegment === "." || invalidSourceSegment === ".." ? `Restriction segment "${invalidSourceSegment}" is not allowed.` : invalidSourceSegment.includes("**") ? `Invalid restriction segment "${invalidSourceSegment}". "**" must be its own path segment.` : `Restriction segment "${invalidSourceSegment}" contains unsupported characters.`
|
|
142378
|
+
});
|
|
142379
|
+
continue;
|
|
142380
|
+
}
|
|
142381
|
+
parsedRules.push({
|
|
142382
|
+
type: "deny",
|
|
142383
|
+
method: "*",
|
|
142384
|
+
pathPattern: source,
|
|
142385
|
+
source
|
|
142386
|
+
});
|
|
142387
|
+
}
|
|
142388
|
+
return {
|
|
142389
|
+
parsedRules,
|
|
142390
|
+
failedRules
|
|
142391
|
+
};
|
|
142392
|
+
}
|
|
142393
|
+
function parseAllowRule(input) {
|
|
142394
|
+
const inputs = typeof input === "string" ? [input] : input;
|
|
142395
|
+
const parsedRules = [];
|
|
142396
|
+
const failedRules = [];
|
|
142397
|
+
for (const source of inputs) {
|
|
142398
|
+
if (!source) {
|
|
142399
|
+
failedRules.push({
|
|
142400
|
+
rule: source,
|
|
142401
|
+
reason: "Allow value must not be empty."
|
|
142402
|
+
});
|
|
142403
|
+
continue;
|
|
142404
|
+
}
|
|
142405
|
+
const sep = source.indexOf(":");
|
|
142406
|
+
if (sep > 0 && !source.startsWith("/")) {
|
|
142407
|
+
const rawMethod = source.slice(0, sep).toUpperCase();
|
|
142408
|
+
const rawPath = source.slice(sep + 1);
|
|
142409
|
+
if (!rawPath) {
|
|
142410
|
+
failedRules.push({
|
|
142411
|
+
rule: source,
|
|
142412
|
+
reason: "Allow path pattern must not be empty."
|
|
142413
|
+
});
|
|
142414
|
+
continue;
|
|
142415
|
+
}
|
|
142416
|
+
if (rawMethod && rawMethod !== "*" && !HTTP_METHOD_SET.has(rawMethod)) {
|
|
142417
|
+
failedRules.push({
|
|
142418
|
+
rule: source,
|
|
142419
|
+
reason: `Unsupported allow method "${source.slice(0, sep)}".`
|
|
142420
|
+
});
|
|
142421
|
+
continue;
|
|
142422
|
+
}
|
|
142423
|
+
if (rawPath.includes("?") || rawPath.includes("#")) {
|
|
142424
|
+
failedRules.push({
|
|
142425
|
+
rule: source,
|
|
142426
|
+
reason: `Allow pattern "${rawPath}" must not include query strings or fragments.`
|
|
142427
|
+
});
|
|
142428
|
+
continue;
|
|
142429
|
+
}
|
|
142430
|
+
if (!rawPath.startsWith("/")) {
|
|
142431
|
+
failedRules.push({
|
|
142432
|
+
rule: source,
|
|
142433
|
+
reason: "Allow path pattern must start with \"/\"."
|
|
142434
|
+
});
|
|
142435
|
+
continue;
|
|
142436
|
+
}
|
|
142437
|
+
const rawPathSegments = rawPath === "/" ? [] : rawPath.slice(1).split("/");
|
|
142438
|
+
if (rawPathSegments.some((segment) => segment.length === 0)) {
|
|
142439
|
+
failedRules.push({
|
|
142440
|
+
rule: source,
|
|
142441
|
+
reason: "Allow path pattern must not contain empty segments."
|
|
142442
|
+
});
|
|
142443
|
+
continue;
|
|
142444
|
+
}
|
|
142445
|
+
const invalidRawPathSegment = rawPathSegments.find((segment) => {
|
|
142446
|
+
if (segment === "**") return false;
|
|
142447
|
+
return segment.includes("**") || segment === "." || segment === ".." || !SEGMENT_PATTERN.test(segment);
|
|
142448
|
+
});
|
|
142449
|
+
if (invalidRawPathSegment) {
|
|
142450
|
+
failedRules.push({
|
|
142451
|
+
rule: source,
|
|
142452
|
+
reason: invalidRawPathSegment === "." || invalidRawPathSegment === ".." ? `Allow segment "${invalidRawPathSegment}" is not allowed.` : invalidRawPathSegment.includes("**") ? `Invalid allow segment "${invalidRawPathSegment}". "**" must be its own path segment.` : `Allow segment "${invalidRawPathSegment}" contains unsupported characters.`
|
|
142453
|
+
});
|
|
142454
|
+
continue;
|
|
142455
|
+
}
|
|
142456
|
+
parsedRules.push({
|
|
142457
|
+
type: "allow",
|
|
142458
|
+
method: !rawMethod || rawMethod === "*" ? "*" : rawMethod,
|
|
142459
|
+
pathPattern: rawPath,
|
|
142460
|
+
source
|
|
142461
|
+
});
|
|
142462
|
+
continue;
|
|
142463
|
+
}
|
|
142464
|
+
if (source.includes("?") || source.includes("#")) {
|
|
142465
|
+
failedRules.push({
|
|
142466
|
+
rule: source,
|
|
142467
|
+
reason: `Allow pattern "${source}" must not include query strings or fragments.`
|
|
142468
|
+
});
|
|
142469
|
+
continue;
|
|
142470
|
+
}
|
|
142471
|
+
if (!source.startsWith("/")) {
|
|
142472
|
+
failedRules.push({
|
|
142473
|
+
rule: source,
|
|
142474
|
+
reason: "Allow path pattern must start with \"/\"."
|
|
142475
|
+
});
|
|
142476
|
+
continue;
|
|
142477
|
+
}
|
|
142478
|
+
const sourceSegments = source === "/" ? [] : source.slice(1).split("/");
|
|
142479
|
+
if (sourceSegments.some((segment) => segment.length === 0)) {
|
|
142480
|
+
failedRules.push({
|
|
142481
|
+
rule: source,
|
|
142482
|
+
reason: "Allow path pattern must not contain empty segments."
|
|
142483
|
+
});
|
|
142484
|
+
continue;
|
|
142485
|
+
}
|
|
142486
|
+
const invalidSourceSegment = sourceSegments.find((segment) => {
|
|
142487
|
+
if (segment === "**") return false;
|
|
142488
|
+
return segment.includes("**") || segment === "." || segment === ".." || !SEGMENT_PATTERN.test(segment);
|
|
142489
|
+
});
|
|
142490
|
+
if (invalidSourceSegment) {
|
|
142491
|
+
failedRules.push({
|
|
142492
|
+
rule: source,
|
|
142493
|
+
reason: invalidSourceSegment === "." || invalidSourceSegment === ".." ? `Allow segment "${invalidSourceSegment}" is not allowed.` : invalidSourceSegment.includes("**") ? `Invalid allow segment "${invalidSourceSegment}". "**" must be its own path segment.` : `Allow segment "${invalidSourceSegment}" contains unsupported characters.`
|
|
142494
|
+
});
|
|
142495
|
+
continue;
|
|
142496
|
+
}
|
|
142497
|
+
parsedRules.push({
|
|
142498
|
+
type: "allow",
|
|
142499
|
+
method: "*",
|
|
142500
|
+
pathPattern: source,
|
|
142501
|
+
source
|
|
142502
|
+
});
|
|
142503
|
+
}
|
|
142504
|
+
return {
|
|
142505
|
+
parsedRules,
|
|
142506
|
+
failedRules
|
|
142507
|
+
};
|
|
142508
|
+
}
|
|
142509
|
+
//#endregion
|
|
142368
142510
|
//#region src/codemode/execute.ts
|
|
142369
142511
|
const NO_DEFAULT_EXPORT_MESSAGE = "Execution completed without returning a value.";
|
|
142370
142512
|
const QUERY_ENTRY_PATH = "/codemode-query.mjs";
|
|
142371
142513
|
const EXECUTE_ENTRY_PATH = "/codemode-execute.mjs";
|
|
142372
142514
|
const runtimeSemaphore = new AsyncSemaphore(3);
|
|
142373
142515
|
const BLOCKED_REQUEST_PREFIX = "Request blocked by MCP connection policy.";
|
|
142374
|
-
function serializeExecuteConfig(tenantUrl, headers, restrictions) {
|
|
142516
|
+
function serializeExecuteConfig(tenantUrl, headers, restrictions, allowRules) {
|
|
142375
142517
|
const normalizedTenantUrl = new URL(tenantUrl).toString();
|
|
142376
|
-
const
|
|
142377
|
-
|
|
142378
|
-
|
|
142379
|
-
|
|
142380
|
-
|
|
142518
|
+
const serializedRestrictions = restrictions.map(({ type, method, pathPattern, source }) => ({
|
|
142519
|
+
type,
|
|
142520
|
+
method,
|
|
142521
|
+
pathPattern,
|
|
142522
|
+
source
|
|
142523
|
+
}));
|
|
142524
|
+
const serializedAllowRules = allowRules.map(({ type, method, pathPattern, source }) => ({
|
|
142525
|
+
type,
|
|
142526
|
+
method,
|
|
142527
|
+
pathPattern,
|
|
142528
|
+
source
|
|
142529
|
+
}));
|
|
142381
142530
|
return JSON.stringify({
|
|
142382
142531
|
tenantUrl: normalizedTenantUrl,
|
|
142383
|
-
tenantOrigin: new URL(normalizedTenantUrl).origin,
|
|
142384
142532
|
authHeaders: headers,
|
|
142385
|
-
|
|
142533
|
+
restrictions: serializedRestrictions,
|
|
142534
|
+
allowRules: serializedAllowRules
|
|
142386
142535
|
});
|
|
142387
142536
|
}
|
|
142388
142537
|
function createQueryRuntime() {
|
|
@@ -142399,11 +142548,11 @@ function createQueryRuntime() {
|
|
|
142399
142548
|
cpuTimeLimitMs: 5e4
|
|
142400
142549
|
});
|
|
142401
142550
|
}
|
|
142402
|
-
function createExecuteRuntime(tenantUrl, restrictions) {
|
|
142551
|
+
function createExecuteRuntime(tenantUrl, restrictions, allowRules) {
|
|
142403
142552
|
return new NodeRuntime({
|
|
142404
142553
|
systemDriver: createNodeDriver({
|
|
142405
142554
|
useDefaultNetwork: true,
|
|
142406
|
-
permissions: { network: (request) => createNetworkPermissionDecision(tenantUrl, request, restrictions) }
|
|
142555
|
+
permissions: { network: (request) => createNetworkPermissionDecision(tenantUrl, request, restrictions, allowRules) }
|
|
142407
142556
|
}),
|
|
142408
142557
|
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
|
|
142409
142558
|
memoryLimit: 128,
|
|
@@ -142415,11 +142564,11 @@ function normalizeCode(functionCode) {
|
|
|
142415
142564
|
normalized = normalized.replace(/^```(?:js|javascript|ts|typescript)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
142416
142565
|
return normalized;
|
|
142417
142566
|
}
|
|
142418
|
-
function buildQueryScript(sourceCode,
|
|
142419
|
-
const
|
|
142567
|
+
function buildQueryScript(sourceCode, _restrictions, _allowRules) {
|
|
142568
|
+
const spec = getCoreOpenApiSpec();
|
|
142420
142569
|
const functionExpression = normalizeCode(sourceCode);
|
|
142421
142570
|
return [
|
|
142422
|
-
`const spec = ${JSON.stringify(
|
|
142571
|
+
`const spec = ${JSON.stringify(spec)};`,
|
|
142423
142572
|
`const __mc8ypQuery = (${functionExpression});`,
|
|
142424
142573
|
"",
|
|
142425
142574
|
"if (typeof __mc8ypQuery !== \"function\") {",
|
|
@@ -142429,26 +142578,25 @@ function buildQueryScript(sourceCode, restrictions) {
|
|
|
142429
142578
|
"export default await __mc8ypQuery();"
|
|
142430
142579
|
].join("\n\n");
|
|
142431
142580
|
}
|
|
142432
|
-
function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
142581
|
+
function buildExecutePrelude(tenantUrl, headers, restrictions = [], allowRules = []) {
|
|
142433
142582
|
return [
|
|
142434
142583
|
"const cumulocity = Object.freeze((() => {",
|
|
142435
|
-
` const config = JSON.parse(${JSON.stringify(serializeExecuteConfig(tenantUrl, headers, restrictions))});`,
|
|
142584
|
+
` const config = JSON.parse(${JSON.stringify(serializeExecuteConfig(tenantUrl, headers, restrictions, allowRules))});`,
|
|
142436
142585
|
" if (!config || typeof config !== \"object\") {",
|
|
142437
142586
|
" throw new TypeError(\"Invalid execute configuration.\");",
|
|
142438
142587
|
" }",
|
|
142439
|
-
" const { tenantUrl,
|
|
142440
|
-
" if (typeof tenantUrl !== \"string\"
|
|
142441
|
-
" throw new TypeError(\"Execute configuration must contain string tenant
|
|
142588
|
+
" const { tenantUrl, authHeaders, restrictions, allowRules } = config;",
|
|
142589
|
+
" if (typeof tenantUrl !== \"string\") {",
|
|
142590
|
+
" throw new TypeError(\"Execute configuration must contain a string tenant URL.\");",
|
|
142442
142591
|
" }",
|
|
142443
|
-
" if (!Array.isArray(
|
|
142444
|
-
" throw new TypeError(\"Execute configuration must contain
|
|
142592
|
+
" if (!Array.isArray(restrictions) || restrictions.some((rule) => !rule || typeof rule !== \"object\" || typeof rule.type !== \"string\" || typeof rule.method !== \"string\" || typeof rule.pathPattern !== \"string\" || typeof rule.source !== \"string\")) {",
|
|
142593
|
+
" throw new TypeError(\"Execute configuration must contain valid restriction rules.\");",
|
|
142594
|
+
" }",
|
|
142595
|
+
" if (!Array.isArray(allowRules) || allowRules.some((rule) => !rule || typeof rule !== \"object\" || typeof rule.type !== \"string\" || typeof rule.method !== \"string\" || typeof rule.pathPattern !== \"string\" || typeof rule.source !== \"string\")) {",
|
|
142596
|
+
" throw new TypeError(\"Execute configuration must contain valid allow rules.\");",
|
|
142445
142597
|
" }",
|
|
142446
142598
|
" const resolveUrl = (descriptor) => {",
|
|
142447
|
-
"
|
|
142448
|
-
" if (resolved.origin !== tenantOrigin) {",
|
|
142449
|
-
" throw new Error(\"Cumulocity requests must target the configured tenant origin.\");",
|
|
142450
|
-
" }",
|
|
142451
|
-
" return resolved;",
|
|
142599
|
+
" return new URL(descriptor, tenantUrl.endsWith(\"/\") ? tenantUrl : tenantUrl + \"/\");",
|
|
142452
142600
|
" };",
|
|
142453
142601
|
" const normalizeRequest = (options) => {",
|
|
142454
142602
|
" if (!options || typeof options !== \"object\") {",
|
|
@@ -142457,34 +142605,85 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142457
142605
|
" const { path, ...rest } = options;",
|
|
142458
142606
|
" return { path, init: rest };",
|
|
142459
142607
|
" };",
|
|
142460
|
-
` const
|
|
142461
|
-
|
|
142462
|
-
` const normalizeAndValidateRestrictionPath = ${normalizeAndValidateRestrictionPath.toString()};`,
|
|
142463
|
-
` const parseRestrictionRule = ${parseRestrictionRule.toString()};`,
|
|
142608
|
+
` const HTTP_METHODS = ${JSON.stringify(HTTP_METHODS)};`,
|
|
142609
|
+
" const HTTP_METHOD_SET = new Set(HTTP_METHODS);",
|
|
142464
142610
|
` const escapeRestrictionRegex = ${escapeRestrictionRegex.toString()};`,
|
|
142465
142611
|
` const compileRestrictionSegment = ${compileRestrictionSegment.toString()};`,
|
|
142466
142612
|
` const matchCompiledSegments = ${matchCompiledSegments.toString()};`,
|
|
142467
142613
|
` const compileRestrictionRule = ${compileRestrictionRule.toString()};`,
|
|
142468
142614
|
` const matchesCompiledRule = ${matchesCompiledRule.toString()};`,
|
|
142469
|
-
|
|
142470
|
-
|
|
142471
|
-
" const
|
|
142472
|
-
"
|
|
142473
|
-
" \"
|
|
142474
|
-
"
|
|
142475
|
-
"
|
|
142476
|
-
"
|
|
142477
|
-
"
|
|
142478
|
-
"
|
|
142479
|
-
"
|
|
142480
|
-
"
|
|
142481
|
-
"
|
|
142482
|
-
"
|
|
142483
|
-
"
|
|
142484
|
-
"
|
|
142485
|
-
"
|
|
142486
|
-
"
|
|
142487
|
-
"
|
|
142615
|
+
" const compiledRestrictions = restrictions.map(compileRestrictionRule);",
|
|
142616
|
+
" const compiledAllowRules = allowRules.map(compileRestrictionRule);",
|
|
142617
|
+
" const findMatchingRules = (compiledRules, method, pathname) => {",
|
|
142618
|
+
" const normalizedMethod = typeof method === \"string\" ? method.trim().toUpperCase() : \"\";",
|
|
142619
|
+
" const pathSegments = pathname === \"/\" ? [] : pathname.slice(1).split(\"/\");",
|
|
142620
|
+
" return compiledRules.filter((rule) => {",
|
|
142621
|
+
" if (!normalizedMethod) {",
|
|
142622
|
+
" return rule.method === \"*\" && matchCompiledSegments(rule.segments, pathSegments);",
|
|
142623
|
+
" }",
|
|
142624
|
+
" return matchesCompiledRule(rule, normalizedMethod, pathSegments);",
|
|
142625
|
+
" });",
|
|
142626
|
+
" };",
|
|
142627
|
+
" const evaluateAccessPolicy = (method, pathname) => {",
|
|
142628
|
+
" const matchingRestrictions = findMatchingRules(compiledRestrictions, method, pathname);",
|
|
142629
|
+
" if (matchingRestrictions.length > 0) {",
|
|
142630
|
+
" return { blocked: true, blockedBy: \"restriction\", matchingRestrictions };",
|
|
142631
|
+
" }",
|
|
142632
|
+
" if (compiledAllowRules.length === 0) {",
|
|
142633
|
+
" return { blocked: false };",
|
|
142634
|
+
" }",
|
|
142635
|
+
" const matchingAllowRules = findMatchingRules(compiledAllowRules, method, pathname);",
|
|
142636
|
+
" if (matchingAllowRules.length > 0) {",
|
|
142637
|
+
" return { blocked: false };",
|
|
142638
|
+
" }",
|
|
142639
|
+
" return { blocked: true, blockedBy: \"allow\" };",
|
|
142640
|
+
" };",
|
|
142641
|
+
" const validateRequestMethod = (method) => {",
|
|
142642
|
+
" if (typeof method !== \"string\" || method.trim().length === 0) {",
|
|
142643
|
+
" throw new TypeError(\"request method must be a non-empty string\");",
|
|
142644
|
+
" }",
|
|
142645
|
+
" const normalizedMethod = method.trim().toUpperCase();",
|
|
142646
|
+
" if (!HTTP_METHOD_SET.has(normalizedMethod)) {",
|
|
142647
|
+
" throw new TypeError(\"request method must be one of: \" + HTTP_METHODS.join(\", \"));",
|
|
142648
|
+
" }",
|
|
142649
|
+
" return normalizedMethod;",
|
|
142650
|
+
" };",
|
|
142651
|
+
" const formatBlockedRequestMessage = (method, path, accessDecision) => {",
|
|
142652
|
+
" if (accessDecision.blockedBy === \"allow\") {",
|
|
142653
|
+
" return [",
|
|
142654
|
+
" \"Request blocked by MCP connection policy.\",",
|
|
142655
|
+
" \"\",",
|
|
142656
|
+
" \"This operation is intentionally blocked because it is not included in the current MCP connection allow list.\",",
|
|
142657
|
+
" \"It did not fail at the Cumulocity API and it was not executed against the tenant.\",",
|
|
142658
|
+
" \"Retrying or trying the same operation again through this connection will not succeed.\",",
|
|
142659
|
+
" \"\",",
|
|
142660
|
+
" \"Report this to the user as a connection-level access restriction.\",",
|
|
142661
|
+
" \"If the operation is needed, the MCP allow list for this connection must be updated by whoever manages that configuration.\",",
|
|
142662
|
+
" \"\",",
|
|
142663
|
+
" \"Blocked operation:\",",
|
|
142664
|
+
" \"Method: \" + method,",
|
|
142665
|
+
" \"Path: \" + path,",
|
|
142666
|
+
" \"Configured allow rules:\",",
|
|
142667
|
+
" ...(allowRules.length > 0 ? allowRules.map((rule) => \"- \" + rule.source) : [\"- (none)\"]),",
|
|
142668
|
+
" ].join(\"\\n\");",
|
|
142669
|
+
" }",
|
|
142670
|
+
" return [",
|
|
142671
|
+
" \"Request blocked by MCP connection policy.\",",
|
|
142672
|
+
" \"\",",
|
|
142673
|
+
" \"This operation is intentionally denied by the current MCP connection configuration.\",",
|
|
142674
|
+
" \"It did not fail at the Cumulocity API and it was not executed against the tenant.\",",
|
|
142675
|
+
" \"Retrying or trying the same operation again through this connection will not succeed.\",",
|
|
142676
|
+
" \"\",",
|
|
142677
|
+
" \"Report this to the user as a connection-level access restriction.\",",
|
|
142678
|
+
" \"If the operation is needed, the MCP restrictions for this connection must be updated by whoever manages that configuration.\",",
|
|
142679
|
+
" \"\",",
|
|
142680
|
+
" \"Blocked operation:\",",
|
|
142681
|
+
" \"Method: \" + method,",
|
|
142682
|
+
" \"Path: \" + path,",
|
|
142683
|
+
" \"Matching restrictions:\",",
|
|
142684
|
+
" ...accessDecision.matchingRestrictions.map((rule) => \"- \" + rule.source),",
|
|
142685
|
+
" ].join(\"\\n\");",
|
|
142686
|
+
" };",
|
|
142488
142687
|
" const normalizeBody = (headers, body) => {",
|
|
142489
142688
|
" if (body == null || typeof body === \"string\") {",
|
|
142490
142689
|
" return body;",
|
|
@@ -142514,11 +142713,11 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142514
142713
|
" if (typeof path !== \"string\" || path.length === 0) {",
|
|
142515
142714
|
" throw new TypeError(\"request path must be a non-empty string\");",
|
|
142516
142715
|
" }",
|
|
142716
|
+
" const method = validateRequestMethod(init.method);",
|
|
142517
142717
|
" const resolvedUrl = resolveUrl(path);",
|
|
142518
|
-
" const
|
|
142519
|
-
" if (
|
|
142520
|
-
"
|
|
142521
|
-
" throw new Error(formatBlockedRequestMessage(method, resolvedUrl.pathname, blockedRules.map((rule) => rule.source)));",
|
|
142718
|
+
" const accessDecision = evaluateAccessPolicy(method, resolvedUrl.pathname);",
|
|
142719
|
+
" if (accessDecision.blocked) {",
|
|
142720
|
+
" throw new Error(formatBlockedRequestMessage(method, resolvedUrl.pathname, accessDecision));",
|
|
142522
142721
|
" }",
|
|
142523
142722
|
" const headers = new Headers(init.headers ?? {});",
|
|
142524
142723
|
" for (const [key, value] of Object.entries(authHeaders)) {",
|
|
@@ -142528,6 +142727,7 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142528
142727
|
" const requestHeaders = Object.fromEntries(headers.entries());",
|
|
142529
142728
|
" const response = await fetch(resolvedUrl.toString(), {",
|
|
142530
142729
|
" ...init,",
|
|
142730
|
+
" method,",
|
|
142531
142731
|
" headers: requestHeaders,",
|
|
142532
142732
|
" body,",
|
|
142533
142733
|
" });",
|
|
@@ -142542,10 +142742,10 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142542
142742
|
"})());"
|
|
142543
142743
|
].join("\n\n");
|
|
142544
142744
|
}
|
|
142545
|
-
function buildExecuteScript(sourceCode, tenantUrl, headers, restrictions = []) {
|
|
142745
|
+
function buildExecuteScript(sourceCode, tenantUrl, headers, restrictions = [], allowRules = []) {
|
|
142546
142746
|
const functionExpression = normalizeCode(sourceCode);
|
|
142547
142747
|
return [
|
|
142548
|
-
buildExecutePrelude(tenantUrl, headers, restrictions),
|
|
142748
|
+
buildExecutePrelude(tenantUrl, headers, restrictions, allowRules),
|
|
142549
142749
|
`const __mc8ypExecute = (${functionExpression});`,
|
|
142550
142750
|
"",
|
|
142551
142751
|
"const __mc8ypErrorMessage = (error) => error instanceof Error ? error.message : String(error);",
|
|
@@ -142579,9 +142779,9 @@ function extractDefaultExport(exportsObject) {
|
|
|
142579
142779
|
if (typeof exportsObject !== "undefined") return exportsObject;
|
|
142580
142780
|
throw new Error(NO_DEFAULT_EXPORT_MESSAGE);
|
|
142581
142781
|
}
|
|
142582
|
-
async function runExecuteScript(code, tenantUrl, restrictions) {
|
|
142782
|
+
async function runExecuteScript(code, tenantUrl, restrictions, allowRules) {
|
|
142583
142783
|
const release = await runtimeSemaphore.acquire();
|
|
142584
|
-
const runtime = createExecuteRuntime(tenantUrl, restrictions);
|
|
142784
|
+
const runtime = createExecuteRuntime(tenantUrl, restrictions, allowRules);
|
|
142585
142785
|
try {
|
|
142586
142786
|
const result = await runtime.run(code, EXECUTE_ENTRY_PATH);
|
|
142587
142787
|
if (result.code !== 0) {
|
|
@@ -142611,14 +142811,14 @@ async function runModule(code, entryPath, runtime) {
|
|
|
142611
142811
|
release();
|
|
142612
142812
|
}
|
|
142613
142813
|
}
|
|
142614
|
-
async function query(functionCode, restrictions = []) {
|
|
142615
|
-
const result = await runQueryScript(buildQueryScript(functionCode, restrictions));
|
|
142814
|
+
async function query(functionCode, restrictions = [], allowRules = []) {
|
|
142815
|
+
const result = await runQueryScript(buildQueryScript(functionCode, restrictions, allowRules));
|
|
142616
142816
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
142617
142817
|
}
|
|
142618
|
-
async function execute(functionCode, input, restrictions = []) {
|
|
142818
|
+
async function execute(functionCode, input, restrictions = [], allowRules = []) {
|
|
142619
142819
|
const auth = await resolveC8yAuth(input);
|
|
142620
142820
|
const authHeaders = createC8yAuthHeaders(auth);
|
|
142621
|
-
const result = await runExecuteScript(buildExecuteScript(functionCode, auth.tenantUrl, authHeaders, restrictions), auth.tenantUrl, restrictions);
|
|
142821
|
+
const result = await runExecuteScript(buildExecuteScript(functionCode, auth.tenantUrl, authHeaders, restrictions, allowRules), auth.tenantUrl, restrictions, allowRules);
|
|
142622
142822
|
if (result.status === "success") return encode(result.result);
|
|
142623
142823
|
return result.error.message;
|
|
142624
142824
|
}
|
|
@@ -142688,8 +142888,8 @@ Recommended shapes:
|
|
|
142688
142888
|
|
|
142689
142889
|
If your function returns a string, it is returned as-is. Otherwise the result is returned as JSON text.
|
|
142690
142890
|
|
|
142691
|
-
The
|
|
142692
|
-
|
|
142891
|
+
The spec exposed by \`query\` is the raw bundled OpenAPI snapshot. It is not rewritten for the current MCP connection policy.
|
|
142892
|
+
The current MCP connection may still block \`execute\` calls through restrictions and/or an allow list, so do not assume every operation visible in the spec is executable on this connection.
|
|
142693
142893
|
|
|
142694
142894
|
Examples:
|
|
142695
142895
|
() => {
|
|
@@ -142713,7 +142913,7 @@ Examples:
|
|
|
142713
142913
|
schema: v.object({ code: createCodeSchema("A JavaScript function expression. The top-level binding `spec` is available automatically. Return the final result from that function. Async functions are supported.") })
|
|
142714
142914
|
}, async (input) => {
|
|
142715
142915
|
try {
|
|
142716
|
-
return tool.text(await query(input.code, server.ctx.custom?.restrictions ?? []));
|
|
142916
|
+
return tool.text(await query(input.code, server.ctx.custom?.restrictions ?? [], server.ctx.custom?.allowRules ?? []));
|
|
142717
142917
|
} catch (error) {
|
|
142718
142918
|
const message = error instanceof Error ? error.message : String(error);
|
|
142719
142919
|
return tool.error(message);
|
|
@@ -142754,7 +142954,8 @@ Tool output behavior:
|
|
|
142754
142954
|
- On success, the actual function result is returned in Toon format.
|
|
142755
142955
|
- On blocked or failed execution, the tool returns a plain text message.
|
|
142756
142956
|
|
|
142757
|
-
The current MCP connection may deny certain method/path combinations
|
|
142957
|
+
The current MCP connection may deny certain method/path combinations and may also use an allow list.
|
|
142958
|
+
The \`query\` tool does not annotate or filter the OpenAPI spec for that policy, and \`cumulocity.request(...)\` will reject blocked calls before sending them.
|
|
142758
142959
|
When that happens, the tool returns a plain text connection-policy message. That is not a Cumulocity API failure and retrying the same operation through the same connection will not help.
|
|
142759
142960
|
|
|
142760
142961
|
${getExecuteEnvironmentNote()}
|
|
@@ -142788,7 +142989,7 @@ async () => {
|
|
|
142788
142989
|
schema: addTenantURLToSchema(v.object({ code: createCodeSchema("An async JavaScript function expression. The top-level binding `cumulocity` is available automatically. Return the final result from that function. `await` is supported.") }))
|
|
142789
142990
|
}, async (input) => {
|
|
142790
142991
|
try {
|
|
142791
|
-
return tool.text(await execute(input.code, input, server.ctx.custom?.restrictions ?? []));
|
|
142992
|
+
return tool.text(await execute(input.code, input, server.ctx.custom?.restrictions ?? [], server.ctx.custom?.allowRules ?? []));
|
|
142792
142993
|
} catch (error) {
|
|
142793
142994
|
const message = error instanceof Error ? error.message : String(error);
|
|
142794
142995
|
return tool.error(message);
|
|
@@ -142826,7 +143027,7 @@ function createC8YMcpServer() {
|
|
|
142826
143027
|
capabilities: {
|
|
142827
143028
|
tools: { listChanged: true },
|
|
142828
143029
|
prompts: { listChanged: true },
|
|
142829
|
-
resources: { listChanged:
|
|
143030
|
+
resources: { listChanged: true }
|
|
142830
143031
|
}
|
|
142831
143032
|
}).withContext();
|
|
142832
143033
|
server.tools(createTools(server));
|
|
@@ -142921,7 +143122,12 @@ runMain(defineCommand({
|
|
|
142921
143122
|
restriction: {
|
|
142922
143123
|
type: "string",
|
|
142923
143124
|
description: "Restriction rule to deny API access (e.g. \"GET:/inventory/**\"). Can be repeated.",
|
|
142924
|
-
alias: "r"
|
|
143125
|
+
alias: ["r", "restrict"]
|
|
143126
|
+
},
|
|
143127
|
+
allowed: {
|
|
143128
|
+
type: "string",
|
|
143129
|
+
description: "Allow rule to permit API access (e.g. \"GET:/inventory/**\"). Can be repeated. When present, non-matching operations are blocked unless another allow rule matches them.",
|
|
143130
|
+
alias: ["a", "allow"]
|
|
142925
143131
|
},
|
|
142926
143132
|
spec: {
|
|
142927
143133
|
type: "string",
|
|
@@ -142942,12 +143148,20 @@ runMain(defineCommand({
|
|
|
142942
143148
|
if (!selected) throw new Error(`Invalid --spec value "${requested}". Available: ${specs.map((s) => s.version).join(", ")}`);
|
|
142943
143149
|
setCoreOpenApiVersion(selected.version);
|
|
142944
143150
|
consola.info(`Using core OpenAPI snapshot: ${getCoreOpenApiLabel()}`);
|
|
142945
|
-
const
|
|
142946
|
-
const restrictions = (Array.isArray(
|
|
143151
|
+
const rawRestrictions = args.restriction;
|
|
143152
|
+
const { parsedRules: restrictions, failedRules: failedRestrictions } = parseRestrictionRule((Array.isArray(rawRestrictions) ? rawRestrictions : rawRestrictions ? [rawRestrictions] : []).filter((value) => typeof value === "string" && value.length > 0));
|
|
143153
|
+
if (failedRestrictions.length > 0) throw new Error(["One or more restriction flags could not be parsed:", ...failedRestrictions.map((rule) => `- ${rule.rule}: ${rule.reason}`)].join("\n"));
|
|
143154
|
+
const rawAllowed = args.allowed;
|
|
143155
|
+
const { parsedRules: allowRules, failedRules: failedAllowRules } = parseAllowRule((Array.isArray(rawAllowed) ? rawAllowed : rawAllowed ? [rawAllowed] : []).filter((value) => typeof value === "string" && value.length > 0));
|
|
143156
|
+
if (failedAllowRules.length > 0) throw new Error(["One or more allow flags could not be parsed:", ...failedAllowRules.map((rule) => `- ${rule.rule}: ${rule.reason}`)].join("\n"));
|
|
142947
143157
|
if (restrictions.length > 0) consola.info(`Applying ${restrictions.length} restriction rule(s):`, restrictions.map((r) => r.source));
|
|
143158
|
+
if (allowRules.length > 0) consola.info(`Applying ${allowRules.length} allow rule(s):`, allowRules.map((rule) => rule.source));
|
|
142948
143159
|
const transport = new StdioTransport(createC8YMcpServer());
|
|
142949
143160
|
consola.info("Starting MCP server over stdio transport...");
|
|
142950
|
-
transport.listen({
|
|
143161
|
+
transport.listen({
|
|
143162
|
+
restrictions,
|
|
143163
|
+
allowRules
|
|
143164
|
+
});
|
|
142951
143165
|
}
|
|
142952
143166
|
}));
|
|
142953
143167
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mc8yp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management",
|
|
6
6
|
"keywords": [
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@schplitt/eslint-config": "^1.4.0",
|
|
51
51
|
"@types/node": "^24.12.2",
|
|
52
|
+
"changelogithub": "^14.0.0",
|
|
52
53
|
"eslint": "^9.39.4",
|
|
53
54
|
"tsdown": "^0.21.9",
|
|
54
55
|
"typescript": "^5.9.3",
|