mc8yp 2.3.1 → 2.3.3

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 (2) hide show
  1. package/dist/cli.mjs +79 -81
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1545,7 +1545,7 @@ const consola = createConsola();
1545
1545
  //#endregion
1546
1546
  //#region package.json
1547
1547
  var name = "mc8yp";
1548
- var version = "2.3.1";
1548
+ var version = "2.3.3";
1549
1549
  var description$1 = "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management";
1550
1550
  //#endregion
1551
1551
  //#region \0virtual:core-openapi
@@ -165490,6 +165490,18 @@ var Client = class Client {
165490
165490
  * Shared client authentication utility.
165491
165491
  * Centralizes the logic for getting an authenticated C8y client.
165492
165492
  */
165493
+ /**
165494
+ * Map a \@c8y/client error (which is thrown as `{ res, data }` for HTTP
165495
+ * statuses >= 400) to a short human-readable string.
165496
+ * @param err - Error thrown by an \@c8y/client service call
165497
+ */
165498
+ function c8yErrorSummary(err) {
165499
+ if (err && typeof err === "object" && "res" in err) {
165500
+ const r = err.res;
165501
+ if (r && typeof r.status === "number") return `${r.status} ${r.statusText ?? ""}`.trim();
165502
+ }
165503
+ return err instanceof Error ? err.message : String(err);
165504
+ }
165493
165505
  async function resolveC8yAuth() {
165494
165506
  const custom = c8yMcpServer.ctx.custom;
165495
165507
  const auth = custom?.auth;
@@ -165898,7 +165910,9 @@ function normalizeCode(functionCode) {
165898
165910
  }
165899
165911
  function buildQueryScript(sourceCode) {
165900
165912
  const functionExpression = normalizeCode(sourceCode);
165901
- const resolved = c8yMcpServer.ctx.custom?.specs;
165913
+ const custom = c8yMcpServer.ctx.custom;
165914
+ const resolved = custom?.specs;
165915
+ 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.");
165902
165916
  return [
165903
165917
  `const coreSpec = ${JSON.stringify(resolved.core)};`,
165904
165918
  `const serviceSpecs = ${JSON.stringify(resolved.specs)};`,
@@ -165969,6 +165983,7 @@ async function query(functionCode) {
165969
165983
  if (!result.ok) throw new Error(`Execution failed with code ${result.error.code}: ${result.error.message}`);
165970
165984
  const value = extractDefaultExport(result.exports);
165971
165985
  const body = typeof value === "string" ? value : JSON.stringify(value);
165986
+ if (c8yMcpServer.ctx.custom?.env !== "cli") return body;
165972
165987
  const tenantUrl = c8yMcpServer.ctx.custom?.auth?.tenantUrl;
165973
165988
  return `${body}\n\n---\n${tenantUrl ? `Query ran against tenant: ${tenantUrl}. Visible specs are everything currently available for that tenant.` : "Query ran against bundled OpenAPI snapshots only — no active tenant. Visibility here does NOT guarantee any service is installed on a tenant."}`;
165974
165989
  }
@@ -166007,23 +166022,22 @@ function getOpenApiNote() {
166007
166022
  return "This MCP exposes a bundled Cumulocity core OpenAPI snapshot. Use `coreSpec` for inventory, alarms, events, measurements, users, tenants, and the broader Cumulocity REST surface. Bundled and discovered microservice APIs available on the current tenant are exposed via `serviceSpecs` (keyed by contextPath).";
166008
166023
  }
166009
166024
  function getQuerySafetyPreface(env) {
166010
- const sharedFooter = "Every result ends with a footer line naming the active tenant (or noting there is none) so you can verify which tenant the result reflects before acting on it.";
166011
- if (env !== "cli") return sharedFooter;
166012
- return [
166013
- "**CLI mode — read first.** The active tenant is global to this CLI session and can be flipped between calls by `set-active-tenant`. Always check the footer line at the bottom of every result. If it says \"no active tenant\" you are looking at bundled reference snapshots — call `cli-status` to see stored credentials and `set-active-tenant` to connect before relying on the result.",
166014
- "",
166015
- sharedFooter
166016
- ].join("\n");
166025
+ if (env === "server") return "Searches the bundled and discovered OpenAPI specs available to the current connection.";
166026
+ return "**Read first.** The active tenant is global to this CLI session and can be flipped between calls by `set-active-tenant`. Every result ends with a footer line naming the active tenant (or noting there is none) so you can verify which tenant the result reflects before acting on it. If the footer says \"no active tenant\" you are looking at bundled reference snapshots — call `cli-status` to see stored credentials and `set-active-tenant` to connect before relying on the result.";
166017
166027
  }
166018
166028
  function getExecuteSafetyPreface(env) {
166019
166029
  const sharedFooter = "An endpoint visible in `query` may still return 404 from `execute` when the service is not actually installed on the current tenant.";
166020
- if (env !== "cli") return sharedFooter;
166030
+ if (env === "server") return sharedFooter;
166021
166031
  return [
166022
- "**CLI mode — read first.** Every result starts with an `Executed against tenant: <url>` marker line followed by a blank line. Verify it matches the tenant you intend to mutate before reporting the result. The active tenant is global to this CLI session and can be flipped between calls by `set-active-tenant`. If no tenant is active `execute` fails with a missing-auth error — call `cli-status` and `set-active-tenant` to connect first.",
166032
+ "**Read first.** Every result starts with an `Executed against tenant: <url>` marker line followed by a blank line. Verify it matches the tenant you intend to mutate before reporting the result. The active tenant is global to this CLI session and can be flipped between calls by `set-active-tenant`. If no tenant is active `execute` fails with a missing-auth error — call `cli-status` and `set-active-tenant` to connect first.",
166023
166033
  "",
166024
166034
  sharedFooter
166025
166035
  ].join("\n");
166026
166036
  }
166037
+ function getQueryResultDescription(env) {
166038
+ if (env === "server") return "If your function returns a string it is returned as-is. Any other value is returned as JSON.";
166039
+ return "If your function returns a string it is returned as-is. Any other value is returned as JSON. A footer line naming the active tenant (or noting there is none) is appended after a `---` separator on every successful result.";
166040
+ }
166027
166041
  function createQueryTool(env) {
166028
166042
  return defineTool({
166029
166043
  name: "query",
@@ -166066,7 +166080,7 @@ declare const serviceSpecs: Record<string, Spec>
166066
166080
  - \`coreSpec\` — the main Cumulocity REST surface. Always present.
166067
166081
  - \`serviceSpecs\` — microservice APIs available on the active tenant, keyed by contextPath. An entry is **present iff** the service is reachable on this tenant. Paths are already prefixed (e.g. \`/service/myservice/items\`). Check with \`serviceSpecs.dtm\` (or \`'dtm' in serviceSpecs\`) before reaching in.
166068
166082
 
166069
- If your function returns a string it is returned as-is. Any other value is returned as JSON. A footer line naming the active tenant (or noting there is none) is appended after a \`---\` separator on every successful result.
166083
+ ${getQueryResultDescription(env)}
166070
166084
  The current MCP connection may still block \`execute\` calls even when an operation is visible in a spec.
166071
166085
 
166072
166086
  Examples:
@@ -166153,57 +166167,28 @@ function createTools(env) {
166153
166167
  * description lambda in CLI mode) can read the last known result without
166154
166168
  * awaiting.
166155
166169
  */
166156
- const specPromises = /* @__PURE__ */ new Map();
166157
- const refreshTimers = /* @__PURE__ */ new Map();
166158
- const DISCOVERY_REFRESH_INTERVAL_MS = 1800 * 1e3;
166159
- function normalizeTenantUrl(url) {
166160
- try {
166161
- return new URL(url).toString().replace(/\/$/, "");
166162
- } catch {
166163
- return url.replace(/\/$/, "");
166164
- }
166165
- }
166166
- function clearRefreshTimer(key) {
166167
- const t = refreshTimers.get(key);
166168
- if (t) {
166169
- clearTimeout(t);
166170
- refreshTimers.delete(key);
166171
- }
166172
- }
166173
- function scheduleRefresh(key, authHeaders) {
166174
- clearRefreshTimer(key);
166175
- const timer = setTimeout(() => {
166176
- refreshTimers.delete(key);
166177
- const empty = {
166178
- specs: [],
166179
- installedContextPaths: /* @__PURE__ */ new Set()
166180
- };
166181
- const promise = discoverApiSpecs(key, authHeaders).catch(() => empty);
166182
- specPromises.set(key, promise);
166183
- promise.then(() => scheduleRefresh(key, authHeaders)).catch(() => {});
166184
- }, DISCOVERY_REFRESH_INTERVAL_MS);
166185
- timer.unref?.();
166186
- refreshTimers.set(key, timer);
166187
- }
166170
+ const cache = /* @__PURE__ */ new Map();
166188
166171
  /**
166189
166172
  * Start discovery for the given tenant if not already started, or return the
166190
- * existing in-flight promise. Idempotent — safe to call on every request.
166191
- * @param tenantUrl - Base URL of the Cumulocity tenant
166192
- * @param authHeaders - Auth headers used for all discovery requests
166173
+ * existing in-flight / last-known-good promise. Idempotent — safe to call
166174
+ * on every request.
166175
+ *
166176
+ * Failure handling: if the discovery promise rejects and there is no
166177
+ * prior cached result, the in-flight entry is removed so the next caller
166178
+ * retries, and the rejection propagates to the current caller (the request
166179
+ * fails). A prior successful entry is never overwritten by a failure.
166180
+ * @param tenantId - Cumulocity tenant ID; used as the cache key
166181
+ * @param client - Configured Cumulocity client for all API reads
166193
166182
  */
166194
- function startDiscovery(tenantUrl, authHeaders) {
166195
- const key = normalizeTenantUrl(tenantUrl);
166196
- if (specPromises.has(key)) return specPromises.get(key);
166197
- const empty = {
166198
- specs: [],
166199
- installedContextPaths: /* @__PURE__ */ new Set()
166200
- };
166201
- const promise = discoverApiSpecs(key, authHeaders).catch((err) => {
166202
- consola.warn(`API spec discovery failed for ${key}:`, err instanceof Error ? err.message : String(err));
166203
- return empty;
166183
+ function startDiscovery(tenantId, client) {
166184
+ const existing = cache.get(tenantId);
166185
+ if (existing) return existing;
166186
+ const promise = discoverApiSpecs(client, tenantId);
166187
+ cache.set(tenantId, promise);
166188
+ promise.catch((err) => {
166189
+ consola.warn(`API spec discovery failed for tenant ${tenantId}:`, c8yErrorSummary(err));
166190
+ if (cache.get(tenantId) === promise) cache.delete(tenantId);
166204
166191
  });
166205
- promise.then(() => scheduleRefresh(key, authHeaders)).catch(() => {});
166206
- specPromises.set(key, promise);
166207
166192
  return promise;
166208
166193
  }
166209
166194
  function rewriteDiscoveredSpecPaths(spec, servicePrefix) {
@@ -166220,23 +166205,28 @@ function rewriteDiscoveredSpecPaths(spec, servicePrefix) {
166220
166205
  }
166221
166206
  /**
166222
166207
  * Fetch and return discovered specs for the given tenant.
166223
- * Throws on fatal errors; individual spec download failures are skipped.
166224
- * @param tenantUrl - Normalised base URL of the Cumulocity tenant
166225
- * @param authHeaders - Auth headers for all HTTP requests
166208
+ * Throws on fatal errors (applications listing); individual spec download
166209
+ * failures are skipped.
166210
+ *
166211
+ * Uses `applicationsByTenant/{tenantId}` (via `listByTenant`) rather than
166212
+ * the user-scoped `applicationsByUser` endpoint so the call works with
166213
+ * service-user credentials — service users cannot call /user/currentUser,
166214
+ * which the user-scoped endpoint depends on. The tenantId is always known
166215
+ * at the call site (it is the discovery cache key).
166216
+ *
166217
+ * All Cumulocity API calls go through the provided \@c8y/client. This module
166218
+ * never touches `fetch` directly so auth strategy choice (Basic, Bearer,
166219
+ * cookie, service-user) stays in the client construction layer.
166220
+ * @param client - Configured Cumulocity client used for all API reads
166221
+ * @param tenantId - Cumulocity tenant ID to list applications for
166226
166222
  */
166227
- async function discoverApiSpecs(tenantUrl, authHeaders) {
166228
- const baseUrl = normalizeTenantUrl(tenantUrl);
166229
- const headers = {
166230
- ...authHeaders,
166231
- Accept: "application/json"
166232
- };
166233
- const userRes = await fetch(`${baseUrl}/user/currentUser`, { headers });
166234
- if (!userRes.ok) throw new Error(`Failed to fetch current user: ${userRes.status} ${userRes.statusText}`);
166235
- const currentUser = await userRes.json();
166236
- if (!currentUser.id) throw new Error("Could not determine current user ID from /user/currentUser response");
166237
- const appsRes = await fetch(`${baseUrl}/application/applicationsByUser/${encodeURIComponent(currentUser.id)}?pageSize=2000`, { headers });
166238
- if (!appsRes.ok) throw new Error(`Failed to fetch applications: ${appsRes.status} ${appsRes.statusText}`);
166239
- const apps = (await appsRes.json()).applications ?? [];
166223
+ async function discoverApiSpecs(client, tenantId) {
166224
+ let apps;
166225
+ try {
166226
+ apps = (await client.application.listByTenant(tenantId, { pageSize: 2e3 })).data ?? [];
166227
+ } catch (err) {
166228
+ throw new Error(`Failed to fetch applications: ${c8yErrorSummary(err)}`);
166229
+ }
166240
166230
  const appsWithSpec = apps.filter((app) => typeof app.contextPath === "string" && app.contextPath.length > 0 && typeof app.name === "string" && app.name.length > 0 && app.manifest != null && app.manifest.openApiSpec != null);
166241
166231
  const seenContextPaths = /* @__PURE__ */ new Map();
166242
166232
  for (const app of appsWithSpec) {
@@ -166256,7 +166246,7 @@ async function discoverApiSpecs(tenantUrl, authHeaders) {
166256
166246
  path: app.manifest.openApiSpec
166257
166247
  }] : app.manifest.openApiSpec;
166258
166248
  for (const entry of entries) try {
166259
- const specRes = await fetch(`${baseUrl}${servicePrefix}/${entry.path.replace(/^\//, "")}`, { headers });
166249
+ const specRes = await client.core.fetch(`${servicePrefix}/${entry.path.replace(/^\//, "")}`);
166260
166250
  if (!specRes.ok) continue;
166261
166251
  specs.push({
166262
166252
  contextPath: app.contextPath,
@@ -174139,8 +174129,14 @@ function clearCliTenantContext() {
174139
174129
  * @param tenantUrl - Base URL of the Cumulocity tenant to activate
174140
174130
  */
174141
174131
  async function setCliTenantContext(tenantUrl) {
174142
- const authHeaders = createC8yAuthHeaders(await globalThis._getCredentialsByTenantUrl(tenantUrl));
174143
- const { specs: discovered, installedContextPaths } = await startDiscovery(tenantUrl, authHeaders);
174132
+ const creds = await globalThis._getCredentialsByTenantUrl(tenantUrl);
174133
+ const authHeaders = createC8yAuthHeaders(creds);
174134
+ const cliClient = new Client(new BasicAuth({
174135
+ tenant: creds.tenantId,
174136
+ user: creds.user,
174137
+ password: creds.password
174138
+ }), tenantUrl);
174139
+ const { specs: discovered, installedContextPaths } = await startDiscovery(creds.tenantId, cliClient);
174144
174140
  _context = {
174145
174141
  tenantUrl,
174146
174142
  authorizationHeader: authHeaders.Authorization,
@@ -174408,8 +174404,10 @@ runMain(defineCommand({
174408
174404
  const activeTenant = readActiveTenantUrl();
174409
174405
  if (activeTenant) try {
174410
174406
  const tenantCtx = await setCliTenantContext(activeTenant);
174411
- const specKeys = ["core", ...Object.keys(tenantCtx.specs.specs)];
174412
- consola.info(`Active tenant: ${activeTenant}. Available specs: ${specKeys.join(", ")}`);
174407
+ const serviceKeys = Object.keys(tenantCtx.specs.specs);
174408
+ consola.info(`Active tenant: ${activeTenant}`);
174409
+ consola.info(`Startup discovery complete: ${serviceKeys.length} microservice API spec(s) found${serviceKeys.length > 0 ? ` [${serviceKeys.join(", ")}]` : ""}`);
174410
+ consola.info(`Available specs: ${["core", ...serviceKeys].join(", ")}`);
174413
174411
  } catch (err) {
174414
174412
  const message = err instanceof Error ? err.message : String(err);
174415
174413
  if (message.includes("No stored credentials found for tenant URL")) {
@@ -174417,7 +174415,7 @@ runMain(defineCommand({
174417
174415
  consola.warn(`Active tenant ${activeTenant} was cleared because no credentials are stored for it. Call set-active-tenant with a known tenant URL or run 'creds add' first.`);
174418
174416
  } else consola.warn(`Could not activate tenant ${activeTenant}:`, message);
174419
174417
  }
174420
- else consola.info("No active tenant set. Call set-active-tenant to connect before using query or execute.");
174418
+ else consola.info("No active tenant set. Call set-active-tenant to connect before using query or execute. Discovery will run once a tenant is activated.");
174421
174419
  setupMcpServer("cli");
174422
174420
  const transport = new StdioTransport(c8yMcpServer);
174423
174421
  const active = getCliTenantContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mc8yp",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "type": "module",
5
5
  "description": "Cumulocity IoT MCP Server - Model Context Protocol integration for IoT device management",
6
6
  "keywords": [