From 7a441a7aa18111e9a98e616e2898b5aec8f3607b Mon Sep 17 00:00:00 2001 From: Yoichiro Hara Date: Mon, 27 Jul 2026 06:54:53 +0900 Subject: [PATCH 1/3] docs: cover oauth-proxy and extract CLI template notes CLAUDE.md had not caught up with `meter oauth-proxy` or the three scaffolding template trees, and it described `verify` more narrowly than it runs. Add the missing surface, then move the template internals to docs/CLI_TEMPLATES.md so the always-loaded file keeps only what most sessions need. The one template fact that stays inline is the trap: nothing under templates/cloudflare-oauth/ is compiled or executed by `npm run verify`, because it sits outside typecheck:templates and needs dependencies this repo does not install. Its proxy.test.ts never runs here either. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 12 ++++++-- docs/CLI_TEMPLATES.md | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 docs/CLI_TEMPLATES.md diff --git a/CLAUDE.md b/CLAUDE.md index 5ad43fb..417990f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,8 +29,12 @@ Scope any script to one package with `-w`, e.g. `npm test -w @meter-mcp/sdk`. There is no ESLint/Prettier config; `npm run lint:packages` lints *package metadata* (publint + are-the-types-wrong), not source style. +`npm run typecheck` is wider than the four packages: it also compiles `examples/meter-sdk` and the CLI scaffolding templates (`packages/cli/templates/tsconfig.json`). Editing an example or a template can break the gate. + `scripts/verify-sdk-packages.mjs` is the strictest part of `verify`: it packs real tarballs, installs them into a clean consumer, and imports them from ESM, CommonJS, TS-ESM, and TS-CJS. Export-map or `.d.cts` regressions surface here, not in `npm test`. +CI (`.github/workflows/ci.yml`) runs `npm run verify` on Node 20, 22, and 24, plus a full-history gitleaks scan. + ## Architecture Four npm workspaces under `packages/`, published together at a **single shared version** (Changesets `fixed` group; `scripts/verify-sdk-release.mjs` fails the release if versions diverge): @@ -38,7 +42,9 @@ Four npm workspaces under `packages/`, published together at a **single shared v - `@meter-mcp/sdk` — zero-dependency HTTP client (`MeterPublicApiClient`), AI-cost math, webhook signature verification. Everything else depends on it. - `@meter-mcp/mcp` — `paidTool` / `registerPaidTool`, which wrap an MCP tool handler in billing and translate a `402`-style error body into an MCP error result. - `@meter-mcp/adapters` — provider usage extractors (`aiUsageFromOpenAI` / `aiUsageFromAnthropic`) and Web-`Request`/`Response` handlers for buyer-portal and operator-console sessions, plus an Express bridge. -- `@meter-mcp/cli` — the `meter` bin for provider developers: `login` / `init` (scaffolds an embedded-metering MCP server from `packages/cli/templates`, which ship in the tarball) / resource CRUD / `usage` + `events tail` / `webhooks` + `listen` (poll-mode webhook forwarding with client-side HMAC signing) / `call`. Commands are pure `run*(ctx, …)` functions in `src/commands/`; `src/cli.ts` only wires commander. Three wiring bugs unit tests cannot catch live in commander/bundle land — bin symlink vs `import.meta.url` realpath, global-vs-subcommand option shadowing, bundled-`dist` resource paths — which is why `test/init.test.ts` spawns the **built** `dist/cli.js` end-to-end; keep that test alive. +- `@meter-mcp/cli` — the `meter` bin for provider developers: `login` / `init` / `oauth-proxy` / resource CRUD / `usage` + `events tail` / `webhooks` + `listen` (poll-mode webhook forwarding with client-side HMAC signing) / `call`. Commands are pure `run*(ctx, …)` functions in `src/commands/`; `src/cli.ts` only wires commander. Three wiring bugs unit tests cannot catch live in commander/bundle land — bin symlink vs `import.meta.url` realpath, global-vs-subcommand option shadowing, bundled-`dist` resource paths — which is why `test/init.test.ts` spawns the **built** `dist/cli.js` end-to-end; keep that test alive. + +`packages/cli/templates/` holds three scaffolding trees (`init` Node, `init --target cloudflare`, `oauth-proxy`). Nothing under `templates/cloudflare-oauth/` is compiled or run by `npm run verify` — see [docs/CLI_TEMPLATES.md](./docs/CLI_TEMPLATES.md) before editing any of them. ### The billing lifecycle @@ -59,7 +65,7 @@ The core of the SDK is the three-phase call in `packages/sdk/src/client.ts`: ## Conventions -- ESM source (`.ts`, NodeNext), `"type": "module"`, relative imports carry the `.js` extension. +- ESM source (`.ts`, NodeNext), `"type": "module"`, relative imports carry the `.js` extension. The one exception is `templates/cloudflare-oauth/`, which is bundler-resolved and extensionless on purpose. - Node >= 20; the SDK relies on global `fetch`, `crypto.randomUUID`, and `crypto.subtle` rather than any runtime dependency. Keep `@meter-mcp/sdk` dependency-free. - New public API needs: an entry in `docs/SDK_API.md`, a test, and a changeset. -- Releases are tag-driven (`sdk-v`) and publish via GitHub OIDC trusted publishing — see `docs/SDK_RELEASE.md`. Never overwrite a published version. +- Releases are tag-driven (`sdk-v`) and publish via GitHub OIDC trusted publishing — see `docs/SDK_RELEASE.md`. Never overwrite a published version. `scripts/verify-sdk-release.mjs` runs in `sdk-publish.yml` (not in `verify`) and hard-fails unless all four versions match, the tag is exactly `sdk-v`, and every manifest carries the `repository.url` and `publishConfig` that trusted publishing is bound to — so renaming the repo or touching `publishConfig` breaks publishing, not CI. diff --git a/docs/CLI_TEMPLATES.md b/docs/CLI_TEMPLATES.md new file mode 100644 index 0000000..c56bdd3 --- /dev/null +++ b/docs/CLI_TEMPLATES.md @@ -0,0 +1,64 @@ +# CLI scaffolding templates + +Maintainer notes for `packages/cli/templates/`, the source trees `meter init` and +`meter oauth-proxy` copy into a user's new project. For the user-facing command +reference see the [CLI README](../packages/cli/README.md). + +## Layout + +`packages/cli/templates/` ships inside the published tarball (it is listed in the +CLI's `files` array) and holds three independent trees: + +| Tree | Scaffolded by | Runtime | +| --- | --- | --- | +| `src/` + root `*.tmpl` | `meter init` (default) | Node.js | +| `cloudflare/` | `meter init --target cloudflare` | Workers | +| `cloudflare-oauth/` | `meter oauth-proxy` | Workers | + +## `oauth-proxy` is not an `init` variant + +`meter init` creates a service and runs onboarding. `meter oauth-proxy` does +neither: it scaffolds a generic Workers OAuth front for an MCP endpoint the +provider already runs, so OAuth-only MCP clients can connect to it. + +The generated Worker source is target-agnostic and is copied verbatim. The target +is supplied at deploy time through Wrangler vars, which the CLI writes into +`wrangler.jsonc`: + +```text +BACKEND_BASE_URL the existing service's origin +MCP_PATH the MCP route on that origin (default /api/mcp) +BUYER_HEADER header carrying the buyer token (default x-meter-buyer-token) +SERVICE_NAME display name on the consent screen +``` + +`test/oauth-proxy.test.ts` asserts the target never leaks into the copied source, +which is what keeps that separation honest. + +## Rendering + +Scaffolding is `__TOKEN__` string replacement over the `.tmpl` files, not a +template engine. Files outside `*.tmpl` are copied byte-for-byte. + +Both `runInit` and `runOAuthProxy` locate the templates with a `packageRoot()` +walk-up to the nearest `package.json`. That is what makes the path resolve +identically under `tsx`, under the tsup-bundled `dist/cli.js`, and from an +installed `node_modules/@meter-mcp/cli`. Do not replace it with a path relative +to `import.meta.url`. + +## What the gate does and does not cover + +`npm run typecheck:templates` compiles the Node and `cloudflare/` trees through +`packages/cli/templates/tsconfig.json`. + +`cloudflare-oauth/` is deliberately outside that tsconfig. It needs +`@cloudflare/workers-oauth-provider`, `@cloudflare/workers-types`, and vitest, +none of which this repository installs, and it uses bundler-style extensionless +imports that the repo's NodeNext resolution would reject. + +The practical consequence: **nothing in `cloudflare-oauth/src/` is compiled or +executed by `npm run verify`.** Its `proxy.test.ts` does not run here either. The +only coverage is the scaffolding assertions in `packages/cli/test/oauth-proxy.test.ts`, +which check that files are emitted and tokens expanded, not that the code is +sound. After editing those files, typecheck them from a generated project against +its own `tsconfig.json`. From ded0c55fa5823a63f9a8cf391cf224da22ad3947 Mon Sep 17 00:00:00 2001 From: Yoichiro Hara Date: Mon, 27 Jul 2026 06:55:02 +0900 Subject: [PATCH 2/3] docs: fix stale repo slug, npm scope, and package list in release runbook The runbook named `masterleopold/meter-sdk`, but the remote and the `repository.url` that verify-sdk-release.mjs asserts are both `VoxTechnologies/meter-sdk`. Running the `npm trust --repo` commands as written would have bound trusted publishing to the wrong repository. It also predated @meter-mcp/cli: the package list, bootstrap publishes, trust registrations, publish order, and the "all three packages" check all omitted it, and the scope was written as `@meter` rather than `@meter-mcp`. Describe what the publish workflow actually enforces (version parity, exact `sdk-v` tag, publishConfig/repository, then the full verify gate) in place of guarantees it does not make, and record the public-repo preflight and the skip-if-already-published re-run safety. Co-Authored-By: Claude Opus 5 (1M context) --- docs/SDK_RELEASE.md | 48 +++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/docs/SDK_RELEASE.md b/docs/SDK_RELEASE.md index a89f95d..e61188b 100644 --- a/docs/SDK_RELEASE.md +++ b/docs/SDK_RELEASE.md @@ -1,23 +1,31 @@ # SDK release runbook -Meter publishes `@meter-mcp/sdk`, `@meter-mcp/mcp`, and `@meter-mcp/adapters` together from -the public `masterleopold/meter-sdk` repository. Meter's service implementation -remains in a separate private repository. +Meter publishes `@meter-mcp/sdk`, `@meter-mcp/mcp`, `@meter-mcp/adapters`, and +`@meter-mcp/cli` together from the public `VoxTechnologies/meter-sdk` repository. +Meter's service implementation remains in a separate private repository. + +That repository slug is not cosmetic: `scripts/verify-sdk-release.mjs` +asserts that every manifest's `repository.url` is +`git+https://github.com/VoxTechnologies/meter-sdk.git`, and npm binds trusted +publishing to that same slug. Renaming or forking the repository breaks +publishing even though CI stays green. ## One-time npm setup 1. Confirm `gitleaks git .` and the GitHub secret-history job complete with zero findings. -2. Sign in to npm with an account that can publish the `@meter` scope. +2. Sign in to npm with an account that can publish the `@meter-mcp` scope. 3. Confirm `npm whoami` and `npm access ls-packages` succeed. 4. Bootstrap each package at `0.1.0` using `npm publish --access public` from its package directory. 5. Configure a trusted GitHub publisher for each package: - - repository: `masterleopold/meter-sdk` + - repository: `VoxTechnologies/meter-sdk` - workflow: `sdk-publish.yml` - environment: `npm` - allowed action: `npm publish` 6. Require approval for the GitHub `npm` environment and disallow token-based publishing on npm. +7. Keep the repository public. The publish workflow's preflight job fails on a + private repository, because npm provenance requires public source. -Bootstrap commands, after creating or joining the `@meter` npm organization: +Bootstrap commands, after creating or joining the `@meter-mcp` npm organization: ```bash npm login @@ -26,16 +34,18 @@ npm run verify npm publish --workspace @meter-mcp/sdk --access public npm publish --workspace @meter-mcp/mcp --access public npm publish --workspace @meter-mcp/adapters --access public +npm publish --workspace @meter-mcp/cli --access public ``` -Then install npm 11.5.1 or newer and register the workflow as the trusted -publisher for each package: +Then install npm 11.11.0 or newer, matching the version the publish workflow +pins, and register the workflow as the trusted publisher for each package: ```bash npm install --global npm@latest -npm trust github @meter-mcp/sdk --file sdk-publish.yml --repo masterleopold/meter-sdk --env npm --allow-publish --yes -npm trust github @meter-mcp/mcp --file sdk-publish.yml --repo masterleopold/meter-sdk --env npm --allow-publish --yes -npm trust github @meter-mcp/adapters --file sdk-publish.yml --repo masterleopold/meter-sdk --env npm --allow-publish --yes +npm trust github @meter-mcp/sdk --file sdk-publish.yml --repo VoxTechnologies/meter-sdk --env npm --allow-publish --yes +npm trust github @meter-mcp/mcp --file sdk-publish.yml --repo VoxTechnologies/meter-sdk --env npm --allow-publish --yes +npm trust github @meter-mcp/adapters --file sdk-publish.yml --repo VoxTechnologies/meter-sdk --env npm --allow-publish --yes +npm trust github @meter-mcp/cli --file sdk-publish.yml --repo VoxTechnologies/meter-sdk --env npm --allow-publish --yes ``` The bootstrap publish is the only step that needs interactive npm credentials. @@ -48,14 +58,22 @@ Subsequent releases use GitHub OIDC and do not use a long-lived npm token. 3. Run `npm run verify` from a clean checkout. 4. Create and push the matching tag, for example `sdk-v0.2.0`. 5. Approve the protected `npm` environment deployment. -6. Confirm all three packages have the expected version, provenance, README, license, and repository link on npm. +6. Confirm all four packages have the expected version, provenance, README, license, and repository link on npm. -The publish workflow rejects mismatched package versions, dirty generated -output, failed tests, malformed tarballs, and tags that do not match the SDK -version. Packages publish in dependency order: SDK, MCP, adapters. +Before anything is published, the workflow runs `scripts/verify-sdk-release.mjs`, +which rejects package versions that have diverged from each other, a tag that is +not exactly `sdk-v`, and any manifest missing the `publishConfig` or +`repository.url` that trusted publishing is bound to. It then runs the full +`npm run verify` gate: build, tests, typecheck, package-metadata lint, OpenAPI +check, packed-consumer install, and `npm audit`. Packages publish in dependency +order: SDK, MCP, adapters, CLI. ## Recovery Never overwrite a published version. Fix the issue, add a changeset, and publish a new patch version. Use npm deprecation messages for a defective version instead of unpublishing it unless npm security policy requires removal. + +Re-running the workflow after a partial failure is safe: each publish step +queries the registry first and skips any version already there, so only the +packages that did not make it are published. From 340f6ba0577f676060c61746aadc1a49e7dde222 Mon Sep 17 00:00:00 2001 From: masterleopold Date: Sun, 26 Jul 2026 22:00:33 +0000 Subject: [PATCH 3/3] chore: override @hono/node-server and fast-uri to clear npm audit The npm audit --audit-level=low gate in npm run verify (audit:release) started failing on three advisories in the dev-only @modelcontextprotocol/sdk@1.29.0 tree: - GHSA-frvp-7c67-39w9 (moderate) in @hono/node-server <2.0.5 - GHSA-v2hh-gcrm-f6hx (high) in fast-uri 3.0.0 - 3.1.3 (via ajv) 1.29.0 is the latest sdk and still pins the vulnerable ranges, so a bump can't fix it. Pin the transitive deps to patched versions via overrides (matching the existing esbuild override). These packages are only used to typecheck the CLI templates and ship in no published package; verify passes end-to-end including typecheck:templates. Co-authored-by: Codesmith --- package-lock.json | 14 +++++++------- package.json | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 29a3ed0..fc21736 100644 --- a/package-lock.json +++ b/package-lock.json @@ -800,13 +800,13 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -2308,9 +2308,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 04a40c5..831a584 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "zod": "^3.25.76" }, "overrides": { - "tsup": { "esbuild": "^0.28.1" } + "tsup": { "esbuild": "^0.28.1" }, + "@hono/node-server": "^2.0.5", + "fast-uri": "^3.1.4" } }