npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

meshadm

v0.4.6

Published

MCP Mesh platform CLI — a thin, agent-friendly client over the MCP Mesh console API.

Readme

meshadm

npm install -g meshadm

Status: released. Published versions are stamped Go binaries, not the 0.0.0 name reservation that first held the npm name. meshadm version prints the version, commit and build date it was built from; npm view meshadm version is the latest published. This file deliberately names no version — it went stale twice that way. It is built and verified by .github/workflows/release-tools.yml, which is dispatch-only: merging to main publishes nothing.

How it is packaged

Five npm packages, one version, published together:

| Package | Contents | |---------|----------| | @mcpmesh/meshadm-darwin-arm64 | one Go binary, os/cpu declared | | @mcpmesh/meshadm-darwin-amd64 | " | | @mcpmesh/meshadm-linux-amd64 | " | | @mcpmesh/meshadm-linux-arm64 | " | | meshadm | bin/meshadm.js, the launcher — the four above as exact-pinned optionalDependencies |

npm installs the one platform package matching your machine and skips the rest; bin/meshadm.js resolves it and execs the binary, forwarding stdin, signals, and the exit code unchanged. There is no postinstall download: the binaries ship inside the packages, so installs work offline and there is no install-time network to trust. No Homebrew tap, and no GitHub Release assets — this repo is private, so npm is the only public channel available.

Windows is not built. Use WSL2, where meshadm is the linux build.

Build the whole release set locally with scripts/ci/build-meshadm-packages.sh --version <x.y.z>.

The Go program lives in this directory (main.go, internal/, man/) and implements the foundation — contexts, service-principal auth, the output/exit-code contract, the embedded manual — plus a generated API client and, on top of it, meshadm reads the whole console surface and writes most of it:

| Noun | Read verbs | Write verbs | |------|-----------|-------------| | todo | (no subcommands) — the live onboarding board: every task, its state, and what it is waiting on. --env narrows, --blocked shows every app's operator asks. Exits 1 when work is available and 10 when everything is blocked | | | status | (no subcommands) — the deploy-status read, and the one to poll after a sync: every component's live Argo sync and health. --env scopes it to one app-environment; without it the columns are the worst-of across all of them. It is the same command as component list — same call, same flags, same output | | | app | list, describe, config describe, platform-env-allowlist list, database-quota describe | config set — every environment realm at once, platform-env-allowlist allow, platform-env-allowlist revoke, database-quota set (--quota required), database-quota reset — the only way to clear the override | | env | list, config describe, namespace-preflight | create — provisions a Keycloak realm, and the delete path is not its mirror, config set, set-public-url, set-data-services — declares which managed data services the app-env uses; --none and --clear are different acts | | component | list | declare, update, restart — deletes pods, one at a time, waiting for each replacement to be Ready; it never patches the Deployment, so Argo stays the only writer of desired state | | mesh-core | | provision | | oidc-client | list, derived-redirect-uris | create — reveal-once secret, import | | role | list, describe | create, update, delete — destructive | | permission | list | | | manifest | download | apply (--dry-run) | | user | list, describe | | | idp | list, registration, login-methods | set <providerId>, set-registration, set-password | | route | describe | set, republish | | branding | describe | set | | theme | describe, status, installed, download, starter | upload, delete — destructive | | email-template | list, describe, preview, starter | set <typeKey>, delete — destructive | | data | list, describe <postgres\|redis\|minio> | postgres provision, redis provision, minio provision — reveal-once credentials | | harbor | describe, preflight | provision — reveal-once secrets, repair-pull-secret | | argo | list-registrations — registration rows only, no sync and no health; the old argo list spelling stays as a permanent alias | register, register-all, adopt, sync, deregister — destructive | | onboarding-token | describe | mint — reveal-once secret | | bundle | download | | | values | (no subcommands) — prints the app-env's env-var list + helm values snippet, no secrets | | | secrets | pubkey — the cluster's public sealing certificate | | | platform-env | list — operator-only | |

That table is the whole console surface meshadm covers, and internal/cmd/readme_test.go fails the build when it drifts from the command tree — it has gone stale before, and a table that under-reports verbs is worse than no table, because it tells an agent a command does not exist and the agent believes it. Four nouns are deliberately absent because they call no console API: auth (tokens and identity, below), config (contexts on disk), man and version. Most verbs also have aliases the table does not repeat; meshadm <noun> --help is the authority on those.

