mc8yp 2.7.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/dist/cli.mjs +26 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,10 +61,46 @@ Behaviour worth knowing:
|
|
|
61
61
|
- **The agent is told which namespaces are external.** `codemode.describe()` labels them `EXTERNAL MCP server at <url> — configured for this connection, NOT part of this tenant`, and the per-method describe repeats it. Whether a tenant namespace is backed by OpenAPI or MCP stays hidden (an implementation detail the agent cannot act on); tenant-vs-third-party is a data boundary it must be able to report on.
|
|
62
62
|
- **Tool lists are fetched on first use and cached per MCP session**, then dropped 15 minutes after last use or immediately when the client closes its session cleanly. Rotating a token or changing a URL mid-session re-handshakes.
|
|
63
63
|
- **A malformed entry fails the request** (HTTP 400 in server mode, a startup error in CLI) rather than silently leaving the agent without a namespace it was supposed to have. A well-formed but _unreachable_ server is reported in the `codemode.describe()` overview and retried on the next call.
|
|
64
|
-
- **The tenant wins name collisions.** An external entry whose `name` matches a tenant namespace is skipped, so a header cannot shadow a real service.
|
|
64
|
+
- **The tenant wins name collisions.** An external entry whose `name` matches a tenant namespace is skipped, so a header cannot shadow a real service. That skip is silent to the caller — use `POST /resolve-mcp-servers` (below) to catch it while configuring rather than discovering it as a missing namespace at run time.
|
|
65
65
|
- **Egress is unrestricted by design.** The URL is used as given — any host, no allowlist, no private-range filtering. The header is part of the connection's trusted configuration (the sandbox cannot set it), but the consequence is that whoever can set headers on the deployed microservice can make it issue requests to any address it can reach, including tenant-internal ones. Front the deployment accordingly.
|
|
66
66
|
- Path-based restriction/allow rules do **not** apply to these namespaces, same as for tenant-discovered MCP tools (see [Access policy](#access-policy)).
|
|
67
67
|
|
|
68
|
+
#### Checking a configuration before you store it — `POST /resolve-mcp-servers`
|
|
69
|
+
|
|
70
|
+
Whoever builds that header — a tenant admin UI, a deployment script — cannot answer three things on its own: which namespace an entry gets, whether that namespace is free, and whether the server actually answers with its credentials. This route answers all three in one call, so no consumer has to reimplement mc8yp's namespace rules or its MCP handshake:
|
|
71
|
+
|
|
72
|
+
```http
|
|
73
|
+
POST /service/mc8yp-server/resolve-mcp-servers
|
|
74
|
+
Content-Type: application/json
|
|
75
|
+
|
|
76
|
+
{ "servers": [{ "name": "MaStR registry", "url": "https://mastr.example/mcp", "token": "…" }] }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{
|
|
81
|
+
"tenantUrl": "https://…cumulocity.com",
|
|
82
|
+
"tenantId": "t123",
|
|
83
|
+
"tenantNamespaces": ["c8y", "dtm", "knowledge_base_ms"], // null = the check could not run, see `warnings`
|
|
84
|
+
"warnings": [],
|
|
85
|
+
"servers": [
|
|
86
|
+
{
|
|
87
|
+
"name": "MaStR registry",
|
|
88
|
+
"namespace": "MaStR_registry", // derived — store THIS and send it in the header from now on
|
|
89
|
+
"status": "ok", // ok | invalid | namespace-taken | unreachable
|
|
90
|
+
"server": { "name": "mastr-mcp", "version": "1.0.0" },
|
|
91
|
+
"tools": [{ "name": "search_units", "description": "…" }]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- **`name` may be free-form here.** It is sanitized into the namespace exactly as a contextPath is (`MaStR registry` → `MaStR_registry`, the same rule that makes `knowledge-base-ms` into `knowledge_base_ms`). An identifier passes through unchanged.
|
|
98
|
+
- **`status` is the field to branch on**, and `namespace-taken` is the interesting one: reserved name, a namespace this tenant already holds, or a duplicate inside the same request. That is the collision that would otherwise be a silent skip at run time — `namespaceTakenBy` says who holds it. Tools are still reported for a taken entry, so renaming is the only fix needed.
|
|
99
|
+
- **The header contract does not change.** Derivation is a configure-time convenience: resolve once, store the `namespace` you got back, keep sending it verbatim. The agent-visible namespace stays stable even if the derivation is refined later.
|
|
100
|
+
- **Validation is the header's own**, so this route cannot green-light an entry the header would then 400.
|
|
101
|
+
- **Nothing is persisted, and nothing is cached.** mc8yp holds no external server configuration of its own, and the handshake bypasses the per-session tool-list cache so the answer always reflects the credentials as sent.
|
|
102
|
+
- Requires user auth (Authorization header or session cookie), like `POST /refresh-apis`.
|
|
103
|
+
|
|
68
104
|
## Two ways to run it
|
|
69
105
|
|
|
70
106
|
- **Microservice mode** (recommended for production) — deploy inside Cumulocity IoT, expose `/mcp`, integrate with [AI Agent Manager](https://cumulocity.com/docs/ai/aim-introduction/). Auth comes from the request and the service user.
|
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.7.
|
|
1549
|
+
var version = "2.7.1";
|
|
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
|
|
@@ -78260,7 +78260,7 @@ function parseExternalMcpServers(sources) {
|
|
|
78260
78260
|
continue;
|
|
78261
78261
|
}
|
|
78262
78262
|
for (const candidate of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
78263
|
-
const result =
|
|
78263
|
+
const result = validateExternalMcpEntry(candidate, usedNames);
|
|
78264
78264
|
if ("reason" in result) {
|
|
78265
78265
|
failedEntries.push({
|
|
78266
78266
|
entry: typeof candidate === "object" ? JSON.stringify(candidate) : String(candidate),
|
|
@@ -78277,7 +78277,14 @@ function parseExternalMcpServers(sources) {
|
|
|
78277
78277
|
failedEntries
|
|
78278
78278
|
};
|
|
78279
78279
|
}
|
|
78280
|
-
|
|
78280
|
+
/**
|
|
78281
|
+
* Validate one entry, exported so the resolve route can answer with the exact
|
|
78282
|
+
* verdict the header path would reach — a route that green-lights a server the
|
|
78283
|
+
* header then rejects is worse than no route at all.
|
|
78284
|
+
* @param candidate Parsed JSON entry.
|
|
78285
|
+
* @param usedNames Namespaces already taken by earlier entries in the same batch.
|
|
78286
|
+
*/
|
|
78287
|
+
function validateExternalMcpEntry(candidate, usedNames) {
|
|
78281
78288
|
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return { reason: "Entry must be a JSON object with \"name\" and \"url\"." };
|
|
78282
78289
|
const entry = candidate;
|
|
78283
78290
|
if (typeof entry.name !== "string" || entry.name === "") return { reason: "\"name\" is required and must be a non-empty string — it becomes the sandbox namespace." };
|
|
@@ -78358,7 +78365,18 @@ function armIdleTimer(sessionKey) {
|
|
|
78358
78365
|
session.timer = setTimeout(() => evictExternalMcpSession(sessionKey, "idle-timeout"), IDLE_TTL_MS);
|
|
78359
78366
|
session.timer.unref?.();
|
|
78360
78367
|
}
|
|
78361
|
-
|
|
78368
|
+
/**
|
|
78369
|
+
* Handshake one external server and read its tool list, bypassing the session
|
|
78370
|
+
* cache entirely.
|
|
78371
|
+
*
|
|
78372
|
+
* Exported for configuration checks (`POST /resolve-mcp-servers`): an operator
|
|
78373
|
+
* asking "does this URL and token work" must not be answered from a tool list
|
|
78374
|
+
* cached up to 15 minutes ago, and a candidate that may never be saved must not
|
|
78375
|
+
* seed the cache either. Runtime resolution goes through
|
|
78376
|
+
* {@link resolveExternalMcpServers}, which is where caching belongs.
|
|
78377
|
+
* @param config One external server config.
|
|
78378
|
+
*/
|
|
78379
|
+
async function probeExternalMcpServer(config) {
|
|
78362
78380
|
const client = new McpHttpClient({
|
|
78363
78381
|
url: config.url,
|
|
78364
78382
|
fetch: createExternalMcpFetch(config)
|
|
@@ -78370,7 +78388,9 @@ async function listExternalTools(config) {
|
|
|
78370
78388
|
return {
|
|
78371
78389
|
config,
|
|
78372
78390
|
tools,
|
|
78373
|
-
instructions: info.instructions
|
|
78391
|
+
instructions: info.instructions,
|
|
78392
|
+
serverName: info.serverName,
|
|
78393
|
+
serverVersion: info.serverVersion
|
|
78374
78394
|
};
|
|
78375
78395
|
} finally {
|
|
78376
78396
|
await client.close();
|
|
@@ -78400,7 +78420,7 @@ async function resolveExternalMcpServers(sessionKey, configs) {
|
|
|
78400
78420
|
const key = configKey(config);
|
|
78401
78421
|
let pending = session.servers.get(key);
|
|
78402
78422
|
if (!pending) {
|
|
78403
|
-
pending =
|
|
78423
|
+
pending = probeExternalMcpServer(config);
|
|
78404
78424
|
session.servers.set(key, pending);
|
|
78405
78425
|
pending.catch(() => {
|
|
78406
78426
|
if (session.servers.get(key) === pending) session.servers.delete(key);
|