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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.mjs +193 -134
  3. 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 ≥22.0.0
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.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/restriction-core.ts
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$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.`);
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 source = input.trim();
85401
- if (!source) throw new Error("Restriction value must not be empty.");
85402
- const sep = source.indexOf(":");
85403
- if (sep > 0 && !source.startsWith("/")) {
85404
- const rawMethod = source.slice(0, sep).trim().toUpperCase();
85405
- const rawPath = source.slice(sep + 1).trim();
85406
- if (!rawPath) throw new Error("Restriction path pattern must not be empty.");
85407
- if (rawMethod && rawMethod !== "*" && !HTTP_METHOD_SET$1.has(rawMethod)) throw new Error(`Unsupported restriction method "${source.slice(0, sep)}".`);
85408
- return {
85409
- method: !rawMethod || rawMethod === "*" ? "*" : rawMethod,
85410
- pathPattern: normalizeAndValidateRestrictionPath(rawPath),
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
- method: "*",
85416
- pathPattern: normalizeAndValidateRestrictionPath(source),
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 seg = pattern[pi];
85433
- if (seg === "**") {
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 || !(seg instanceof RegExp) || !seg.test(path[si])) return false;
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 compileRestrictionSources(restrictionSources) {
85454
- return restrictionSources.map(parseRestrictionRule).map(compileRestrictionRule);
85455
- }
85456
- function getBlockedCompiledRestrictions(compiledRestrictions, method, pathname) {
85457
- const normalizedMethod = String(method ?? "GET").trim().toUpperCase() || "GET";
85458
- const normalizedPath = normalizeRestrictionMatchPath(pathname);
85459
- const pathSegments = normalizedPath === "/" ? [] : normalizedPath.slice(1).split("/");
85460
- return compiledRestrictions.filter((rule) => matchesCompiledRule(rule, normalizedMethod, pathSegments));
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 restrictionSources = restrictions.map((rule) => {
142377
- const parsedRule = parseRestrictionRule(rule.source);
142378
- if (parsedRule.method !== rule.method || parsedRule.pathPattern !== rule.pathPattern) throw new TypeError(`Restriction source "${rule.source}" does not match its parsed shape.`);
142379
- return parsedRule.source;
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
- restrictionSources
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, tenantOrigin, authHeaders, restrictionSources } = config;",
142440
- " if (typeof tenantUrl !== \"string\" || typeof tenantOrigin !== \"string\") {",
142441
- " throw new TypeError(\"Execute configuration must contain string tenant values.\");",
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(restrictionSources) || restrictionSources.some((source) => typeof source !== \"string\")) {",
142444
- " throw new TypeError(\"Execute configuration must contain string restriction sources.\");",
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
- " const resolved = new URL(descriptor, tenantUrl.endsWith(\"/\") ? tenantUrl : tenantUrl + \"/\");",
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 HTTP_METHOD_SET = new Set(${JSON.stringify(HTTP_METHODS)});`,
142461
- ` const normalizeRestrictionMatchPath = ${normalizeRestrictionMatchPath.toString()};`,
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
- ` const compileRestrictionSources = ${compileRestrictionSources.toString()};`,
142470
- ` const getBlockedCompiledRestrictions = ${getBlockedCompiledRestrictions.toString()};`,
142471
- " const compiledRestrictions = compileRestrictionSources(restrictionSources);",
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 = getBlockedCompiledRestrictions(compiledRestrictions, init.method, resolvedUrl.pathname);",
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: false }
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(Boolean).map(parseRestrictionRule);
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.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",