mc8yp 2.6.1 → 2.7.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.
Files changed (3) hide show
  1. package/README.md +66 -4
  2. package/dist/cli.mjs +1440 -433
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1546,7 +1546,7 @@ const consola = createConsola();
1546
1546
  //#endregion
1547
1547
  //#region package.json
1548
1548
  var name = "mc8yp";
1549
- var version = "2.6.1";
1549
+ var version = "2.7.0";
1550
1550
  var description$1 = "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management";
1551
1551
  //#endregion
1552
1552
  //#region \0virtual:core-openapi
@@ -5390,7 +5390,7 @@ const specs = Object.freeze([
5390
5390
  "operationId": "putCRLSettings",
5391
5391
  "tags": ["Trusted certificates"],
5392
5392
  "summary": "Add revoked certificates",
5393
- "description": "> **&#9432; Info:** A certificate revocation list (CRL) is a list of digital certificates\n that have been revoked by the issuing certificate authority (CA) before expiration date.\n In Cumulocity, a CRL check can be in online or offline mode or both.\n\nAn endpoint to add revoked certificate serial numbers for offline CRL check via payload or file.\n\nFor payload, a JSON object required with list of CRL entries, for example:\n ```json\n {\n \"crls\": [\n {\n \"serialNumberInHex\": \"1000\",\n \"revocationDate\": \"2023-01-11T16:12:36.288Z\"\n }\n ]\n }\n ```\nEach entry is composed of:\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g As (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the existing entry stays</br>\n * `revocationDate` - accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, for example: `2023-01-11T16:12:36.288Z`.\n This is an optional parameter and defaults to the current server UTC date time if not specified in the payload.\n If specified and the date is in future then those entries will be also defaulted to current date.\n\nFor file upload, each file can hold at maximum 5000 revocation entries.\nMultiple upload is allowed.\nIn case of duplicates, the latest (last uploaded) entry is considered.\n\nSee below for a sample CSV file:\n\n| SERIAL NO. | REVOCATION DATE |\n|--|--|\n| 1000 | 2023-01-11T16:12:36.288Z |\n\n Each entry is composed of :\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the latest entry will be taken.</br>\n * revocationDate: Accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` e.g: 2023-01-11T16:12:36.288Z.\n This is an optional and will be default to current server UTC date time if not specified in payload.\n If specified and the date is in future then those entries will be skipped.\n\nThe CRL setting for offline and online check can be enabled/disabled using <kbd><a href=\"#operation/putOptionResource\">/tenant/options</a></kbd>.\nKeys are `crl.online.check.enabled` and `crl.offline.check.enabled` under the category `configuration`.\n\n<section><h5>Required roles</h5>\n(ROLE_TENANT_MANAGEMENT_ADMIN <b>OR</b> ROLE_TENANT_ADMIN) <b>AND</b> is the current tenant\n</section>\n\n**⚠️ Important:** According to CRL policy, added serial numbers cannot be reversed.\n",
5393
+ "description": "> **&#9432; Info:** A certificate revocation list (CRL) is a list of digital certificates\n that have been revoked by the issuing certificate authority (CA) before expiration date.\n In Cumulocity, a CRL check can be in online or offline mode or both.\n\nAn endpoint to add revoked certificate serial numbers for offline CRL check via payload or file.\nA file can be uploaded with either `PUT` or `POST`, a JSON payload is accepted by `PUT` only.\n\nFor payload, a JSON object required with list of CRL entries, for example:\n ```json\n {\n \"crls\": [\n {\n \"serialNumberInHex\": \"1000\",\n \"revocationDate\": \"2023-01-11T16:12:36.288Z\"\n }\n ]\n }\n ```\nEach entry is composed of:\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g As (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the existing entry stays</br>\n * `revocationDate` - accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, for example: `2023-01-11T16:12:36.288Z`.\n This is an optional parameter and defaults to the current server UTC date time if not specified in the payload.\n If specified and the date is in future then those entries will be also defaulted to current date.\n\nFor file upload, each file can hold at maximum 5000 revocation entries.\nMultiple upload is allowed.\nIn case of duplicates, the latest (last uploaded) entry is considered.\n\nSee below for a sample CSV file:\n\n| SERIAL NO. | REVOCATION DATE |\n|--|--|\n| 1000 | 2023-01-11T16:12:36.288Z |\n\n Each entry is composed of :\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the latest entry will be taken.</br>\n * revocationDate: Accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` e.g: 2023-01-11T16:12:36.288Z.\n This is an optional and will be default to current server UTC date time if not specified in payload.\n If specified and the date is in future then those entries will be skipped.\n\nThe CRL setting for offline and online check can be enabled/disabled using <kbd><a href=\"#operation/putOptionResource\">/tenant/options</a></kbd>.\nKeys are `crl.online.check.enabled` and `crl.offline.check.enabled` under the category `configuration`.\n\n<section><h5>Required roles</h5>\n(ROLE_TENANT_MANAGEMENT_ADMIN <b>OR</b> ROLE_TENANT_ADMIN) <b>AND</b> is the current tenant\n</section>\n\n**⚠️ Important:** According to CRL policy, added serial numbers cannot be reversed.\n",
5394
5394
  "requestBody": {
5395
5395
  "required": true,
5396
5396
  "content": {
@@ -5408,6 +5408,25 @@ const specs = Object.freeze([
5408
5408
  },
5409
5409
  "responses": { "204": { "description": "CRLs updated successfully." } }
5410
5410
  },
5411
+ "post": {
5412
+ "operationId": "postCRLSettings",
5413
+ "tags": ["Trusted certificates"],
5414
+ "summary": "Add revoked certificates",
5415
+ "description": "> **&#9432; Info:** A certificate revocation list (CRL) is a list of digital certificates\n that have been revoked by the issuing certificate authority (CA) before expiration date.\n In Cumulocity, a CRL check can be in online or offline mode or both.\n\nAn endpoint to add revoked certificate serial numbers for offline CRL check via payload or file.\nA file can be uploaded with either `PUT` or `POST`, a JSON payload is accepted by `PUT` only.\n\nFor payload, a JSON object required with list of CRL entries, for example:\n ```json\n {\n \"crls\": [\n {\n \"serialNumberInHex\": \"1000\",\n \"revocationDate\": \"2023-01-11T16:12:36.288Z\"\n }\n ]\n }\n ```\nEach entry is composed of:\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g As (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the existing entry stays</br>\n * `revocationDate` - accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'`, for example: `2023-01-11T16:12:36.288Z`.\n This is an optional parameter and defaults to the current server UTC date time if not specified in the payload.\n If specified and the date is in future then those entries will be also defaulted to current date.\n\nFor file upload, each file can hold at maximum 5000 revocation entries.\nMultiple upload is allowed.\nIn case of duplicates, the latest (last uploaded) entry is considered.\n\nSee below for a sample CSV file:\n\n| SERIAL NO. | REVOCATION DATE |\n|--|--|\n| 1000 | 2023-01-11T16:12:36.288Z |\n\n Each entry is composed of :\n * serialNumberInHex: Needs to be in `Hexadecimal Value`. e.g (1000)^16 == (4096)^10, So we have to enter 1000.\n If duplicate serial number exists in payload, the latest entry will be taken.</br>\n * revocationDate: Accepted Date format: `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` e.g: 2023-01-11T16:12:36.288Z.\n This is an optional and will be default to current server UTC date time if not specified in payload.\n If specified and the date is in future then those entries will be skipped.\n\nThe CRL setting for offline and online check can be enabled/disabled using <kbd><a href=\"#operation/putOptionResource\">/tenant/options</a></kbd>.\nKeys are `crl.online.check.enabled` and `crl.offline.check.enabled` under the category `configuration`.\n\n<section><h5>Required roles</h5>\n(ROLE_TENANT_MANAGEMENT_ADMIN <b>OR</b> ROLE_TENANT_ADMIN) <b>AND</b> is the current tenant\n</section>\n\n**⚠️ Important:** According to CRL policy, added serial numbers cannot be reversed.\n",
5416
+ "requestBody": {
5417
+ "required": true,
5418
+ "content": { "multipart/form-data": { "schema": {
5419
+ "type": "object",
5420
+ "required": ["file"],
5421
+ "properties": { "file": {
5422
+ "description": "File to be uploaded.",
5423
+ "type": "string",
5424
+ "format": "binary"
5425
+ } }
5426
+ } } }
5427
+ },
5428
+ "responses": { "204": { "description": "CRLs updated successfully." } }
5429
+ },
5411
5430
  "get": {
5412
5431
  "operationId": "getCRLSettings",
5413
5432
  "tags": ["Trusted certificates"],
@@ -5888,7 +5907,11 @@ const specs = Object.freeze([
5888
5907
  } },
5889
5908
  "/tenant/oauth": { "post": {
5890
5909
  "operationId": "postLoginFormCookie",
5891
- "parameters": [{ "$ref": "#/components/parameters/queryParam_tenant_id" }, { "$ref": "#/components/parameters/acceptHeader" }],
5910
+ "parameters": [
5911
+ { "$ref": "#/components/parameters/queryParam_tenant_id" },
5912
+ { "$ref": "#/components/parameters/tfaCodeHeader" },
5913
+ { "$ref": "#/components/parameters/acceptHeader" }
5914
+ ],
5892
5915
  "tags": ["Login tokens"],
5893
5916
  "summary": "Obtain access tokens in cookies",
5894
5917
  "description": "Obtain an OAI-Secure and XSRF tokens in cookies.\n",
@@ -5914,7 +5937,11 @@ const specs = Object.freeze([
5914
5937
  } },
5915
5938
  "/tenant/oauth/token": { "post": {
5916
5939
  "operationId": "postLoginFormBody",
5917
- "parameters": [{ "$ref": "#/components/parameters/queryParam_tenant_id" }, { "$ref": "#/components/parameters/acceptHeader" }],
5940
+ "parameters": [
5941
+ { "$ref": "#/components/parameters/queryParam_tenant_id" },
5942
+ { "$ref": "#/components/parameters/tfaCodeHeader" },
5943
+ { "$ref": "#/components/parameters/acceptHeader" }
5944
+ ],
5918
5945
  "tags": ["Login tokens"],
5919
5946
  "summary": "Obtain an access token",
5920
5947
  "description": "Obtain an OAI-Secure access token.",
@@ -5927,6 +5954,32 @@ const specs = Object.freeze([
5927
5954
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/accessToken" } } }
5928
5955
  } }
5929
5956
  } },
5957
+ "/tenant/oauth/certificate": { "post": {
5958
+ "operationId": "postCertificateAccessToken",
5959
+ "parameters": [
5960
+ { "$ref": "#/components/parameters/queryParam_tenant_id" },
5961
+ { "$ref": "#/components/parameters/tenantIdInHeader" },
5962
+ { "$ref": "#/components/parameters/tokenResponseModeHeader" },
5963
+ { "$ref": "#/components/parameters/tfaCodeHeader" },
5964
+ { "$ref": "#/components/parameters/acceptHeader" }
5965
+ ],
5966
+ "tags": ["Login tokens"],
5967
+ "summary": "Obtain an access token with a certificate",
5968
+ "description": "Obtain a platform access token by presenting an X.509 certificate over standard HTTPS, without requiring mutual TLS (mTLS) or MQTT. This provides a REST-native, standard-port alternative for certificate-authenticated clients that cannot use MQTT or the dedicated mTLS endpoint.\n\nThe request body contains the leaf certificate, or the full certificate chain, in PEM format. By default the returned access token is encrypted as a JWE with the public key from the presented certificate, so that only the holder of the corresponding private key can decrypt it and use it as a `Bearer` token for subsequent REST API requests. The response format can be controlled with the `X-Cumulocity-Token-Response-Mode` header. JWE mode requires the certificate to use an RSA key, because the platform encrypts the token with `RSA-OAEP-256`; clients presenting a certificate with a non-RSA key must request a plain JWT with the `X-Cumulocity-Token-Response-Mode: jwt` header.\n\nThe platform derives the identity from the certificate subject, validates the certificate material against the tenant-trusted certificate authorities (including validity dates and revocation checks where configured) and confirms that the certificate is authorized for the resolved user. The tenant context must be established from the request before validation, for example through the `tenant_id` query parameter or the `X-Cumulocity-TenantId` header. The certificate is not used as the primary tenant identification mechanism.\n\nIf the resolved certificate identity is ambiguous, for example when the same certificate common name could resolve to both a regular user and a device user, the request is rejected and no token is issued. Untrusted, expired, revoked, malformed, unsupported or unmapped certificates are rejected as well.\n\nIf two-factor authentication (TFA) is active for the resolved user, it must be passed with the request as well, because the certificate replaces the password, not the second factor. Send the current TFA code in the `X-Cumulocity-TFA-Code` header. Both the TOTP and the SMS strategy are supported, in the same way as for the OAI-Secure login. Device users are never asked for a second factor.\n",
5969
+ "requestBody": {
5970
+ "required": true,
5971
+ "content": { "application/x-pem-file": { "schema": {
5972
+ "type": "string",
5973
+ "format": "binary",
5974
+ "description": "Leaf certificate or full certificate chain in PEM format.",
5975
+ "example": "-----BEGIN CERTIFICATE-----\nMIIDTzCCAjegAwIBAgIUB1a5GM9ubBpN5tyU7YO8D3C8zUUwDQYJKoZIhvcNAQEL\n...\nzngrsOfyKr8YYlDRy6RiAR2HQode00Hs4WoakfuTpaISZIs=\n-----END CERTIFICATE-----"
5976
+ } } }
5977
+ },
5978
+ "responses": { "200": {
5979
+ "description": "The access token is sent in the response. By default it is a JWE encrypted with the public key from the presented certificate.",
5980
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/certificateAccessToken" } } }
5981
+ } }
5982
+ } },
5930
5983
  "/tenant/options": {
5931
5984
  "post": {
5932
5985
  "operationId": "postOptionCollectionResource",
@@ -6197,6 +6250,26 @@ const specs = Object.freeze([
6197
6250
  "example": "t07007007"
6198
6251
  }
6199
6252
  },
6253
+ "tokenResponseModeHeader": {
6254
+ "name": "X-Cumulocity-Token-Response-Mode",
6255
+ "in": "header",
6256
+ "description": "Optional. Selects the format of the returned access token. `jwe` (default) returns the access token as a JWE encrypted with the public key from the presented certificate (RSA-OAEP-256 for key encryption and AES-256-GCM for content encryption), so that only the holder of the corresponding private key can decrypt it; it requires the certificate to use an RSA key. `jwt` returns a plain, unencrypted JWT and is rejected with a 403 error unless plain JWT responses are enabled for the platform or the certificate does not use an RSA key. Unsupported values are rejected with a 400 error.\n",
6257
+ "schema": {
6258
+ "type": "string",
6259
+ "enum": ["jwe", "jwt"],
6260
+ "default": "jwe",
6261
+ "example": "jwe"
6262
+ }
6263
+ },
6264
+ "tfaCodeHeader": {
6265
+ "name": "X-Cumulocity-TFA-Code",
6266
+ "in": "header",
6267
+ "description": "Optional. Current TFA code of the user, if a TFA code is required to log in. For form-based OAI-Secure login, the code can be sent as the `tfa_code` field in the request body; this header is useful for requests that cannot include that field (for example when the request body is a certificate chain). If both are sent, the request body field takes precedence.\n",
6268
+ "schema": {
6269
+ "type": "string",
6270
+ "example": "123433"
6271
+ }
6272
+ },
6200
6273
  "alarmId": {
6201
6274
  "name": "id",
6202
6275
  "in": "path",
@@ -8427,6 +8500,17 @@ const specs = Object.freeze([
8427
8500
  }
8428
8501
  } }
8429
8502
  },
8503
+ "jweUnsupportedCertificateKeyUnprocessableEntity": {
8504
+ "description": "The presented certificate does not use an RSA key, so the access token cannot be encrypted as a JWE. Request a plain JWT with the `X-Cumulocity-Token-Response-Mode: jwt` header instead.\n",
8505
+ "content": { "application/vnd.com.nsn.cumulocity.error+json": {
8506
+ "schema": { "$ref": "#/components/schemas/error" },
8507
+ "example": {
8508
+ "error": "certificate-token/Unprocessable Entity",
8509
+ "message": "Only RSA certificates are supported for JWE mode. Use header X-Cumulocity-Token-Response-Mode: jwt for non-RSA certificates.",
8510
+ "info": "https://www.cumulocity.com/guides/reference-guide/#error_reporting"
8511
+ }
8512
+ } }
8513
+ },
8430
8514
  "unableToParseCRLEntries": {
8431
8515
  "description": "Unsupported date time format.",
8432
8516
  "content": { "application/vnd.com.nsn.cumulocity.error+json": {
@@ -14584,6 +14668,33 @@ const specs = Object.freeze([
14584
14668
  }
14585
14669
  }
14586
14670
  },
14671
+ "certificateAccessToken": {
14672
+ "description": "Access token obtained by presenting an X.509 certificate. By default the token is returned as a JWE encrypted with the public key from the presented certificate, so that only the holder of the corresponding private key can decrypt it. This requires the certificate to use an RSA key, because the platform encrypts the token with `RSA-OAEP-256`.\n\nA plain, unencrypted JWT can be requested with the `X-Cumulocity-Token-Response-Mode: jwt` header, but such requests are rejected with a 403 error unless plain JWT responses have been explicitly enabled for the platform. Clients whose certificate does not use an RSA key are exempt from that restriction, as JWE mode is not available to them: they may always request `jwt`, and must do so, because a request left in the default `jwe` mode is rejected with a 422 error.\n",
14673
+ "type": "object",
14674
+ "properties": {
14675
+ "access_token": {
14676
+ "description": "The access token generated by the Cumulocity platform. A JWE compact-serialized, encrypted JWT when `response_mode` is `jwe`, or a plain, unencrypted JWT when `response_mode` is `jwt`.",
14677
+ "type": "string",
14678
+ "example": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0.g_hEwksO1Ax8Qn7HoN-BVeBoa8FXe0kpyk_XdcSmxvcM5_P296JXXtoHISr_DD_M...48V95cLzdVlCyoQ"
14679
+ },
14680
+ "token_type": {
14681
+ "description": "The type of the returned token.",
14682
+ "type": "string",
14683
+ "example": "Bearer"
14684
+ },
14685
+ "response_mode": {
14686
+ "description": "The format of the returned access token. `jwe` unless a plain JWT was requested with the `X-Cumulocity-Token-Response-Mode` header and permitted.",
14687
+ "type": "string",
14688
+ "enum": ["jwe", "jwt"],
14689
+ "example": "jwe"
14690
+ },
14691
+ "expires_in": {
14692
+ "description": "The token lifetime in seconds.",
14693
+ "type": "integer",
14694
+ "example": 3600
14695
+ }
14696
+ }
14697
+ },
14587
14698
  "SystemOptionCollection": {
14588
14699
  "description": "All available system options of the tenant.",
14589
14700
  "type": "object",
@@ -58548,17 +58659,17 @@ function buildMcpTools(server) {
58548
58659
  return tools;
58549
58660
  }
58550
58661
  /**
58551
- * Build the per-connection namespace list: core as `c8y` plus one namespace
58552
- * per available service. A service exposing an MCP server becomes an MCP
58553
- * namespace (its spec is skipped) unless opted out via `noMcp` — then its
58554
- * spec is used as the fallback. Operations blocked by the connection policy
58555
- * are omitted from OpenAPI namespaces; path templates are matched as-is.
58662
+ * Build the per-connection namespace list: core as `c8y`, one namespace per
58663
+ * available service, then one per connection-supplied external MCP server. A
58664
+ * service exposing an MCP server becomes an MCP namespace (its spec is skipped)
58665
+ * unless opted out via `noMcp` — then its spec is used as the fallback.
58666
+ * Operations blocked by the connection policy are omitted from OpenAPI
58667
+ * namespaces; path templates are matched as-is.
58556
58668
  * @param resolved
58557
- * @param restrictions
58558
- * @param allowRules
58559
- * @param noMcp Per-connection MCP-wrapping opt-out.
58669
+ * @param options Per-connection policy, opt-outs, and external servers.
58560
58670
  */
58561
- function buildNamespaces(resolved, restrictions = [], allowRules = [], noMcp) {
58671
+ function buildNamespaces(resolved, options = {}) {
58672
+ const { restrictions = [], allowRules = [], noMcp, externalServers = [] } = options;
58562
58673
  const visibleOperations = (spec) => deriveOperations(spec).filter((op) => !evaluateAccessPolicy(restrictions, allowRules, op.method, op.path).blocked);
58563
58674
  const namespaces = [{
58564
58675
  kind: "openapi",
@@ -58601,6 +58712,31 @@ function buildNamespaces(resolved, restrictions = [], allowRules = [], noMcp) {
58601
58712
  });
58602
58713
  }
58603
58714
  }
58715
+ for (const external of externalServers) {
58716
+ const name = external.config.name;
58717
+ if (RESERVED_NAMESPACES.has(name) || used.has(name)) {
58718
+ consola.warn(`[codemode] external MCP server "${name}" (${external.config.url}) maps to a namespace that is ${RESERVED_NAMESPACES.has(name) ? "reserved" : "already used by this tenant"} — skipping this server.`);
58719
+ continue;
58720
+ }
58721
+ used.add(name);
58722
+ const server = {
58723
+ contextPath: name,
58724
+ appLabel: name,
58725
+ mcpName: name,
58726
+ description: external.config.description ?? external.instructions,
58727
+ url: external.config.url,
58728
+ sendAuthentication: false,
58729
+ tools: external.tools
58730
+ };
58731
+ namespaces.push({
58732
+ kind: "mcp",
58733
+ name,
58734
+ specKey: name,
58735
+ external: external.config,
58736
+ server,
58737
+ tools: buildMcpTools(server)
58738
+ });
58739
+ }
58604
58740
  return namespaces;
58605
58741
  }
58606
58742
  /**
@@ -58664,10 +58800,15 @@ function createCodeModeGuidePrompt() {
58664
58800
  const restrictions = c8yMcpServer.ctx.custom?.restrictions ?? [];
58665
58801
  const allowRules = c8yMcpServer.ctx.custom?.allowRules ?? [];
58666
58802
  const resolvedSpecs = c8yMcpServer.ctx.custom?.specs;
58667
- const namespaceNames = resolvedSpecs ? buildNamespaces(resolvedSpecs, restrictions, allowRules).map((ns) => ns.name) : ["c8y"];
58803
+ const namespaceNames = [...resolvedSpecs ? buildNamespaces(resolvedSpecs, {
58804
+ restrictions,
58805
+ allowRules
58806
+ }).map((ns) => ns.name) : ["c8y"], ...(c8yMcpServer.ctx.custom?.externalMcpServers ?? []).map((s) => s.name)];
58807
+ const externalServers = c8yMcpServer.ctx.custom?.externalMcpServers ?? [];
58808
+ const externalLine = externalServers.length > 0 ? `\n- ${externalServers.map((s) => `\`${s.name}\``).join(", ")} ${externalServers.length === 1 ? "is an" : "are"} external MCP server${externalServers.length === 1 ? "" : "s"} configured for THIS connection — not part of the tenant. Same typed-method surface; describe and search work identically.` : "";
58668
58809
  const policyLines = [...restrictions.map((rule) => `- deny: \`${rule.source}\``), ...allowRules.map((rule) => `- allow: \`${rule.source}\``)];
58669
58810
  const restrictionSection = policyLines.length > 0 ? `\n## Current Connection Access Policy\n${policyLines.join("\n")}\n\nOperations blocked by these rules are omitted from discovery (search/describe) entirely, and any live request that matches a deny rule (or misses the allow list) fails before reaching the tenant.\n` : "";
58670
- const sandboxSection = c8yMcpServer.ctx.custom?.env === "server" ? `\n## Sandbox (scratch compute)
58811
+ const sandboxSection = c8yMcpServer.ctx.custom?.env === "server" && c8yMcpServer.ctx.custom?.enableSandbox ? `\n## Sandbox (scratch compute)
58671
58812
 
58672
58813
  \`sandbox\` is your workspace: a persistent in-memory filesystem + Unix shell, separate from the API. It is where you keep files and process data. When the user asks to save or write a file, write it here (\`sandbox.writeFile\`) — never dump a file into the chat instead. Use the shell for text/data processing that is awkward in plain JS — \`jq\`, \`awk\`, \`sed\`, \`grep\`, \`sort\`, \`uniq\`, \`cut\`, \`sqlite3\`, etc. via \`sandbox.exec\`. It has NO network access and NO host filesystem access; it never reaches Cumulocity. Fetch data with \`c8y\`/namespaces, write it into the sandbox, process it, read the result back.
58673
58814
 
@@ -58707,7 +58848,7 @@ declare const docs: {
58707
58848
  API namespaces currently visible: ${namespaceNames.map((n) => `\`${n}\``).join(", ")}.
58708
58849
 
58709
58850
  - \`c8y\` is the Cumulocity core REST surface (inventory, alarms, events, measurements, identity, device control, users, tenants, audit). Always present.
58710
- - Each additional namespace is a microservice available on the current tenant (e.g. \`dtm\`). A namespace exists only when the service is actually reachable.
58851
+ - Each additional namespace is a microservice available on the current tenant (e.g. \`dtm\`). A namespace exists only when the service is actually reachable.${externalLine}
58711
58852
  - Every namespace has one typed method per API operation. The namespaces are the complete surface — there is no raw-request escape hatch. If a method seems missing, re-search with different wording; if it truly does not exist, report that instead of improvising.
58712
58853
  - Method inputs are a single flat object: path/query/header parameters as top-level keys, the request payload under \`body\`.
58713
58854
 
@@ -77296,7 +77437,18 @@ const indexCache$1 = /* @__PURE__ */ new WeakMap();
77296
77437
  function getMethodIndex(cacheKey, items) {
77297
77438
  const cached = indexCache$1.get(cacheKey);
77298
77439
  if (cached) return cached;
77299
- const list = items();
77440
+ const index = buildMethodIndex(items());
77441
+ indexCache$1.set(cacheKey, index);
77442
+ return index;
77443
+ }
77444
+ /**
77445
+ * Build an index without caching it. Used for connection-scoped method sets
77446
+ * (external MCP namespaces): those cannot enter the per-tenant cache, and
77447
+ * indexing a few hundred short documents costs single-digit milliseconds
77448
+ * against a codemode call measured in seconds.
77449
+ * @param list
77450
+ */
77451
+ function buildMethodIndex(list) {
77300
77452
  const mini = new MiniSearch({
77301
77453
  idField: "target",
77302
77454
  fields: [
@@ -77308,12 +77460,10 @@ function getMethodIndex(cacheKey, items) {
77308
77460
  tokenize
77309
77461
  });
77310
77462
  mini.addAll([...list]);
77311
- const index = {
77463
+ return {
77312
77464
  mini,
77313
77465
  methods: new Map(list.map((m) => [m.target, m]))
77314
77466
  };
77315
- indexCache$1.set(cacheKey, index);
77316
- return index;
77317
77467
  }
77318
77468
  /**
77319
77469
  * Search the method index with one query or several phrasings at once
@@ -77542,7 +77692,7 @@ function truncateLine(text) {
77542
77692
  return flattened.length > 220 ? `${flattened.slice(0, 220)}…` : flattened;
77543
77693
  }
77544
77694
  const SANDBOX_METHOD_COUNT = (SANDBOX_INTERFACE_TS.match(/^ {2}\w+:/gm) ?? []).length;
77545
- function renderOverview(namespaces, sandboxEnabled) {
77695
+ function renderOverview(namespaces, sandboxEnabled, externalFailures) {
77546
77696
  const lines = ["Available namespaces on this tenant (do not assume capabilities from prior knowledge — search each relevant domain):"];
77547
77697
  for (const ns of namespaces) if (ns.kind === "openapi") {
77548
77698
  const info = ns.spec.info;
@@ -77550,9 +77700,14 @@ function renderOverview(namespaces, sandboxEnabled) {
77550
77700
  lines.push(`- ${ns.name}${info?.title ? ` — ${info.title}` : ""} (${ns.operations.length} methods)${short ? `: ${short}` : ""}`);
77551
77701
  } else {
77552
77702
  const short = truncateLine(ns.server.description);
77553
- lines.push(`- ${ns.name} — ${ns.server.mcpName} (${ns.tools.length} methods)${short ? `: ${short}` : ""}`);
77703
+ const label = ns.external ? `EXTERNAL MCP server at ${ns.external.url} — configured for this connection, NOT part of this tenant` : ns.server.mcpName;
77704
+ lines.push(`- ${ns.name} — ${label} (${ns.tools.length} methods)${short ? `: ${short}` : ""}`);
77554
77705
  }
77555
77706
  if (sandboxEnabled) lines.push(`- sandbox — in-memory shell + virtual filesystem (${SANDBOX_METHOD_COUNT} methods): jq/awk/grep/sed/sort/sqlite over data you fetched; no network, no host FS. codemode.describe("sandbox") for its methods.`);
77707
+ if (externalFailures.length > 0) {
77708
+ lines.push("", "Configured but unreachable right now (report this to the user; retrying may work if it is transient):");
77709
+ for (const failure of externalFailures) lines.push(`- ${failure.name} (${failure.url}): ${failure.reason}`);
77710
+ }
77556
77711
  lines.push("", "Workflow:", "- codemode.search(\"keywords\") — find methods by name/path/summary (top 20 by score)", "- codemode.describe(\"<namespace>.<method>\") — types and docs for one method", "- docs.search(\"keywords\") / docs.read(id) — documentation topics (domain query languages, concepts)", "- <namespace>.<method>({ ...params, body }) — call the API");
77557
77712
  return lines.join("\n");
77558
77713
  }
@@ -77584,6 +77739,7 @@ function renderMcpTool(namespace, tool) {
77584
77739
  outputSchema: tool.outputSchema
77585
77740
  });
77586
77741
  const lines = [`${namespace.name}.${tool.name}`];
77742
+ if (namespace.external) lines.push("", `External MCP server (${namespace.external.url}) — configured for this MCP connection, not part of this tenant. Calls go to that host with its own credentials; tenant credentials are never sent.`);
77587
77743
  if (tool.description) lines.push("", tool.description);
77588
77744
  lines.push("", "```ts", signature, "", types, "```");
77589
77745
  return lines.join("\n");
@@ -77624,14 +77780,15 @@ function renderSearchRedirect(target, namespaces, methodIndex) {
77624
77780
  * @param namespaces
77625
77781
  * @param methodIndex
77626
77782
  * @param target
77627
- * @param sandboxEnabled - whether the opt-in `sandbox` surface is exposed this run.
77783
+ * @param options Run-scoped extras for the overview.
77628
77784
  */
77629
- function describeTarget(namespaces, methodIndex, target, sandboxEnabled = false) {
77785
+ function describeTarget(namespaces, methodIndex, target, options = {}) {
77786
+ const { sandboxEnabled = false, externalFailures = [] } = options;
77630
77787
  const trimmed = target?.trim() ?? "";
77631
77788
  if (trimmed === "") return {
77632
77789
  target: "",
77633
77790
  kind: "overview",
77634
- content: renderOverview(namespaces, sandboxEnabled)
77791
+ content: renderOverview(namespaces, sandboxEnabled, externalFailures)
77635
77792
  };
77636
77793
  const [maybeNamespace, maybeMethod] = trimmed.includes(".") ? [trimmed.slice(0, trimmed.indexOf(".")), trimmed.slice(trimmed.indexOf(".") + 1)] : [trimmed, void 0];
77637
77794
  if (sandboxEnabled && maybeNamespace === "sandbox") return {
@@ -77808,29 +77965,29 @@ function createJustBashAdapter() {
77808
77965
  }
77809
77966
  //#endregion
77810
77967
  //#region src/codemode/sandbox/index.ts
77811
- const IDLE_TTL_MS = 900 * 1e3;
77812
- const sessions = /* @__PURE__ */ new Map();
77813
- function armIdleTimer(sessionId) {
77814
- const session = sessions.get(sessionId);
77968
+ const IDLE_TTL_MS$1 = 900 * 1e3;
77969
+ const sessions$1 = /* @__PURE__ */ new Map();
77970
+ function armIdleTimer$1(sessionId) {
77971
+ const session = sessions$1.get(sessionId);
77815
77972
  if (!session) return;
77816
77973
  if (session.timer) clearTimeout(session.timer);
77817
- session.timer = setTimeout(() => evictSandboxSession(sessionId, "idle-timeout"), IDLE_TTL_MS);
77974
+ session.timer = setTimeout(() => evictSandboxSession(sessionId, "idle-timeout"), IDLE_TTL_MS$1);
77818
77975
  session.timer.unref?.();
77819
77976
  }
77820
77977
  function getSessionAdapter(sessionId) {
77821
- let session = sessions.get(sessionId);
77978
+ let session = sessions$1.get(sessionId);
77822
77979
  if (!session) {
77823
77980
  session = { adapter: createJustBashAdapter() };
77824
- sessions.set(sessionId, session);
77981
+ sessions$1.set(sessionId, session);
77825
77982
  }
77826
- armIdleTimer(sessionId);
77983
+ armIdleTimer$1(sessionId);
77827
77984
  return session.adapter;
77828
77985
  }
77829
77986
  function resetSessionAdapter(sessionId) {
77830
- const session = sessions.get(sessionId);
77987
+ const session = sessions$1.get(sessionId);
77831
77988
  session?.adapter.dispose?.();
77832
77989
  if (session) session.adapter = createJustBashAdapter();
77833
- armIdleTimer(sessionId);
77990
+ armIdleTimer$1(sessionId);
77834
77991
  }
77835
77992
  /**
77836
77993
  * Drop a session's sandbox and its timer. Called by the clean-close (DELETE)
@@ -77839,18 +77996,18 @@ function resetSessionAdapter(sessionId) {
77839
77996
  * @param reason - Why the sandbox is being dropped; included in the eviction log line.
77840
77997
  */
77841
77998
  function evictSandboxSession(sessionId, reason) {
77842
- const session = sessions.get(sessionId);
77999
+ const session = sessions$1.get(sessionId);
77843
78000
  if (!session) return;
77844
78001
  if (session.timer) clearTimeout(session.timer);
77845
78002
  session.adapter.dispose?.();
77846
- sessions.delete(sessionId);
78003
+ sessions$1.delete(sessionId);
77847
78004
  consola.info(`[sandbox] evicted workspace for session ${sessionId} (reason: ${reason})`);
77848
78005
  }
77849
78006
  /**
77850
78007
  * Evict every session (process exit, test cleanup).
77851
78008
  */
77852
78009
  function disposeAllSandboxSessions() {
77853
- for (const sessionId of [...sessions.keys()]) evictSandboxSession(sessionId, "shutdown");
78010
+ for (const sessionId of [...sessions$1.keys()]) evictSandboxSession(sessionId, "shutdown");
77854
78011
  }
77855
78012
  /**
77856
78013
  * Build the `sandbox` host-module leaf for one codemode run: the full Flue
@@ -77903,6 +78060,394 @@ function buildSandboxApi(sessionId) {
77903
78060
  };
77904
78061
  }
77905
78062
  //#endregion
78063
+ //#region src/utils/mcp-client.ts
78064
+ const MCP_PROTOCOL_VERSION = "2025-06-18";
78065
+ const REQUEST_TIMEOUT_MS = 3e4;
78066
+ var McpHttpClient = class {
78067
+ #url;
78068
+ #fetch;
78069
+ #timeoutMs;
78070
+ #sessionId;
78071
+ #nextId = 1;
78072
+ #initialized;
78073
+ constructor(options) {
78074
+ this.#url = options.url;
78075
+ this.#fetch = options.fetch;
78076
+ this.#timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
78077
+ }
78078
+ /**
78079
+ * Initialize the session (idempotent — concurrent callers share one
78080
+ * handshake). Advertises no client capabilities: no elicitation, no
78081
+ * sampling, no roots.
78082
+ */
78083
+ initialize() {
78084
+ this.#initialized ??= this.#doInitialize();
78085
+ return this.#initialized;
78086
+ }
78087
+ async #doInitialize() {
78088
+ const result = await this.#request("initialize", {
78089
+ protocolVersion: MCP_PROTOCOL_VERSION,
78090
+ capabilities: {},
78091
+ clientInfo: {
78092
+ name: "mc8yp",
78093
+ version: "0.0.0"
78094
+ }
78095
+ });
78096
+ await this.#notify("notifications/initialized");
78097
+ return {
78098
+ serverName: result?.serverInfo?.name,
78099
+ serverVersion: result?.serverInfo?.version,
78100
+ instructions: result?.instructions
78101
+ };
78102
+ }
78103
+ /**
78104
+ * List every tool, following pagination cursors.
78105
+ */
78106
+ async listTools() {
78107
+ await this.initialize();
78108
+ const tools = [];
78109
+ let cursor;
78110
+ do {
78111
+ const result = await this.#request("tools/list", cursor ? { cursor } : {});
78112
+ tools.push(...result?.tools ?? []);
78113
+ cursor = result?.nextCursor;
78114
+ } while (cursor);
78115
+ return tools;
78116
+ }
78117
+ /**
78118
+ * Call a tool and unwrap the result: structured content when present,
78119
+ * otherwise joined text content (JSON-parsed when possible). `isError`
78120
+ * results throw with the server's message.
78121
+ * @param name
78122
+ * @param args
78123
+ */
78124
+ async callTool(name, args) {
78125
+ await this.initialize();
78126
+ const result = await this.#request("tools/call", {
78127
+ name,
78128
+ arguments: args && typeof args === "object" ? args : {}
78129
+ });
78130
+ if (!result || typeof result !== "object") return result;
78131
+ if (result.isError) {
78132
+ const message = (result.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n") || `MCP tool "${name}" failed`;
78133
+ throw new Error(message);
78134
+ }
78135
+ if (result.structuredContent != null) return result.structuredContent;
78136
+ const content = result.content ?? [];
78137
+ if (content.length === 0 || !content.every((c) => c.type === "text")) return result;
78138
+ const text = content.map((c) => c.text ?? "").join("\n");
78139
+ try {
78140
+ return JSON.parse(text);
78141
+ } catch {
78142
+ return text;
78143
+ }
78144
+ }
78145
+ /**
78146
+ * Best-effort session teardown. Never throws.
78147
+ */
78148
+ async close() {
78149
+ if (!this.#sessionId) return;
78150
+ try {
78151
+ await this.#fetch(this.#url, {
78152
+ method: "DELETE",
78153
+ headers: { "mcp-session-id": this.#sessionId }
78154
+ });
78155
+ } catch {}
78156
+ this.#sessionId = void 0;
78157
+ this.#initialized = void 0;
78158
+ }
78159
+ async #notify(method) {
78160
+ await this.#post({
78161
+ jsonrpc: "2.0",
78162
+ method
78163
+ });
78164
+ }
78165
+ async #request(method, params) {
78166
+ const id = this.#nextId++;
78167
+ const response = await this.#post({
78168
+ jsonrpc: "2.0",
78169
+ id,
78170
+ method,
78171
+ params
78172
+ });
78173
+ const message = await this.#readResponse(response, id);
78174
+ if (message.error) throw new Error(`MCP ${method} failed: ${message.error.message}`);
78175
+ return message.result;
78176
+ }
78177
+ async #post(payload) {
78178
+ const controller = new AbortController();
78179
+ const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
78180
+ try {
78181
+ const response = await this.#fetch(this.#url, {
78182
+ method: "POST",
78183
+ headers: {
78184
+ "content-type": "application/json",
78185
+ "accept": "application/json, text/event-stream",
78186
+ ...this.#sessionId ? { "mcp-session-id": this.#sessionId } : {}
78187
+ },
78188
+ body: JSON.stringify(payload),
78189
+ signal: controller.signal
78190
+ });
78191
+ this.#sessionId ??= response.headers.get("mcp-session-id") ?? void 0;
78192
+ if (!response.ok && response.status !== 202) throw new Error(`MCP endpoint responded with ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
78193
+ return response;
78194
+ } finally {
78195
+ clearTimeout(timer);
78196
+ }
78197
+ }
78198
+ /**
78199
+ * Read the JSON-RPC response for `id` from a plain-JSON or SSE-framed
78200
+ * response body. Server→client requests encountered on the stream are
78201
+ * declined immediately (fire-and-forget error response) — mc8yp does not
78202
+ * forward elicitation or sampling.
78203
+ * @param response
78204
+ * @param id
78205
+ */
78206
+ async #readResponse(response, id) {
78207
+ const contentType = response.headers.get("content-type") ?? "";
78208
+ const text = await response.text();
78209
+ const messages = contentType.includes("text/event-stream") ? text.split(/\n\n/).flatMap((event) => event.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6))).filter(Boolean).map((data) => JSON.parse(data)) : text.trim() ? [JSON.parse(text)] : [];
78210
+ for (const message of messages) {
78211
+ if (message.method && message.id !== void 0) {
78212
+ this.#declineServerRequest(message).catch(() => void 0);
78213
+ continue;
78214
+ }
78215
+ if (message.id === id) return message;
78216
+ }
78217
+ throw new Error(`MCP endpoint returned no response for request ${id}`);
78218
+ }
78219
+ async #declineServerRequest(request) {
78220
+ consola.warn(`[mcp-client] declining server-initiated request "${request.method}" — mc8yp does not forward elicitation or sampling.`);
78221
+ try {
78222
+ await this.#post({
78223
+ jsonrpc: "2.0",
78224
+ id: request.id,
78225
+ error: {
78226
+ code: -32601,
78227
+ message: `mc8yp does not forward ${request.method === "elicitation/create" ? "elicitation" : request.method === "sampling/createMessage" ? "sampling" : "server-initiated"} requests. The tool cannot interact with the user through this connection.`
78228
+ }
78229
+ });
78230
+ } catch {}
78231
+ }
78232
+ };
78233
+ //#endregion
78234
+ //#region src/utils/external-mcp.ts
78235
+ const NAMESPACE_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
78236
+ /**
78237
+ * Parse external MCP server entries from header values or CLI flags. Every
78238
+ * entry is a JSON object `{ name, url, token?, headers?, description? }`, or a
78239
+ * JSON array of them.
78240
+ *
78241
+ * Validation is strict and fail-loud rather than skip-and-continue: a
78242
+ * malformed entry means the operator intended a server the agent would
78243
+ * otherwise silently not have, so callers turn `failedEntries` into a 400
78244
+ * (server mode) or a startup error (CLI).
78245
+ * @param sources Raw JSON texts, one per entry.
78246
+ */
78247
+ function parseExternalMcpServers(sources) {
78248
+ const servers = [];
78249
+ const failedEntries = [];
78250
+ const usedNames = /* @__PURE__ */ new Set();
78251
+ for (const source of sources) {
78252
+ let parsed;
78253
+ try {
78254
+ parsed = JSON.parse(source);
78255
+ } catch (err) {
78256
+ failedEntries.push({
78257
+ entry: source,
78258
+ reason: `Not valid JSON (${err instanceof Error ? err.message : String(err)}). Expected {"name":"…","url":"https://…","token":"…"}.`
78259
+ });
78260
+ continue;
78261
+ }
78262
+ for (const candidate of Array.isArray(parsed) ? parsed : [parsed]) {
78263
+ const result = validateEntry(candidate, usedNames);
78264
+ if ("reason" in result) {
78265
+ failedEntries.push({
78266
+ entry: typeof candidate === "object" ? JSON.stringify(candidate) : String(candidate),
78267
+ reason: result.reason
78268
+ });
78269
+ continue;
78270
+ }
78271
+ usedNames.add(result.config.name);
78272
+ servers.push(result.config);
78273
+ }
78274
+ }
78275
+ return {
78276
+ servers,
78277
+ failedEntries
78278
+ };
78279
+ }
78280
+ function validateEntry(candidate, usedNames) {
78281
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return { reason: "Entry must be a JSON object with \"name\" and \"url\"." };
78282
+ const entry = candidate;
78283
+ if (typeof entry.name !== "string" || entry.name === "") return { reason: "\"name\" is required and must be a non-empty string — it becomes the sandbox namespace." };
78284
+ if (!NAMESPACE_PATTERN.test(entry.name)) return { reason: `"name" must be a valid JavaScript identifier (letters, digits, underscore; not starting with a digit), got "${entry.name}".` };
78285
+ if (RESERVED_NAMESPACES.has(entry.name)) return { reason: `"name" must not be a reserved namespace (${[...RESERVED_NAMESPACES].join(", ")}), got "${entry.name}".` };
78286
+ if (usedNames.has(entry.name)) return { reason: `Duplicate namespace "${entry.name}" — each external MCP server needs its own name.` };
78287
+ if (typeof entry.url !== "string" || entry.url === "") return { reason: `"url" is required and must be a non-empty string (server "${entry.name}").` };
78288
+ let url;
78289
+ try {
78290
+ url = new URL(entry.url);
78291
+ } catch {
78292
+ return { reason: `"url" must be an absolute URL, got "${entry.url}" (server "${entry.name}").` };
78293
+ }
78294
+ if (url.protocol !== "http:" && url.protocol !== "https:") return { reason: `"url" must use http or https, got "${url.protocol}" (server "${entry.name}").` };
78295
+ if (entry.token !== void 0 && (typeof entry.token !== "string" || entry.token === "")) return { reason: `"token" must be a non-empty string when present (server "${entry.name}").` };
78296
+ if (entry.description !== void 0 && typeof entry.description !== "string") return { reason: `"description" must be a string when present (server "${entry.name}").` };
78297
+ let headers;
78298
+ if (entry.headers !== void 0) {
78299
+ if (typeof entry.headers !== "object" || entry.headers === null || Array.isArray(entry.headers)) return { reason: `"headers" must be an object of string values when present (server "${entry.name}").` };
78300
+ headers = {};
78301
+ for (const [key, value] of Object.entries(entry.headers)) {
78302
+ if (typeof value !== "string") return { reason: `"headers.${key}" must be a string (server "${entry.name}").` };
78303
+ headers[key] = value;
78304
+ }
78305
+ }
78306
+ return { config: {
78307
+ name: entry.name,
78308
+ url: entry.url,
78309
+ ...typeof entry.token === "string" ? { token: entry.token } : {},
78310
+ ...headers ? { headers } : {},
78311
+ ...typeof entry.description === "string" ? { description: entry.description } : {}
78312
+ } };
78313
+ }
78314
+ /**
78315
+ * Request headers for one external server: the `token` bearer shorthand,
78316
+ * then the explicit `headers` map so it can override.
78317
+ * @param config External server config.
78318
+ */
78319
+ function externalMcpHeaders(config) {
78320
+ return {
78321
+ ...config.token ? { authorization: `Bearer ${config.token}` } : {},
78322
+ ...config.headers
78323
+ };
78324
+ }
78325
+ /**
78326
+ * Transport for an external MCP server: plain `fetch` against the absolute
78327
+ * URL with the entry's own credentials attached. No tenant auth, no safeFetch
78328
+ * host pinning — the target is an arbitrary operator-chosen host, not the
78329
+ * tenant.
78330
+ * @param config External server config.
78331
+ */
78332
+ function createExternalMcpFetch(config) {
78333
+ const authHeaders = externalMcpHeaders(config);
78334
+ return (url, init) => fetch(url, {
78335
+ ...init,
78336
+ headers: {
78337
+ ...init.headers,
78338
+ ...authHeaders
78339
+ }
78340
+ });
78341
+ }
78342
+ //#endregion
78343
+ //#region src/codemode/external-mcp-session.ts
78344
+ const IDLE_TTL_MS = 900 * 1e3;
78345
+ const sessions = /* @__PURE__ */ new Map();
78346
+ function configKey(config) {
78347
+ return JSON.stringify([
78348
+ config.name,
78349
+ config.url,
78350
+ config.token ?? "",
78351
+ config.headers ?? {}
78352
+ ]);
78353
+ }
78354
+ function armIdleTimer(sessionKey) {
78355
+ const session = sessions.get(sessionKey);
78356
+ if (!session) return;
78357
+ if (session.timer) clearTimeout(session.timer);
78358
+ session.timer = setTimeout(() => evictExternalMcpSession(sessionKey, "idle-timeout"), IDLE_TTL_MS);
78359
+ session.timer.unref?.();
78360
+ }
78361
+ async function listExternalTools(config) {
78362
+ const client = new McpHttpClient({
78363
+ url: config.url,
78364
+ fetch: createExternalMcpFetch(config)
78365
+ });
78366
+ try {
78367
+ const info = await client.initialize();
78368
+ const tools = await client.listTools();
78369
+ consola.info(`[external-mcp] "${config.name}" at ${config.url}: ${tools.length} tool(s)`);
78370
+ return {
78371
+ config,
78372
+ tools,
78373
+ instructions: info.instructions
78374
+ };
78375
+ } finally {
78376
+ await client.close();
78377
+ }
78378
+ }
78379
+ /**
78380
+ * Resolve every configured external MCP server for a session, using the cached
78381
+ * tool list where one exists. Servers whose handshake fails are reported in
78382
+ * `failures` and get no namespace — mirroring how a discovered service with a
78383
+ * failed spec download is skipped, except that these were explicitly requested
78384
+ * so the agent is told about them.
78385
+ * @param sessionKey MCP session id, or {@link CLI_EXTERNAL_MCP_SESSION} in CLI mode.
78386
+ * @param configs Parsed connection config.
78387
+ */
78388
+ async function resolveExternalMcpServers(sessionKey, configs) {
78389
+ if (configs.length === 0) return {
78390
+ servers: [],
78391
+ failures: []
78392
+ };
78393
+ let session = sessions.get(sessionKey);
78394
+ if (!session) {
78395
+ session = { servers: /* @__PURE__ */ new Map() };
78396
+ sessions.set(sessionKey, session);
78397
+ }
78398
+ armIdleTimer(sessionKey);
78399
+ const settled = await Promise.all(configs.map(async (config) => {
78400
+ const key = configKey(config);
78401
+ let pending = session.servers.get(key);
78402
+ if (!pending) {
78403
+ pending = listExternalTools(config);
78404
+ session.servers.set(key, pending);
78405
+ pending.catch(() => {
78406
+ if (session.servers.get(key) === pending) session.servers.delete(key);
78407
+ });
78408
+ }
78409
+ try {
78410
+ return {
78411
+ ok: true,
78412
+ server: await pending
78413
+ };
78414
+ } catch (err) {
78415
+ const reason = err instanceof Error ? err.message : String(err);
78416
+ consola.warn(`[external-mcp] "${config.name}" at ${config.url} unavailable:`, reason);
78417
+ return {
78418
+ ok: false,
78419
+ failure: {
78420
+ name: config.name,
78421
+ url: config.url,
78422
+ reason
78423
+ }
78424
+ };
78425
+ }
78426
+ }));
78427
+ return {
78428
+ servers: settled.flatMap((r) => r.ok ? [r.server] : []),
78429
+ failures: settled.flatMap((r) => r.ok ? [] : [r.failure])
78430
+ };
78431
+ }
78432
+ /**
78433
+ * Drop a session's cached external tool lists and its timer.
78434
+ * @param sessionKey MCP session id (or the CLI key).
78435
+ * @param reason Why the entry is being dropped; included in the log line.
78436
+ */
78437
+ function evictExternalMcpSession(sessionKey, reason) {
78438
+ const session = sessions.get(sessionKey);
78439
+ if (!session) return;
78440
+ if (session.timer) clearTimeout(session.timer);
78441
+ sessions.delete(sessionKey);
78442
+ consola.info(`[external-mcp] dropped cached tool lists for session ${sessionKey} (reason: ${reason})`);
78443
+ }
78444
+ /**
78445
+ * Evict every session (process exit, test cleanup).
78446
+ */
78447
+ function disposeAllExternalMcpSessions() {
78448
+ for (const sessionKey of [...sessions.keys()]) evictExternalMcpSession(sessionKey, "shutdown");
78449
+ }
78450
+ //#endregion
77906
78451
  //#region node_modules/.pnpm/tslib@2.8.1/node_modules/tslib/tslib.es6.mjs
77907
78452
  function __awaiter(thisArg, _arguments, P, generator) {
77908
78453
  function adopt(value) {
@@ -134718,177 +135263,6 @@ function createC8yAuthHeaders(auth) {
134718
135263
  throw new Error("Invalid authentication credentials");
134719
135264
  }
134720
135265
  //#endregion
134721
- //#region src/utils/mcp-client.ts
134722
- const MCP_PROTOCOL_VERSION = "2025-06-18";
134723
- const REQUEST_TIMEOUT_MS = 3e4;
134724
- var McpHttpClient = class {
134725
- #url;
134726
- #fetch;
134727
- #timeoutMs;
134728
- #sessionId;
134729
- #nextId = 1;
134730
- #initialized;
134731
- constructor(options) {
134732
- this.#url = options.url;
134733
- this.#fetch = options.fetch;
134734
- this.#timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
134735
- }
134736
- /**
134737
- * Initialize the session (idempotent — concurrent callers share one
134738
- * handshake). Advertises no client capabilities: no elicitation, no
134739
- * sampling, no roots.
134740
- */
134741
- initialize() {
134742
- this.#initialized ??= this.#doInitialize();
134743
- return this.#initialized;
134744
- }
134745
- async #doInitialize() {
134746
- const result = await this.#request("initialize", {
134747
- protocolVersion: MCP_PROTOCOL_VERSION,
134748
- capabilities: {},
134749
- clientInfo: {
134750
- name: "mc8yp",
134751
- version: "0.0.0"
134752
- }
134753
- });
134754
- await this.#notify("notifications/initialized");
134755
- return {
134756
- serverName: result?.serverInfo?.name,
134757
- serverVersion: result?.serverInfo?.version,
134758
- instructions: result?.instructions
134759
- };
134760
- }
134761
- /**
134762
- * List every tool, following pagination cursors.
134763
- */
134764
- async listTools() {
134765
- await this.initialize();
134766
- const tools = [];
134767
- let cursor;
134768
- do {
134769
- const result = await this.#request("tools/list", cursor ? { cursor } : {});
134770
- tools.push(...result?.tools ?? []);
134771
- cursor = result?.nextCursor;
134772
- } while (cursor);
134773
- return tools;
134774
- }
134775
- /**
134776
- * Call a tool and unwrap the result: structured content when present,
134777
- * otherwise joined text content (JSON-parsed when possible). `isError`
134778
- * results throw with the server's message.
134779
- * @param name
134780
- * @param args
134781
- */
134782
- async callTool(name, args) {
134783
- await this.initialize();
134784
- const result = await this.#request("tools/call", {
134785
- name,
134786
- arguments: args && typeof args === "object" ? args : {}
134787
- });
134788
- if (!result || typeof result !== "object") return result;
134789
- if (result.isError) {
134790
- const message = (result.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n") || `MCP tool "${name}" failed`;
134791
- throw new Error(message);
134792
- }
134793
- if (result.structuredContent != null) return result.structuredContent;
134794
- const content = result.content ?? [];
134795
- if (content.length === 0 || !content.every((c) => c.type === "text")) return result;
134796
- const text = content.map((c) => c.text ?? "").join("\n");
134797
- try {
134798
- return JSON.parse(text);
134799
- } catch {
134800
- return text;
134801
- }
134802
- }
134803
- /**
134804
- * Best-effort session teardown. Never throws.
134805
- */
134806
- async close() {
134807
- if (!this.#sessionId) return;
134808
- try {
134809
- await this.#fetch(this.#url, {
134810
- method: "DELETE",
134811
- headers: { "mcp-session-id": this.#sessionId }
134812
- });
134813
- } catch {}
134814
- this.#sessionId = void 0;
134815
- this.#initialized = void 0;
134816
- }
134817
- async #notify(method) {
134818
- await this.#post({
134819
- jsonrpc: "2.0",
134820
- method
134821
- });
134822
- }
134823
- async #request(method, params) {
134824
- const id = this.#nextId++;
134825
- const response = await this.#post({
134826
- jsonrpc: "2.0",
134827
- id,
134828
- method,
134829
- params
134830
- });
134831
- const message = await this.#readResponse(response, id);
134832
- if (message.error) throw new Error(`MCP ${method} failed: ${message.error.message}`);
134833
- return message.result;
134834
- }
134835
- async #post(payload) {
134836
- const controller = new AbortController();
134837
- const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
134838
- try {
134839
- const response = await this.#fetch(this.#url, {
134840
- method: "POST",
134841
- headers: {
134842
- "content-type": "application/json",
134843
- "accept": "application/json, text/event-stream",
134844
- ...this.#sessionId ? { "mcp-session-id": this.#sessionId } : {}
134845
- },
134846
- body: JSON.stringify(payload),
134847
- signal: controller.signal
134848
- });
134849
- this.#sessionId ??= response.headers.get("mcp-session-id") ?? void 0;
134850
- if (!response.ok && response.status !== 202) throw new Error(`MCP endpoint responded with ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
134851
- return response;
134852
- } finally {
134853
- clearTimeout(timer);
134854
- }
134855
- }
134856
- /**
134857
- * Read the JSON-RPC response for `id` from a plain-JSON or SSE-framed
134858
- * response body. Server→client requests encountered on the stream are
134859
- * declined immediately (fire-and-forget error response) — mc8yp does not
134860
- * forward elicitation or sampling.
134861
- * @param response
134862
- * @param id
134863
- */
134864
- async #readResponse(response, id) {
134865
- const contentType = response.headers.get("content-type") ?? "";
134866
- const text = await response.text();
134867
- const messages = contentType.includes("text/event-stream") ? text.split(/\n\n/).flatMap((event) => event.split("\n").filter((line) => line.startsWith("data: ")).map((line) => line.slice(6))).filter(Boolean).map((data) => JSON.parse(data)) : text.trim() ? [JSON.parse(text)] : [];
134868
- for (const message of messages) {
134869
- if (message.method && message.id !== void 0) {
134870
- this.#declineServerRequest(message).catch(() => void 0);
134871
- continue;
134872
- }
134873
- if (message.id === id) return message;
134874
- }
134875
- throw new Error(`MCP endpoint returned no response for request ${id}`);
134876
- }
134877
- async #declineServerRequest(request) {
134878
- consola.warn(`[mcp-client] declining server-initiated request "${request.method}" — mc8yp does not forward elicitation or sampling.`);
134879
- try {
134880
- await this.#post({
134881
- jsonrpc: "2.0",
134882
- id: request.id,
134883
- error: {
134884
- code: -32601,
134885
- message: `mc8yp does not forward ${request.method === "elicitation/create" ? "elicitation" : request.method === "sampling/createMessage" ? "sampling" : "server-initiated"} requests. The tool cannot interact with the user through this connection.`
134886
- }
134887
- });
134888
- } catch {}
134889
- }
134890
- };
134891
- //#endregion
134892
135266
  //#region src/codemode/execute.ts
134893
135267
  const EXECUTE_ENTRY_PATH = "/codemode-execute.mjs";
134894
135268
  const BLOCKED_REQUEST_PREFIX = "Request blocked by MCP connection policy.";
@@ -134905,6 +135279,7 @@ async function getSandbox() {
134905
135279
  }
134906
135280
  process$1.once("exit", () => {
134907
135281
  disposeAllSandboxSessions();
135282
+ disposeAllExternalMcpSessions();
134908
135283
  if (sandboxPromise) sandboxPromise.then((s) => s.dispose()).catch(() => void 0);
134909
135284
  });
134910
135285
  function formatRestrictionBlockMessage(method, pathname, matching) {
@@ -135030,29 +135405,39 @@ function buildAgentModule(functionCode) {
135030
135405
  "export default __mc8ypExecute"
135031
135406
  ].join("\n");
135032
135407
  }
135033
- function createLiveCalls(safeFetch, tenantUrl, authHeaders) {
135408
+ function createLiveCalls(tenant) {
135034
135409
  const mcpClients = /* @__PURE__ */ new Map();
135035
- const base = tenantUrl.endsWith("/") ? tenantUrl : `${tenantUrl}/`;
135036
135410
  const mcpClientFor = (namespace) => {
135037
135411
  let client = mcpClients.get(namespace.name);
135038
135412
  if (!client) {
135039
- client = new McpHttpClient({
135040
- url: namespace.server.url,
135041
- fetch: (path, init) => fetch(new URL(path.replace(/^\//, ""), base), {
135042
- ...init,
135043
- headers: {
135044
- ...init.headers,
135045
- ...namespace.server.sendAuthentication ? authHeaders : {}
135046
- }
135047
- })
135413
+ if (namespace.external) client = new McpHttpClient({
135414
+ url: namespace.external.url,
135415
+ fetch: createExternalMcpFetch(namespace.external)
135048
135416
  });
135417
+ else {
135418
+ if ("error" in tenant) throw new Error(tenant.error);
135419
+ const { tenantUrl, authHeaders } = tenant;
135420
+ const base = tenantUrl.endsWith("/") ? tenantUrl : `${tenantUrl}/`;
135421
+ client = new McpHttpClient({
135422
+ url: namespace.server.url,
135423
+ fetch: (path, init) => fetch(new URL(path.replace(/^\//, ""), base), {
135424
+ ...init,
135425
+ headers: {
135426
+ ...init.headers,
135427
+ ...namespace.server.sendAuthentication ? authHeaders : {}
135428
+ }
135429
+ })
135430
+ });
135431
+ }
135049
135432
  mcpClients.set(namespace.name, client);
135050
135433
  }
135051
135434
  return client;
135052
135435
  };
135053
135436
  return {
135054
135437
  operation: async (namespace, opName, input) => {
135055
- return performRequest(safeFetch, tenantUrl, toRequest(namespace.operations.find((o) => o.name === opName), input));
135438
+ if ("error" in tenant) throw new Error(tenant.error);
135439
+ const op = namespace.operations.find((o) => o.name === opName);
135440
+ return performRequest(tenant.safeFetch, tenant.tenantUrl, toRequest(op, input));
135056
135441
  },
135057
135442
  mcpCall: async (namespace, toolName, args) => {
135058
135443
  return mcpClientFor(namespace).callTool(toolName, args);
@@ -135063,17 +135448,7 @@ function createLiveCalls(safeFetch, tenantUrl, authHeaders) {
135063
135448
  }
135064
135449
  };
135065
135450
  }
135066
- function createUnauthenticatedCalls(message) {
135067
- const fail = async () => {
135068
- throw new Error(message);
135069
- };
135070
- return {
135071
- operation: fail,
135072
- mcpCall: fail,
135073
- dispose: async () => {}
135074
- };
135075
- }
135076
- function buildApiModule(namespaces, methodIndex, docsIndex, live, sandbox) {
135451
+ function buildApiModule(namespaces, methodIndex, docsIndex, live, sandbox, externalFailures) {
135077
135452
  const visibleTargets = new Set(namespaces.flatMap((ns) => ns.kind === "openapi" ? ns.operations.map((op) => `${ns.name}.${op.name}`) : ns.tools.map((tool) => `${ns.name}.${tool.name}`)));
135078
135453
  const sandboxEnabled = sandbox !== void 0;
135079
135454
  return {
@@ -135090,9 +135465,15 @@ function buildApiModule(namespaces, methodIndex, docsIndex, live, sandbox) {
135090
135465
  const targets = target.filter((t) => typeof t === "string" && t.trim() !== "");
135091
135466
  if (targets.length === 0) throw new TypeError("codemode.describe(targets): pass method targets like \"c8y.getAlarmCollectionResource\"");
135092
135467
  if (targets.length > 5) throw new TypeError(`codemode.describe(targets): at most 5 targets per call (got ${targets.length}) — shortlist candidates via search first`);
135093
- return targets.map((t) => describeTarget(namespaces, methodIndex, t, sandboxEnabled));
135468
+ return targets.map((t) => describeTarget(namespaces, methodIndex, t, {
135469
+ sandboxEnabled,
135470
+ externalFailures
135471
+ }));
135094
135472
  }
135095
- return describeTarget(namespaces, methodIndex, target == null ? void 0 : String(target), sandboxEnabled);
135473
+ return describeTarget(namespaces, methodIndex, target == null ? void 0 : String(target), {
135474
+ sandboxEnabled,
135475
+ externalFailures
135476
+ });
135096
135477
  }
135097
135478
  },
135098
135479
  docs: {
@@ -135110,18 +135491,28 @@ function buildApiModule(namespaces, methodIndex, docsIndex, live, sandbox) {
135110
135491
  namespaces: Object.fromEntries(namespaces.map((namespace) => [namespace.name, namespace.kind === "openapi" ? Object.fromEntries(namespace.operations.map((op) => [op.name, async (...args) => live.operation(namespace, op.name, args[0])])) : Object.fromEntries(namespace.tools.map((tool) => [tool.name, async (...args) => live.mcpCall(namespace, tool.toolName, args[0])]))]))
135111
135492
  };
135112
135493
  }
135113
- function resolveRuntime() {
135494
+ async function resolveRuntime() {
135114
135495
  const custom = c8yMcpServer.ctx.custom;
135115
135496
  const resolved = custom?.specs;
135116
135497
  if (!resolved) throw new Error(custom?.env === "cli" ? "No active tenant set. Call set-active-tenant first." : "No tenant specs available for this MCP connection. This usually means the request reached the server without a resolvable tenant context (e.g. a platform probe). Reconnect with valid tenant auth.");
135117
135498
  const restrictions = custom?.restrictions ?? [];
135118
135499
  const allowRules = custom?.allowRules ?? [];
135119
135500
  const noMcp = custom?.noMcp;
135501
+ const { servers: externalServers, failures: externalFailures } = await resolveExternalMcpServers(c8yMcpServer.ctx.sessionId ?? "cli", custom?.externalMcpServers ?? []);
135502
+ const namespaces = buildNamespaces(resolved, {
135503
+ restrictions,
135504
+ allowRules,
135505
+ noMcp,
135506
+ externalServers
135507
+ });
135508
+ const externalNames = new Set(namespaces.filter((ns) => ns.kind === "mcp" && ns.external).map((ns) => ns.name));
135120
135509
  return {
135121
135510
  resolved,
135122
135511
  restrictions,
135123
135512
  allowRules,
135124
- namespaces: buildNamespaces(resolved, restrictions, allowRules, noMcp)
135513
+ namespaces,
135514
+ externalFailures,
135515
+ externalMethods: toSearchableMethods(namespaces.filter((ns) => externalNames.has(ns.name)))
135125
135516
  };
135126
135517
  }
135127
135518
  const NO_DEFAULT_EXPORT_MESSAGE = "Execution completed without returning a value.";
@@ -135137,38 +135528,43 @@ function withCliTenantMarker(text, tenantUrl) {
135137
135528
  return `${tenantUrl ? `Executed against tenant: ${tenantUrl}` : "No active tenant — discovery only. Live API calls require set-active-tenant, and visible specs are bundled reference snapshots that may not exist on any tenant."}\n\n${text}`;
135138
135529
  }
135139
135530
  async function execute(functionCode) {
135140
- const { resolved, namespaces, restrictions, allowRules } = resolveRuntime();
135531
+ const { resolved, namespaces, restrictions, allowRules, externalFailures, externalMethods } = await resolveRuntime();
135141
135532
  const SPEC_VIEW = {
135142
135533
  all: true,
135143
135534
  contextPaths: /* @__PURE__ */ new Set()
135144
135535
  };
135145
- const docsIndex = getDocsIndex(resolved, () => buildNamespaces(resolved, [], [], SPEC_VIEW).filter((ns) => ns.kind === "openapi").map((ns) => ({
135536
+ const docsIndex = getDocsIndex(resolved, () => buildNamespaces(resolved, { noMcp: SPEC_VIEW }).filter((ns) => ns.kind === "openapi").map((ns) => ({
135146
135537
  namespace: ns.name,
135147
135538
  spec: ns.spec
135148
135539
  })));
135149
- const methodIndex = getMethodIndex(resolved, () => {
135540
+ const tenantMethodIndex = getMethodIndex(resolved, () => {
135150
135541
  const byTarget = /* @__PURE__ */ new Map();
135151
- for (const item of [...toSearchableMethods(buildNamespaces(resolved)), ...toSearchableMethods(buildNamespaces(resolved, [], [], SPEC_VIEW))]) byTarget.set(item.target, item);
135542
+ for (const item of [...toSearchableMethods(buildNamespaces(resolved)), ...toSearchableMethods(buildNamespaces(resolved, { noMcp: SPEC_VIEW }))]) byTarget.set(item.target, item);
135152
135543
  return [...byTarget.values()];
135153
135544
  });
135545
+ const methodIndex = externalMethods.length === 0 ? tenantMethodIndex : buildMethodIndex([...tenantMethodIndex.methods.values(), ...externalMethods]);
135154
135546
  let tenantUrl = null;
135155
135547
  let live;
135156
135548
  try {
135157
135549
  const auth = await resolveC8yAuth();
135158
135550
  tenantUrl = auth.tenantUrl;
135159
135551
  const authHeaders = createC8yAuthHeaders(auth);
135160
- live = createLiveCalls(createCumulocitySafeFetch(auth.tenantUrl, authHeaders, restrictions, allowRules), auth.tenantUrl, authHeaders);
135552
+ live = createLiveCalls({
135553
+ safeFetch: createCumulocitySafeFetch(auth.tenantUrl, authHeaders, restrictions, allowRules),
135554
+ tenantUrl: auth.tenantUrl,
135555
+ authHeaders
135556
+ });
135161
135557
  } catch (error) {
135162
- live = createUnauthenticatedCalls(error instanceof Error ? error.message : String(error));
135558
+ live = createLiveCalls({ error: error instanceof Error ? error.message : String(error) });
135163
135559
  }
135164
135560
  const sessionId = c8yMcpServer.ctx.sessionId;
135165
- const sandboxApi = c8yMcpServer.ctx.custom?.env === "server" && sessionId ? buildSandboxApi(sessionId) : void 0;
135561
+ const sandboxApi = c8yMcpServer.ctx.custom?.env === "server" && sessionId && c8yMcpServer.ctx.custom?.enableSandbox ? buildSandboxApi(sessionId) : void 0;
135166
135562
  const result = await (await getSandbox()).run({
135167
135563
  code: ENTRY_SOURCE,
135168
135564
  filename: EXECUTE_ENTRY_PATH,
135169
135565
  limits: SANDBOX_LIMITS,
135170
135566
  imports: {
135171
- [API_MODULE_SPECIFIER]: buildApiModule(namespaces, methodIndex, docsIndex, live, sandboxApi),
135567
+ [API_MODULE_SPECIFIER]: buildApiModule(namespaces, methodIndex, docsIndex, live, sandboxApi, externalFailures),
135172
135568
  [AGENT_MODULE_SPECIFIER]: buildAgentModule(functionCode)
135173
135569
  }
135174
135570
  }).finally(() => live.dispose());
@@ -135187,16 +135583,19 @@ function getSafetyPreface(env) {
135187
135583
  ].join("\n");
135188
135584
  }
135189
135585
  /**
135190
- * Purpose framing for the `sandbox` workspace. Server-only: deployed sessions
135191
- * always have it, so the always-read tool description can state it plainly. CLI
135192
- * has no sandbox. This is deliberately about PURPOSE (files/data processing
135193
- * live here), not procedure — the observed failure was the agent not realizing
135194
- * the sandbox is where files go and dumping output to chat.
135586
+ * Purpose framing for the `sandbox` workspace. Server-only, and disabled by
135587
+ * default — a connection must opt in (`mc8yp-enable-sandbox` header /
135588
+ * `enableSandbox` query param) before the `sandbox` global exists, so this
135589
+ * always-read tool description cannot assert it is present the way it can
135590
+ * for `codemode`/`docs`. CLI never has a sandbox regardless. This is
135591
+ * deliberately about PURPOSE (files/data processing live here), not
135592
+ * procedure — the observed failure was the agent not realizing the sandbox
135593
+ * is where files go and dumping output to chat.
135195
135594
  * @param env - execution environment.
135196
135595
  */
135197
135596
  function getSandboxNote(env) {
135198
135597
  if (env !== "server") return "";
135199
- return "\nYour workspace — `sandbox`: this session has a persistent in-memory filesystem + Unix shell, separate from the API. It is where you keep files and process data. When the user asks to save or write a file, or when you need to filter/transform/aggregate fetched data (jq, awk, grep, sort, sqlite), do it here — `sandbox.writeFile(path, text)`, `sandbox.readFile(path)`, `sandbox.exec(command)` — never dump a file into the chat instead. Files persist across codemode calls in this session. Full method list: `codemode.describe(\"sandbox\")`.\n";
135598
+ return "\nYour workspace — `sandbox`: if this connection has it enabled (check `typeof sandbox !== 'undefined'`), it is a persistent in-memory filesystem + Unix shell, separate from the API, for keeping files and processing data. When the user asks to save or write a file, or when you need to filter/transform/aggregate fetched data (jq, awk, grep, sort, sqlite), do it here — `sandbox.writeFile(path, text)`, `sandbox.readFile(path)`, `sandbox.exec(command)` — never dump a file into the chat instead. Files persist across codemode calls in this session. Full method list: `codemode.describe(\"sandbox\")`.\n";
135200
135599
  }
135201
135600
  function createCodemodeTool(env) {
135202
135601
  return defineTool({
@@ -135252,10 +135651,11 @@ declare const docs: {
135252
135651
  }
135253
135652
 
135254
135653
  // API namespaces: \`c8y\` (Cumulocity core — always present) plus one global
135255
- // per microservice available on the current tenant (e.g. \`dtm\`), each with
135256
- // one typed method per operation. If a method seems missing, search
135257
- // with different wording; if it truly does not exist, say so instead of
135258
- // improvising:
135654
+ // per microservice available on the current tenant (e.g. \`dtm\`), plus any
135655
+ // external MCP server configured for this connection — each with one typed
135656
+ // method per operation. \`codemode.describe()\` lists what this connection
135657
+ // actually has. If a method seems missing, search with different wording; if
135658
+ // it truly does not exist, say so instead of improvising:
135259
135659
  // await c8y.getManagedObjectCollectionResource({ pageSize: 5 })
135260
135660
  \`\`\`
135261
135661
 
@@ -140734,17 +141134,17 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140734
141134
  "openapi": "3.0.1",
140735
141135
  "info": {
140736
141136
  "title": "DTM Asset and Definition API",
140737
- "description": "The Digital Twin Manager (DTM) enables schema-based modeling in Cumulocity.\nIt allows creating and managing data model schemas, which serve as blueprints for all Cumulocity domain model entities such as assets and their properties, but also, for example, events, alarms, and measurements.\nThese blueprints act as reusable templates within the platform and are used to describe the logical structure, hierarchies and constraints of\ncomplex [business assets](https://cumulocity.com/docs/glossary/#assets) also known as [digital twins](https://cumulocity.com/docs/glossary/#digital-twin).\n\nThe DTM API is the interface to manage the schema definitions as well as the (asset) instances based on these definitions. The API is structured to separate schema governance (Definition API) from instance management (Asset API):\n\nThe **Definition API** provides the governance layer of the DTM to manage the reusable schema elements that define the structure used to describe, create or validate schema for domain model entities, called _definitions_. \n\nThe **Asset API** allows managing asset instances based on predefined Asset Definitions (also known as [Asset Models](https://cumulocity.com/docs/glossary/#asset-models)). Asset instances created from an Asset Model inherit the structure and constraints defined in the model.\n\n# Cumulocity REST API\n\nThe DTM Definition API and Asset API are an extension of the Cumulocity REST API and follow the same design principles and aspects common to all REST-based interfaces of Cumulocity. For general information about the Cumulocity REST API, see the [Cumulocity REST API documentation](https://cumulocity.com/api/core/).\n\n# Authorization\n\nAll requests issued to DTM Definition API and Asset API are subject to authentication and authorization. For detailed information about authentication, refer to the Cumulocity Core OpenAPI specification details on [Authentication](https://cumulocity.com/api/core/#section/Authentication). To determine the required permissions, see the \"Required user role\" entries for the individual requests.\n\nFor general information about permissions and the concept of ownership in Cumulocity, see [Getting started > Technical concepts > Security aspects > Access control > Managing roles and assigning permissions](https://www.cumulocity.com/docs/concepts/security/#managing-roles-and-assigning-permissions) in the [Cumulocity user documentation](https://cumulocity.com/docs/).\n<br>",
141137
+ "description": "The Digital Twin Manager (DTM) enables schema-based modeling in Cumulocity.\nIt allows creating and managing data model schemas, which serve as blueprints for all Cumulocity domain model entities such as assets and their properties, but also, for example, events, alarms, and measurements.\nThese blueprints act as reusable templates within the platform and are used to describe the logical structure, hierarchies and constraints of complex [business assets](https://cumulocity.com/docs/glossary/#assets) also known as [digital twins]\n(https://cumulocity.com/docs/glossary/#digital-twin).\n\nThe DTM API is the interface to manage the schema definitions as well as the (asset) instances based on these definitions. The API is structured to separate schema governance (Definition API) from instance management (Asset API):\n\n### Definition API\n\nThe **Definition API** provides the governance layer of the DTM to manage the reusable schema elements that define the structure used to describe, create or validate schema for domain model entities, called _definitions_.\nBy default, the definitions are maintained on each tenant individually, but they can also be shared across tenants in a Cumulocity multi-tenant environment. To achieve that, the tenant option `definitions.multitenant.sharing.mode` needs to be set\nto `enabled` on the enterprise tenant and the subtenants. When sharing is enabled, the definitions can be created and updated only in the enterprise tenant. The subtenants will have read-only access to the shared definitions.\n\n### Asset API\n\nThe **Asset API** allows managing asset instances based on predefined Asset Definitions (also known as [Asset Models](https://cumulocity.com/docs/glossary/#asset-models)). Asset instances created from an Asset Model inherit the structure and\nconstraints defined in the model.\n\n# Cumulocity REST API\n\nThe DTM Definition API and Asset API are an extension of the Cumulocity REST API and follow the same design principles and aspects common to all REST-based interfaces of Cumulocity. For general information about the Cumulocity REST API, see the\n[Cumulocity REST API documentation](https://cumulocity.com/api/core/).\n\n# Authorization\n\nAll requests issued to DTM Definition API and Asset API are subject to authentication and authorization. For detailed information about authentication, refer to the Cumulocity Core OpenAPI specification details on [Authentication]\n(https://cumulocity.com/api/core/#section/Authentication). To determine the required permissions, see the \"Required user role\" entries for the individual requests.\n\nFor general information about permissions and the concept of ownership in Cumulocity,\nsee [Getting started > Technical concepts > Security aspects > Access control > Managing roles and assigning permissions](https://www.cumulocity.com/docs/concepts/security/#managing-roles-and-assigning-permissions) in\nthe [Cumulocity user documentation](https://cumulocity.com/docs/).\n<br>\n# Accessing the DTM API\n**Info:** The URL paths of proxied requests consist of:\n* the path of the microservice, which you will find in the application properties of the microservice\n* the corresponding DTM REST API path.\n\n**Example:** For the system `eu-latest.cumulocity.com` and tenant domain name `dtm-demo`, the base URL would be `https://dtm-demo.eu-latest.cumulocity.com/service/dtm/`. The endpoint to query assets is `/assets`. Thus, the complete endpoint path is `https://dtm-demo.eu-latest.cumulocity.com/service/dtm/assets`.\n",
140738
141138
  "version": "Latest"
140739
141139
  },
140740
141140
  "servers": [{
140741
- "url": "https://<TENANT_DOMAIN>",
141141
+ "url": "/",
140742
141142
  "description": "The Digital Twin Manager service."
140743
141143
  }],
140744
141144
  "tags": [
140745
141145
  {
140746
141146
  "name": "Assets",
140747
- "description": "The Asset API extends the Cumulocity core capabilities and domain model to manage assets with schema-based governance by linking asset instances to predefined Asset Definitions (also known as Asset Models). The Asset Definition serves as a blueprint\nfor the asset instances, defining\ntheir structure and constraints and relationships within an asset hierarchy.\n\nAsset instances without a linked Asset Definition are treated as generic assets without schema governance or constraints of the logical structure defined in an Asset Definition.\n\nThe Asset API extends the Cumulocity core capabilities with\n* Schema Governance: Enforcing and managing the underlying data schemas.\n* Integration: Synchronizing external sources of assets and digital twins.\n* Bulk Operations: Facilitating large-scale asset management.\n* Linked Series: Linking of time-series context to assets.\n* Permissions: Elevated permissions for synchronization and linked series management.\n\n### Asset Synchronization\n\nThe Asset API provides optimized operations specifically designed to efficiently create or update assets within synchronization workflows from external systems.\n\nFor referencing an asset from an external system, a unique identifier is required to map the asset between Cumulocity and the external system. This unique identifier is used in addition to the Cumulocity asset id and can be used to query the asset via the Asset API. This has a significant performance benefit over using any custom fragment property or even properties to identify the asset. Internally the Asset API creates an external id in the Identity API of type `c8y_Asset` for each asset that has an external identifier set via the `c8y_ExternalId` fragment.\n\nUsing the external identifier, the Asset API can perform idempotent create or update operations. This means that if an asset with the specified external identifier already exists, it will be updated; otherwise, a new asset will be created. This is particularly useful in synchronization scenarios where the same asset data may be processed multiple times, ensuring that duplicate assets are not created.\n\nIf the asset has the `c8y_ExternalAsset` fragment, it is considered to be externally managed applying additional restrictions on update and delete operations to avoid unintended modifications of externally managed assets. See the Permissions section below for more details.\n\nThe `c8y_ExternalAsset` fragment allows defining a `source` to indicate the origin of the external asset.\n\n```json\n{\n \"c8y_ExternalAsset\": {\n \"source\": \"string\"\n }\n}\n```\n\n### Permissions\n\nBy default, for all Asset API operations, users require the corresponding *ROLE_INVENTORY_\\** permission to create, update or delete assets. The *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions are elevated permissions that are specifically designed to control modifications of externally managed assets in addition to the *ROLE_INVENTORY_\\** roles and permissions.\n\nThe *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions are only required for updating or deleting externally managed assets (assets having the `c8y_ExternalAsset` fragment).\nThis is especially useful for using the Asset API for synchronization of assets from an external source and to control modifications of these externally managed assets within Cumulocity while still allowing regular\n(not external) assets to be managed without additional permissions.\n\nTo enforce always requiring the *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions, no matter if the asset is external or not, the tenant option `assets.permission.mode` can be configured to `all`. In this case, the *ROLE_DIGITAL_TWIN_ASSETS_CREATE* permission is required to create assets. To disable the requirement of *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions completely, the tenant option can be set to `none`.\n\nThe following elevated permissions are available for the Asset API:\n\n- *ROLE_DIGITAL_TWIN_ASSETS_CREATE*: Users can create new assets including LinkedSeries and their Source.\n- *ROLE_DIGITAL_TWIN_ASSETS_UPDATE*: Users can update existing assets including LinkedSeries and their Source.\n- *ROLE_DIGITAL_TWIN_ASSETS_ADMIN*: Users can manage all assets, including creating, updating, and deleting them.\n"
141147
+ "description": "The Asset API extends the Cumulocity core capabilities and domain model to manage assets with schema-based governance by linking asset instances to predefined Asset Definitions (also known as Asset Models). The Asset Definition serves as a blueprint\nfor the asset instances, defining\ntheir structure and constraints and relationships within an asset hierarchy.\n\nAsset instances without a linked Asset Definition are treated as generic assets without schema governance or constraints of the logical structure defined in an Asset Definition.\n\nThe Asset API extends the Cumulocity core capabilities with\n* Schema Governance: Enforcing and managing the underlying data schemas.\n* Integration: Synchronizing external sources of assets and digital twins.\n* Bulk Operations: Facilitating large-scale asset management.\n* Linked Series: Linking of time-series context to assets.\n* Permissions: Elevated permissions for synchronization and linked series management.\n\n### Asset Synchronization\n\nThe Asset API provides optimized operations specifically designed to efficiently create or update assets within synchronization workflows from external systems.\n\nFor referencing an asset from an external system, a unique identifier is required to map the asset between Cumulocity and the external system. This unique identifier is used in addition to the Cumulocity asset id and can be used to query the asset via the Asset API. This has a significant performance benefit over using any custom fragment property or even properties to identify the asset. Internally the Asset API creates an external id in the Identity API of type `c8y_Asset` for each asset that has an external identifier set via the `c8y_ExternalId` fragment.\n\nUsing the external identifier, the Asset API can perform idempotent create or update operations. This means that if an asset with the specified external identifier already exists, it will be updated; otherwise, a new asset will be created. This is particularly useful in synchronization scenarios where the same asset data may be processed multiple times, ensuring that duplicate assets are not created.\n\nIf the asset has the `c8y_ExternalAsset` fragment, it is considered to be externally managed applying additional restrictions on update and delete operations to avoid unintended modifications of externally managed assets. See the Permissions section below for more details.\n\nThe `c8y_ExternalAsset` fragment allows defining a `source` to indicate the origin of the external asset.\n\n```json\n{\n \"c8y_ExternalAsset\": {\n \"source\": \"string\"\n }\n}\n```\n\n### Permissions\n\nBy default, for all Asset API operations, users require the corresponding *ROLE_INVENTORY_\\** permission to create, update or delete assets. The *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions are elevated permissions that are specifically designed to control modifications of externally managed assets in addition to the *ROLE_INVENTORY_\\** roles and permissions.\n\nBy default, the *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions are only required for updating (including assigning devices) or deleting externally managed assets (assets having the `c8y_ExternalAsset` fragment).\nThis is especially useful for using the Asset API for synchronization of assets from an external source and to control modifications of these externally managed assets within Cumulocity while still allowing regular\n(not external) assets to be managed without additional permissions.\n\nThis behavior can be configured by setting the tenant option `assets.permission.mode`: to enforce **always** requiring the *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions, no matter if the asset is external or not, the tenant option can be configured to\n`all`. In this case, the *ROLE_DIGITAL_TWIN_ASSETS_CREATE* permission is required to create assets. To disable the requirement of *ROLE_DIGITAL_TWIN_ASSETS_\\** permissions completely, the tenant option can be set to `none`.\n\nThe following elevated permissions are available for the Asset API:\n\n- *ROLE_DIGITAL_TWIN_ASSETS_CREATE*: Users can create new assets including LinkedSeries and their Source.\n- *ROLE_DIGITAL_TWIN_ASSETS_UPDATE*: Users can update existing assets including LinkedSeries and their Source.\n- *ROLE_DIGITAL_TWIN_ASSETS_ADMIN*: Users can manage all assets, including creating, updating, and deleting them.\n"
140748
141148
  },
140749
141149
  {
140750
141150
  "name": "Linked Series",
@@ -140776,7 +141176,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140776
141176
  "get": {
140777
141177
  "tags": ["Property Definitions"],
140778
141178
  "summary": "Retrieve a Property Definition by identifier",
140779
- "description": "Finds a `Property Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Property Definition`. The `applicableTo` parameter is used to find the `Property Definition` that is applicable to the specified domain entity. Otherwise, this operation responds in `HTTP 404`. If no `applicableTo` is specified, the operation will return the `Property Definition` that is not applicable to any domain entity, if it exists.",
141179
+ "description": "Finds a `Property Definition` by its `identifier`.\n\n If `applicableTo` is specified, the `identifier` needs to point to an existing `Property Definition` that is applicable to that domain entity. If `applicableTo` is not specified, the operation will return the `Property Definition` that is **not applicable to any** domain entity (i.e. where no context is applied). If no such `Property Definition` exists, it will respond with `HTTP 404`. ",
140780
141180
  "operationId": "getPropertyDefinition",
140781
141181
  "parameters": [{
140782
141182
  "name": "identifier",
@@ -140787,7 +141187,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140787
141187
  }, {
140788
141188
  "name": "applicableTo",
140789
141189
  "in": "query",
140790
- "description": "Limits the response to Property Definitions applicable to the specified domain entity.",
141190
+ "description": "Limits the response to the Property Definitions that are applicable to the specified domain entity.",
140791
141191
  "schema": {
140792
141192
  "type": "string",
140793
141193
  "enum": [
@@ -140809,7 +141209,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140809
141209
  "put": {
140810
141210
  "tags": ["Property Definitions"],
140811
141211
  "summary": "Update an existing Property Definition",
140812
- "description": "Updates an existing `Property Definition`.\n\n Updates the `Property Definition` that is exactly identified by the `identifier` and `contexts` in the path. The `Property Definition` must be applicable to all the specified domain entities (and no others). If no contexts is specified, the `Property Definition` that is not applicable to any domain entity will be updated. The `requestBody` is used to update the `Property Definition` with the new values. This operation is restricted to updating the existing JSON Schema (including `title` and `description`), `contexts`, `tags`, and other custom fragments. Updating the `identifier` is not permitted.",
141212
+ "description": "Updates an existing `Property Definition`.\n\n Updates the `Property Definition` that is exactly identified by the `identifier` and `contexts` in the path. The `Property Definition` must be applicable to all the specified domain entities (and no others). If no contexts is specified, the `Property Definition` that is not applicable to any domain entity will be updated. The `requestBody` is used to update the `Property Definition` with the new values. This operation is restricted to updating the existing JSON Schema (including `title` and `description`), `contexts`, `tags`, and other custom fragments. Updating the `identifier` is not permitted. ",
140813
141213
  "operationId": "updatePropertyDefinition",
140814
141214
  "parameters": [{
140815
141215
  "name": "identifier",
@@ -140820,7 +141220,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140820
141220
  }, {
140821
141221
  "name": "contexts",
140822
141222
  "in": "query",
140823
- "description": "Limits the response to Property Definitions matching exactly the specified domain entities.",
141223
+ "description": "Indicates the resource to filter for the Property Definition that matches exactly the specified domain entities.",
140824
141224
  "explode": false,
140825
141225
  "schema": {
140826
141226
  "type": "array",
@@ -140851,7 +141251,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140851
141251
  "delete": {
140852
141252
  "tags": ["Property Definitions"],
140853
141253
  "summary": "Delete an existing Property Definition",
140854
- "description": "Deletes an existing `Property Definition` by its `identifier` and `contexts`.\n\n Deletes only the Property Definition that is exactly applicable to all the specified domain entities (and no others). If no contexts is specified, the operation will delete the Property Definition that is not applicable to any domain entity.",
141254
+ "description": "Deletes an existing `Property Definition` by its `identifier` and `contexts`.\n\n Deletes only the Property Definition that is exactly applicable to all the specified domain entities (and no others). If no contexts is specified, the operation will delete the Property Definition that is not applicable to any domain entity. ",
140855
141255
  "operationId": "deletePropertyDefinition",
140856
141256
  "parameters": [{
140857
141257
  "name": "identifier",
@@ -140862,7 +141262,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140862
141262
  }, {
140863
141263
  "name": "contexts",
140864
141264
  "in": "query",
140865
- "description": "Limits the response to Property Definitions matching exactly the specified domain entities.",
141265
+ "description": "Indicates the resource to filter for the Property Definition that matches exactly the specified domain entities.",
140866
141266
  "explode": false,
140867
141267
  "schema": {
140868
141268
  "type": "array",
@@ -140887,7 +141287,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140887
141287
  "get": {
140888
141288
  "tags": ["Measurement Definitions"],
140889
141289
  "summary": "Retrieve Measurement Definitions",
140890
- "description": "Finds a collection of `Measurement Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Measurement Definition`s.\n\n * Passing only one `title` results in `0...1` `Measurement Definition`s.\n\n\n\n\n\n The following rules will be applied when searching for the `Measurement Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
141290
+ "description": "Finds a collection of `Measurement Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Measurement Definition`s.\n\n * Passing only one `title` results in `0...1` `Measurement Definition`s.\n\n \n\n \n\n The following rules will be applied when searching for the `Measurement Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
140891
141291
  "operationId": "getMeasurementDefinitions",
140892
141292
  "parameters": [
140893
141293
  {
@@ -140948,9 +141348,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140948
141348
  "in": "query",
140949
141349
  "description": "The current page number to be retrieved.",
140950
141350
  "schema": {
140951
- "type": "integer",
140952
- "format": "int32",
140953
- "default": 1
141351
+ "minimum": 1,
141352
+ "type": "integer"
140954
141353
  }
140955
141354
  },
140956
141355
  {
@@ -140972,7 +141371,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140972
141371
  "put": {
140973
141372
  "tags": ["Measurement Definitions"],
140974
141373
  "summary": "Update an existing Measurement Definition",
140975
- "description": "Updates an existing `Measurement Definition`.\n\n The Measurement Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted.",
141374
+ "description": "Updates an existing `Measurement Definition`.\n\n The Measurement Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted. ",
140976
141375
  "operationId": "updateMeasurementDefinition",
140977
141376
  "requestBody": {
140978
141377
  "description": "The data payload representing the `Measurement Definition` to be updated.",
@@ -140987,7 +141386,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
140987
141386
  "post": {
140988
141387
  "tags": ["Measurement Definitions"],
140989
141388
  "summary": "Create a new Measurement Definition",
140990
- "description": "Creates a new `Measurement Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Measurement Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n\n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema).",
141389
+ "description": "Creates a new `Measurement Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Measurement Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n \n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema). ",
140991
141390
  "operationId": "createMeasurementDefinition",
140992
141391
  "requestBody": {
140993
141392
  "description": "The data payload representing the `Measurement Definition` to be created.",
@@ -141004,7 +141403,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141004
141403
  "get": {
141005
141404
  "tags": ["Event Definitions"],
141006
141405
  "summary": "Retrieve Event Definitions",
141007
- "description": "Finds a collection of `Event Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Event Definition`s.\n\n * Passing only one `title` results in `0...1` `Event Definition`s.\n\n\n\n\n\n The following rules will be applied when searching for the `Event Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
141406
+ "description": "Finds a collection of `Event Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Event Definition`s.\n\n * Passing only one `title` results in `0...1` `Event Definition`s.\n\n \n\n \n\n The following rules will be applied when searching for the `Event Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
141008
141407
  "operationId": "getEventDefinitions",
141009
141408
  "parameters": [
141010
141409
  {
@@ -141065,9 +141464,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141065
141464
  "in": "query",
141066
141465
  "description": "The current page number to be retrieved.",
141067
141466
  "schema": {
141068
- "type": "integer",
141069
- "format": "int32",
141070
- "default": 1
141467
+ "minimum": 1,
141468
+ "type": "integer"
141071
141469
  }
141072
141470
  },
141073
141471
  {
@@ -141089,7 +141487,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141089
141487
  "put": {
141090
141488
  "tags": ["Event Definitions"],
141091
141489
  "summary": "Update an existing Event Definition",
141092
- "description": "Updates an existing `Event Definition`.\n\n The Event Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted.",
141490
+ "description": "Updates an existing `Event Definition`.\n\n The Event Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted. ",
141093
141491
  "operationId": "updateEventDefinition",
141094
141492
  "requestBody": {
141095
141493
  "description": "The data payload representing the `Event Definition` to be updated.",
@@ -141104,7 +141502,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141104
141502
  "post": {
141105
141503
  "tags": ["Event Definitions"],
141106
141504
  "summary": "Create a new Event Definition",
141107
- "description": "Creates a new `Event Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Event Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n\n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema).",
141505
+ "description": "Creates a new `Event Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Event Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n \n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema). ",
141108
141506
  "operationId": "createEventDefinition",
141109
141507
  "requestBody": {
141110
141508
  "description": "The data payload representing the `Event Definition` to be created.",
@@ -141121,7 +141519,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141121
141519
  "get": {
141122
141520
  "tags": ["Asset Definitions"],
141123
141521
  "summary": "Retrieve Asset Definitions",
141124
- "description": "Finds a collection of `Asset Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Asset Definition`s.\n\n * Passing only one `title` results in `0...1` `Asset Definition`s.\n\n\n\n\n\n The following rules will be applied when searching for the `Asset Definition`s: \n\n * `identifiers`, `titles`, `onlyRoots` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
141522
+ "description": "Finds a collection of `Asset Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Asset Definition`s.\n\n * Passing only one `title` results in `0...1` `Asset Definition`s.\n\n \n\n \n\n The following rules will be applied when searching for the `Asset Definition`s: \n\n * `identifiers`, `titles`, `onlyRoots` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
141125
141523
  "operationId": "getAssetDefinitions",
141126
141524
  "parameters": [
141127
141525
  {
@@ -141191,9 +141589,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141191
141589
  "in": "query",
141192
141590
  "description": "The current page number to be retrieved.",
141193
141591
  "schema": {
141194
- "type": "integer",
141195
- "format": "int32",
141196
- "default": 1
141592
+ "minimum": 1,
141593
+ "type": "integer"
141197
141594
  }
141198
141595
  },
141199
141596
  {
@@ -141215,7 +141612,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141215
141612
  "put": {
141216
141613
  "tags": ["Asset Definitions"],
141217
141614
  "summary": "Update an existing Asset Definition",
141218
- "description": "Updates the `Asset Definition`.\n\n Throws a `ConflictException` if validation fails due to conflicts in the specified properties or sub-assets. Such conflicts occur when one or more of the provided allowed properties or sub-assets either do not exist or are not applicable within the current `asset` context. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
141615
+ "description": "Updates the `Asset Definition`.\n\n Throws a `ConflictException` if validation fails due to conflicts in the specified properties or sub-assets. Such conflicts occur when one or more of the provided allowed properties or sub-assets either do not exist or are not applicable within the current `asset` context. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`* ",
141219
141616
  "operationId": "updateAssetDefinition",
141220
141617
  "requestBody": {
141221
141618
  "description": "The data payload containing only the fields that need to be updated or added to the existing `Asset Definition`. Fields\nnot included in the payload will remain unchanged.",
@@ -141247,7 +141644,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141247
141644
  "get": {
141248
141645
  "tags": ["Alarm Definitions"],
141249
141646
  "summary": "Retrieve a collection of Alarm Definitions",
141250
- "description": "Finds a collection of `Alarm Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Alarm Definition`s.\n\n * Passing only one `title` results in `0...1` `Alarm Definition`s.\n\n\n\n\n\n The following rules will be applied when searching for the `Alarm Definition`s: \n\n * `identifiers`, `titles` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
141647
+ "description": "Finds a collection of `Alarm Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Alarm Definition`s.\n\n * Passing only one `title` results in `0...1` `Alarm Definition`s.\n\n \n\n \n\n The following rules will be applied when searching for the `Alarm Definition`s: \n\n * `identifiers`, `titles` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
141251
141648
  "operationId": "getAlarmDefinitions",
141252
141649
  "parameters": [
141253
141650
  {
@@ -141308,9 +141705,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141308
141705
  "in": "query",
141309
141706
  "description": "The current page number to be retrieved.",
141310
141707
  "schema": {
141311
- "type": "integer",
141312
- "format": "int32",
141313
- "default": 1
141708
+ "minimum": 1,
141709
+ "type": "integer"
141314
141710
  }
141315
141711
  },
141316
141712
  {
@@ -141332,7 +141728,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141332
141728
  "put": {
141333
141729
  "tags": ["Alarm Definitions"],
141334
141730
  "summary": "Update an existing Alarm Definition",
141335
- "description": "Updates an existing `Alarm Definition`.\n\n The Alarm Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted.",
141731
+ "description": "Updates an existing `Alarm Definition`.\n\n The Alarm Definition's `identifier` is taken from the `requestBody`. This operation is restricted to updating the existing JSON Schema (including `title` and `description`). No other modifications are permitted. ",
141336
141732
  "operationId": "updateAlarmDefinition",
141337
141733
  "requestBody": {
141338
141734
  "description": "The data payload representing the `Alarm Definition` to be updated.",
@@ -141347,7 +141743,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141347
141743
  "post": {
141348
141744
  "tags": ["Alarm Definitions"],
141349
141745
  "summary": "Create a new Alarm Definition",
141350
- "description": "Creates a new `Alarm Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Alarm Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n\n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema).",
141746
+ "description": "Creates a new `Alarm Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Alarm Definition` must be unique.\n\n * If a `title` is provided, it must also be unique.\n\n \n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema). ",
141351
141747
  "operationId": "createAlarmDefinition",
141352
141748
  "requestBody": {
141353
141749
  "description": "The data payload representing the `Alarm Definition` to be created.",
@@ -141385,7 +141781,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141385
141781
  {
141386
141782
  "name": "withSubAssets",
141387
141783
  "in": "query",
141388
- "description": "Indicates the resource to add the sub-assets in the response.",
141784
+ "description": "**Deprecated** – use the dedicated `/assets/{assetId}/subAssets`, `/assets/externalIds/{externalId}/subAssets` or `/assets/subAssets` endpoint instead!<br>Indicates the resource to add the sub-assets in the response.",
141785
+ "deprecated": true,
141389
141786
  "schema": {
141390
141787
  "type": "boolean",
141391
141788
  "default": false
@@ -141399,6 +141796,24 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141399
141796
  "type": "boolean",
141400
141797
  "default": false
141401
141798
  }
141799
+ },
141800
+ {
141801
+ "name": "withParents",
141802
+ "in": "query",
141803
+ "description": "Indicates the resource to add the parent assets in the response.",
141804
+ "schema": {
141805
+ "type": "boolean",
141806
+ "default": false
141807
+ }
141808
+ },
141809
+ {
141810
+ "name": "withChildrenCount",
141811
+ "in": "query",
141812
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
141813
+ "schema": {
141814
+ "type": "boolean",
141815
+ "default": false
141816
+ }
141402
141817
  }
141403
141818
  ],
141404
141819
  "responses": { "200": {
@@ -141409,7 +141824,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141409
141824
  "put": {
141410
141825
  "tags": ["Assets"],
141411
141826
  "summary": "Update an existing asset",
141412
- "description": "Updates an existing asset. All properties of the asset can be updated, including `name`, `type`,\n`c8y_ExternalId`, and fragments - except the `id`. Missing properties in the request body will not be removed but stay\nunchanged. Fragments, however, will be replaced completely, so missing properties in a fragment will be removed. If you want to remove a\nfragment, you have to explicitly set it to `null`. If the `c8y_ExternalId` is different to the existing Asset, it will be\nupdated accordingly. If the new `c8y_ExternalId` already exists, a conflict error will be returned.\n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` **AND** `ROLE_INVENTORY_ADMIN`*",
141827
+ "description": "Updates an existing asset. All properties of the asset can be updated, including `name`, `type`,\n`c8y_ExternalId`, and fragments - except the `id`. Missing properties in the request body will not be removed but stay\nunchanged. Fragments, however, will be replaced completely, so missing properties in a fragment will be removed. If you want to remove a\nfragment, you have to explicitly set it to `null`. If the `c8y_ExternalId` is different to the existing Asset, it will be\nupdated accordingly. If the new `c8y_ExternalId` already exists, a conflict error will be returned.\n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
141413
141828
  "operationId": "updateAsset",
141414
141829
  "parameters": [{
141415
141830
  "name": "assetId",
@@ -141437,14 +141852,34 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141437
141852
  "delete": {
141438
141853
  "tags": ["Assets"],
141439
141854
  "summary": "Delete an existing asset",
141440
- "description": "Deletes an existing asset.\n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
141855
+ "description": "Deletes an existing asset and optionally its hierarchy consisting of all assigned sub-assets and/or all assigned devices. Sub-assets and\ndevices which are also assigned to other parent assets outside the hierarchy, will not be deleted but only unassigned from the deleted asset.\n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
141441
141856
  "operationId": "deleteAsset",
141442
- "parameters": [{
141443
- "name": "assetId",
141444
- "in": "path",
141445
- "required": true,
141446
- "schema": { "type": "string" }
141447
- }],
141857
+ "parameters": [
141858
+ {
141859
+ "name": "assetId",
141860
+ "in": "path",
141861
+ "required": true,
141862
+ "schema": { "type": "string" }
141863
+ },
141864
+ {
141865
+ "name": "deleteSubAssets",
141866
+ "in": "query",
141867
+ "description": "Indicates whether the resource also deletes sub-assets. If `false`, sub-assets are only unassigned from the deleted asset but not deleted themselves.",
141868
+ "schema": {
141869
+ "type": "boolean",
141870
+ "default": true
141871
+ }
141872
+ },
141873
+ {
141874
+ "name": "deleteDevices",
141875
+ "in": "query",
141876
+ "description": "Indicates whether the resource also deletes devices (managed objects with the `c8y_IsDevice` fragment) whose all parent assets are being deleted.",
141877
+ "schema": {
141878
+ "type": "boolean",
141879
+ "default": false
141880
+ }
141881
+ }
141882
+ ],
141448
141883
  "responses": { "204": { "description": "No Content" } }
141449
141884
  }
141450
141885
  },
@@ -141452,7 +141887,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141452
141887
  "get": {
141453
141888
  "tags": ["Linked Series"],
141454
141889
  "summary": "Retrieve the source for a given linked series",
141455
- "description": "Retrieves the source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`.",
141890
+ "description": "Retrieves the source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. ",
141456
141891
  "operationId": "getLinkedSeriesSource",
141457
141892
  "parameters": [
141458
141893
  {
@@ -141482,7 +141917,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141482
141917
  "put": {
141483
141918
  "tags": ["Linked Series"],
141484
141919
  "summary": "Update the source for a given linked series",
141485
- "description": "Updates an existing source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_UPDATE` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*.",
141920
+ "description": "Updates an existing source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_UPDATE` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*. ",
141486
141921
  "operationId": "updateLinkedSeriesSource",
141487
141922
  "parameters": [
141488
141923
  {
@@ -141525,7 +141960,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141525
141960
  "post": {
141526
141961
  "tags": ["Linked Series"],
141527
141962
  "summary": "Set the source for a given linked series",
141528
- "description": "Adds a new source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_CREATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_CREATE` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*.",
141963
+ "description": "Adds a new source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_CREATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_CREATE` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*. ",
141529
141964
  "operationId": "createLinkedSeriesSource",
141530
141965
  "parameters": [
141531
141966
  {
@@ -141568,7 +142003,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141568
142003
  "delete": {
141569
142004
  "tags": ["Linked Series"],
141570
142005
  "summary": "Delete the source for a given linked series",
141571
- "description": "Deletes an existing source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*.",
142006
+ "description": "Deletes an existing source for a given linked series of an asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_ADMIN`*. ",
141572
142007
  "operationId": "deleteLinkedSeriesSource",
141573
142008
  "parameters": [
141574
142009
  {
@@ -141596,7 +142031,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141596
142031
  "/service/dtm/assets/{assetId}/linkedSeries/{fragment}/{series}/source/type": { "put": {
141597
142032
  "tags": ["Linked Series"],
141598
142033
  "summary": "Update the measurement type of a linked series source",
141599
- "description": "Modifies the `type` of a source of a linked series for a given asset. If the `fetchTypeFromSource` is set to\n`true`, the type specified in the body of the request will be ignored. Instead, the value is fetched from the last measurement of\nthe device defined by the which `source.id`.",
142034
+ "description": "Modifies the `type` of a source of a linked series for a given asset. If the `fetchTypeFromSource` is set to\n`true`, the type specified in the body of the request will be ignored. Instead, the value is fetched from the last measurement of\nthe device defined by the which `source.id`.\n\n Using `content-type=application/json` is deprecated, use `content-type=text/plain` instead. ",
141600
142035
  "operationId": "updateMeasurementType",
141601
142036
  "parameters": [
141602
142037
  {
@@ -141627,16 +142062,61 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141627
142062
  }
141628
142063
  }
141629
142064
  ],
141630
- "requestBody": { "content": { "application/json": { "schema": { "type": "string" } } } },
142065
+ "requestBody": { "content": {
142066
+ "text/plain": { "schema": { "type": "string" } },
142067
+ "application/json": { "schema": { "type": "string" } }
142068
+ } },
141631
142069
  "responses": { "200": {
141632
142070
  "description": "OK",
141633
142071
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Source" } } }
141634
142072
  } }
141635
142073
  } },
142074
+ "/service/dtm/assets/{assetId}/linkedSeries/{fragment}/{series}/reconcileOpposite": { "put": {
142075
+ "tags": ["Linked Series"],
142076
+ "summary": "Reconcile the opposite link for a linked series",
142077
+ "description": "Reconciles the opposite link (MeasurementSourceLink) for a given linked series.\n\n Recreates or updates the LinkedAsset representation in the device's MeasurementSourceLink (c8y_LinkedSeriesReverseIndex) referenced by the LinkedSeries' `source.id`, ensuring bidirectional link consistency. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*. ",
142078
+ "operationId": "reconcileOppositeLink",
142079
+ "parameters": [
142080
+ {
142081
+ "name": "assetId",
142082
+ "in": "path",
142083
+ "required": true,
142084
+ "schema": { "type": "string" }
142085
+ },
142086
+ {
142087
+ "name": "fragment",
142088
+ "in": "path",
142089
+ "required": true,
142090
+ "schema": { "type": "string" }
142091
+ },
142092
+ {
142093
+ "name": "series",
142094
+ "in": "path",
142095
+ "required": true,
142096
+ "schema": { "type": "string" }
142097
+ },
142098
+ {
142099
+ "name": "removeMissingSourceId",
142100
+ "in": "query",
142101
+ "description": "Indicates the resource to remove the `id` of the source of the Linked Series if the device with the given id does not exist. This can be used to automatically clean up Linked Series with non-existing sources.",
142102
+ "schema": {
142103
+ "type": "boolean",
142104
+ "default": false
142105
+ }
142106
+ }
142107
+ ],
142108
+ "responses": {
142109
+ "200": {
142110
+ "description": "OK",
142111
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkedAsset" } } }
142112
+ },
142113
+ "204": { "description": "No Content" }
142114
+ }
142115
+ } },
141636
142116
  "/service/dtm/assets/{assetId}/linkedSeries/{fragment}/{series}/label": { "put": {
141637
142117
  "tags": ["Linked Series"],
141638
142118
  "summary": "Update the label of a linked series",
141639
- "description": "Modifies the `label` of a linked series for a given asset.",
142119
+ "description": "Modifies the `label` of a linked series for a given asset.\n\n Using `content-type=application/json` is deprecated, use `content-type=text/plain` instead. ",
141640
142120
  "operationId": "updateLabel",
141641
142121
  "parameters": [
141642
142122
  {
@@ -141659,7 +142139,10 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141659
142139
  }
141660
142140
  ],
141661
142141
  "requestBody": {
141662
- "content": { "application/json": { "schema": { "type": "string" } } },
142142
+ "content": {
142143
+ "text/plain": { "schema": { "type": "string" } },
142144
+ "application/json": { "schema": { "type": "string" } }
142145
+ },
141663
142146
  "required": true
141664
142147
  },
141665
142148
  "responses": { "200": {
@@ -141667,11 +142150,41 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141667
142150
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkedSeries" } } }
141668
142151
  } }
141669
142152
  } },
142153
+ "/service/dtm/assets/{assetId}/linkedSeries/reconcileOpposite": { "put": {
142154
+ "tags": ["Linked Series"],
142155
+ "summary": "Reconcile the opposite links for all linked series of an asset",
142156
+ "description": "Reconciles the opposite links (MeasurementSourceLink) for all LinkedSeries with a `source.id` in the given asset.\n\n For each LinkedSeries with a `source.id`, recreates or updates the LinkedAsset representation in the device's MeasurementSourceLink (c8y_LinkedSeriesReverseIndex), ensuring bidirectional link consistency across the whole asset. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*. ",
142157
+ "operationId": "reconcileOppositeLinks",
142158
+ "parameters": [{
142159
+ "name": "assetId",
142160
+ "in": "path",
142161
+ "required": true,
142162
+ "schema": { "type": "string" }
142163
+ }, {
142164
+ "name": "removeMissingSourceId",
142165
+ "in": "query",
142166
+ "description": "Indicates the resource to remove the `id` of the source of the Linked Series if the device with the given id does not exist. This can be used to automatically clean up Linked Series with non-existing sources.",
142167
+ "schema": {
142168
+ "type": "boolean",
142169
+ "default": false
142170
+ }
142171
+ }],
142172
+ "responses": {
142173
+ "200": {
142174
+ "description": "OK",
142175
+ "content": { "application/json": { "schema": {
142176
+ "type": "array",
142177
+ "items": { "$ref": "#/components/schemas/LinkedAsset" }
142178
+ } } }
142179
+ },
142180
+ "204": { "description": "No Content" }
142181
+ }
142182
+ } },
141670
142183
  "/service/dtm/definitions/properties": {
141671
142184
  "get": {
141672
142185
  "tags": ["Property Definitions"],
141673
142186
  "summary": "Retrieve all Property Definitions",
141674
- "description": "Finds a collection of `Property Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Property Definition`s.\n\n * Passing only one `title` results in `0...1` `Property Definition`s.\n\n\n\n\n\n The following rules will be applied when searching for the `Property Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `<i>Will be executed in context of an `Storage` tenant.</i>",
142187
+ "description": "Finds a collection of `Property Definition`s.\n\n This resource offers diverse filtering capabilities, yet it's essential to consider data consistency rules: \n\n * Passing only one `identifier` results in `0...1` `Property Definition`s.\n\n * Passing only one `title` results in `0...1` `Property Definition`s.\n\n \n\n \n\n The following rules will be applied when searching for the `Property Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `<i>Will be executed in context of an `Storage` tenant.</i>",
141675
142188
  "operationId": "getPropertyDefinitions",
141676
142189
  "parameters": [
141677
142190
  {
@@ -141736,7 +142249,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141736
142249
  {
141737
142250
  "name": "applicableTo",
141738
142251
  "in": "query",
141739
- "description": "Limits the response to Property Definitions applicable to the specified domain entity.",
142252
+ "description": "Limits the response to the Property Definitions that are applicable to the specified domain entity.",
141740
142253
  "schema": {
141741
142254
  "type": "string",
141742
142255
  "enum": [
@@ -141755,9 +142268,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141755
142268
  "in": "query",
141756
142269
  "description": "The current page number to be retrieved.",
141757
142270
  "schema": {
141758
- "type": "integer",
141759
- "format": "int32",
141760
- "default": 1
142271
+ "minimum": 1,
142272
+ "type": "integer"
141761
142273
  }
141762
142274
  },
141763
142275
  {
@@ -141779,7 +142291,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141779
142291
  "post": {
141780
142292
  "tags": ["Property Definitions"],
141781
142293
  "summary": "Create a new Property Definition",
141782
- "description": "Creates a new `Property Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Property Definition` must be unique for the given `context`s.\n\n * If a `title` is provided, it must also be unique.\n\n * None of the `context`s must not be used by another `Property Definition` with the same `identifier`.\n\n\n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema).",
142294
+ "description": "Creates a new `Property Definition`.\n\n This operation ensures consistency by enforcing the following rules: \n\n * The `identifier` of the `Property Definition` must be unique for the given `context`s.\n\n * If a `title` is provided, it must also be unique.\n\n * None of the `context`s must not be used by another `Property Definition` with the same `identifier`.\n\n \n\n\n\n The supplied JSON Schema must conform to the [JSON Schema Draft-7 specification](http://json-schema.org/draft-07/schema). ",
141783
142295
  "operationId": "createPropertyDefinition",
141784
142296
  "requestBody": {
141785
142297
  "description": "The data payload representing the `Property Definition` to be created.",
@@ -141795,7 +142307,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141795
142307
  "/service/dtm/definitions/properties/compose": { "post": {
141796
142308
  "tags": ["Property Definitions"],
141797
142309
  "summary": "Create JSON schema from Property Definitions",
141798
- "description": "Creates a JSON schema object type.\n\n This operation constructs a JSON Schema object type that encapsulates one or more `Property Definition`s as its properties. The resulting schema will include all provided `Property Definition`s. \n\n The `Property Definition`s to be included are supplied in the request body. At least one `Property Definition` must be provided and identified by its `identifier`. If a `Property Definition` applies to a specific domain entity, a corresponding `context` must also be supplied.",
142310
+ "description": "Creates a JSON schema object type.\n\n This operation constructs a JSON Schema object type that encapsulates one or more `Property Definition`s as its properties. The resulting schema will include all provided `Property Definition`s. \n\n The `Property Definition`s to be included are supplied in the request body. At least one `Property Definition` must be provided and identified by its `identifier`. If a `Property Definition` applies to a specific domain entity, a corresponding `context` must also be supplied. ",
141799
142311
  "operationId": "composePropertyDefinitions",
141800
142312
  "requestBody": {
141801
142313
  "content": { "application/json": { "schema": {
@@ -141813,7 +142325,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141813
142325
  "get": {
141814
142326
  "tags": ["Assets"],
141815
142327
  "summary": "Retrieve assets",
141816
- "description": "Retrieves all assets registered in your tenant.\n\n The `query` parameter supports a flexible syntax for filtering and ordering results.",
142328
+ "description": "Retrieves all assets registered in your tenant.\n\n The `query` parameter supports a flexible syntax for filtering and ordering results. ",
141817
142329
  "operationId": "getAssets",
141818
142330
  "parameters": [
141819
142331
  {
@@ -141851,7 +142363,17 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141851
142363
  {
141852
142364
  "name": "withSubAssets",
141853
142365
  "in": "query",
141854
- "description": "Indicates the resource to add the sub-assets in the response.",
142366
+ "description": "**Deprecated** – use the dedicated `/assets/{assetId}/subAssets`, `/assets/externalIds/{externalId}/subAssets` or `/assets/subAssets` endpoint instead!<br>Indicates the resource to add the sub-assets in the response.",
142367
+ "deprecated": true,
142368
+ "schema": {
142369
+ "type": "boolean",
142370
+ "default": false
142371
+ }
142372
+ },
142373
+ {
142374
+ "name": "withParents",
142375
+ "in": "query",
142376
+ "description": "Indicates the resource to add the parent assets in the response.",
141855
142377
  "schema": {
141856
142378
  "type": "boolean",
141857
142379
  "default": false
@@ -141865,13 +142387,30 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141865
142387
  "default": false
141866
142388
  }
141867
142389
  },
142390
+ {
142391
+ "name": "includeGroups",
142392
+ "in": "query",
142393
+ "description": "Indicates the resource to include assets and device groups in the response. If `false`, only assets are included.",
142394
+ "schema": {
142395
+ "type": "boolean",
142396
+ "default": false
142397
+ }
142398
+ },
142399
+ {
142400
+ "name": "withChildrenCount",
142401
+ "in": "query",
142402
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
142403
+ "schema": {
142404
+ "type": "boolean",
142405
+ "default": false
142406
+ }
142407
+ },
141868
142408
  {
141869
142409
  "name": "currentPage",
141870
142410
  "in": "query",
141871
142411
  "schema": {
141872
- "type": "integer",
141873
- "format": "int32",
141874
- "default": 1
142412
+ "minimum": 1,
142413
+ "type": "integer"
141875
142414
  }
141876
142415
  },
141877
142416
  {
@@ -141892,7 +142431,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141892
142431
  "post": {
141893
142432
  "tags": ["Assets"],
141894
142433
  "summary": "Create or update an asset",
141895
- "description": "Creates or updates an Asset, for example, a room within a building.\n\n In general, each asset may consist of: \n\n * The name of the asset.\n\n * The most specific type of the asset.\n\n * Fragments with specific meanings, for example, c8y_Position, c8y_SupportedOperations.\n\n * Fragment `c8y_ExternalId` which uniquely identifies the asset in an external system.\n\n * If a value for the property `id` is provided, it will be ignored.\n\n\n\n If an asset with the given external ID exists and `X-Upsert-Mode` is true, the asset will be updated and the response code will be 200. If an asset with the given external ID exists and `X-Upsert-Mode` is false, a conflict error will be returned. If no asset with the given external ID exists, a new asset will be created (independent of the `X-Upsert-Mode`) and the response code will be 201.",
142434
+ "description": "Creates or updates an Asset, for example, a room within a building.\n\n In general, each asset may consist of: \n\n * The name of the asset.\n\n * The most specific type of the asset.\n\n * Fragments with specific meanings, for example, c8y_Position, c8y_SupportedOperations.\n\n * Fragment `c8y_ExternalId` which uniquely identifies the asset in an external system.\n\n * If a value for the property `id` is provided, it will be ignored.\n\n \n\n If an asset with the given external ID exists and `X-Upsert-Mode` is true, the asset will be updated and the response code will be 200. If an asset with the given external ID exists and `X-Upsert-Mode` is false, a conflict error will be returned. If no asset with the given external ID exists, a new asset will be created (independent of the `X-Upsert-Mode`) and the response code will be 201. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_CREATE` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
141896
142435
  "operationId": "createAsset",
141897
142436
  "parameters": [{
141898
142437
  "name": "fetchTypeFromSource",
@@ -141924,11 +142463,127 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141924
142463
  }
141925
142464
  }
141926
142465
  },
142466
+ "/service/dtm/assets/{assetId}/subAssets": {
142467
+ "get": {
142468
+ "tags": ["Assets"],
142469
+ "summary": "Retrieve sub-assets of an asset",
142470
+ "description": "Retrieves sub-assets of an asset.\n\n Fetches all sub-assets of the parent asset specified by `assetId`. ",
142471
+ "operationId": "getSubAssets",
142472
+ "parameters": [
142473
+ {
142474
+ "name": "assetId",
142475
+ "in": "path",
142476
+ "required": true,
142477
+ "schema": { "type": "string" }
142478
+ },
142479
+ {
142480
+ "name": "query",
142481
+ "in": "query",
142482
+ "description": "Use the `$filter` keyword to specify filtering criteria. Filtering can be applied to properties of the asset. Detailed information can be found within the Cumulocity core OpenAPI [here](https://cumulocity.com/api/core/#tag/Query-language).",
142483
+ "schema": {
142484
+ "type": "string",
142485
+ "format": "c8y:query"
142486
+ },
142487
+ "examples": {
142488
+ "Filter by name": {
142489
+ "description": "Filter by name",
142490
+ "value": "$filter=name eq 'Windfarm'"
142491
+ },
142492
+ "Filter by type": {
142493
+ "description": "Filter by type",
142494
+ "value": "$filter=type eq 'c8y_*'"
142495
+ },
142496
+ "Using orderby": {
142497
+ "description": "Using orderby",
142498
+ "value": "$orderby=id asc"
142499
+ }
142500
+ }
142501
+ },
142502
+ {
142503
+ "name": "withLinkedSeries",
142504
+ "in": "query",
142505
+ "description": "Indicates the resource to add the `c8y_LinkedSeries` fragment in the response.",
142506
+ "schema": {
142507
+ "type": "boolean",
142508
+ "default": false
142509
+ }
142510
+ },
142511
+ {
142512
+ "name": "withParents",
142513
+ "in": "query",
142514
+ "description": "Indicates the resource to add the parent assets in the response.",
142515
+ "schema": {
142516
+ "type": "boolean",
142517
+ "default": false
142518
+ }
142519
+ },
142520
+ {
142521
+ "name": "includeGroups",
142522
+ "in": "query",
142523
+ "description": "Indicates the resource to include assets and device groups in the response. If `false`, only assets are included.",
142524
+ "schema": {
142525
+ "type": "boolean",
142526
+ "default": false
142527
+ }
142528
+ },
142529
+ {
142530
+ "name": "withChildrenCount",
142531
+ "in": "query",
142532
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
142533
+ "schema": {
142534
+ "type": "boolean",
142535
+ "default": false
142536
+ }
142537
+ },
142538
+ {
142539
+ "name": "currentPage",
142540
+ "in": "query",
142541
+ "schema": {
142542
+ "minimum": 1,
142543
+ "type": "integer"
142544
+ }
142545
+ },
142546
+ {
142547
+ "name": "pageSize",
142548
+ "in": "query",
142549
+ "schema": {
142550
+ "maximum": 2e3,
142551
+ "minimum": 1,
142552
+ "type": "integer"
142553
+ }
142554
+ }
142555
+ ],
142556
+ "responses": { "200": {
142557
+ "description": "OK",
142558
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedAssetList" } } }
142559
+ } }
142560
+ },
142561
+ "post": {
142562
+ "tags": ["Assets"],
142563
+ "summary": "Create and assign a sub-asset in one operation",
142564
+ "description": "Creates a new sub-asset and assigns it to the parent asset in one operation.\n\n Creates a new asset from the provided payload and immediately assigns it as a sub-asset to the specified parent asset. If the parent asset does not exist, the operation responds with `HTTP 404`. If the parent asset or the payload is not of type Asset, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
142565
+ "operationId": "createAndAssignSubAsset",
142566
+ "parameters": [{
142567
+ "name": "assetId",
142568
+ "in": "path",
142569
+ "required": true,
142570
+ "schema": { "type": "string" }
142571
+ }],
142572
+ "requestBody": {
142573
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Asset" } } },
142574
+ "required": true
142575
+ },
142576
+ "responses": { "201": {
142577
+ "description": "Created",
142578
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Asset" } } }
142579
+ } }
142580
+ }
142581
+ },
141927
142582
  "/service/dtm/assets/{assetId}/subAssets/{subAssetId}": {
141928
142583
  "post": {
141929
142584
  "tags": ["Assets"],
141930
142585
  "summary": "Assign an asset as a sub-asset",
141931
- "description": "Assigns an asset as a sub-asset.\n\n Associates the specified asset, identified by its ID, with a parent asset. The sub-asset must already exist. If one of the IDs does not exist, the operation responds with `HTTP 404`. If one of the IDs is not an Asset, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
142586
+ "description": "Assigns an asset as a sub-asset.\n\n Associates the specified asset, identified by its ID, with a parent asset. The sub-asset must already exist. If one of the IDs does not exist, the operation responds with `HTTP 404`. If one of the IDs is not an Asset, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
141932
142587
  "operationId": "assignSubAsset",
141933
142588
  "parameters": [{
141934
142589
  "name": "assetId",
@@ -141949,7 +142604,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141949
142604
  "delete": {
141950
142605
  "tags": ["Assets"],
141951
142606
  "summary": "Remove a sub-asset from its parent asset",
141952
- "description": "Removes a sub-asset from its parent asset.\n\n Removes the association of the specified asset, identified by its ID, with a parent asset. If one of the IDs does not exist, the operation responds with `HTTP 404`. If one of the IDs is not an Asset, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
142607
+ "description": "Removes a sub-asset from its parent asset.\n\n Removes the association of the specified asset, identified by its ID, with a parent asset. If one of the IDs does not exist, the operation responds with `HTTP 404`. If one of the IDs is not an Asset, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
141953
142608
  "operationId": "unassignSubAsset",
141954
142609
  "parameters": [{
141955
142610
  "name": "assetId",
@@ -141969,7 +142624,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
141969
142624
  "get": {
141970
142625
  "tags": ["Linked Series"],
141971
142626
  "summary": "Retrieve linked series for an asset",
141972
- "description": "Retrieves linked series for a given asset.\n\n Supports optionally searching for a specific `fragment` and/or `series`. \n\n The `withMeasurementType` query parameter is used to explicitly query the `type` of the measurements which are linked via the `source.id`. This parameter is only relevant if only one `LinkedSeries` is to be returned.",
142627
+ "description": "Retrieves linked series for a given asset.\n\n Supports optionally searching for a specific `fragment` and/or `series`. \n\n The `withMeasurementType` query parameter is used to explicitly query the `type` of the measurements which are linked via the `source.id`. This parameter is only relevant if only one `LinkedSeries` is to be returned. ",
141973
142628
  "operationId": "getLinkedSeries",
141974
142629
  "parameters": [
141975
142630
  {
@@ -142002,7 +142657,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142002
142657
  {
142003
142658
  "name": "withJsonSchema",
142004
142659
  "in": "query",
142005
- "description": "If true, the response includes the inferred JSON schema describing all linked series of this Asset.",
142660
+ "description": "If `true`, the response includes the inferred JSON schema describing all linked series of this Asset.",
142006
142661
  "schema": {
142007
142662
  "type": "boolean",
142008
142663
  "default": false
@@ -142012,9 +142667,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142012
142667
  "name": "currentPage",
142013
142668
  "in": "query",
142014
142669
  "schema": {
142015
- "type": "integer",
142016
- "format": "int32",
142017
- "default": 1
142670
+ "minimum": 1,
142671
+ "type": "integer"
142018
142672
  }
142019
142673
  },
142020
142674
  {
@@ -142041,7 +142695,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142041
142695
  "post": {
142042
142696
  "tags": ["Linked Series"],
142043
142697
  "summary": "Add new and update existing linked series",
142044
- "description": "Adds new and updates existing linked series for a given asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n For each LinkedSeries provided in the body: \n\n * Adds the new linked series when it does not exist.\n\n * Updates the linked series when it already exists.\n\n\n\n\n\n *Required user role for adding a linked series: `ROLE_DIGITAL_TWIN_ASSETS_CREATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*.\n\n *Required user role for updating a linked series: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*.",
142698
+ "description": "Adds new and updates existing linked series for a given asset.\n\n Linked series are identified by their `fragment` **and** `series`. \n\n For each LinkedSeries provided in the body: \n\n * Adds the new linked series when it does not exist.\n\n * Updates the linked series when it already exists.\n\n \n\n \n\n The update behavior preserves the existing `source.id` when the Linked Series to be updated does not explicitly provide a non-blank `source.id` in the request body. If the `source.id` is provided, it will be updated accordingly. \n\n *Required user role for adding a linked series: `ROLE_DIGITAL_TWIN_ASSETS_CREATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*.\n\n *Required user role for updating a linked series: `ROLE_DIGITAL_TWIN_ASSETS_UPDATE` or `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*. ",
142045
142699
  "operationId": "addAndUpdateLinkedSeries",
142046
142700
  "parameters": [{
142047
142701
  "name": "assetId",
@@ -142072,7 +142726,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142072
142726
  "delete": {
142073
142727
  "tags": ["Linked Series"],
142074
142728
  "summary": "Delete a specific linked series",
142075
- "description": "Deletes a linked series for a given asset.\n\n Deletes all linked series. Specify `fragment` and/or `series` as query parameters to be more specific about the linked series to be deleted. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*.",
142729
+ "description": "Deletes a linked series for a given asset.\n\n Deletes all linked series. Specify `fragment` and/or `series` as query parameters to be more specific about the linked series to be deleted. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN`*. ",
142076
142730
  "operationId": "deleteLinkedSeries",
142077
142731
  "parameters": [
142078
142732
  {
@@ -142101,7 +142755,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142101
142755
  "post": {
142102
142756
  "tags": ["Assets"],
142103
142757
  "summary": "Assign a device to an asset",
142104
- "description": "Assigns a device to an asset.\n\n Associates the specified device, identified by its ID, with an asset. The device must already exist. If one of the IDs does not exist, the operation responds with `HTTP 404`. If the managed object for the assetId is not an asset or of the managed object for the deviceId is not a Device, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
142758
+ "description": "Assigns a device to an asset.\n\n Associates the specified device, identified by its ID, with an asset. The device must already exist. If one of the IDs does not exist, the operation responds with `HTTP 404`. If the managed object for the assetId is not an asset or if the managed object for the deviceId is not a Device, the operation responds with `HTTP 422`. \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_UPDATE`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
142105
142759
  "operationId": "assignDevice",
142106
142760
  "parameters": [{
142107
142761
  "name": "assetId",
@@ -142122,7 +142776,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142122
142776
  "delete": {
142123
142777
  "tags": ["Assets"],
142124
142778
  "summary": "Remove a device from an asset",
142125
- "description": "Removes a device from an asset.\n\n Removes the association of the specified device, identified by its ID, with an asset. If one of the IDs does not exist, the operation responds with `HTTP 404`. If the managed object for the assetId is not an asset or of the managed object for the deviceId is not a Device, the operation responds with `HTTP 422`. \n\n\n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN`*",
142779
+ "description": "Removes a device from an asset.\n\n Removes the association of the specified device, identified by its ID, with an asset. If one of the IDs does not exist, the operation responds with `HTTP 404`. If the managed object for the assetId is not an asset or if the managed object for the deviceId is not a Device, the operation responds with `HTTP 422`. \n\n \n\n *Required user role: `ROLE_DIGITAL_TWIN_ASSETS_ADMIN` **AND** `ROLE_INVENTORY_ADMIN` or `ROLE_DIGITAL_TWIN_LINKING_UPDATE`* (depending on the setting of `assets.permission.mode` as described in the Permissions section) ",
142126
142780
  "operationId": "unassignDevice",
142127
142781
  "parameters": [{
142128
142782
  "name": "assetId",
@@ -142173,7 +142827,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142173
142827
  "/service/dtm/definitions/properties/count": { "get": {
142174
142828
  "tags": ["Property Definitions"],
142175
142829
  "summary": "Get the count of all Property Definitions",
142176
- "description": "Counts the collection of `Property Definition`s.\n\n The following rules will be applied when searching for the `Property Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
142830
+ "description": "Counts the collection of `Property Definition`s.\n\n The following rules will be applied when searching for the `Property Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
142177
142831
  "operationId": "countPropertyDefinitions",
142178
142832
  "parameters": [
142179
142833
  {
@@ -142209,7 +142863,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142209
142863
  {
142210
142864
  "name": "applicableTo",
142211
142865
  "in": "query",
142212
- "description": "Limits the response to Property Definitions applicable to the specified domain entity.",
142866
+ "description": "Limits the response to the Property Definitions that are applicable to the specified domain entity.",
142213
142867
  "schema": {
142214
142868
  "type": "string",
142215
142869
  "enum": [
@@ -142233,7 +142887,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142233
142887
  "get": {
142234
142888
  "tags": ["Measurement Definitions"],
142235
142889
  "summary": "Retrieve a Measurement Definition by identifier",
142236
- "description": "Finds a `Measurement Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Measurement Definition`, otherwise this operation responds in `HTTP 404`.",
142890
+ "description": "Finds a `Measurement Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Measurement Definition`, otherwise this operation responds in `HTTP 404`. ",
142237
142891
  "operationId": "getMeasurementDefinition",
142238
142892
  "parameters": [{
142239
142893
  "name": "identifier",
@@ -142297,7 +142951,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142297
142951
  "/service/dtm/definitions/measurements/count": { "get": {
142298
142952
  "tags": ["Measurement Definitions"],
142299
142953
  "summary": "Get the count of all Measurement Definitions",
142300
- "description": "Counts the collection of `Measurement Definition`s.\n\n The following rules will be applied when searching for the `Measurement Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
142954
+ "description": "Counts the collection of `Measurement Definition`s.\n\n The following rules will be applied when searching for the `Measurement Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
142301
142955
  "operationId": "countMeasurementDefinitions",
142302
142956
  "parameters": [
142303
142957
  {
@@ -142340,7 +142994,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142340
142994
  "get": {
142341
142995
  "tags": ["Event Definitions"],
142342
142996
  "summary": "Retrieve an Event Definition by identifier",
142343
- "description": "Finds a `Event Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Event Definition`, otherwise this operation responds in `HTTP 404`.",
142997
+ "description": "Finds a `Event Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Event Definition`, otherwise this operation responds in `HTTP 404`. ",
142344
142998
  "operationId": "getEventDefinition",
142345
142999
  "parameters": [{
142346
143000
  "name": "identifier",
@@ -142404,7 +143058,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142404
143058
  "/service/dtm/definitions/events/count": { "get": {
142405
143059
  "tags": ["Event Definitions"],
142406
143060
  "summary": "Get the count of all Event Definitions",
142407
- "description": "Counts the collection of `Event Definition`s.\n\n The following rules will be applied when searching for the `Event Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
143061
+ "description": "Counts the collection of `Event Definition`s.\n\n The following rules will be applied when searching for the `Event Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
142408
143062
  "operationId": "countEventDefinitions",
142409
143063
  "parameters": [
142410
143064
  {
@@ -142447,7 +143101,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142447
143101
  "get": {
142448
143102
  "tags": ["Asset Definitions"],
142449
143103
  "summary": "Retrieve an Asset Definition by identifier",
142450
- "description": "Finds a `Asset Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Asset Definition`, otherwise this operation responds in `HTTP 404`.",
143104
+ "description": "Finds a `Asset Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Asset Definition`, otherwise this operation responds in `HTTP 404`. ",
142451
143105
  "operationId": "getAssetDefinition",
142452
143106
  "parameters": [{
142453
143107
  "name": "identifier",
@@ -142479,7 +143133,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142479
143133
  "/service/dtm/definitions/assets/count": { "get": {
142480
143134
  "tags": ["Asset Definitions"],
142481
143135
  "summary": "Get count of Asset Definitions",
142482
- "description": "Counts the collection of `Asset Definition`s.\n\n The following rules will be applied when searching for the `Asset Definition`s: \n\n * `identifiers`, `titles`, `onlyRoots` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
143136
+ "description": "Counts the collection of `Asset Definition`s.\n\n The following rules will be applied when searching for the `Asset Definition`s: \n\n * `identifiers`, `titles`, `onlyRoots` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
142483
143137
  "operationId": "countAssetDefinitions",
142484
143138
  "parameters": [
142485
143139
  {
@@ -142531,7 +143185,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142531
143185
  "get": {
142532
143186
  "tags": ["Alarm Definitions"],
142533
143187
  "summary": "Retrieve an Alarm Definition by identifier",
142534
- "description": "Retrieves an `Alarm Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Alarm Definition`, otherwise this operation respond in `HTTP 404`.",
143188
+ "description": "Retrieves an `Alarm Definition` by its `identifier`.\n\n The `identifier` needs to point to an existing `Alarm Definition`, otherwise this operation respond in `HTTP 404`. ",
142535
143189
  "operationId": "getAlarmDefinition",
142536
143190
  "parameters": [{
142537
143191
  "name": "identifier",
@@ -142595,7 +143249,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142595
143249
  "/service/dtm/definitions/alarms/count": { "get": {
142596
143250
  "tags": ["Alarm Definitions"],
142597
143251
  "summary": "Get count of Alarm Definitions",
142598
- "description": "Counts the collection of `Alarm Definition`s.\n\n The following rules will be applied when searching for the `Alarm Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.*",
143252
+ "description": "Counts the collection of `Alarm Definition`s.\n\n The following rules will be applied when searching for the `Alarm Definition`s: \n\n * `identifiers`, `titles`, `tags` will be concatenated using logical AND operators\n\n * Values within a filter will be concatenated using a logical OR operator\n\n For instance, the query filter `?identifiers=id_a,id_b&title=position` results in the following query filter:` (identifier equals id_a OR identifier equals id_b) AND title equals position `\n\n *Will be executed in context of an `Storage` tenant.* ",
142599
143253
  "operationId": "countAlarmDefinitions",
142600
143254
  "parameters": [
142601
143255
  {
@@ -142634,11 +143288,41 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142634
143288
  "content": { "application/json": { "schema": { "type": "integer" } } }
142635
143289
  } }
142636
143290
  } },
142637
- "/service/dtm/assets/{assetId}/subAssets": { "get": {
143291
+ "/service/dtm/assets/{assetId}/linkedSeries/count": { "get": {
143292
+ "tags": ["Linked Series"],
143293
+ "summary": "Get count of linked series for an asset",
143294
+ "description": "Counts linked series for a given asset.\n\n Provides a count of linked series, with optional `fragment` and `series` query parameters to filter and count specific linked series. ",
143295
+ "operationId": "countLinkedSeries",
143296
+ "parameters": [
143297
+ {
143298
+ "name": "assetId",
143299
+ "in": "path",
143300
+ "required": true,
143301
+ "schema": { "type": "string" }
143302
+ },
143303
+ {
143304
+ "name": "fragment",
143305
+ "in": "query",
143306
+ "description": "A characteristic which identifies the measurement.",
143307
+ "schema": { "type": "string" }
143308
+ },
143309
+ {
143310
+ "name": "series",
143311
+ "in": "query",
143312
+ "description": "The specific series to search for.",
143313
+ "schema": { "type": "string" }
143314
+ }
143315
+ ],
143316
+ "responses": { "200": {
143317
+ "description": "OK",
143318
+ "content": { "application/json": { "schema": { "type": "integer" } } }
143319
+ } }
143320
+ } },
143321
+ "/service/dtm/assets/{assetId}/devices": { "get": {
142638
143322
  "tags": ["Assets"],
142639
- "summary": "Retrieve sub-assets of an asset",
142640
- "description": "Retrieves sub-assets of an asset.\n\n Fetches all sub-assets of the parent asset specified by `assetId`. You can optionally apply the filter `query`.",
142641
- "operationId": "getSubAssets",
143323
+ "summary": "Retrieve child devices of an asset",
143324
+ "description": "Retrieves child devices of an asset.\n\n Fetches all child devices of the parent asset specified by `assetId`. ",
143325
+ "operationId": "getChildDevices",
142642
143326
  "parameters": [
142643
143327
  {
142644
143328
  "name": "assetId",
@@ -142669,22 +143353,12 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142669
143353
  }
142670
143354
  }
142671
143355
  },
142672
- {
142673
- "name": "withLinkedSeries",
142674
- "in": "query",
142675
- "description": "Indicates the resource to add the `c8y_LinkedSeries` fragment in the response.",
142676
- "schema": {
142677
- "type": "boolean",
142678
- "default": false
142679
- }
142680
- },
142681
143356
  {
142682
143357
  "name": "currentPage",
142683
143358
  "in": "query",
142684
143359
  "schema": {
142685
- "type": "integer",
142686
- "format": "int32",
142687
- "default": 1
143360
+ "minimum": 1,
143361
+ "type": "integer"
142688
143362
  }
142689
143363
  },
142690
143364
  {
@@ -142699,50 +143373,26 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142699
143373
  ],
142700
143374
  "responses": { "200": {
142701
143375
  "description": "OK",
142702
- "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedAssetList" } } }
142703
- } }
142704
- } },
142705
- "/service/dtm/assets/{assetId}/linkedSeries/count": { "get": {
142706
- "tags": ["Linked Series"],
142707
- "summary": "Get count of linked series for an asset",
142708
- "description": "Counts linked series for a given asset.\n\n Provides a count of linked series, with optional `fragment` and `series` query parameters to filter and count specific linked series.",
142709
- "operationId": "countLinkedSeries",
142710
- "parameters": [
142711
- {
142712
- "name": "assetId",
142713
- "in": "path",
142714
- "required": true,
142715
- "schema": { "type": "string" }
142716
- },
142717
- {
142718
- "name": "fragment",
142719
- "in": "query",
142720
- "description": "A characteristic which identifies the measurement.",
142721
- "schema": { "type": "string" }
142722
- },
142723
- {
142724
- "name": "series",
142725
- "in": "query",
142726
- "description": "The specific series to search for.",
142727
- "schema": { "type": "string" }
142728
- }
142729
- ],
142730
- "responses": { "200": {
142731
- "description": "OK",
142732
- "content": { "application/json": { "schema": { "type": "integer" } } }
143376
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedDeviceList" } } }
142733
143377
  } }
142734
143378
  } },
142735
- "/service/dtm/assets/{assetId}/devices": { "get": {
143379
+ "/service/dtm/assets/subAssets": { "get": {
142736
143380
  "tags": ["Assets"],
142737
- "summary": "Retrieve child devices of an asset",
142738
- "description": "Retrieves child devices of an asset.\n\n Fetches all child devices of the parent asset specified by `assetId`.",
142739
- "operationId": "getChildDevices",
143381
+ "summary": "Retrieve sub-assets by parent IDs",
143382
+ "description": "Retrieves sub-assets across multiple parent assets.\n\n Fetches all direct sub-assets that are children of given parent asset IDs. ",
143383
+ "operationId": "getSubAssetsByParents",
142740
143384
  "parameters": [
142741
143385
  {
142742
- "name": "assetId",
142743
- "in": "path",
143386
+ "name": "parents",
143387
+ "in": "query",
143388
+ "description": "List of parent asset IDs used to scope the sub-assets query. At least one parent ID must be provided. Sub-assets that are direct children of any of the given parent assets are returned.",
142744
143389
  "required": true,
142745
- "schema": { "type": "string" }
143390
+ "explode": false,
143391
+ "schema": {
143392
+ "minItems": 1,
143393
+ "type": "array",
143394
+ "items": { "type": "string" }
143395
+ }
142746
143396
  },
142747
143397
  {
142748
143398
  "name": "query",
@@ -142767,13 +143417,48 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142767
143417
  }
142768
143418
  }
142769
143419
  },
143420
+ {
143421
+ "name": "withLinkedSeries",
143422
+ "in": "query",
143423
+ "description": "Indicates the resource to add the `c8y_LinkedSeries` fragment in the response.",
143424
+ "schema": {
143425
+ "type": "boolean",
143426
+ "default": false
143427
+ }
143428
+ },
143429
+ {
143430
+ "name": "withParents",
143431
+ "in": "query",
143432
+ "description": "Indicates the resource to add the parent assets in the response.",
143433
+ "schema": {
143434
+ "type": "boolean",
143435
+ "default": false
143436
+ }
143437
+ },
143438
+ {
143439
+ "name": "includeGroups",
143440
+ "in": "query",
143441
+ "description": "Indicates the resource to include assets and device groups in the response. If `false`, only assets are included.",
143442
+ "schema": {
143443
+ "type": "boolean",
143444
+ "default": false
143445
+ }
143446
+ },
143447
+ {
143448
+ "name": "withChildrenCount",
143449
+ "in": "query",
143450
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
143451
+ "schema": {
143452
+ "type": "boolean",
143453
+ "default": false
143454
+ }
143455
+ },
142770
143456
  {
142771
143457
  "name": "currentPage",
142772
143458
  "in": "query",
142773
143459
  "schema": {
142774
- "type": "integer",
142775
- "format": "int32",
142776
- "default": 1
143460
+ "minimum": 1,
143461
+ "type": "integer"
142777
143462
  }
142778
143463
  },
142779
143464
  {
@@ -142788,13 +143473,13 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142788
143473
  ],
142789
143474
  "responses": { "200": {
142790
143475
  "description": "OK",
142791
- "content": { "application/json": { "schema": { "type": "string" } } }
143476
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedAssetList" } } }
142792
143477
  } }
142793
143478
  } },
142794
143479
  "/service/dtm/assets/linkedSeries": { "get": {
142795
143480
  "tags": ["Linked Series"],
142796
143481
  "summary": "Retrieve linked series for all assets",
142797
- "description": "Retrieves linked series for all assets on your tenant.\n\n Provides a list of linked series, with a set of optional query parameters to filter for specific linked series, asset types or asset ids.",
143482
+ "description": "Retrieves linked series for all assets on your tenant.\n\n Provides a list of linked series, with a set of optional query parameters to filter for specific linked series, asset types or asset ids. ",
142798
143483
  "operationId": "getLinkedSeriesByQuery",
142799
143484
  "parameters": [
142800
143485
  {
@@ -142829,9 +143514,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142829
143514
  "name": "currentPage",
142830
143515
  "in": "query",
142831
143516
  "schema": {
142832
- "type": "integer",
142833
- "format": "int32",
142834
- "default": 1
143517
+ "minimum": 1,
143518
+ "type": "integer"
142835
143519
  }
142836
143520
  },
142837
143521
  {
@@ -142849,15 +143533,66 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142849
143533
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedAssetList" } } }
142850
143534
  } }
142851
143535
  } },
143536
+ "/service/dtm/assets/linkedSeries/opposites/{deviceId}": { "get": {
143537
+ "tags": ["Linked Series"],
143538
+ "summary": "Get all assets linked to a specific device by its ID.",
143539
+ "description": "The API response with the default `Accept` header `application/json` is deprecated and will be removed in a future\nversion. Please use the `Accept` header `application/vnd.com.nsn.cumulocity.linkedassetscollection+json` which offers\nbetter performance and filter parameters.",
143540
+ "operationId": "getOppositeAssets",
143541
+ "parameters": [
143542
+ {
143543
+ "name": "deviceId",
143544
+ "in": "path",
143545
+ "description": "the ID of the device",
143546
+ "required": true,
143547
+ "schema": { "type": "string" }
143548
+ },
143549
+ {
143550
+ "name": "assetIds",
143551
+ "in": "query",
143552
+ "description": "The asset IDs to search for.",
143553
+ "explode": false,
143554
+ "schema": {
143555
+ "type": "array",
143556
+ "items": { "type": "string" }
143557
+ }
143558
+ },
143559
+ {
143560
+ "name": "fragment",
143561
+ "in": "query",
143562
+ "description": "A characteristic which identifies the measurement.",
143563
+ "schema": { "type": "string" }
143564
+ },
143565
+ {
143566
+ "name": "series",
143567
+ "in": "query",
143568
+ "description": "The specific series to search for.",
143569
+ "schema": { "type": "string" }
143570
+ }
143571
+ ],
143572
+ "responses": { "200": {
143573
+ "description": "a set of assets linked to the specified device",
143574
+ "content": {
143575
+ "application/vnd.com.nsn.cumulocity.linkedassetscollection+json": { "schema": {
143576
+ "type": "array",
143577
+ "items": { "$ref": "#/components/schemas/LinkedAsset" }
143578
+ } },
143579
+ "application/json": { "schema": {
143580
+ "type": "array",
143581
+ "items": { "$ref": "#/components/schemas/Asset" }
143582
+ } }
143583
+ }
143584
+ } }
143585
+ } },
142852
143586
  "/service/dtm/assets/externalIds/{externalId}": { "get": {
142853
143587
  "tags": ["Assets"],
142854
143588
  "summary": "Retrieve an asset by its external ID",
142855
- "description": "Retrieves an Asset by its external ID of type `c8y_ExternalId`.\n\n This endpoint allows clients to look up assets using identifiers from external systems. It resolves the external ID to a ManagedObject and returns the corresponding Asset if found.",
143589
+ "description": "Retrieves an Asset by its external ID of type `c8y_ExternalId`.\n\n This endpoint allows clients to look up assets using identifiers from external systems. It resolves the external ID to a ManagedObject and returns the corresponding Asset if found. ",
142856
143590
  "operationId": "getAssetByExternalId",
142857
143591
  "parameters": [
142858
143592
  {
142859
143593
  "name": "externalId",
142860
143594
  "in": "path",
143595
+ "description": "The external identifier of type `c8y_Asset` of the asset.",
142861
143596
  "required": true,
142862
143597
  "schema": { "type": "string" }
142863
143598
  },
@@ -142873,7 +143608,26 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142873
143608
  {
142874
143609
  "name": "withSubAssets",
142875
143610
  "in": "query",
142876
- "description": "Indicates the resource to add the sub-assets in the response.",
143611
+ "description": "**Deprecated** – use the dedicated `/assets/{assetId}/subAssets`, `/assets/externalIds/{externalId}/subAssets` or `/assets/subAssets` endpoint instead!<br>Indicates the resource to add the sub-assets in the response.",
143612
+ "deprecated": true,
143613
+ "schema": {
143614
+ "type": "boolean",
143615
+ "default": false
143616
+ }
143617
+ },
143618
+ {
143619
+ "name": "withParents",
143620
+ "in": "query",
143621
+ "description": "Indicates the resource to add the parent assets in the response.",
143622
+ "schema": {
143623
+ "type": "boolean",
143624
+ "default": false
143625
+ }
143626
+ },
143627
+ {
143628
+ "name": "withChildrenCount",
143629
+ "in": "query",
143630
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
142877
143631
  "schema": {
142878
143632
  "type": "boolean",
142879
143633
  "default": false
@@ -142885,41 +143639,148 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142885
143639
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Asset" } } }
142886
143640
  } }
142887
143641
  } },
143642
+ "/service/dtm/assets/externalIds/{externalId}/subAssets": { "get": {
143643
+ "tags": ["Assets"],
143644
+ "summary": "Retrieve sub-assets of an asset by its external ID",
143645
+ "description": "Retrieves direct sub-assets of the asset identified by the given external ID.\n\n Resolves the asset by its `c8y_ExternalId` and returns a paginated list of its immediate child assets. ",
143646
+ "operationId": "getSubAssetsByExternalId",
143647
+ "parameters": [
143648
+ {
143649
+ "name": "externalId",
143650
+ "in": "path",
143651
+ "description": "The external identifier of type `c8y_Asset` of the asset.",
143652
+ "required": true,
143653
+ "schema": { "type": "string" }
143654
+ },
143655
+ {
143656
+ "name": "query",
143657
+ "in": "query",
143658
+ "description": "Use the `$filter` keyword to specify filtering criteria. Filtering can be applied to properties of the asset. Detailed information can be found within the Cumulocity core OpenAPI [here](https://cumulocity.com/api/core/#tag/Query-language).",
143659
+ "schema": {
143660
+ "type": "string",
143661
+ "format": "c8y:query"
143662
+ },
143663
+ "examples": {
143664
+ "Filter by name": {
143665
+ "description": "Filter by name",
143666
+ "value": "$filter=name eq 'Windfarm'"
143667
+ },
143668
+ "Filter by type": {
143669
+ "description": "Filter by type",
143670
+ "value": "$filter=type eq 'c8y_*'"
143671
+ },
143672
+ "Using orderby": {
143673
+ "description": "Using orderby",
143674
+ "value": "$orderby=id asc"
143675
+ }
143676
+ }
143677
+ },
143678
+ {
143679
+ "name": "withLinkedSeries",
143680
+ "in": "query",
143681
+ "description": "Indicates the resource to add the `c8y_LinkedSeries` fragment in the response.",
143682
+ "schema": {
143683
+ "type": "boolean",
143684
+ "default": false
143685
+ }
143686
+ },
143687
+ {
143688
+ "name": "withParents",
143689
+ "in": "query",
143690
+ "description": "Indicates the resource to add the parent assets in the response.",
143691
+ "schema": {
143692
+ "type": "boolean",
143693
+ "default": false
143694
+ }
143695
+ },
143696
+ {
143697
+ "name": "includeGroups",
143698
+ "in": "query",
143699
+ "description": "Indicates the resource to include assets and device groups in the response. If `false`, only assets are included.",
143700
+ "schema": {
143701
+ "type": "boolean",
143702
+ "default": false
143703
+ }
143704
+ },
143705
+ {
143706
+ "name": "withChildrenCount",
143707
+ "in": "query",
143708
+ "description": "Indicates the resource to include the total number of child entities (sub-assets and devices) in the response.",
143709
+ "schema": {
143710
+ "type": "boolean",
143711
+ "default": false
143712
+ }
143713
+ },
143714
+ {
143715
+ "name": "currentPage",
143716
+ "in": "query",
143717
+ "schema": {
143718
+ "minimum": 1,
143719
+ "type": "integer"
143720
+ }
143721
+ },
143722
+ {
143723
+ "name": "pageSize",
143724
+ "in": "query",
143725
+ "schema": {
143726
+ "maximum": 2e3,
143727
+ "minimum": 1,
143728
+ "type": "integer"
143729
+ }
143730
+ }
143731
+ ],
143732
+ "responses": { "200": {
143733
+ "description": "OK",
143734
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaginatedAssetList" } } }
143735
+ } }
143736
+ } },
142888
143737
  "/service/dtm/assets/count": { "get": {
142889
143738
  "tags": ["Assets"],
142890
143739
  "summary": "Get count of assets",
142891
- "description": "Counts all assets on your tenant.\n\n You can optionally apply the filters `query` and `onlyRoots`.",
143740
+ "description": "Counts all assets on your tenant.\n\n You can optionally apply the filters `query`, `onlyRoots` and `includeGroups`. ",
142892
143741
  "operationId": "countAssets",
142893
- "parameters": [{
142894
- "name": "query",
142895
- "in": "query",
142896
- "description": "Use the `$filter` keyword to specify filtering criteria. Filtering can be applied to properties of the asset. Detailed information can be found within the Cumulocity core OpenAPI [here](https://cumulocity.com/api/core/#tag/Query-language).",
142897
- "schema": {
142898
- "type": "string",
142899
- "format": "c8y:query"
142900
- },
142901
- "examples": {
142902
- "Filter by name": {
142903
- "description": "Filter by name",
142904
- "value": "$filter=name eq 'Windfarm'"
142905
- },
142906
- "Filter by type": {
142907
- "description": "Filter by type",
142908
- "value": "$filter=type eq 'c8y_*'"
143742
+ "parameters": [
143743
+ {
143744
+ "name": "query",
143745
+ "in": "query",
143746
+ "description": "Use the `$filter` keyword to specify filtering criteria. Filtering can be applied to properties of the asset. Detailed information can be found within the Cumulocity core OpenAPI [here](https://cumulocity.com/api/core/#tag/Query-language).",
143747
+ "schema": {
143748
+ "type": "string",
143749
+ "format": "c8y:query"
142909
143750
  },
142910
- "Using orderby": {
142911
- "description": "Using orderby",
142912
- "value": "$orderby=id asc"
143751
+ "examples": {
143752
+ "Filter by name": {
143753
+ "description": "Filter by name",
143754
+ "value": "$filter=name eq 'Windfarm'"
143755
+ },
143756
+ "Filter by type": {
143757
+ "description": "Filter by type",
143758
+ "value": "$filter=type eq 'c8y_*'"
143759
+ },
143760
+ "Using orderby": {
143761
+ "description": "Using orderby",
143762
+ "value": "$orderby=id asc"
143763
+ }
143764
+ }
143765
+ },
143766
+ {
143767
+ "name": "onlyRoots",
143768
+ "in": "query",
143769
+ "schema": {
143770
+ "type": "boolean",
143771
+ "default": false
143772
+ }
143773
+ },
143774
+ {
143775
+ "name": "includeGroups",
143776
+ "in": "query",
143777
+ "description": "Indicates the resource to include assets and device groups in the response. If `false`, only assets are included.",
143778
+ "schema": {
143779
+ "type": "boolean",
143780
+ "default": false
142913
143781
  }
142914
143782
  }
142915
- }, {
142916
- "name": "onlyRoots",
142917
- "in": "query",
142918
- "schema": {
142919
- "type": "boolean",
142920
- "default": false
142921
- }
142922
- }],
143783
+ ],
142923
143784
  "responses": { "200": {
142924
143785
  "description": "OK",
142925
143786
  "content": { "application/json": { "schema": { "type": "integer" } } }
@@ -142946,7 +143807,11 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
142946
143807
  "pattern": "^(?:[A-Za-z0-9](?:\\s*[-\\w:.!])*\\s*)?$",
142947
143808
  "type": "string"
142948
143809
  },
142949
- "description": { "type": "string" }
143810
+ "description": { "type": "string" },
143811
+ "additionalProperties": {
143812
+ "type": "boolean",
143813
+ "writeOnly": true
143814
+ }
142950
143815
  },
142951
143816
  "example": {
142952
143817
  "title": "Position",
@@ -143007,12 +143872,20 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143007
143872
  }
143008
143873
  },
143009
143874
  "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" },
143875
+ "c8y_SharedDefinition": { "$ref": "#/components/schemas/SharedDefinition" },
143010
143876
  "c8y_Origin": {
143011
143877
  "type": "string",
143012
143878
  "description": "Specifies the origin of this Property Definition and identifies the owning component. If the value is <code>library</code>, the Property Definition is system-defined and read-only."
143013
143879
  }
143014
143880
  }
143015
143881
  },
143882
+ "SharedDefinition": {
143883
+ "type": "object",
143884
+ "properties": { "self": { "type": "string" } },
143885
+ "additionalProperties": false,
143886
+ "description": "Indicates that this definition is maintained on the enterprise tenant and is shared on its subtenants.",
143887
+ "readOnly": true
143888
+ },
143016
143889
  "AllowedPropertyDefinition": {
143017
143890
  "required": ["identifier"],
143018
143891
  "type": "object",
@@ -143069,7 +143942,8 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143069
143942
  } },
143070
143943
  "additionalProperties": false
143071
143944
  },
143072
- "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" }
143945
+ "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" },
143946
+ "c8y_SharedDefinition": { "$ref": "#/components/schemas/SharedDefinition" }
143073
143947
  }
143074
143948
  },
143075
143949
  "EventDefinition": {
@@ -143102,6 +143976,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143102
143976
  }
143103
143977
  },
143104
143978
  "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" },
143979
+ "c8y_SharedDefinition": { "$ref": "#/components/schemas/SharedDefinition" },
143105
143980
  "composition": {
143106
143981
  "type": "object",
143107
143982
  "properties": { "allowedProperties": {
@@ -143143,6 +144018,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143143
144018
  }
143144
144019
  },
143145
144020
  "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" },
144021
+ "c8y_SharedDefinition": { "$ref": "#/components/schemas/SharedDefinition" },
143146
144022
  "composition": {
143147
144023
  "type": "object",
143148
144024
  "properties": {
@@ -143199,6 +144075,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143199
144075
  }
143200
144076
  },
143201
144077
  "c8y_AvailableActions": { "$ref": "#/components/schemas/AvailableActions" },
144078
+ "c8y_SharedDefinition": { "$ref": "#/components/schemas/SharedDefinition" },
143202
144079
  "composition": {
143203
144080
  "type": "object",
143204
144081
  "properties": { "allowedProperties": {
@@ -143243,6 +144120,11 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143243
144120
  "readOnly": true,
143244
144121
  "items": { "$ref": "#/components/schemas/Asset" }
143245
144122
  },
144123
+ "assetParents": {
144124
+ "type": "array",
144125
+ "readOnly": true,
144126
+ "items": { "$ref": "#/components/schemas/Asset" }
144127
+ },
143246
144128
  "c8y_LinkedSeries": {
143247
144129
  "type": "array",
143248
144130
  "items": { "$ref": "#/components/schemas/LinkedSeries" }
@@ -143253,7 +144135,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143253
144135
  },
143254
144136
  "c8y_LatestMeasurements": { "$ref": "#/components/schemas/LatestMeasurements" },
143255
144137
  "c8y_ExternalId": {
143256
- "maxLength": 2147483647,
144138
+ "maxLength": 680,
143257
144139
  "minLength": 1,
143258
144140
  "type": "string",
143259
144141
  "description": "Represents an external identifier used to reference this asset in external systems. When set, an identity of type 'c8y_Asset' is created for this asset with the value of this property."
@@ -143261,6 +144143,11 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143261
144143
  "c8y_ExternalAsset": {
143262
144144
  "type": "object",
143263
144145
  "description": "Represents Assets synchronized from external systems into Cumulocity IoT. The fragment allows storing free-form metadata relevant to external systems."
144146
+ },
144147
+ "assignedChildrenCount": {
144148
+ "type": "integer",
144149
+ "format": "int32",
144150
+ "readOnly": true
143264
144151
  }
143265
144152
  }
143266
144153
  },
@@ -143340,6 +144227,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143340
144227
  "example": "T"
143341
144228
  },
143342
144229
  "id": {
144230
+ "pattern": "^(?!\\s*$).+$",
143343
144231
  "type": "string",
143344
144232
  "example": "9688123"
143345
144233
  },
@@ -143354,6 +144242,51 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143354
144242
  },
143355
144243
  "additionalProperties": false
143356
144244
  },
144245
+ "AssetReference": {
144246
+ "type": "object",
144247
+ "properties": {
144248
+ "fragment": {
144249
+ "type": "string",
144250
+ "example": "c8y_Temperature"
144251
+ },
144252
+ "series": {
144253
+ "type": "string",
144254
+ "example": "T"
144255
+ },
144256
+ "id": {
144257
+ "type": "string",
144258
+ "example": "9688123"
144259
+ },
144260
+ "label": {
144261
+ "type": "string",
144262
+ "example": "Room temperature"
144263
+ }
144264
+ },
144265
+ "additionalProperties": false
144266
+ },
144267
+ "LinkedAsset": {
144268
+ "type": "object",
144269
+ "properties": {
144270
+ "fragment": {
144271
+ "type": "string",
144272
+ "example": "c8y_Temperature"
144273
+ },
144274
+ "series": {
144275
+ "type": "string",
144276
+ "example": "T"
144277
+ },
144278
+ "type": {
144279
+ "type": "string",
144280
+ "example": "c8y_TemperatureMeasurement"
144281
+ },
144282
+ "name": {
144283
+ "type": "string",
144284
+ "example": "MyTemperatureMeasurement"
144285
+ },
144286
+ "asset": { "$ref": "#/components/schemas/AssetReference" }
144287
+ },
144288
+ "additionalProperties": false
144289
+ },
143357
144290
  "PageStatistics": {
143358
144291
  "required": [
143359
144292
  "currentPage",
@@ -143389,7 +144322,7 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143389
144322
  "additionalProperties": false
143390
144323
  },
143391
144324
  "PaginatedDefinitionList": {
143392
- "required": ["definitions", "statistics"],
144325
+ "required": ["statistics"],
143393
144326
  "type": "object",
143394
144327
  "properties": {
143395
144328
  "self": {
@@ -143494,6 +144427,71 @@ const BUNDLED_SERVICE_SPECS = Object.freeze([{
143494
144427
  },
143495
144428
  "additionalProperties": false
143496
144429
  },
144430
+ "Device": {
144431
+ "title": "Device",
144432
+ "required": ["id"],
144433
+ "type": "object",
144434
+ "properties": {
144435
+ "creationTime": {
144436
+ "type": "string",
144437
+ "readOnly": true,
144438
+ "example": "2017-12-12T22:09:06.881+01:00"
144439
+ },
144440
+ "lastUpdated": {
144441
+ "type": "string",
144442
+ "readOnly": true,
144443
+ "example": "2018-07-19T12:01:50.731Z"
144444
+ },
144445
+ "id": {
144446
+ "type": "string",
144447
+ "readOnly": true,
144448
+ "example": "4512412"
144449
+ },
144450
+ "type": {
144451
+ "type": "string",
144452
+ "example": "c8y_TemperatureSensor"
144453
+ },
144454
+ "name": {
144455
+ "type": "string",
144456
+ "example": "TemperatureSensor"
144457
+ },
144458
+ "owner": { "type": "string" },
144459
+ "c8y_LatestMeasurements": { "$ref": "#/components/schemas/LatestMeasurements" }
144460
+ }
144461
+ },
144462
+ "PaginatedDeviceList": {
144463
+ "required": ["devices", "statistics"],
144464
+ "type": "object",
144465
+ "properties": {
144466
+ "self": {
144467
+ "title": "SelfURL",
144468
+ "type": "string",
144469
+ "description": "A URL linking to this resource.",
144470
+ "format": "uri",
144471
+ "readOnly": true
144472
+ },
144473
+ "next": {
144474
+ "title": "NextPageURL",
144475
+ "type": "string",
144476
+ "description": "A URI reference [[RFC3986](https://tools.ietf.org/html/rfc3986)] to a potential next page.",
144477
+ "format": "uri",
144478
+ "readOnly": true
144479
+ },
144480
+ "prev": {
144481
+ "title": "PreviousPageURL",
144482
+ "type": "string",
144483
+ "description": "A URI reference [[RFC3986](https://tools.ietf.org/html/rfc3986)] to a potential previous page.",
144484
+ "format": "uri",
144485
+ "readOnly": true
144486
+ },
144487
+ "devices": {
144488
+ "type": "array",
144489
+ "items": { "$ref": "#/components/schemas/Device" }
144490
+ },
144491
+ "statistics": { "$ref": "#/components/schemas/PageStatistics" }
144492
+ },
144493
+ "additionalProperties": false
144494
+ },
143497
144495
  "AllowedSubAssetDefinition": {
143498
144496
  "required": ["identifier"],
143499
144497
  "type": "object",
@@ -143895,6 +144893,10 @@ runMain(defineCommand({
143895
144893
  "no-mcp": {
143896
144894
  type: "string",
143897
144895
  description: "Disable MCP wrapping: pass \"*\" (or no value) for all services, or a contextPath. Can be repeated. Opted-out services fall back to their OpenAPI spec."
144896
+ },
144897
+ "mcp-server": {
144898
+ type: "string",
144899
+ description: "External MCP server to expose as a codemode namespace, as JSON: '{\"name\":\"github\",\"url\":\"https://api.githubcopilot.com/mcp/\",\"token\":\"…\"}'. Can be repeated."
143898
144900
  }
143899
144901
  },
143900
144902
  setup: () => {
@@ -143919,6 +144921,10 @@ runMain(defineCommand({
143919
144921
  const rawNoMcp = args["no-mcp"];
143920
144922
  const noMcp = parseNoMcp(Array.isArray(rawNoMcp) ? rawNoMcp : rawNoMcp !== void 0 ? [rawNoMcp] : []);
143921
144923
  if (noMcp.all || noMcp.contextPaths.size > 0) consola.info(`MCP wrapping disabled for: ${noMcp.all ? "all services" : [...noMcp.contextPaths].join(", ")}`);
144924
+ const rawMcpServers = args["mcp-server"];
144925
+ const { servers: externalMcpServers, failedEntries: failedMcpServers } = parseExternalMcpServers((Array.isArray(rawMcpServers) ? rawMcpServers : rawMcpServers ? [rawMcpServers] : []).filter((v) => typeof v === "string" && v.length > 0));
144926
+ if (failedMcpServers.length > 0) throw new Error(["One or more --mcp-server flags could not be parsed:", ...failedMcpServers.map((e) => `- ${e.entry}: ${e.reason}`)].join("\n"));
144927
+ if (externalMcpServers.length > 0) consola.info(`External MCP namespaces: ${externalMcpServers.map((s) => `${s.name} (${s.url})`).join(", ")}`);
143922
144928
  const activeTenant = readActiveTenantUrl();
143923
144929
  if (activeTenant) try {
143924
144930
  const tenantCtx = await setCliTenantContext(activeTenant);
@@ -143943,6 +144949,7 @@ runMain(defineCommand({
143943
144949
  restrictions,
143944
144950
  allowRules: parsedAllowRules,
143945
144951
  noMcp,
144952
+ externalMcpServers,
143946
144953
  specs: active?.specs ?? getBundledOnlyCapabilities(),
143947
144954
  auth: active ? {
143948
144955
  tenantUrl: active.tenantUrl,