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.
- package/dist/cli.mjs +79 -81
- 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.
|
|
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
|
|
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
|
-
|
|
166011
|
-
|
|
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
|
|
166030
|
+
if (env === "server") return sharedFooter;
|
|
166021
166031
|
return [
|
|
166022
|
-
"**
|
|
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
|
-
|
|
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
|
|
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
|
|
166191
|
-
*
|
|
166192
|
-
*
|
|
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(
|
|
166195
|
-
const
|
|
166196
|
-
if (
|
|
166197
|
-
const
|
|
166198
|
-
|
|
166199
|
-
|
|
166200
|
-
|
|
166201
|
-
|
|
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
|
|
166224
|
-
*
|
|
166225
|
-
*
|
|
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(
|
|
166228
|
-
|
|
166229
|
-
|
|
166230
|
-
|
|
166231
|
-
|
|
166232
|
-
|
|
166233
|
-
|
|
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(`${
|
|
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
|
|
174143
|
-
const
|
|
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
|
|
174412
|
-
consola.info(`Active tenant: ${activeTenant}
|
|
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();
|