Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ 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):

- `@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

Expand All @@ -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<version>`) and publish via GitHub OIDC trusted publishing — see `docs/SDK_RELEASE.md`. Never overwrite a published version.
- Releases are tag-driven (`sdk-v<version>`) 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<version>`, 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.
64 changes: 64 additions & 0 deletions docs/CLI_TEMPLATES.md
Original file line number Diff line number Diff line change
@@ -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`.
48 changes: 33 additions & 15 deletions docs/SDK_RELEASE.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.
Expand All @@ -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<version>`, 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.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}