mc8yp is a Model Context Protocol (MCP) server that gives AI agents access to the full Cumulocity API surface through a single code-mode tool instead of a huge fixed tool inventory:
codemode— run an async JavaScript function in a sandbox where API discovery, documentation search, and typed live API calls are all available as globals
Inside the sandbox the agent sees one typed namespace per API: c8y for the Cumulocity core REST surface plus one namespace per microservice available on the tenant (e.g. dtm), each with one method per operation derived from the OpenAPI specs — c8y.getAlarmCollectionResource({ pageSize: 10 }) instead of hand-built REST calls. Discovery happens in-sandbox through codemode.search/codemode.describe (ranked method search + on-demand TypeScript interfaces) and docs.search/docs.read (fuzzy full-text search over the specs' prose documentation, e.g. query-language grammars).
The agent sees not only the bundled Core and DTM specs, but any microservice installed on the tenant that declares an OpenAPI spec in its manifest — mc8yp discovers those live and derives namespaces for them alongside the bundled ones. Services that expose an MCP server (exposeMcpServers in their manifest) are wrapped as MCP namespaces instead — one typed method per MCP tool, with MCP preferred over the OpenAPI spec when a service declares both. No code changes or rebuild required to support a new service.
Operators stay in control through per-connection restrictions and allow rules, so the same broad capability can be deployed as a read-only agent, a non-destructive production agent, or anything in between.
- mc8yp discovers every microservice installed on the tenant that declares an OpenAPI spec, and derives typed method namespaces from those alongside the bundled Core (+ DTM) specs.
- Inside a
codemoderun, the agent finds the right method withcodemode.search, inspects its exact input/output types withcodemode.describe, and reads prose documentation (domain query languages, parameter syntax) withdocs.search/docs.read. - In the same run, the agent calls the live Cumulocity API through the derived methods (
c8y.<method>({ ... })). The namespaces are the complete surface — there is no raw-request escape hatch. - mc8yp enforces configured restrictions and allow rules before any request leaves the host — blocked operations are also omitted from discovery entirely.
When a tenant is active, mc8yp asks Cumulocity which applications the tenant is subscribed to, reads the openApiSpec declaration from each application manifest, fetches the spec, prefixes its paths with the service's contextPath, and exposes it to the sandbox as a typed namespace named after the contextPath. Results are cached per tenant for 30 minutes.
The practical effect: any Cumulocity microservice that ships an OpenAPI spec is automatically usable by the agent, whether it is one of the bundled snapshots, a Cumulocity-provided service, or a custom microservice built in-house. The bundled specs are just guaranteed offline coverage; the discovery layer fills in everything else.
The same discovery run also picks up MCP servers declared via exposeMcpServers (type http) and fetches their tool lists. A service exposing both an MCP server and an OpenAPI spec is wrapped as an MCP namespace; per connection you can opt services back to their spec with the mc8yp-no-mcp header / noMcp query param (server mode) or --no-mcp (CLI) — pass * for all services or a comma-separated contextPath list. Wrapped MCP tools run with the end user's credentials; elicitation and sampling are NOT forwarded (mc8yp advertises no such capabilities, so compliant servers use their fallbacks).
When a new service is subscribed mid-session, CLI agents can call the status tool with refresh: true to bust the cache without waiting for the 30-minute window. Server mode does not yet expose an in-protocol refresh trigger — use the POST /refresh-apis HTTP route from ops/CI scripts that sit outside the MCP protocol.
- Microservice mode (recommended for production) — deploy inside Cumulocity IoT, expose
/mcp, integrate with AI Agent Manager. Auth comes from the request and the service user. - CLI mode (local development) — run locally over stdio with an MCP client such as Claude Desktop. Credentials are stored in the OS keyring.
-
Download the latest release zip from GitHub Releases.
-
Upload the
.zipin Cumulocity Application Management. -
Subscribe the application in your tenant.
-
Point your agent at:
https://<tenant>.cumulocity.com/service/mc8yp-server/mcp
No extra credential setup is required — the microservice uses Cumulocity's deployment environment and request authentication.
The microservice manifest declares exposeMcpServers, so once the application is subscribed it auto-registers with AI Agent Manager as an MCP server at /service/mc8yp-server/mcp (with the user's authentication forwarded). No manual MCP server entry is needed in AI Agent Manager.
Example: read-only production agent (allow only safe GETs):
/mcp?allow=GET:/inventory/**&allow=GET:/alarm/**&allow=GET:/measurement/**Or via headers:
POST /mcp HTTP/1.1
mc8yp-allow: GET:/inventory/**
mc8yp-allow: GET:/alarm/**
mc8yp-allow: GET:/measurement/**See Access policy for the full rule syntax.
| Platform | Supported | Notes |
|---|---|---|
| macOS | ✅ native | Keychain is used for credentials |
| Linux | ✅ native | Secret Service (libsecret) is used for credentials |
| Windows | ❌ | Use WSL 2 (see WSL 2 one-time setup below) or the microservice mode instead |
The sandboxed V8 runtime (@iso4/sandbox) communicates with a Rust subprocess over Unix domain sockets, which is why native Windows is not supported.
# Run directly (recommended)
pnpm dlx mc8yp
# Pick a specific bundled core OpenAPI build for the codemode tool
pnpm dlx mc8yp --spec 2025
# Or install globally
npm install -g mc8yp
mc8ypmc8yp creds add prompts for tenant URL, username, and a masked password, and writes them to the OS keyring.
pnpm dlx mc8yp creds add # add credentials (interactive, masked password)
pnpm dlx mc8yp creds list # list stored credentials
pnpm dlx mc8yp creds remove # remove stored credentialsOn macOS and standard desktop Linux this works out of the box. On WSL 2 the keyring stack is not wired up by default and needs a one-time bootstrap:
WSL 2 one-time setup — required before mc8yp creds add works on WSL
A fresh WSL 2 distro has no Secret Service provider, no session D-Bus, and no login keyring collection, so @napi-rs/keyring (used by mc8yp creds add) has nothing to talk to. On a normal desktop Linux all of this is wired up automatically by the display manager and PAM; on WSL you have to do it once manually.
1. Inside WSL, install the keyring stack:
sudo apt install -y libsecret-tools dbus-x11
sudo apt install -y libpam-gnome-keyringlibsecret-toolsprovidessecret-tooland pulls inlibsecret(the client library@napi-rs/keyringuses).dbus-x11providesdbus-launchso a session D-Bus can be started in a headless shell.libpam-gnome-keyringinstallsgnome-keyring-daemon(the actual Secret Service provider) and its PAM module.
2. Force the keyring database to initialize:
secret-tool store --label="init" init initA throwaway write so gnome-keyring-daemon creates its on-disk store.
3. Wire up PAM so the keyring auto-unlocks at login:
sudo bash -c 'cat >> /etc/pam.d/login <<EOF
auth optional pam_gnome_keyring.so
session optional pam_gnome_keyring.so auto_start
EOF'4. From PowerShell, fully restart WSL so PAM picks up the new config:
wsl --shutdown5. Back in WSL, start a session D-Bus (WSL doesn't get one by default):
echo $DBUS_SESSION_BUS_ADDRESS # should be empty
eval $(dbus-launch --sh-syntax)6. Create the login collection that libsecret writes into. On a normal desktop this is created by the graphical login session; on WSL it does not exist and credential writes will fail without it:
gdbus call --session \
--dest org.freedesktop.secrets \
--object-path /org/freedesktop/secrets \
--method org.freedesktop.Secret.Service.OpenSession \
"plain" \
"<''>"
gdbus call --session \
--dest org.freedesktop.secrets \
--object-path /org/freedesktop/secrets \
--method org.freedesktop.Secret.Service.CreateCollection \
"{'org.freedesktop.Secret.Collection.Label': <'login'>}" \
""7. Trigger the keyring passphrase prompt once:
secret-tool store --label="test" service myservice username myuserThis opens a prompt to set the keyring passphrase. You can leave it empty — the keyring will then auto-unlock without prompting later, which is what you want for headless WSL.
After this, mc8yp creds add will work.
Adding credentials does not auto-activate a tenant. Live API calls only run against a tenant once one has been selected, and the agent does that itself through MCP tools:
- The agent calls
statusto see stored credentials, the current active tenant, and the API namespaces currently visible. - The agent calls
set-active-tenantwith one of the tenant URLs. The selection is written to~/.config/mc8yp/active-tenant.jsonand reused across CLI restarts. - The agent runs
codemodeas needed. Each result starts with a marker line showing which tenant it ran against.
To switch tenants, call set-active-tenant again. To stop targeting any tenant (browse bundled specs only), call it with tenantUrl: null — discovery (codemode.search/describe, docs) keeps working against the bundled reference snapshots, while live API calls return a missing-auth error so the agent cannot accidentally hit a tenant.
If the active tenant's credentials are removed via mc8yp creds remove, the next status call clears the active tenant automatically.
For Claude Desktop or any stdio MCP client:
{
"servers": {
"mc8yp": {
"type": "stdio",
"command": "pnpm",
"args": ["dlx", "mc8yp"]
}
}
}With read-only access rules:
{
"servers": {
"mc8yp": {
"type": "stdio",
"command": "pnpm",
"args": [
"dlx",
"mc8yp",
"-a", "GET:/inventory/**",
"-a", "GET:/alarm/**",
"-a", "GET:/measurement/**"
]
}
}
}The quickest way to register mc8yp is the Claude Code CLI. Everything after -- is passed to the mc8yp subprocess, so access-policy flags go there:
# Local CLI (stdio) — default scope is this project only
claude mcp add mc8yp -- pnpm dlx mc8yp
# Make it available in every project (user scope)
claude mcp add -s user mc8yp -- pnpm dlx mc8yp
# Pin a bundled core spec and add read-only access rules
claude mcp add mc8yp -- pnpm dlx mc8yp --spec 2025 \
-a "GET:/inventory/**" -a "GET:/alarm/**" -a "GET:/measurement/**"For deployed microservice mode, add it as an HTTP server instead:
claude mcp add --transport http mc8yp \
https://<tenant>.cumulocity.com/service/mc8yp-server/mcp \
--header "Authorization: Bearer <token>"Manage the entry with claude mcp list, claude mcp get mc8yp, and claude mcp remove mc8yp.
| Tool | Description |
|---|---|
codemode |
Run an async JavaScript function in a sandbox with discovery (codemode.search/describe), documentation (docs.search/read), and typed API namespaces (c8y, per-service globals) available. Returns the function result in Toon format. |
status |
(CLI only) Show the active tenant, stored credentials, and the API namespaces currently visible. Auto-clears the active tenant if its credentials are gone. Pass refresh: true to bust the 30-minute discovery cache and re-run discovery for the active tenant — useful right after (un)subscribing a microservice. Noop when no tenant is active. |
set-active-tenant |
(CLI only) Select the tenant codemode operates against. Pass tenantUrl: null to clear. |
The codemode tool runs in a sandboxed V8 runtime (@iso4/sandbox) hosted in a separate Rust subprocess. The sandbox has no fetch global — every live call is dispatched host-side through a hardened request funnel built on @iso4/fetch, which injects auth, enforces the access policy, and parses responses before anything reaches the sandbox.
The code-mode-guide prompt contains the full reference for the codemode tool, including types, examples, and the active access policy for the current connection.
async () => {
// 1. Find the method
const { results } = await codemode.search('managed objects')
// 2. Inspect its exact typed interface (input/output types, per-field docs)
const { content } = await codemode.describe(results[0].target)
// 3. When parameter syntax is unknown, search the prose documentation
const hits = await docs.search('inventory query language')
const grammar = await docs.read(hits[0].id)
// 4. Call it — path/query/header params and `body` share one flat object
const devices = await c8y.getManagedObjectCollectionResource({
query: '$filter=(type eq \'c8y_Device\')',
pageSize: 20,
})
return devices.managedObjects?.map((d) => ({ id: d.id, name: d.name }))
}There is deliberately no raw-request escape hatch — the typed namespaces are the complete surface. Whether a namespace wraps an OpenAPI spec or an MCP server is invisible to the agent; the backing protocol is an operator concern.
Operations can be hidden from derivation and discovery by annotating them in the OpenAPI spec with the vendor extension x-mc8yp-exclude: true — with no escape hatch, exclusion is absolute for the sandbox.
Experimental. Available in deployed microservice mode only, and disabled by default; not exposed in the local CLI (agent harnesses there bring their own file I/O).
In microservice mode, codemode has one more optional global — sandbox — an in-memory shell with a virtual filesystem for wrangling data you fetched from the API (jq, awk, sed, grep, sort, uniq, cut, sqlite3, …). It has no network access and no host filesystem access — it never reaches Cumulocity. Fetch with c8y/service namespaces, process in the sandbox, read the result back.
It is off unless you turn it on. Enable it per connection with the mc8yp-enable-sandbox header or the enableSandbox query param (any of empty, *, or true). Without it, sandbox is absent — same as CLI mode.
async () => {
const alarms = await c8y.getAlarmCollectionResource({ pageSize: 2000 })
await sandbox.writeFile('/alarms.json', JSON.stringify(alarms.alarms ?? []))
const { stdout } = await sandbox.exec('jq "group_by(.severity) | map({severity: .[0].severity, count: length})" /alarms.json')
return JSON.parse(stdout)
}The surface mirrors Flue's SandboxApi: readFile, readFileBuffer, writeFile, stat, readdir, exists, mkdir, rm, exec, plus an mc8yp-specific clear() that wipes the filesystem. It is backed by a swappable adapter (currently just-bash, core shell only) so the provider can be replaced later without changing agent-facing code.
Lifecycle: one in-memory sandbox per MCP session, so files persist across codemode calls within a session. It is evicted from memory 15 minutes after its last use (or immediately via sandbox.clear()). Nothing is ever written to disk, and sessions never share state. (Cross-call persistence relies on your MCP client maintaining the mcp-session-id, which standard clients do.)
mc8yp supports two per-connection rule types:
- Restrictions — deny rules that block matching API operations.
- Allow rules — allow-list rules. When at least one allow rule is set, anything not matching is blocked.
If both apply to the same operation, restrictions win. This is how you expose broad API knowledge while still running an agent in a read-only or otherwise constrained mode.
<path-pattern>
<method>:<path-pattern>- No method prefix → matches all HTTP methods.
- With a method prefix → only that method. Supported:
DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,QUERY,TRACE, or*. Case-insensitive. - Patterns must start with
/. Query strings and fragments are not allowed in patterns. - Wildcards:
*matches within a single path segment;**matches zero or more whole segments and must be its own segment.
Path pattern examples
| Pattern | Matches | Does Not Match |
|---|---|---|
/inventory |
/inventory |
/inventory/managedObjects |
/inventory/** |
/inventory, /inventory/managedObjects, /inventory/x/y |
/alarm/alarms |
/i* |
/inventory, /identity, /i |
/inventory/managedObjects |
/i*/** |
/inventory, /inventory/managedObjects, /identity/x |
/alarm/alarms |
/inventory/m* |
/inventory/managedObjects, /inventory/measurements |
/inventory/events, /inventory/m/x |
/inventory/*/child |
/inventory/device-1/child, /inventory/x/child |
/inventory/child, /inventory/a/b/child |
/inventory/**/child |
/inventory/child, /inventory/a/b/child |
/inventory/a/b/sibling |
Notes:
/inventory/**already matches/inventoryitself./i**is invalid —**must be its own segment. Use/i*/**instead.- Rule patterns may not contain
//,.,.., query strings, or fragments.
Common rule examples
| Rule | Restriction Effect | Allow-list Effect |
|---|---|---|
/inventory/** |
Block all methods on /inventory and below |
Permit all methods on /inventory and below |
DELETE:/inventory/** |
Block only DELETE on /inventory and below |
Permit only DELETE on /inventory and below |
/alarm/alarms |
Block all methods on the exact path /alarm/alarms |
Permit all methods on the exact path /alarm/alarms |
GET:/measurement/measurements |
Block only GET on the exact path /measurement/measurements |
Permit only GET on the exact path /measurement/measurements |
POST:/inventory/managedObjects |
Block creating new managed objects | Permit creating new managed objects |
/user/** |
Block all user management paths | Permit all user management paths |
Repeat -r, --restrict, or --restriction for deny rules; -a, --allow, or --allowed for allow rules:
# Block all inventory writes and all alarm access
mc8yp -r "DELETE:/inventory/**" -r "/alarm/**"
# Only permit GET inventory + POST alarms
mc8yp --allow "GET:/inventory/**" --allowed "POST:/alarm/**"
# Allow inventory broadly, but still block one path
mc8yp -a "/inventory/**" -r "/inventory/managedObjects"Use query parameters or project-scoped headers on the /mcp endpoint:
- Deny rules:
restriction,restrict, orrquery params, ormc8yp-restrictionheader. - Allow rules:
allowed,allow, oraquery params, ormc8yp-allowheader.
Both headers accept either repeated header instances or a comma-separated list. Query parameters and headers can be combined.
/mcp?r=/inventory/**&r=DELETE:/alarm/**&allow=GET:/measurement/**POST /mcp HTTP/1.1
Authorization: Bearer <token>
mc8yp-restriction: /inventory/**
mc8yp-allow: GET:/measurement/**When a live call is blocked by connection policy, the codemode run returns explanatory text, no request is sent to Cumulocity, and retrying through the same connection will not help. Blocked operations are additionally omitted from codemode.search/describe and the docs index, so the agent never plans around a method it cannot call.
Note: path-based restriction/allow rules apply to OpenAPI-derived namespaces only. Wrapped MCP tools have no METHOD:path identity and are not covered by these rules — use the
noMcpopt-out to disable MCP wrapping for a connection if that matters for your deployment.
What the agent sees through codemode discovery comes from two layers:
- Live-discovered specs — every microservice subscribed on the active tenant whose manifest declares an
openApiSpec. Discovered at runtime, cached for 30 minutes per tenant, exposed as a typed namespace named after the contextPath. This works for any service, not just the ones bundled here. - Bundled snapshots — shipped with the build so Core and DTM are always available even when discovery hasn't run yet:
- Core snapshots:
release,2026,2025,2024 - DTM snapshot bundled alongside each supported core build
- Core snapshots:
With an active tenant, services not installed on that tenant get no namespace at all, so the agent only sees what is actually reachable.
In CLI mode, pick which core snapshot the c8y namespace derives from:
mc8yp # default: latest bundled release
mc8yp --spec 2025 # use the 2025 snapshot
mc8yp -s 2024 # short formThis only affects the bundled core view. Live calls always hit the Cumulocity API of the selected tenant or deployed service environment.
Requires Node.js ≥ 24 and pnpm.
pnpm install
pnpm test:run # tests
pnpm lint:fix # lint with autofix
pnpm typecheck # tsc --noEmit
pnpm build # CLI bundle in dist/, server bundles in .output/<version>/Run locally from source by pointing your MCP client at the built CLI:
{
"servers": {
"local_mc8yp": {
"type": "stdio",
"command": "node",
"args": ["/path/to/your/project/dist/cli.mjs"]
}
}
}Release packaging
pnpm package:microservicesProduces one Docker-based Cumulocity zip per bundled server variant in the repository root, e.g.:
mc8yp-core-release-dtm-v1.2.3.zipmc8yp-core-2026-dtm-v1.2.3.zipmc8yp-core-2025-dtm-v1.2.3.zipmc8yp-core-2024-dtm-v1.2.3.zip
The packaging step writes a temporary generated Dockerfile under .c8y/, copies the selected versioned server bundle into /app/server/, and installs production dependencies inside a linux/amd64 Docker image so the per-platform native binaries of @iso4/sandbox resolve correctly. The deployed HTTP transport is POST-only (GET /mcp returns 405) because some reverse proxies and Cumulocity ingress layers do not keep a long-lived SSE channel stable enough for reliable MCP tool calls.
The build matrix is driven by openapi-builds.json. Core snapshots live under openapi/core/, DTM snapshots under openapi/dtm/.
MIT