NOTE: This README and accompanying documentation is still a work in progress and is incomplete.
main:
develop:
Run npm install to install all dependencies. It is also important that you install the following tools:
This project's workspace is designed to run in a Unix-like environment. Development in a Windows environment is not tested/supported.
Make sure you create a .env.local file. The npm postinstall setup process should take care of this for you, but if you get an error then create it.
Run nx build demo to build the project. The build artifacts will be stored in the dist/ directory. Use the --prod flag for a production build.
Run nx affected:test to execute the unit tests affected by a change.
Unit tests that require firebase are run through the Docker container. This lets our tests access the emulator. Since all tests are run within the container, and the container is not configured to use service ports, it can be run at the same time as the demo.
Run nx watch firebase to execute the unit tests and watch for changes.
Run nx test firebase to execute the unit tests.
This library has a working demo frontend and backend attached to it.
There are two development servers: the Angular context, and the Firebase emulator context.
The angular development server can run directly on your machine. Run ./serve-web.sh to start the angular development server and start the demo in your browser.
It will talk with the firebase development server by default.
The firebase emulator is run within a docker container. This is to allow consistent configuration described in the Dockerfile.
Run ./serve-server.sh to start the firebase development server.
demo-api'swatch-emulatorsnx target uses entr to watch for changes in the demo-api's dist directory and restart the emulators automatically. The firebase emulator suite does not support hot-reload of functions.- Firebase rules files are hot-reloaded by the emulators.
demo-api exposes an MCP (Model Context Protocol) endpoint backed by its call-model tools. The repo's .mcp.json registers it for Claude Code as demo-api-mcp-dev:
"demo-api-mcp-dev": {
"type": "http",
"url": "http://localhost:9901/mcp"
}Both development servers must be running. The endpoint itself is served by the Firebase hosting emulator on :9901, but it is guarded by the app's OIDC provider, whose issuer is derived from appUrl (http://localhost:9010) — so the browser login leg is served by the Angular dev server:
./serve-server.sh— the emulator suite (hosting on:9901, functions on:9902)../serve-web.sh— the Angular dev server on:9010, which hosts the OIDC issuer at/oidc.
Then run /mcp in Claude Code, select demo-api-mcp-dev, and choose Authenticate. A browser window opens for the OIDC login; the client registers itself dynamically (DCR is enabled outside production).
A dynamically registered client requests the protected-resource document's scopes_supported verbatim, and its base list — OidcProviderConfigService.clientRequestableScopesSupported — drops the scopes an admin unlocks per-client by assigning an OIDC provider profile (lms, reports, token.cli in the demo). A resource may still advertise one deliberately, as demo-api does for token.cli: the consent URL builder withholds an assignment-only scope from any client whose profiles do not unlock it, so the scope lands in the consent submit's rejected set and the flow completes without it, exactly as an admin-only scope does for a non-admin. The unlock gate then judges what was consented to rather than what was requested, and stays as defense in depth.
So advertising a gated scope is safe for every client, and the switch that decides who can actually obtain it is the profile assignment (dbx_provider_profiles), not the document. A connector that should mint CLI credentials needs the cli-handoff profile assigned to its client and an admin user — advertising token.cli alone grants nothing.
The URL must be an origin where the app is served at the root. OAuth discovery (RFC 9728) probes /.well-known/oauth-protected-resource at the origin, and the functions emulator only routes /<project>/<region>/<function>/… — an app mounted under that prefix cannot answer at the root. A client pointed straight at http://localhost:9902/dereekb-components/us-central1/api/mcp can only discover its authorization server from the WWW-Authenticate header on a 401, which means it works only if the emulator happened to be running the first time the client connected. If it was not, the client caches a failed state and Authenticate never recovers.
firebase.json rewrites /mcp and /mcp/** to the api function so the hosting emulator (and production hosting) reach it. apps/demo-api/src/environments/environment.ts's appMcpUrl must match the .mcp.json URL byte-for-byte — it is the source for the protected-resource resource, the RFC 8707 resourceServers key, and the token audience.
Note that appMcpUrl is compiled into dist/apps/demo-api/main.js, so changing it requires a rebuild and an emulator restart.
TODO
TODO: move to contributing guide
This library uses https://github.com/jscutlery/semver to maintain versions. All versions are synchronized/shared between all sub-libraries.
Commits made should follow the following conventions:
https://www.conventionalcommits.org/en/v1.0.0/
The workspace formats with oxfmt rather than prettier, and lints with two tiers — oxlint and ESLint — which are additive rather than alternatives. The v13 to v14 upgrade notes cover why each was adopted and how to adopt them downstream; this section is how to run them here.
| Command | Runs |
|---|---|
npm run format (npx nx run workspace:format) |
oxfmt --write . |
npm run format-check (npx nx run workspace:format-check) |
oxfmt --check . |
- Config lives in
.oxfmtrc.json. ItsignorePatternsreplaces.prettierignore— there is no separate ignore file, and the patterns do not apply to oxlint, which reads its own config. nx format/nx format:write/nx format:checkdo not work on the currently pinned Nx (23.1.3): that version'sformatcommand imports prettier unconditionally and fails withPrettier is not installed.. Use the commands above. Nx does support oxfmt in later versions, selected by detection — a root oxfmt config file wins — so.oxfmtrc.jsonalready makes the workspace resolve to oxfmt once Nx is upgraded.- Staged files are formatted automatically by the husky
pre-commithook. - Suppress formatting for one statement with
// oxfmt-ignore(oxfmt also still honors// prettier-ignore). - ESLint does not depend on
eslint-config-prettier. The two rules that conflict with formatter output (no-unexpected-multiline,no-extra-semi) are disabled explicitly at the end ofeslint.config.mjs.
| Tier | Engine | Target | Owns |
|---|---|---|---|
| Fast | oxlint | oxlint (inferred by @nx/oxlint) |
the correctness category on .ts/.tsx/.js/.mjs/.cjs |
| Deep | ESLint | lint (explicit, 90 project.json files) |
everything else: the 5 in-repo plugins, type-aware rules, .html templates, {package,project}.json, jsdoc/sonarjs/unicorn |
Config: .oxlintrc.json (root) and eslint.config.mjs + eslint.config.angular.mjs + eslint.config.library.mjs.
| Command | Runs |
|---|---|
npx nx run workspace:oxlint-all |
oxlint over every project (~2.5 s) |
npx nx run workspace:lint-all |
ESLint over every project (~30 s warm, ~110 s cold) |
npx nx run workspace:oxlint-cache / workspace:lint-fix-cache |
the same two through dbx-cli-lint-cache, which writes .tmp/lint-cache/ — <project>.json + index.json for ESLint, <project>.oxlint.json + index.oxlint.json for oxlint |
The run immediately after any edit to a root ESLint config costs ~1 m 50 s instead of ~25 s, because Nx re-runs plugin inference. Discard one settle run before timing anything, or a no-op change reads as a 4x regression.
dbx-claude-commit blocks on both tiers with no configuration: its gate is a single lint run, so an error-severity finding from either engine on a changed file fails it identically. The workspace is at 0 oxlint errors, so this starts green and costs ~2.5 s.
Things that are load-bearing and easy to get wrong:
- The tier boundary is drawn in
.oxlintrc.json, where every rule ESLint also runs is explicitly"off". A missed disable on the oxlint side is a duplicate report (visible); a missed disable on the ESLint side would be a coverage hole (invisible). oxlint hard-errors on an unknown rule name, so that disable list cannot silently rot. - Only the
correctnesscategory is enabled. Measured 2026-09-03: addingsuspiciousyields 4,237 findings that are ~90% conflicts with deliberate workspace conventions — 3,066no-underscore-dangle(this workspace prefixes intentionally unused bindings with_), 578no-shadow, 300no-extraneous-class(every Angular/NestJS module). Do not enable it without re-measuring. - Never pass
--silentto oxlint. It suppresses the diagnostics inside--format=jsonwhile still reporting the scanned-file count, so a broken run is indistinguishable from a clean one. - Type-aware oxlint rules (
--type-aware/oxlint-tsgolint) and thejsPluginsbridge for the in-repo ESLint plugins are deliberately off. Three of the type-aware first-party rules go silently green underjsPluginsrather than erroring, and Angular template rules cannot move at all — oxlint has no.htmlsupport and no processor concept. - The five in-repo
dereekb-*plugins stay on ESLint entirely. Porting them is not pending; it was measured and rejected. 34 of their 56 rule names do run unmodified underjsPlugins(byte-identical message andline:col), but moving them changeslint-allby less than the ±1.5 s run-to-run noise: all 45 active first-party rules cost 101.6 ms of a 2.65 s rule budget onpackages/date, while ~58% of a lint invocation is startup plus TypeScript program construction and five type-aware/import-graph rules are 75% of rule time.jsPluginsis also still alpha as of oxlint 1.81.0. Do not re-open without re-measuring.