Contributing
Root CONTRIBUTING.md is the canonical, GitHub-native reference (it's what GitHub links from the "new PR" banner and the repo's Community Standards). This page is the searchable, cross-linked version for readers already in the docs site — a superset that links out to Architecture/Configuration/Security rather than re-explaining them, so keep both in sync if you change either.
Before you start
Read Architecture → and Concepts & glossary → first — the request-path mental model (every policy enforced at dispatch, never as HTTP middleware) explains a lot of decisions you'll otherwise have to re-derive from the code. For anything beyond a small fix, open an issue to discuss the approach before writing code — it saves rework on both sides.
Dev environment setup
Prerequisites
- Bun
1.x— the gateway uses Bun's built-ins (bun:sqlite,Bun.dns,Bun.password) directly, so Node.js is not a substitute.
Clone, install, configure
git clone https://github.com/CarlxsMG/mcpbridge.git
cd mcpbridge
bun install
cp .env.example .env # then set BOOTSTRAP_ADMIN_PASSWORD (min 12 chars)
cd admin-ui && bun install && cd ..Set ADMIN_API_KEYS in .env too if you'll be scripting against /register or /admin-api directly rather than only using the UI — see Getting started → for the full first-run walkthrough (including the exact env vars each local option needs).
Run the full stack
bun run dev:all # backend :8790 + admin UI :8791, both with hot reloadRunning tests, typecheck & lint
| Command | What it runs |
|---|---|
bun run test | Backend test suite (src/**/__tests__/). Should be 100% green. |
bun run typecheck | Backend typecheck (tsc --noEmit) |
cd admin-ui && bun run test | Admin UI component/unit tests (Vitest) |
cd admin-ui && bun run typecheck | Admin UI typecheck (vue-tsc -b --noEmit) |
cd admin-ui && bun run build | Admin UI production build — catches a few things typecheck alone doesn't (e.g. unused Vite imports) |
bun run test:e2e | Playwright end-to-end (e2e/): smoke, MCP-protocol, auth-fail-closed |
bun run test:mutate | Stryker mutation testing (stryker.config.mjs) |
The bridge is covered by several test systems, not one: Bun's runner for the backend suite (330+ files under src/**/__tests__/), Vitest for the admin UI, Playwright for end-to-end, and Stryker mutation testing on top — which injects faults into the source and fails if the tests don't catch them, so coverage measures effectiveness, not just line execution. Mutation runs are much heavier than a normal test pass; scope them to the files you changed while iterating (see stryker.config.mjs).
The root-vs-package gotcha
Backend and admin UI are separate TypeScript projects with their own lint/typecheck scripts — running only the package you touched is not the same as the repo's actual CI gate. In particular, bun run format:check (Prettier) is a root-level script only — admin-ui's own package.json has no format/format:check of its own. It's easy to run cd admin-ui && bun run lint && bun run typecheck && bun run test && bun run build, see everything pass, and still have formatting drift that only the root check catches.
One command before you push
bun run checkRuns the full gate in order — format check → root lint → admin-ui lint → admin-ui i18n parity → root typecheck → root typecheck (tools) → root tests → admin-ui typecheck → admin-ui tests → admin-ui build — stopping at the first failure. This covers CI's test job; treat a clean bun run check as the bar for "ready to open a PR," not just a green package-scoped run. CI additionally requires a Playwright e2e job (needs: test) plus docs-build, docker-build, and helm-lint checks, and a Windows test leg on every push/PR — those aren't part of bun run check.
Code style & conventions
- No enforced personal style beyond ESLint + Prettier — match the formatting, naming, and file organization already present in the module you're editing.
- TypeScript is strict on both projects — don't work around type errors with
anyor non-null assertions unless there's no reasonable alternative; prefer narrowing/guards over casts. - Keep modules focused: security-sensitive logic lives under
src/security/, route handlers undersrc/routes/, DB access undersrc/db/. Admin UI components live underadmin-ui/src/components/{ui,charts,guard-editor,server-detail,layout}/by role. - Every policy is enforced at the dispatch point (
proxyToolCall), never as HTTP middleware — MCP multiplexes many tools over onePOST /mcproute, so the bridge has to know which tool is being called before it can apply per-tool rules. Don't add a new guard as Express middleware; wire it into the dispatch pipeline instead.
A known gotcha worth not rediscovering
Don't set display (flex/grid/etc.) directly on a <td> in an admin-ui table — it overrides the default table-cell display and visually breaks the row layout (column sizing and alignment collapse). Wrap the cell's content in a child <div> and apply the display-changing class to that instead; the <td> itself stays bare.
Database migrations
Schema changes live in src/db/migrations.ts, exported as an append-only, ordered array of { id, name, sql } objects.
- Never edit or renumber an existing migration. Once merged to
main, itsidandsqlare frozen — fix a mistake with a new migration that alters/repairs the schema. - Migrations are forward-only. There's no down-migration mechanism — write the SQL defensively (
CREATE TABLE IF NOT EXISTS, additiveALTER TABLE) and think about what happens to existing rows/databases when it runs. - IDs are sequential integers, applied in ascending order at startup, each inside its own transaction. Check the tail of
src/db/migrations.tsfor the current highestidbefore adding a new one. - Test a new migration locally against a throwaway DB (
DB_PATH=:memory:or a scratch file) before committing — see Deployment → for the upgrade/backup story this feeds into in production.
Branch, commit & PR conventions
Commits follow a type(scope): summary convention (the (scope) is optional):
feat:/feat(scope):— new functionalityfix:/fix(scope):— bug fixes, including hardening passesdocs:— documentation-only changeschore:— tooling/config changes with no runtime behavior changerefactor:— internal restructuring with no behavior changetest:— test-only additions/changes
Feat-then-harden. Larger changes frequently land as a feat commit followed by one or more fix commits that harden it (edge cases found on review) — rather than one giant diff, or folding hardening back into the feature commit after the fact.
Priority tiers. PR descriptions and hardening commits in this repo often use [P0]/[P1]/[P2] suffixes — P0 = correctness/security-critical, P1 = important robustness, P2 = polish. Not required, but consistent with the project's history if you want to signal urgency:
fix(admin-ui): stop the .field input recipe double-bordering child components' own inputs [P2]Keep PRs scoped to one logical change. Reference the issue you discussed the approach in, if there was one.
Demo fixture i18n
The public demo build (VITE_DEMO=true, served at https://<org>.github.io/<repo>/demo/) ships its own Spanish translations for every user-visible fixture string — tool descriptions, bundle summaries, API key labels, alert names, team/policy/composite names, snapshot labels, catalog descriptions, and the audit-log detail rewrites for the demo entries.
The translations live under demo.fixtures.* in admin-ui/src/locales/{en,es}.json; the runner is admin-ui/src/demo/resolve.ts, wired into demoFetch() so every demo response gets localized against the active vue-i18n locale.
When adding a new translatable string to a fixture
Add the literal English text to the fixture file alongside a sibling
*Keyfield:ts// admin-ui/src/demo/fixtures/tools.ts { name: "my_new_tool", method: "GET", endpoint: "/new", description: "My new tool's purpose", // EN fallback descriptionKey: demoKey("tools", "myclient.my_new_tool", "description"), ... }The runtime helper
demoKey/demoKeyByValue/demoDetailKeyfromadmin-ui/src/demo/i18n-keys.tsis the only way to compose fixture keys — it handles the vue-i18ndot-as-separator escape (entity IDs use__instead of.) and the bracket-notation escape for free-form values that contain spaces or parens.Add the English literal to
scripts/seed-demo-i18n.pyunder the matching domain — re-run the script to (re-)generate thedemo.fixtures.*keys inen.json. The script REPLACES (not merges) the demo section so the namespace stays canonical.Add the Spanish translation to
scripts/translate-demo-i18n.pyunder the matching domain — re-run the script to land the translation ines.json.Run
bun run check— thelint:i18nstage fails ifen.jsonandes.jsondrift, so a missing translation is caught in CI, not on the demo page.
When NOT to translate
- Tool names like
search_issues, client names likegithub, and any other string that flows as a URL/path identifier stays verbatim — these are real backend identifiers, not user-facing labels. - Backend error messages and JSON-RPC payloads stay English-only (matches the backend no-i18n contract documented in the repo).
- Usernames (
demo,ops-oncall,auditor) stay verbatim — usernames are identifiers.
PR checklist
- [ ]
bun run checkpasses (this is the actual CI gate — see the root-vs-package note above) - [ ] New/changed schema is a new, appended
src/db/migrations.tsentry, tested against a fresh DB — never an edit to an existing one - [ ] Docs updated if user-facing behavior, config, or API changed
- [ ] Commit messages follow the
type(scope): summaryconvention above - [ ] Screenshots included for any admin-ui visual change
- [ ] New translatable demo fixture strings have a
*Keyfield, an entry inscripts/seed-demo-i18n.py, and a Spanish entry inscripts/translate-demo-i18n.py
Next: Changelog → · Security policy →