Two per-app guardrails sit under app, and both bound an app that can build its own environments. app platform-env-allowlist decides where the app may create app-environments — the platform-environment on a create request is caller-supplied, so without it an app principal could name prod from a dev-realm credential and have a realm provisioned on the production Keycloak. app database-quota decides how many managed databases the app may hold, in addition to the cluster connection budget rather than instead of it: the budget bounds the shared pool, the quota bounds one app's share, and both must pass. Reading either is app-scoped — an agent that knows it may only bind dev, or that it is at its database quota, can act instead of tripping over a 403. Changing either is platform-admin only and requires a platform-realm credential, because a principal that could raise its own ceiling would not be bounded by it. Two things that read backwards if you skim them: an empty allowlist means nothing permitted, not no restriction, and no quota override means the platform default, never unlimited.

database-quota set requires --quota and will not infer it. The endpoint reads an absent quota as "clear the override and revert to the default", answers 200, and looks exactly like a successful set — a mistyped field has already changed a ceiling that way. So meshadm splits the intents: set --quota <n> sends a number and exits 2 without calling the console when the flag is missing, and reset is the only thing that clears an override. A forgotten flag must never be a silent write.

Destructive verbs exist now, and nothing client-side gates them. role delete, theme delete, email-template delete and argo deregister are implemented, and none of them has a --yes, a --confirm or a prompt — nor may they grow one. The grant is the boundary: Delete is a separate permission per section, withheld by default, so a principal that lacks it gets a 403 at the endpoint. A confirmation flag is a string the caller supplies about itself, which for an agent that writes its own argv is theatre, and theatre that reads like a control is worse than nothing. All four answer 204, so meshadm emits its own receipt (deleted, kind, name, app, env, status) rather than letting --output json print null. See internal/cmd/deletion.go.

argo sync replaces a technique agents are already using. Syncing an Argo Application by kubectl patching .operation onto the CR works, and that is the problem: Argo does not authenticate a direct CR write, so initiatedBy.username there is self-declared and unverifiable. Prod syncs on this cluster are attributed to a claude-code account that exists in neither argocd-cm nor argocd-rbac-cm — the manual sync policy on those Applications is a deliberate human gate, and it held only where an agent's own instructions happened to forbid mutating kubectl. meshadm argo sync routes the same write through the console, where the caller is authenticated, checked for APP_ARGO_MANAGE on that app, attributed from the token, and audited. It also cannot name an Application: you pass an app-environment, and the console derives the names from your app's own registration rows, so one app can never sync another's — or the control plane's own. It syncs mesh-core first and refuses to touch the app while core is unhealthy, because agents re-register against the mesh registry on startup. See meshadm man argo.

Still unimplemented: app create (platform-realm and human by design — it is the trust anchor, see docs/MESHADM-DESIGN.md §3), any data * deprovision, and any delete for components, users or OIDC clients.

Reveal-once secrets go to a file, never stdout

A handful of console endpoints return material that exists exactly once and that no read endpoint can give back. meshadm handles all of them the same way, and the mechanism lives in one file (internal/cmd/secret.go):

  • The secret is written to a 0600 file as a JSON object — --secret-file <path>, or ~/.meshadm/secrets/<app>-<env>-<what>.secret.json by default, in a 0700 directory. That default is pinned to $HOME: never the working directory (a git checkout, one ignore rule from a commit), never a repo-local .meshadm/ (config.yaml follows the walk-up; reveal-once secrets do not), never /tmp (predictable-by-name on a shared machine). --secret-file is the only thing that moves it. meshadm man irreversible records why plaintext-0600-in-$HOME is the bar and where its limit is.
  • stdout carries a receipt, through the normal --output json|yaml|table path: what was created, the file, and per secret a sha256 prefix plus byte length — never the value.
  • In --output json the secret field is redacted with a marker, not omitted: "clientSecret": "***REDACTED-BY-MESHADM***". An absent field means the response carried no secret (a PKCE client has none); the marker means there is one and it is in the file. An agent must be able to tell those apart.
  • --show-secret is the explicit opt-in that puts values on stdout, and it warns on stderr. The file is written either way.
  • meshadm refuses to overwrite an existing secret file (O_EXCL, exit 5) — that file may be the only copy of an earlier reveal. --overwrite-secret-file says otherwise.
  • Multiple secrets go into one file. Redis returns a data-node password and a sentinel-discovery password; they are one credential in two halves, useless apart.

Why a file. gcloud iam service-accounts keys create --key-file= writes the key to a path and never prints it; AWS prints access keys to stdout and has been criticised for it for a decade. The difference matters more for an agent than for a person: an agent's stdout is logged, echoed into transcripts and pasted into issues by tooling that has no idea one field of that JSON was the only copy of a password.

Wired today — seven verbs, one mechanism:

