mc8yp 2.0.0 → 2.0.1
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 +5 -1
- package/dist/cli.mjs +193 -134
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -234,6 +234,10 @@ The versions built are driven by [`openapi-versions.json`](openapi-versions.json
|
|
|
234
234
|
|
|
235
235
|
Use the dedicated packaging command after `pnpm build` to create Docker-based Cumulocity release zips:
|
|
236
236
|
|
|
237
|
+
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.
|
|
238
|
+
|
|
239
|
+
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.
|
|
240
|
+
|
|
237
241
|
```sh
|
|
238
242
|
pnpm package:microservices
|
|
239
243
|
```
|
|
@@ -251,7 +255,7 @@ The GitHub release workflow uses that packaging command when building tagged rel
|
|
|
251
255
|
|
|
252
256
|
### Prerequisites
|
|
253
257
|
|
|
254
|
-
- Node.js ≥
|
|
258
|
+
- Node.js ≥24.0.0
|
|
255
259
|
- pnpm
|
|
256
260
|
|
|
257
261
|
### 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.0.
|
|
43
|
+
var version = "2.0.1";
|
|
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
|
|
@@ -85363,7 +85362,7 @@ function createPrompts(server) {
|
|
|
85363
85362
|
return [createCodeModeGuidePrompt(server)];
|
|
85364
85363
|
}
|
|
85365
85364
|
//#endregion
|
|
85366
|
-
//#region src/utils/
|
|
85365
|
+
//#region src/utils/restrictions.ts
|
|
85367
85366
|
const HTTP_METHODS = [
|
|
85368
85367
|
"DELETE",
|
|
85369
85368
|
"GET",
|
|
@@ -85374,49 +85373,130 @@ const HTTP_METHODS = [
|
|
|
85374
85373
|
"PUT",
|
|
85375
85374
|
"TRACE"
|
|
85376
85375
|
];
|
|
85377
|
-
const HTTP_METHOD_SET
|
|
85378
|
-
|
|
85379
|
-
|
|
85380
|
-
|
|
85381
|
-
|
|
85382
|
-
|
|
85383
|
-
|
|
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.`);
|
|
85396
|
-
}
|
|
85397
|
-
return pathPattern;
|
|
85398
|
-
}
|
|
85376
|
+
const HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
85377
|
+
const RESTRICTION_EXTENSION_KEY = "x-mc8yp-restrictions";
|
|
85378
|
+
const RESTRICTED_OPERATION_FLAG = "x-mc8yp-restricted";
|
|
85379
|
+
const RESTRICTED_OPERATION_MESSAGE = "x-mc8yp-restrictionMessage";
|
|
85380
|
+
const RESTRICTED_OPERATION_RULES = "x-mc8yp-restrictionRules";
|
|
85381
|
+
const RESTRICTED_OPERATION_TYPE = "x-mc8yp-restrictionType";
|
|
85382
|
+
const RESTRICTED_AGENT_NOTE = "x-mc8yp-agentNote";
|
|
85399
85383
|
function parseRestrictionRule(input) {
|
|
85400
|
-
const
|
|
85401
|
-
|
|
85402
|
-
const
|
|
85403
|
-
|
|
85404
|
-
|
|
85405
|
-
|
|
85406
|
-
|
|
85407
|
-
|
|
85408
|
-
|
|
85409
|
-
|
|
85410
|
-
|
|
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,
|
|
85411
85490
|
source
|
|
85412
|
-
};
|
|
85491
|
+
});
|
|
85413
85492
|
}
|
|
85414
85493
|
return {
|
|
85415
|
-
|
|
85416
|
-
|
|
85417
|
-
source
|
|
85494
|
+
parsedRules,
|
|
85495
|
+
failedRules
|
|
85418
85496
|
};
|
|
85419
85497
|
}
|
|
85498
|
+
//#endregion
|
|
85499
|
+
//#region src/utils/restriction-matcher.ts
|
|
85420
85500
|
function escapeRestrictionRegex(value) {
|
|
85421
85501
|
let escaped = "";
|
|
85422
85502
|
for (const char of value) escaped += "\\^$.*+?()[]{}|".includes(char) ? `\\${char}` : char;
|
|
@@ -85429,13 +85509,13 @@ function compileRestrictionSegment(segment) {
|
|
|
85429
85509
|
}
|
|
85430
85510
|
function matchCompiledSegments(pattern, path, pi = 0, si = 0) {
|
|
85431
85511
|
while (pi < pattern.length) {
|
|
85432
|
-
const
|
|
85433
|
-
if (
|
|
85512
|
+
const segment = pattern[pi];
|
|
85513
|
+
if (segment === "**") {
|
|
85434
85514
|
if (pi === pattern.length - 1) return true;
|
|
85435
85515
|
for (let i = si; i <= path.length; i++) if (matchCompiledSegments(pattern, path, pi + 1, i)) return true;
|
|
85436
85516
|
return false;
|
|
85437
85517
|
}
|
|
85438
|
-
if (si >= path.length || !(
|
|
85518
|
+
if (si >= path.length || !(segment instanceof RegExp) || !segment.test(path[si])) return false;
|
|
85439
85519
|
pi++;
|
|
85440
85520
|
si++;
|
|
85441
85521
|
}
|
|
@@ -85450,69 +85530,14 @@ function compileRestrictionRule(rule) {
|
|
|
85450
85530
|
function matchesCompiledRule(rule, method, pathSegments) {
|
|
85451
85531
|
return (rule.method === "*" || rule.method === method) && matchCompiledSegments(rule.segments, pathSegments);
|
|
85452
85532
|
}
|
|
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 || "/";
|
|
85481
|
-
}
|
|
85482
|
-
function normalizeMethod(value) {
|
|
85483
|
-
const upper = value?.trim().toUpperCase() || "GET";
|
|
85484
|
-
if (HTTP_METHOD_SET.has(upper)) return upper;
|
|
85485
|
-
throw new Error(`Unsupported HTTP method "${value}".`);
|
|
85486
|
-
}
|
|
85487
|
-
function evaluateRestrictions(rules, method, path) {
|
|
85488
|
-
const m = normalizeMethod(method);
|
|
85489
|
-
const p = normalizePath(path);
|
|
85490
|
-
const segs = p === "/" ? [] : p.slice(1).split("/");
|
|
85491
|
-
return {
|
|
85492
|
-
method: m,
|
|
85493
|
-
path: p,
|
|
85494
|
-
matchingRules: rules.filter((rule) => matchesCompiledRule(compileRestrictionRule(rule), m, segs))
|
|
85495
|
-
};
|
|
85496
|
-
}
|
|
85497
|
-
function createNetworkPermissionDecision(tenantUrl, request, rules = []) {
|
|
85498
|
-
const tenantHostname = new URL(tenantUrl).hostname;
|
|
85499
|
-
if (request.op !== "connect") return {
|
|
85500
|
-
allow: false,
|
|
85501
|
-
reason: `Unsupported network operation "${request.op}". Only "connect" is allowed.`
|
|
85502
|
-
};
|
|
85503
|
-
if (request.hostname !== tenantHostname) return {
|
|
85504
|
-
allow: false,
|
|
85505
|
-
reason: `Network connect blocked: only ${tenantHostname} is allowed in execute mode.`
|
|
85506
|
-
};
|
|
85507
|
-
if (typeof request.method === "string") {
|
|
85508
|
-
const requestPath = typeof request.url === "string" ? new URL(request.url).pathname : "/";
|
|
85509
|
-
const restrictionMatch = evaluateRestrictions(rules, request.method, requestPath);
|
|
85510
|
-
if (restrictionMatch.matchingRules.length > 0) return {
|
|
85511
|
-
allow: false,
|
|
85512
|
-
reason: `Network connect blocked by MCP restrictions: ${restrictionMatch.matchingRules.map((rule) => rule.source).join(", ")}`
|
|
85513
|
-
};
|
|
85514
|
-
}
|
|
85515
|
-
return { allow: true };
|
|
85533
|
+
function findBlockingRestrictions(rules, method, pathname) {
|
|
85534
|
+
const normalizedMethod = typeof method === "string" ? method.trim().toUpperCase() : "";
|
|
85535
|
+
const pathSegments = pathname === "/" ? [] : pathname.slice(1).split("/");
|
|
85536
|
+
return rules.filter((rule) => {
|
|
85537
|
+
const compiledRule = compileRestrictionRule(rule);
|
|
85538
|
+
if (!normalizedMethod) return compiledRule.method === "*" && matchCompiledSegments(compiledRule.segments, pathSegments);
|
|
85539
|
+
return matchesCompiledRule(compiledRule, normalizedMethod, pathSegments);
|
|
85540
|
+
});
|
|
85516
85541
|
}
|
|
85517
85542
|
//#endregion
|
|
85518
85543
|
//#region src/codemode/openapi-restrictions.ts
|
|
@@ -85594,6 +85619,27 @@ var AsyncSemaphore = class {
|
|
|
85594
85619
|
}
|
|
85595
85620
|
};
|
|
85596
85621
|
//#endregion
|
|
85622
|
+
//#region src/codemode/network-permissions.ts
|
|
85623
|
+
function createNetworkPermissionDecision(tenantUrl, request, rules = []) {
|
|
85624
|
+
const tenantHostname = new URL(tenantUrl).hostname;
|
|
85625
|
+
if (request.op !== "connect") return {
|
|
85626
|
+
allow: false,
|
|
85627
|
+
reason: `Unsupported network operation "${request.op}". Only "connect" is allowed.`
|
|
85628
|
+
};
|
|
85629
|
+
if (request.hostname !== tenantHostname) return {
|
|
85630
|
+
allow: false,
|
|
85631
|
+
reason: `Network connect blocked: only ${tenantHostname} is allowed in execute mode.`
|
|
85632
|
+
};
|
|
85633
|
+
if (typeof request.url === "string") {
|
|
85634
|
+
const blockingRules = findBlockingRestrictions(rules, typeof request.method === "string" ? request.method.trim() : void 0, new URL(request.url).pathname);
|
|
85635
|
+
if (blockingRules.length > 0) return {
|
|
85636
|
+
allow: false,
|
|
85637
|
+
reason: `Network connect blocked by MCP restrictions: ${blockingRules.map((rule) => rule.source).join(", ")}`
|
|
85638
|
+
};
|
|
85639
|
+
}
|
|
85640
|
+
return { allow: true };
|
|
85641
|
+
}
|
|
85642
|
+
//#endregion
|
|
85597
85643
|
//#region node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
|
|
85598
85644
|
function __awaiter(thisArg, _arguments, P, generator) {
|
|
85599
85645
|
function adopt(value) {
|
|
@@ -142373,16 +142419,15 @@ const runtimeSemaphore = new AsyncSemaphore(3);
|
|
|
142373
142419
|
const BLOCKED_REQUEST_PREFIX = "Request blocked by MCP connection policy.";
|
|
142374
142420
|
function serializeExecuteConfig(tenantUrl, headers, restrictions) {
|
|
142375
142421
|
const normalizedTenantUrl = new URL(tenantUrl).toString();
|
|
142376
|
-
const
|
|
142377
|
-
|
|
142378
|
-
|
|
142379
|
-
|
|
142380
|
-
});
|
|
142422
|
+
const serializedRestrictions = restrictions.map(({ method, pathPattern, source }) => ({
|
|
142423
|
+
method,
|
|
142424
|
+
pathPattern,
|
|
142425
|
+
source
|
|
142426
|
+
}));
|
|
142381
142427
|
return JSON.stringify({
|
|
142382
142428
|
tenantUrl: normalizedTenantUrl,
|
|
142383
|
-
tenantOrigin: new URL(normalizedTenantUrl).origin,
|
|
142384
142429
|
authHeaders: headers,
|
|
142385
|
-
|
|
142430
|
+
restrictions: serializedRestrictions
|
|
142386
142431
|
});
|
|
142387
142432
|
}
|
|
142388
142433
|
function createQueryRuntime() {
|
|
@@ -142436,19 +142481,15 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142436
142481
|
" if (!config || typeof config !== \"object\") {",
|
|
142437
142482
|
" throw new TypeError(\"Invalid execute configuration.\");",
|
|
142438
142483
|
" }",
|
|
142439
|
-
" const { tenantUrl,
|
|
142440
|
-
" if (typeof tenantUrl !== \"string\"
|
|
142441
|
-
" throw new TypeError(\"Execute configuration must contain string tenant
|
|
142484
|
+
" const { tenantUrl, authHeaders, restrictions } = config;",
|
|
142485
|
+
" if (typeof tenantUrl !== \"string\") {",
|
|
142486
|
+
" throw new TypeError(\"Execute configuration must contain a string tenant URL.\");",
|
|
142442
142487
|
" }",
|
|
142443
|
-
" if (!Array.isArray(
|
|
142444
|
-
" throw new TypeError(\"Execute configuration must contain
|
|
142488
|
+
" if (!Array.isArray(restrictions) || restrictions.some((rule) => !rule || typeof rule !== \"object\" || typeof rule.method !== \"string\" || typeof rule.pathPattern !== \"string\" || typeof rule.source !== \"string\")) {",
|
|
142489
|
+
" throw new TypeError(\"Execute configuration must contain valid restriction rules.\");",
|
|
142445
142490
|
" }",
|
|
142446
142491
|
" 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;",
|
|
142492
|
+
" return new URL(descriptor, tenantUrl.endsWith(\"/\") ? tenantUrl : tenantUrl + \"/\");",
|
|
142452
142493
|
" };",
|
|
142453
142494
|
" const normalizeRequest = (options) => {",
|
|
142454
142495
|
" if (!options || typeof options !== \"object\") {",
|
|
@@ -142457,18 +142498,34 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142457
142498
|
" const { path, ...rest } = options;",
|
|
142458
142499
|
" return { path, init: rest };",
|
|
142459
142500
|
" };",
|
|
142460
|
-
` const
|
|
142461
|
-
|
|
142462
|
-
` const normalizeAndValidateRestrictionPath = ${normalizeAndValidateRestrictionPath.toString()};`,
|
|
142463
|
-
` const parseRestrictionRule = ${parseRestrictionRule.toString()};`,
|
|
142501
|
+
` const HTTP_METHODS = ${JSON.stringify(HTTP_METHODS)};`,
|
|
142502
|
+
" const HTTP_METHOD_SET = new Set(HTTP_METHODS);",
|
|
142464
142503
|
` const escapeRestrictionRegex = ${escapeRestrictionRegex.toString()};`,
|
|
142465
142504
|
` const compileRestrictionSegment = ${compileRestrictionSegment.toString()};`,
|
|
142466
142505
|
` const matchCompiledSegments = ${matchCompiledSegments.toString()};`,
|
|
142467
142506
|
` const compileRestrictionRule = ${compileRestrictionRule.toString()};`,
|
|
142468
142507
|
` const matchesCompiledRule = ${matchesCompiledRule.toString()};`,
|
|
142469
|
-
|
|
142470
|
-
|
|
142471
|
-
"
|
|
142508
|
+
" const compiledRestrictions = restrictions.map(compileRestrictionRule);",
|
|
142509
|
+
" const findBlockingRestrictions = (method, pathname) => {",
|
|
142510
|
+
" const normalizedMethod = typeof method === \"string\" ? method.trim().toUpperCase() : \"\";",
|
|
142511
|
+
" const pathSegments = pathname === \"/\" ? [] : pathname.slice(1).split(\"/\");",
|
|
142512
|
+
" return compiledRestrictions.filter((rule) => {",
|
|
142513
|
+
" if (!normalizedMethod) {",
|
|
142514
|
+
" return rule.method === \"*\" && matchCompiledSegments(rule.segments, pathSegments);",
|
|
142515
|
+
" }",
|
|
142516
|
+
" return matchesCompiledRule(rule, normalizedMethod, pathSegments);",
|
|
142517
|
+
" });",
|
|
142518
|
+
" };",
|
|
142519
|
+
" const validateRequestMethod = (method) => {",
|
|
142520
|
+
" if (typeof method !== \"string\" || method.trim().length === 0) {",
|
|
142521
|
+
" throw new TypeError(\"request method must be a non-empty string\");",
|
|
142522
|
+
" }",
|
|
142523
|
+
" const normalizedMethod = method.trim().toUpperCase();",
|
|
142524
|
+
" if (!HTTP_METHOD_SET.has(normalizedMethod)) {",
|
|
142525
|
+
" throw new TypeError(\"request method must be one of: \" + HTTP_METHODS.join(\", \"));",
|
|
142526
|
+
" }",
|
|
142527
|
+
" return normalizedMethod;",
|
|
142528
|
+
" };",
|
|
142472
142529
|
" const formatBlockedRequestMessage = (method, path, matchingRules) => [",
|
|
142473
142530
|
" \"Request blocked by MCP connection policy.\",",
|
|
142474
142531
|
" \"\",",
|
|
@@ -142514,10 +142571,10 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142514
142571
|
" if (typeof path !== \"string\" || path.length === 0) {",
|
|
142515
142572
|
" throw new TypeError(\"request path must be a non-empty string\");",
|
|
142516
142573
|
" }",
|
|
142574
|
+
" const method = validateRequestMethod(init.method);",
|
|
142517
142575
|
" const resolvedUrl = resolveUrl(path);",
|
|
142518
|
-
" const blockedRules =
|
|
142576
|
+
" const blockedRules = findBlockingRestrictions(method, resolvedUrl.pathname);",
|
|
142519
142577
|
" if (blockedRules.length > 0) {",
|
|
142520
|
-
" const method = typeof init.method === \"string\" && init.method.trim() ? init.method.trim().toUpperCase() : \"GET\";",
|
|
142521
142578
|
" throw new Error(formatBlockedRequestMessage(method, resolvedUrl.pathname, blockedRules.map((rule) => rule.source)));",
|
|
142522
142579
|
" }",
|
|
142523
142580
|
" const headers = new Headers(init.headers ?? {});",
|
|
@@ -142528,6 +142585,7 @@ function buildExecutePrelude(tenantUrl, headers, restrictions = []) {
|
|
|
142528
142585
|
" const requestHeaders = Object.fromEntries(headers.entries());",
|
|
142529
142586
|
" const response = await fetch(resolvedUrl.toString(), {",
|
|
142530
142587
|
" ...init,",
|
|
142588
|
+
" method,",
|
|
142531
142589
|
" headers: requestHeaders,",
|
|
142532
142590
|
" body,",
|
|
142533
142591
|
" });",
|
|
@@ -142826,7 +142884,7 @@ function createC8YMcpServer() {
|
|
|
142826
142884
|
capabilities: {
|
|
142827
142885
|
tools: { listChanged: true },
|
|
142828
142886
|
prompts: { listChanged: true },
|
|
142829
|
-
resources: { listChanged:
|
|
142887
|
+
resources: { listChanged: true }
|
|
142830
142888
|
}
|
|
142831
142889
|
}).withContext();
|
|
142832
142890
|
server.tools(createTools(server));
|
|
@@ -142943,7 +143001,8 @@ runMain(defineCommand({
|
|
|
142943
143001
|
setCoreOpenApiVersion(selected.version);
|
|
142944
143002
|
consola.info(`Using core OpenAPI snapshot: ${getCoreOpenApiLabel()}`);
|
|
142945
143003
|
const raw = args.restriction;
|
|
142946
|
-
const restrictions = (Array.isArray(raw) ? raw : raw ? [raw] : []).filter(
|
|
143004
|
+
const { parsedRules: restrictions, failedRules } = parseRestrictionRule((Array.isArray(raw) ? raw : raw ? [raw] : []).filter((value) => typeof value === "string" && value.length > 0));
|
|
143005
|
+
if (failedRules.length > 0) throw new Error(["One or more restriction flags could not be parsed:", ...failedRules.map((rule) => `- ${rule.rule}: ${rule.reason}`)].join("\n"));
|
|
142947
143006
|
if (restrictions.length > 0) consola.info(`Applying ${restrictions.length} restriction rule(s):`, restrictions.map((r) => r.source));
|
|
142948
143007
|
const transport = new StdioTransport(createC8YMcpServer());
|
|
142949
143008
|
consola.info("Starting MCP server over stdio transport...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mc8yp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
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",
|