mc8yp 2.0.1 → 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 +104 -27
- package/dist/cli.mjs +425 -270
- package/package.json +1 -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:
|
|
175
207
|
|
|
176
|
-
|
|
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
|
|
177
210
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
| `/
|
|
183
|
-
|
|
|
184
|
-
| `
|
|
185
|
-
| `/
|
|
211
|
+
### Path Pattern Examples
|
|
212
|
+
|
|
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
|
|
package/dist/cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
40
40
|
//#endregion
|
|
41
41
|
//#region package.json
|
|
42
42
|
var name = "mc8yp";
|
|
43
|
-
var version = "2.0
|
|
43
|
+
var version = "2.1.0";
|
|
44
44
|
var description = "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management";
|
|
45
45
|
//#endregion
|
|
46
46
|
//#region \0virtual:core-openapi
|
|
@@ -85233,7 +85233,9 @@ function createCodeModeGuidePrompt(server) {
|
|
|
85233
85233
|
description: "Guide for the two code-mode tools: query and execute, including available shapes and examples."
|
|
85234
85234
|
}, () => {
|
|
85235
85235
|
const restrictions = server.ctx.custom?.restrictions ?? [];
|
|
85236
|
-
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` : "";
|
|
85237
85239
|
return prompt.message(`# Cumulocity Code Mode
|
|
85238
85240
|
|
|
85239
85241
|
You have exactly two MCP tools available.
|
|
@@ -85246,8 +85248,8 @@ Use \`query\` when you need to inspect the core OpenAPI spec.
|
|
|
85246
85248
|
- Return the exact value you want back from that function
|
|
85247
85249
|
- Sync and async functions are both supported
|
|
85248
85250
|
- Strings are returned as-is; other results are returned as JSON text
|
|
85249
|
-
-
|
|
85250
|
-
-
|
|
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
|
|
85251
85253
|
|
|
85252
85254
|
### Available Shape
|
|
85253
85255
|
\`\`\`ts
|
|
@@ -85295,8 +85297,8 @@ Use \`execute\` when you want to call the real Cumulocity API.
|
|
|
85295
85297
|
- Return the value you want from that function; async functions are usually the right choice here
|
|
85296
85298
|
- On success, the returned value is sent back in Toon format
|
|
85297
85299
|
- If execution is blocked or fails, execute returns a plain text error message
|
|
85298
|
-
- The current MCP connection may reject restricted method/path combinations before network access
|
|
85299
|
-
- 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
|
|
85300
85302
|
|
|
85301
85303
|
### Available Shape
|
|
85302
85304
|
\`\`\`ts
|
|
@@ -85362,139 +85364,32 @@ function createPrompts(server) {
|
|
|
85362
85364
|
return [createCodeModeGuidePrompt(server)];
|
|
85363
85365
|
}
|
|
85364
85366
|
//#endregion
|
|
85365
|
-
//#region src/
|
|
85366
|
-
|
|
85367
|
-
|
|
85368
|
-
|
|
85369
|
-
|
|
85370
|
-
|
|
85371
|
-
|
|
85372
|
-
|
|
85373
|
-
|
|
85374
|
-
|
|
85375
|
-
|
|
85376
|
-
|
|
85377
|
-
|
|
85378
|
-
|
|
85379
|
-
|
|
85380
|
-
const RESTRICTED_OPERATION_RULES = "x-mc8yp-restrictionRules";
|
|
85381
|
-
const RESTRICTED_OPERATION_TYPE = "x-mc8yp-restrictionType";
|
|
85382
|
-
const RESTRICTED_AGENT_NOTE = "x-mc8yp-agentNote";
|
|
85383
|
-
function parseRestrictionRule(input) {
|
|
85384
|
-
const segmentPattern = /^[A-Za-z0-9._~*-]+$/;
|
|
85385
|
-
const inputs = typeof input === "string" ? [input] : input;
|
|
85386
|
-
const parsedRules = [];
|
|
85387
|
-
const failedRules = [];
|
|
85388
|
-
for (const source of inputs) {
|
|
85389
|
-
if (!source) {
|
|
85390
|
-
failedRules.push({
|
|
85391
|
-
rule: source,
|
|
85392
|
-
reason: "Restriction value must not be empty."
|
|
85393
|
-
});
|
|
85394
|
-
continue;
|
|
85395
|
-
}
|
|
85396
|
-
const sep = source.indexOf(":");
|
|
85397
|
-
if (sep > 0 && !source.startsWith("/")) {
|
|
85398
|
-
const rawMethod = source.slice(0, sep).toUpperCase();
|
|
85399
|
-
const rawPath = source.slice(sep + 1);
|
|
85400
|
-
if (!rawPath) {
|
|
85401
|
-
failedRules.push({
|
|
85402
|
-
rule: source,
|
|
85403
|
-
reason: "Restriction path pattern must not be empty."
|
|
85404
|
-
});
|
|
85405
|
-
continue;
|
|
85406
|
-
}
|
|
85407
|
-
if (rawMethod && rawMethod !== "*" && !HTTP_METHOD_SET.has(rawMethod)) {
|
|
85408
|
-
failedRules.push({
|
|
85409
|
-
rule: source,
|
|
85410
|
-
reason: `Unsupported restriction method "${source.slice(0, sep)}".`
|
|
85411
|
-
});
|
|
85412
|
-
continue;
|
|
85413
|
-
}
|
|
85414
|
-
if (rawPath.includes("?") || rawPath.includes("#")) {
|
|
85415
|
-
failedRules.push({
|
|
85416
|
-
rule: source,
|
|
85417
|
-
reason: `Restriction pattern "${rawPath}" must not include query strings or fragments.`
|
|
85418
|
-
});
|
|
85419
|
-
continue;
|
|
85420
|
-
}
|
|
85421
|
-
if (!rawPath.startsWith("/")) {
|
|
85422
|
-
failedRules.push({
|
|
85423
|
-
rule: source,
|
|
85424
|
-
reason: "Restriction path pattern must start with \"/\"."
|
|
85425
|
-
});
|
|
85426
|
-
continue;
|
|
85427
|
-
}
|
|
85428
|
-
const rawPathSegments = rawPath === "/" ? [] : rawPath.slice(1).split("/");
|
|
85429
|
-
if (rawPathSegments.some((segment) => segment.length === 0)) {
|
|
85430
|
-
failedRules.push({
|
|
85431
|
-
rule: source,
|
|
85432
|
-
reason: "Restriction path pattern must not contain empty segments."
|
|
85433
|
-
});
|
|
85434
|
-
continue;
|
|
85435
|
-
}
|
|
85436
|
-
const invalidRawPathSegment = rawPathSegments.find((segment) => {
|
|
85437
|
-
if (segment === "**") return false;
|
|
85438
|
-
return segment.includes("**") || segment === "." || segment === ".." || !segmentPattern.test(segment);
|
|
85439
|
-
});
|
|
85440
|
-
if (invalidRawPathSegment) {
|
|
85441
|
-
failedRules.push({
|
|
85442
|
-
rule: source,
|
|
85443
|
-
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.`
|
|
85444
|
-
});
|
|
85445
|
-
continue;
|
|
85446
|
-
}
|
|
85447
|
-
parsedRules.push({
|
|
85448
|
-
method: !rawMethod || rawMethod === "*" ? "*" : rawMethod,
|
|
85449
|
-
pathPattern: rawPath,
|
|
85450
|
-
source
|
|
85451
|
-
});
|
|
85452
|
-
continue;
|
|
85453
|
-
}
|
|
85454
|
-
if (source.includes("?") || source.includes("#")) {
|
|
85455
|
-
failedRules.push({
|
|
85456
|
-
rule: source,
|
|
85457
|
-
reason: `Restriction pattern "${source}" must not include query strings or fragments.`
|
|
85458
|
-
});
|
|
85459
|
-
continue;
|
|
85460
|
-
}
|
|
85461
|
-
if (!source.startsWith("/")) {
|
|
85462
|
-
failedRules.push({
|
|
85463
|
-
rule: source,
|
|
85464
|
-
reason: "Restriction path pattern must start with \"/\"."
|
|
85465
|
-
});
|
|
85466
|
-
continue;
|
|
85467
|
-
}
|
|
85468
|
-
const sourceSegments = source === "/" ? [] : source.slice(1).split("/");
|
|
85469
|
-
if (sourceSegments.some((segment) => segment.length === 0)) {
|
|
85470
|
-
failedRules.push({
|
|
85471
|
-
rule: source,
|
|
85472
|
-
reason: "Restriction path pattern must not contain empty segments."
|
|
85473
|
-
});
|
|
85474
|
-
continue;
|
|
85475
|
-
}
|
|
85476
|
-
const invalidSourceSegment = sourceSegments.find((segment) => {
|
|
85477
|
-
if (segment === "**") return false;
|
|
85478
|
-
return segment.includes("**") || segment === "." || segment === ".." || !segmentPattern.test(segment);
|
|
85479
|
-
});
|
|
85480
|
-
if (invalidSourceSegment) {
|
|
85481
|
-
failedRules.push({
|
|
85482
|
-
rule: source,
|
|
85483
|
-
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.`
|
|
85484
|
-
});
|
|
85485
|
-
continue;
|
|
85486
|
-
}
|
|
85487
|
-
parsedRules.push({
|
|
85488
|
-
method: "*",
|
|
85489
|
-
pathPattern: source,
|
|
85490
|
-
source
|
|
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;
|
|
85375
|
+
}
|
|
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);
|
|
85491
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()?.();
|
|
85390
|
+
};
|
|
85492
85391
|
}
|
|
85493
|
-
|
|
85494
|
-
parsedRules,
|
|
85495
|
-
failedRules
|
|
85496
|
-
};
|
|
85497
|
-
}
|
|
85392
|
+
};
|
|
85498
85393
|
//#endregion
|
|
85499
85394
|
//#region src/utils/restriction-matcher.ts
|
|
85500
85395
|
function escapeRestrictionRegex(value) {
|
|
@@ -85530,7 +85425,7 @@ function compileRestrictionRule(rule) {
|
|
|
85530
85425
|
function matchesCompiledRule(rule, method, pathSegments) {
|
|
85531
85426
|
return (rule.method === "*" || rule.method === method) && matchCompiledSegments(rule.segments, pathSegments);
|
|
85532
85427
|
}
|
|
85533
|
-
function
|
|
85428
|
+
function findMatchingRules(rules, method, pathname) {
|
|
85534
85429
|
const normalizedMethod = typeof method === "string" ? method.trim().toUpperCase() : "";
|
|
85535
85430
|
const pathSegments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
85536
85431
|
return rules.filter((rule) => {
|
|
@@ -85539,88 +85434,26 @@ function findBlockingRestrictions(rules, method, pathname) {
|
|
|
85539
85434
|
return matchesCompiledRule(compiledRule, normalizedMethod, pathSegments);
|
|
85540
85435
|
});
|
|
85541
85436
|
}
|
|
85542
|
-
|
|
85543
|
-
|
|
85544
|
-
const OPENAPI_OPERATION_METHODS = [
|
|
85545
|
-
"delete",
|
|
85546
|
-
"get",
|
|
85547
|
-
"head",
|
|
85548
|
-
"options",
|
|
85549
|
-
"patch",
|
|
85550
|
-
"post",
|
|
85551
|
-
"put",
|
|
85552
|
-
"trace"
|
|
85553
|
-
];
|
|
85554
|
-
function annotateRestrictedOperation(operation, matchingRules) {
|
|
85555
|
-
return {
|
|
85556
|
-
...operation,
|
|
85557
|
-
[RESTRICTED_OPERATION_FLAG]: true,
|
|
85558
|
-
[RESTRICTED_OPERATION_TYPE]: "deny",
|
|
85559
|
-
[RESTRICTED_OPERATION_RULES]: matchingRules.map((rule) => rule.source),
|
|
85560
|
-
[RESTRICTED_OPERATION_MESSAGE]: "This operation is blocked by the current MCP connection restrictions.",
|
|
85561
|
-
[RESTRICTED_AGENT_NOTE]: "The route exists, but it is intentionally restricted for this MCP connection."
|
|
85562
|
-
};
|
|
85437
|
+
function findBlockingRestrictions(rules, method, pathname) {
|
|
85438
|
+
return findMatchingRules(rules, method, pathname);
|
|
85563
85439
|
}
|
|
85564
|
-
function
|
|
85565
|
-
|
|
85566
|
-
|
|
85567
|
-
|
|
85568
|
-
|
|
85569
|
-
|
|
85570
|
-
|
|
85571
|
-
|
|
85572
|
-
|
|
85573
|
-
if (!operation || typeof operation !== "object") continue;
|
|
85574
|
-
const matchingRules = compiledRules.filter((rule) => matchesCompiledRule(rule, method.toUpperCase(), pathSegments));
|
|
85575
|
-
if (matchingRules.length === 0) continue;
|
|
85576
|
-
nextPathItem ??= { ...pathItem };
|
|
85577
|
-
nextPathItem[method] = annotateRestrictedOperation(operation, matchingRules);
|
|
85578
|
-
}
|
|
85579
|
-
if (nextPathItem) {
|
|
85580
|
-
nextPaths ??= { ...spec.paths };
|
|
85581
|
-
nextPaths[path] = nextPathItem;
|
|
85582
|
-
}
|
|
85583
|
-
}
|
|
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 };
|
|
85584
85449
|
return {
|
|
85585
|
-
|
|
85586
|
-
|
|
85587
|
-
[RESTRICTION_EXTENSION_KEY]: {
|
|
85588
|
-
mode: "deny",
|
|
85589
|
-
rules: rules.map((rule) => rule.source),
|
|
85590
|
-
message: "Operations marked with x-mc8yp-restricted are intentionally blocked for the current MCP connection."
|
|
85591
|
-
}
|
|
85450
|
+
blocked: true,
|
|
85451
|
+
blockedBy: "allow"
|
|
85592
85452
|
};
|
|
85593
85453
|
}
|
|
85594
85454
|
//#endregion
|
|
85595
|
-
//#region src/codemode/semaphore.ts
|
|
85596
|
-
var AsyncSemaphore = class {
|
|
85597
|
-
#maxConcurrency;
|
|
85598
|
-
#activeCount = 0;
|
|
85599
|
-
#waiters = [];
|
|
85600
|
-
constructor(maxConcurrency) {
|
|
85601
|
-
if (!Number.isInteger(maxConcurrency) || maxConcurrency <= 0) throw new Error("maxConcurrency must be a positive integer.");
|
|
85602
|
-
this.#maxConcurrency = maxConcurrency;
|
|
85603
|
-
}
|
|
85604
|
-
get activeCount() {
|
|
85605
|
-
return this.#activeCount;
|
|
85606
|
-
}
|
|
85607
|
-
async acquire() {
|
|
85608
|
-
if (this.#activeCount >= this.#maxConcurrency) await new Promise((resolve) => {
|
|
85609
|
-
this.#waiters.push(resolve);
|
|
85610
|
-
});
|
|
85611
|
-
this.#activeCount += 1;
|
|
85612
|
-
let released = false;
|
|
85613
|
-
return () => {
|
|
85614
|
-
if (released) return;
|
|
85615
|
-
released = true;
|
|
85616
|
-
this.#activeCount -= 1;
|
|
85617
|
-
this.#waiters.shift()?.();
|
|
85618
|
-
};
|
|
85619
|
-
}
|
|
85620
|
-
};
|
|
85621
|
-
//#endregion
|
|
85622
85455
|
//#region src/codemode/network-permissions.ts
|
|
85623
|
-
function createNetworkPermissionDecision(tenantUrl, request,
|
|
85456
|
+
function createNetworkPermissionDecision(tenantUrl, request, restrictions = [], allowRules = []) {
|
|
85624
85457
|
const tenantHostname = new URL(tenantUrl).hostname;
|
|
85625
85458
|
if (request.op !== "connect") return {
|
|
85626
85459
|
allow: false,
|
|
@@ -85631,11 +85464,28 @@ function createNetworkPermissionDecision(tenantUrl, request, rules = []) {
|
|
|
85631
85464
|
reason: `Network connect blocked: only ${tenantHostname} is allowed in execute mode.`
|
|
85632
85465
|
};
|
|
85633
85466
|
if (typeof request.url === "string") {
|
|
85634
|
-
const
|
|
85635
|
-
|
|
85636
|
-
|
|
85637
|
-
|
|
85638
|
-
|
|
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
|
+
};
|
|
85488
|
+
}
|
|
85639
85489
|
}
|
|
85640
85490
|
return { allow: true };
|
|
85641
85491
|
}
|
|
@@ -142411,15 +142261,268 @@ function createC8yAuthHeaders(auth) {
|
|
|
142411
142261
|
throw new Error("Invalid authentication credentials");
|
|
142412
142262
|
}
|
|
142413
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
|
|
142414
142510
|
//#region src/codemode/execute.ts
|
|
142415
142511
|
const NO_DEFAULT_EXPORT_MESSAGE = "Execution completed without returning a value.";
|
|
142416
142512
|
const QUERY_ENTRY_PATH = "/codemode-query.mjs";
|
|
142417
142513
|
const EXECUTE_ENTRY_PATH = "/codemode-execute.mjs";
|
|
142418
142514
|
const runtimeSemaphore = new AsyncSemaphore(3);
|
|
142419
142515
|
const BLOCKED_REQUEST_PREFIX = "Request blocked by MCP connection policy.";
|
|
142420
|
-
function serializeExecuteConfig(tenantUrl, headers, restrictions) {
|
|
142516
|
+
function serializeExecuteConfig(tenantUrl, headers, restrictions, allowRules) {
|
|
142421
142517
|
const normalizedTenantUrl = new URL(tenantUrl).toString();
|
|
142422
|
-
const serializedRestrictions = restrictions.map(({ method, pathPattern, source }) => ({
|
|
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,
|
|
142423
142526
|
method,
|
|
142424
142527
|
pathPattern,
|
|
142425
142528
|
source
|
|
@@ -142427,7 +142530,8 @@ function serializeExecuteConfig(tenantUrl, headers, restrictions) {
|
|
|
142427
142530
|
return JSON.stringify({
|
|
142428
142531
|
tenantUrl: normalizedTenantUrl,
|
|
142429
142532
|
authHeaders: headers,
|
|
142430
|
-
restrictions: serializedRestrictions
|
|
142533
|
+
restrictions: serializedRestrictions,
|
|
142534
|
+
allowRules: serializedAllowRules
|
|
142431
142535
|
});
|
|
142432
142536
|
}
|
|
142433
142537
|
function createQueryRuntime() {
|
|
@@ -142444,11 +142548,11 @@ function createQueryRuntime() {
|
|
|
142444
142548
|
cpuTimeLimitMs: 5e4
|
|
142445
142549
|
});
|
|
142446
142550
|
}
|
|
142447
|
-
function createExecuteRuntime(tenantUrl, restrictions) {
|
|
142551
|
+
function createExecuteRuntime(tenantUrl, restrictions, allowRules) {
|
|
142448
142552
|
return new NodeRuntime({
|
|
142449
142553
|
systemDriver: createNodeDriver({
|
|
142450
142554
|
useDefaultNetwork: true,
|
|
142451
|
-
permissions: { network: (request) => createNetworkPermissionDecision(tenantUrl, request, restrictions) }
|
|
142555
|
+
permissions: { network: (request) => createNetworkPermissionDecision(tenantUrl, request, restrictions, allowRules) }
|
|
142452
142556
|
}),
|
|
142453
142557
|
runtimeDriverFactory: createNodeRuntimeDriverFactory(),
|
|
142454
142558
|
memoryLimit: 128,
|
|
@@ -142460,11 +142564,11 @@ function normalizeCode(functionCode) {
|
|
|
142460
142564
|
normalized = normalized.replace(/^```(?:js|javascript|ts|typescript)?\s*/i, "").replace(/\s*```$/, "").trim();
|
|
142461
142565
|
return normalized;
|
|
142462
142566
|
}
|
|
142463
|
-
function buildQueryScript(sourceCode,
|
|
142464
|
-
const
|
|
142567
|
+
function buildQueryScript(sourceCode, _restrictions, _allowRules) {
|
|
142568
|
+
const spec = getCoreOpenApiSpec();
|
|
142465
142569
|
const functionExpression = normalizeCode(sourceCode);
|
|
142466
142570
|
return [
|
|
142467
|
-
`const spec = ${JSON.stringify(
|
|
142571
|
+
`const spec = ${JSON.stringify(spec)};`,
|
|
142468
142572
|
`const __mc8ypQuery = (${functionExpression});`,
|
|
142469
142573
|
"",
|
|
142470
142574
|
"if (typeof __mc8ypQuery !== \"function\") {",
|
|
@@ -142474,20 +142578,23 @@ function buildQueryScript(sourceCode, restrictions) {
|
|
|
142474
142578
|
"export default await __mc8ypQuery();"
|
|
142475
142579
|
].join("\n\n");
|
|
142476
142580
|
}
|
|
142477
|
-
function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
142581
|
+
function buildExecutePrelude(tenantUrl, headers, restrictions = [], allowRules = []) {
|
|
142478
142582
|
return [
|
|
142479
142583
|
"const cumulocity = Object.freeze((() => {",
|
|
142480
|
-
` const config = JSON.parse(${JSON.stringify(serializeExecuteConfig(tenantUrl, headers, restrictions))});`,
|
|
142584
|
+
` const config = JSON.parse(${JSON.stringify(serializeExecuteConfig(tenantUrl, headers, restrictions, allowRules))});`,
|
|
142481
142585
|
" if (!config || typeof config !== \"object\") {",
|
|
142482
142586
|
" throw new TypeError(\"Invalid execute configuration.\");",
|
|
142483
142587
|
" }",
|
|
142484
|
-
" const { tenantUrl, authHeaders, restrictions } = config;",
|
|
142588
|
+
" const { tenantUrl, authHeaders, restrictions, allowRules } = config;",
|
|
142485
142589
|
" if (typeof tenantUrl !== \"string\") {",
|
|
142486
142590
|
" throw new TypeError(\"Execute configuration must contain a string tenant URL.\");",
|
|
142487
142591
|
" }",
|
|
142488
|
-
" if (!Array.isArray(restrictions) || restrictions.some((rule) => !rule || typeof rule !== \"object\" || typeof rule.method !== \"string\" || typeof rule.pathPattern !== \"string\" || typeof rule.source !== \"string\")) {",
|
|
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\")) {",
|
|
142489
142593
|
" throw new TypeError(\"Execute configuration must contain valid restriction rules.\");",
|
|
142490
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.\");",
|
|
142597
|
+
" }",
|
|
142491
142598
|
" const resolveUrl = (descriptor) => {",
|
|
142492
142599
|
" return new URL(descriptor, tenantUrl.endsWith(\"/\") ? tenantUrl : tenantUrl + \"/\");",
|
|
142493
142600
|
" };",
|
|
@@ -142506,16 +142613,31 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142506
142613
|
` const compileRestrictionRule = ${compileRestrictionRule.toString()};`,
|
|
142507
142614
|
` const matchesCompiledRule = ${matchesCompiledRule.toString()};`,
|
|
142508
142615
|
" const compiledRestrictions = restrictions.map(compileRestrictionRule);",
|
|
142509
|
-
" const
|
|
142616
|
+
" const compiledAllowRules = allowRules.map(compileRestrictionRule);",
|
|
142617
|
+
" const findMatchingRules = (compiledRules, method, pathname) => {",
|
|
142510
142618
|
" const normalizedMethod = typeof method === \"string\" ? method.trim().toUpperCase() : \"\";",
|
|
142511
142619
|
" const pathSegments = pathname === \"/\" ? [] : pathname.slice(1).split(\"/\");",
|
|
142512
|
-
" return
|
|
142620
|
+
" return compiledRules.filter((rule) => {",
|
|
142513
142621
|
" if (!normalizedMethod) {",
|
|
142514
142622
|
" return rule.method === \"*\" && matchCompiledSegments(rule.segments, pathSegments);",
|
|
142515
142623
|
" }",
|
|
142516
142624
|
" return matchesCompiledRule(rule, normalizedMethod, pathSegments);",
|
|
142517
142625
|
" });",
|
|
142518
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
|
+
" };",
|
|
142519
142641
|
" const validateRequestMethod = (method) => {",
|
|
142520
142642
|
" if (typeof method !== \"string\" || method.trim().length === 0) {",
|
|
142521
142643
|
" throw new TypeError(\"request method must be a non-empty string\");",
|
|
@@ -142526,22 +142648,42 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142526
142648
|
" }",
|
|
142527
142649
|
" return normalizedMethod;",
|
|
142528
142650
|
" };",
|
|
142529
|
-
" const formatBlockedRequestMessage = (method, path,
|
|
142530
|
-
"
|
|
142531
|
-
"
|
|
142532
|
-
"
|
|
142533
|
-
"
|
|
142534
|
-
"
|
|
142535
|
-
"
|
|
142536
|
-
"
|
|
142537
|
-
"
|
|
142538
|
-
"
|
|
142539
|
-
"
|
|
142540
|
-
"
|
|
142541
|
-
"
|
|
142542
|
-
"
|
|
142543
|
-
"
|
|
142544
|
-
"
|
|
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
|
+
" };",
|
|
142545
142687
|
" const normalizeBody = (headers, body) => {",
|
|
142546
142688
|
" if (body == null || typeof body === \"string\") {",
|
|
142547
142689
|
" return body;",
|
|
@@ -142573,9 +142715,9 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142573
142715
|
" }",
|
|
142574
142716
|
" const method = validateRequestMethod(init.method);",
|
|
142575
142717
|
" const resolvedUrl = resolveUrl(path);",
|
|
142576
|
-
" const
|
|
142577
|
-
" if (
|
|
142578
|
-
" throw new Error(formatBlockedRequestMessage(method, resolvedUrl.pathname,
|
|
142718
|
+
" const accessDecision = evaluateAccessPolicy(method, resolvedUrl.pathname);",
|
|
142719
|
+
" if (accessDecision.blocked) {",
|
|
142720
|
+
" throw new Error(formatBlockedRequestMessage(method, resolvedUrl.pathname, accessDecision));",
|
|
142579
142721
|
" }",
|
|
142580
142722
|
" const headers = new Headers(init.headers ?? {});",
|
|
142581
142723
|
" for (const [key, value] of Object.entries(authHeaders)) {",
|
|
@@ -142600,10 +142742,10 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142600
142742
|
"})());"
|
|
142601
142743
|
].join("\n\n");
|
|
142602
142744
|
}
|
|
142603
|
-
function buildExecuteScript(sourceCode, tenantUrl, headers, restrictions = []) {
|
|
142745
|
+
function buildExecuteScript(sourceCode, tenantUrl, headers, restrictions = [], allowRules = []) {
|
|
142604
142746
|
const functionExpression = normalizeCode(sourceCode);
|
|
142605
142747
|
return [
|
|
142606
|
-
buildExecutePrelude(tenantUrl, headers, restrictions),
|
|
142748
|
+
buildExecutePrelude(tenantUrl, headers, restrictions, allowRules),
|
|
142607
142749
|
`const __mc8ypExecute = (${functionExpression});`,
|
|
142608
142750
|
"",
|
|
142609
142751
|
"const __mc8ypErrorMessage = (error) => error instanceof Error ? error.message : String(error);",
|
|
@@ -142637,9 +142779,9 @@ function extractDefaultExport(exportsObject) {
|
|
|
142637
142779
|
if (typeof exportsObject !== "undefined") return exportsObject;
|
|
142638
142780
|
throw new Error(NO_DEFAULT_EXPORT_MESSAGE);
|
|
142639
142781
|
}
|
|
142640
|
-
async function runExecuteScript(code, tenantUrl, restrictions) {
|
|
142782
|
+
async function runExecuteScript(code, tenantUrl, restrictions, allowRules) {
|
|
142641
142783
|
const release = await runtimeSemaphore.acquire();
|
|
142642
|
-
const runtime = createExecuteRuntime(tenantUrl, restrictions);
|
|
142784
|
+
const runtime = createExecuteRuntime(tenantUrl, restrictions, allowRules);
|
|
142643
142785
|
try {
|
|
142644
142786
|
const result = await runtime.run(code, EXECUTE_ENTRY_PATH);
|
|
142645
142787
|
if (result.code !== 0) {
|
|
@@ -142669,14 +142811,14 @@ async function runModule(code, entryPath, runtime) {
|
|
|
142669
142811
|
release();
|
|
142670
142812
|
}
|
|
142671
142813
|
}
|
|
142672
|
-
async function query(functionCode, restrictions = []) {
|
|
142673
|
-
const result = await runQueryScript(buildQueryScript(functionCode, restrictions));
|
|
142814
|
+
async function query(functionCode, restrictions = [], allowRules = []) {
|
|
142815
|
+
const result = await runQueryScript(buildQueryScript(functionCode, restrictions, allowRules));
|
|
142674
142816
|
return typeof result === "string" ? result : JSON.stringify(result);
|
|
142675
142817
|
}
|
|
142676
|
-
async function execute(functionCode, input, restrictions = []) {
|
|
142818
|
+
async function execute(functionCode, input, restrictions = [], allowRules = []) {
|
|
142677
142819
|
const auth = await resolveC8yAuth(input);
|
|
142678
142820
|
const authHeaders = createC8yAuthHeaders(auth);
|
|
142679
|
-
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);
|
|
142680
142822
|
if (result.status === "success") return encode(result.result);
|
|
142681
142823
|
return result.error.message;
|
|
142682
142824
|
}
|
|
@@ -142746,8 +142888,8 @@ Recommended shapes:
|
|
|
142746
142888
|
|
|
142747
142889
|
If your function returns a string, it is returned as-is. Otherwise the result is returned as JSON text.
|
|
142748
142890
|
|
|
142749
|
-
The
|
|
142750
|
-
|
|
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.
|
|
142751
142893
|
|
|
142752
142894
|
Examples:
|
|
142753
142895
|
() => {
|
|
@@ -142771,7 +142913,7 @@ Examples:
|
|
|
142771
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.") })
|
|
142772
142914
|
}, async (input) => {
|
|
142773
142915
|
try {
|
|
142774
|
-
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 ?? []));
|
|
142775
142917
|
} catch (error) {
|
|
142776
142918
|
const message = error instanceof Error ? error.message : String(error);
|
|
142777
142919
|
return tool.error(message);
|
|
@@ -142812,7 +142954,8 @@ Tool output behavior:
|
|
|
142812
142954
|
- On success, the actual function result is returned in Toon format.
|
|
142813
142955
|
- On blocked or failed execution, the tool returns a plain text message.
|
|
142814
142956
|
|
|
142815
|
-
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.
|
|
142816
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.
|
|
142817
142960
|
|
|
142818
142961
|
${getExecuteEnvironmentNote()}
|
|
@@ -142846,7 +142989,7 @@ async () => {
|
|
|
142846
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.") }))
|
|
142847
142990
|
}, async (input) => {
|
|
142848
142991
|
try {
|
|
142849
|
-
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 ?? []));
|
|
142850
142993
|
} catch (error) {
|
|
142851
142994
|
const message = error instanceof Error ? error.message : String(error);
|
|
142852
142995
|
return tool.error(message);
|
|
@@ -142979,7 +143122,12 @@ runMain(defineCommand({
|
|
|
142979
143122
|
restriction: {
|
|
142980
143123
|
type: "string",
|
|
142981
143124
|
description: "Restriction rule to deny API access (e.g. \"GET:/inventory/**\"). Can be repeated.",
|
|
142982
|
-
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"]
|
|
142983
143131
|
},
|
|
142984
143132
|
spec: {
|
|
142985
143133
|
type: "string",
|
|
@@ -143000,13 +143148,20 @@ runMain(defineCommand({
|
|
|
143000
143148
|
if (!selected) throw new Error(`Invalid --spec value "${requested}". Available: ${specs.map((s) => s.version).join(", ")}`);
|
|
143001
143149
|
setCoreOpenApiVersion(selected.version);
|
|
143002
143150
|
consola.info(`Using core OpenAPI snapshot: ${getCoreOpenApiLabel()}`);
|
|
143003
|
-
const
|
|
143004
|
-
const { parsedRules: restrictions, failedRules } = parseRestrictionRule((Array.isArray(
|
|
143005
|
-
if (
|
|
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"));
|
|
143006
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));
|
|
143007
143159
|
const transport = new StdioTransport(createC8YMcpServer());
|
|
143008
143160
|
consola.info("Starting MCP server over stdio transport...");
|
|
143009
|
-
transport.listen({
|
|
143161
|
+
transport.listen({
|
|
143162
|
+
restrictions,
|
|
143163
|
+
allowRules
|
|
143164
|
+
});
|
|
143010
143165
|
}
|
|
143011
143166
|
}));
|
|
143012
143167
|
//#endregion
|