| Verb | Secret fields | Default file (in ~/.meshadm/secrets/) | |------|---------------|--------------| | auth bootstrap | clientSecret | <app>-<env>-service-principal.secret.json | | oidc-client create | clientSecret | <app>-<env>-<client>.secret.json | | data postgres provision | password | <app>-<env>-postgres.secret.json | | data redis provision | password, sentinelPassword | <app>-<env>-redis.secret.json | | data minio provision | secretKey | <app>-<env>-minio.secret.json | | harbor provision | ciRobotSecret, pullRobotSecret | <app>-harbor.secret.json | | onboarding-token mint | token | <app>-onboarding-token.secret.json |

Inheriting it unchanged when it lands: app create (clientSecret, operator-only).

data postgres provision can be refused by the connection budget. max_connections is one number shared across every tenant on the cluster, and CONNECTION LIMIT is a per-role cap rather than a reservation — so over-committing does not fail at provision time, it fails later at CONNECT time for whoever asks last. The console meters it and answers 409 with error: connection_budget_exceeded. meshadm gives that its own reason rather than flattening it into a generic conflict, and keeps the numbers: table format prints committed=380 requested=50 max_connections=400 remaining=20, and --output json carries them under error.details as numbers.

data minio provision mints a bucket-scoped key, and its secretKey is never stored. The bucket is t-<data-slug> (hyphens preserved — an S3 bucket name has to survive being a DNS label, so it is not Postgres's t_<slug_>), and the key's policy names that one bucket and omits s3:ListAllMyBuckets, so it cannot even enumerate what it may not read. MinIO's ROOT credentials stay on the platform side and are never returned. Because the secret key is generated at provision time and kept nowhere, the 0600 file meshadm writes is the only copy that will exist — so a failure to write that file fails the command, and re-running the provision mints a new key rather than re-revealing the old one. data minio provision accepts APP_DATA_PROVISION, which is what lets an app's own service principal call it. Deprovision is deliberately not a meshadm verb: its endpoint is gated on APP_CREATE/TENANT_CREATE only, so an app may create its own storage and may not destroy it.

Every whole-document write has its read. env config set and manifest apply both REPLACE the whole document, so env config describe and manifest download exist to make read-modify-write possible — without them the only available move is to overwrite what you cannot see. The read's output is the write's input, unedited:

meshadm env config describe --output yaml > config.yaml   # edit it
meshadm env config set --file config.yaml

meshadm manifest download --file manifest.yaml            # edit it
meshadm manifest apply --file manifest.yaml --dry-run

The manifest read is download, not describe, because the console returns a YAML DOCUMENT with comments (application/x-yaml) that apply sends back verbatim — so it takes the same non-JSON path as theme download / bundle download: the document goes to a file, a receipt (path, size, sha256) goes through the renderer, and --file - streams it for piping. describe is an alias.

Scope is named with --app and --env on every noun, and both default to the active context. A positional argument is always the object's own identity — role describe <name>, user describe <userId>, idp set google — never the app.

How a write takes its input, decided by the API's own request schema and not per verb:

| Body the API declares | How meshadm takes it | Verbs | |-----------------------|----------------------|-------| | a document | -f/--file <path>, JSON or YAML, - reads stdin | role create/update, app config set, env config set, route set, branding set, oidc-client create | | raw bytes | -f/--file, sent verbatim | manifest apply (YAML body), email-template set and theme upload (multipart zip) | | scalars | flags | component declare/update, idp set --enabled, idp set-registration, idp set-password, env set-public-url, env set-data-services, oidc-client import, argo register, harbor provision, data postgres/redis provision |

A key the API does not declare is rejected before the request, because the console ignores unknown properties — a typo'd field would otherwise report success having applied nothing. --dry-run exists only on manifest apply, where the API implements it; no other verb offers one, because faking it client-side would be a CLI-side guarantee the platform never made.

cd tools/meshadm
go build -o meshadm .
./meshadm man
./meshadm app list --output json
./meshadm role list --app happyfeet --env dev --output json

The sealing certificate is the opposite kind of secret

meshadm secrets pubkey prints the cluster's sealed-secrets public certificate. It sits next to the reveal-once machinery above and is its mirror image, which is why the noun is secrets and not part of it: reveal-once handles a credential nobody may see twice, and this handles a certificate everybody may see forever. Nothing here reads a secret value.

meshadm secrets pubkey > sealed-secrets-pub.pem
kubeseal --cert sealed-secrets-pub.pem < secret.yaml > sealed.yaml

The default output is the raw PEM on stdout, so > and a pipe both work; --output json|yaml puts it inside a document with its size and sha256 instead, which is meshadm man's rule rather than a new one. --file <path> writes it and prints a receipt, and refuses to overwrite an existing file (exit 5, --overwrite says otherwise) — not because the bytes are precious, but because that path usually sits in a working tree beside secrets sealed with a particular certificate. It is the only file meshadm writes 0644: everything else it produces is 0600 because it holds a credential, and marking a public key the same way would say "secret" about the one artifact that is not.

There is no --app and no --env, on purpose. One sealed-secrets-controller in kube-system serves every namespace on the cluster, so there is one certificate and it unseals everywhere; a per-environment flavour would have to return a cert some controller does not hold the private half of. The certificate is fetched live on every call — the controller rotates roughly monthly and keeps the old private keys, so anything already sealed keeps working, but the copy in an onboarding bundle you downloaded in July is not the one to seal with today. That staleness is the reason this command exists.

A failed fetch is never a success. The console answers 503 with error: sealed_secrets_cert_unavailable rather than 200 with an empty body, and meshadm exits 9 with a clean stdout. Sealing against an empty or non-PEM input produces a SealedSecret that no controller can open, kubeseal accepts it, Argo syncs it, and the first sign of trouble is a Deployment waiting on a Secret that will never arrive. meshadm man irreversible has the rest.

The API client is generated

internal/api/openapi.gen.go is generated by oapi-codegen from api/openapi.json at the repo root — the spec springdoc emits from the console's own controllers. Do not edit it, and do not hand-write a path or a DTO beside it. A hand-maintained client is a second source of truth about the wire format; generated, a controller change the CLI does not track fails to compile. See docs/MESHADM-DESIGN.md §6.

Regenerating, after a controller or DTO change:

./mvnw -pl apps/auth-manager -am -B verify      # emits target/openapi.json
scripts/ci/verify-openapi-snapshot.sh --write   # refreshes api/openapi.json
cd tools/meshadm && go generate ./...           # regenerates the client

Commit all three. scripts/ci/verify-openapi-snapshot.sh runs in CI (in the jvm job, which already emits the spec) and fails when the committed snapshot drifts from what the console emits — so an API change shows up as a reviewable spec diff in the PR that causes it. The generated code is committed too, per Go convention: go build must work with no generator, no JDK and no Postgres.

Everything hand-written in internal/api/ is in client.go, and it is deliberately small: construction, bearer-token injection from internal/auth's cache, and pulling the console's own message out of an error body. None of it knows a path or a field name.

What it is

meshadm is the CLI for the MCP Mesh platform. It is a thin client over the existing console API — the same bundle / adopt / provision / reveal operations the console already exposes — and nothing more.

It is explicitly not a second control plane. The console owns the model of apps, envs and realms; a CLI that grows its own opinions about that model becomes a split brain.

Correction: device-code was never the blocking prerequisite

An earlier version of this file claimed "there is no non-browser token flow … meshadm needs device-code or PKCE against the platform realm before it can call a single endpoint." That was wrong on both counts, and docs/MESHADM-DESIGN.md §2.4 records the correction:

  1. The console is a plain multi-issuer OAuth2 resource server and accepts a raw Authorization: Bearer <jwt>. The cookie-to-Bearer translation happens at the edge, not in the API.
  2. The platform realm is the wrong target anyway. An agent's credential must be issued by the app's own realm — a platform-realm token carrying APP_* passes the console's app-scoped check for every app.

So the flow is client_credentials against the app's realm, which is what meshadm auth login does. Device-code is human ergonomics and ships later.

The real prerequisite was console-side: materializing the app-level permission catalog into app-env realms and minting a per-app service principal there. That shipped — it is what meshadm auth bootstrap spends an onboarding token on.

auth status and auth whoami are different questions

auth is the one noun outside the table above, and its two identity verbs are easy to confuse:

| Verb | Where the answer comes from | What it tells you | |------|-----------------------------|-------------------| | auth status | local, no network call — the cached token, decoded | issuer, realm, subject, expiry, and the resource_access.usermanagement.roles claim verbatim. Answers with the control plane unreachable, and is still the first thing to run when a call 403s | | auth whoami | GET /api/v1/me — the console | context, realm, user, isPlatformAdmin/isTenantAdmin, tenant, and the permissions the console recognises |

The lists differ on purpose. The console filters your token's claims to the permissions it knows about, so a name in auth status but missing from auth whoami is one it does not recognise — a grant that looks present and grants nothing, which is invisible to a local decode. Neither list is scoped: both say a permission is held somewhere, not that it resolves on the app you addressed.

Breaking, as of this release: whoami used to be an alias of auth status. It is its own command now and it makes a network call; the alias is gone. A script that ran meshadm auth whoami for an offline decode wants meshadm auth status, which is unchanged.

Where it lives

tools/meshadm/ in mcp-mesh-org/mesh-control-plane, not its own repo — it tracks the console API, so it has to move in lockstep with it. See docs/REPO-RENAME-PLAN.md §2.

License

Apache-2.0.