[![txn2/mcp-data-platform](docs/images/MCP-data-platform-logo-banner.svg)](https://mcp-data-platform.txn2.com) [![GitHub license](https://img.shields.io/github/license/txn2/mcp-data-platform.svg)](https://github.com/txn2/mcp-data-platform/blob/main/LICENSE) [![Go Reference](https://pkg.go.dev/badge/github.com/txn2/mcp-data-platform.svg)](https://pkg.go.dev/github.com/txn2/mcp-data-platform) [![MCP](https://img.shields.io/badge/MCP-Model_Context_Protocol-blue)](https://modelcontextprotocol.io) [![Release](https://img.shields.io/github/v/release/txn2/mcp-data-platform)](https://github.com/txn2/mcp-data-platform/releases/latest) [![CI](https://github.com/txn2/mcp-data-platform/actions/workflows/ci.yml/badge.svg)](https://github.com/txn2/mcp-data-platform/actions/workflows/ci.yml) [![CodeQL](https://github.com/txn2/mcp-data-platform/actions/workflows/codeql.yml/badge.svg)](https://github.com/txn2/mcp-data-platform/actions/workflows/codeql.yml) [![codecov](https://codecov.io/gh/txn2/mcp-data-platform/graph/badge.svg)](https://codecov.io/gh/txn2/mcp-data-platform) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13548/badge)](https://www.bestpractices.dev/projects/13548) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/txn2/mcp-data-platform/badge)](https://scorecard.dev/viewer/?uri=github.com/txn2/mcp-data-platform) [![Signed by Cosign](https://img.shields.io/badge/artifacts-signed_by_cosign-blue?logo=sigstore&logoColor=white)](https://github.com/sigstore/cosign) [![Docker](https://img.shields.io/badge/ghcr.io-txn2%2Fmcp--data--platform-blue?logo=docker)](https://github.com/txn2/mcp-data-platform/pkgs/container/mcp-data-platform) [![Benchmark Report: Knowledge Layer](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21438044-blue?label=Report%3A%20Knowledge%20Layer)](https://doi.org/10.5281/zenodo.21438044) [![Benchmark Report: Knowledge Use](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21614058-blue?label=Report%3A%20Knowledge%20Use)](https://doi.org/10.5281/zenodo.21614058) [![Benchmark Report: Knowledge Pollution](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21834812-blue?label=Report%3A%20Knowledge%20Pollution)](https://doi.org/10.5281/zenodo.21834812) [![Benchmark Report: Graph Completion](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.21881797-blue?label=Report%3A%20Graph%20Completion)](https://doi.org/10.5281/zenodo.21881797) **[Documentation](https://mcp-data-platform.txn2.com/)** | **[Installation](https://mcp-data-platform.txn2.com/server/installation/)** | **[Quick Start](#quick-start)** | **[Go Library](https://mcp-data-platform.txn2.com/library/overview/)** **Your AI assistant can run SQL. But it doesn't know that `cust_id` contains PII, that the table has an open data-quality incident, or who to ask when something breaks.** mcp-data-platform fixes that. It is a single MCP server that connects AI assistants to your data infrastructure and enriches every response with business context from its own catalog: query a table and get its meaning, owners, tags, glossary terms, and open incidents in the same call. It is a platform, not just a bridge. The same endpoint gives agents persistent memory and a governed path to write knowledge back to the catalog, proxies third-party MCP servers and REST APIs through one authentication, persona, and audit pipeline, and ships a web portal where AI-generated assets are saved, organized into collections, and shared with teammates. --- ## The catalog is built in The platform keeps its own data catalog in PostgreSQL. Every Trino, API and GraphQL connection is scanned into it; people curate it in the portal's Catalog section and agents curate it through `apply_knowledge`, with every change versioned and reversible. It is the semantic layer cross-enrichment reads, so there is no separate metadata service to run. [Trino](https://trino.io/) and [S3](https://aws.amazon.com/s3/) are components the platform composes behind provider interfaces; add them when you're ready. [Learn why this stack.](https://mcp-data-platform.txn2.com/concepts/components/) **Everything runs on PostgreSQL.** The gateways, knowledge layer, memory, portal, catalog, and `search`/`fetch` need only the database. Without Trino there is nothing to query or enrich, not a smaller platform: `query:` and `toolkits:` are independent config blocks. See [Deployment Shapes](https://mcp-data-platform.txn2.com/server/deployment-shapes/) for exactly what each shape includes and leaves out. --- ## Why AI assistants are powerful at querying data, but they work blind. When an agent asks "What's in the orders table?", it gets column names and types. It doesn't know that `customer_id` is PII, that the table has an open data-quality incident, or who to contact when something looks wrong. ``` # Without mcp-data-platform ───────────────────────────────────────────────────────────────────── User: "Describe the orders table" AI: Queries Trino → gets columns and types User: "Who owns this data?" AI: Searches the catalog → finds owners User: "Can I trust this table right now?" AI: Reads the catalog again → finds an open incident User: "What does customer_id actually mean?" AI: Reads the catalog again → finds column descriptions ───────────────────────────────────────────────────────────────────── 4 round trips. Context scattered across conversations. Easy to miss warnings. ``` ``` # With mcp-data-platform ───────────────────────────────────────────────────────────────────── User: "Describe the orders table" AI: Gets everything in one response: → Schema: columns and types → Open incident: late partition for yesterday → Owners: Data Platform Team → Tags: pii, financial → Column meanings and business definitions ───────────────────────────────────────────────────────────────────── 1 call. Complete context. Warnings front and center. ``` ## How It Works ```mermaid sequenceDiagram participant AI as AI Assistant participant P as mcp-data-platform participant T as Trino participant C as Platform catalog AI->>P: trino_describe_table "orders" P->>T: DESCRIBE orders T-->>P: columns, types P->>C: Get semantic context C-->>P: description, owners, tags, terms, incidents P-->>AI: Schema + Full Business Context ``` The platform intercepts tool responses at the protocol level and enriches them with context from the other services. This **cross-enrichment** is bidirectional: - **Trino to catalog**: query results include descriptions, owners, tags, glossary terms, and open incidents - **Catalog to Trino**: a dataset read from the catalog says whether it is queryable, on which connection, and roughly how many rows it holds ## Does it work? Measured effectiveness The knowledge layer is not just a design claim; it is benchmarked. On knowledge-trap questions, the ones an agent answers plausibly but wrongly without business context, connecting the agent to the platform's semantic knowledge layer lifts accuracy from **42.7% (raw data tools) to 98.7%**, a **+56-point** gain (95% CI +44 to +67). On plain lookups and arithmetic, where no business context is needed, the platform and bare tools are statistically tied, so the gain is specific to knowledge-gated questions, not a blanket accuracy boost. This is an **arm-vs-arm** result on a single pinned model (same model, same tasks, only the platform configuration changes). It measures the **knowledge layer** specifically, that is cross-enrichment, `search`, and the memory/`apply_knowledge` lifecycle, not the whole platform. Every number is recomputed from committed raw data by a notebook that needs no API key. See the full, citable [Benchmark Report](https://mcp-data-platform.txn2.com/reference/benchmark-report/) (four-arm ablation, cold-start learning curve, lifecycle scorecard, threats to validity) and the [benchmark report series](bench/README.md), which indexes every study's protocol, toolchain, and archived run data. ## Features Each feature links to its full documentation. ### Semantic data access | Feature | Description | |---------|-------------| | [Cross-enrichment](https://mcp-data-platform.txn2.com/cross-enrichment/overview/) | Business context added to every tool response automatically, with session dedup to save tokens | | [Platform catalog](https://mcp-data-platform.txn2.com/server/catalog/) | Every connection scanned into a catalog the portal and agents curate, with lineage recorded as data moves and every change versioned | | [Universal search](https://mcp-data-platform.txn2.com/knowledge/overview/) | One `search` tool fans a query across the catalog, knowledge pages, memory, insights, assets, prompts, and APIs; `fetch` dereferences any result | | [Workflow gating](https://mcp-data-platform.txn2.com/reference/middleware/) | Session-aware guidance that steers agents to discovery before SQL, with escalating warnings | | [Tools](https://mcp-data-platform.txn2.com/server/tools/) | Full tool reference for Trino, S3, knowledge, memory, portal, and gateway toolkits | ### Knowledge and memory | Feature | Description | |---------|-------------| | [Memory layer](https://mcp-data-platform.txn2.com/memory/overview/) | Persistent agent memory across sessions, PostgreSQL + pgvector, hybrid semantic/lexical recall | | [Knowledge capture](https://mcp-data-platform.txn2.com/knowledge/overview/) | Agents record domain insights during sessions; approved knowledge is written to the catalog or canonical knowledge pages | | [Governance workflow](https://mcp-data-platform.txn2.com/knowledge/governance/) | Human-in-the-loop review, approve/reject, changeset tracking, and rollback for every applied change | | [Managed resources](https://mcp-data-platform.txn2.com/portal/resources/) | Human-uploaded reference files (data, visual, playbooks, samples, templates, references) served to agents as MCP resources | ### Gateways and extensibility | Feature | Description | |---------|-------------| | [MCP gateway](https://mcp-data-platform.txn2.com/server/gateway/) | Re-expose any third-party MCP server through the platform's auth, persona, and audit pipeline | | [API gateway](https://mcp-data-platform.txn2.com/server/api-gateway/) | Proxy REST/HTTP APIs (Salesforce, Google, GitHub, Stripe) with four tools instead of one tool per endpoint | | [API catalogs](https://mcp-data-platform.txn2.com/server/api-catalogs/) | Versioned OpenAPI bundles shared across connections, with semantic endpoint ranking | | [REST invoke shim](https://mcp-data-platform.txn2.com/server/api-gateway/#rest-gateway-for-non-mcp-clients) | Call gateway endpoints from NiFi, Airflow, or `curl` under the same auth and audit pipeline | | [Self-configuration](https://mcp-data-platform.txn2.com/server/self-configuration/) | Admins manage personas, connections, and prompts by asking the agent instead of clicking | | [MCP Apps](https://mcp-data-platform.txn2.com/mcpapps/overview/) | Interactive UI panels rendered inline in the MCP host | | [Go library](https://mcp-data-platform.txn2.com/library/overview/) | Import the platform as a library: custom toolkits, providers, and middleware | ### Security and operations | Feature | Description | |---------|-------------| | [Authentication](https://mcp-data-platform.txn2.com/auth/overview/) | Fail-closed model: OIDC (Keycloak, Auth0, Okta, Azure AD) and API keys for service accounts | | [OAuth 2.1 server](https://mcp-data-platform.txn2.com/auth/oauth-server/) | A broker, [not an identity provider](#a-broker-not-an-identity-provider): authorization server with PKCE and Dynamic Client Registration toward MCP clients, delegating every human login upstream to your IdP | | [Outbound OAuth](https://mcp-data-platform.txn2.com/auth/oauth-gateway/) | OAuth to upstream MCPs and APIs with encrypted refresh tokens that survive restarts | | [Personas](https://mcp-data-platform.txn2.com/personas/overview/) | Role-mapped allow/deny tool and connection filtering, default-deny; roles that match no persona reach nothing, in the portal as well as over MCP | | [Authorization model](https://mcp-data-platform.txn2.com/concepts/authorization/) | Why the connection, not the end user, is the unit of access; what that enforces, what it does not, and when to add a connection instead of a role | | [Audit logging](https://mcp-data-platform.txn2.com/server/audit/) | Every tool call logged to PostgreSQL with identity, persona, sanitized parameters, and timing | | [Observability](https://mcp-data-platform.txn2.com/server/observability/) | Prometheus metrics and optional OpenTelemetry distributed tracing | | [Session externalization](https://mcp-data-platform.txn2.com/server/session-externalization/) | PostgreSQL-backed sessions for zero-downtime restarts, horizontal scaling, and live tool-inventory updates | | [Explicit session handles](https://mcp-data-platform.txn2.com/server/configuration/#explicit-session-handles) | `platform_info` mints a `session_id` the agent threads on every call, making orientation unskippable; every HTTP deployment serves the sessionless MCP 2026-07-28 protocol as well as 2025-11-25 and earlier, and `platform_info` reports the revision a session speaks | | [Multi-provider](https://mcp-data-platform.txn2.com/server/multi-provider/) | Multiple instances of each service behind one endpoint, with isolated failure domains | | [Operating modes](https://mcp-data-platform.txn2.com/server/operating-modes/) | Standalone (no database) or file + database with live config overrides resolved per read | | [Deployment shapes](https://mcp-data-platform.txn2.com/server/deployment-shapes/) | Which backends you need: the semantic stack for cross-enrichment, PostgreSQL alone for the gateways and knowledge layer, or both | | [Email notifications](https://mcp-data-platform.txn2.com/server/notifications/) | Branded emails for shares and feedback: admin-configured SMTP, per-user preferences (immediate, daily digest, or off), durable queue with retries, a per-share notify toggle and optional plain-text note, and delivery history for admins and for each recipient | | [Session-start notices](https://mcp-data-platform.txn2.com/server/session-notices/) | `platform_info` opens a session by telling the person what is waiting: unresolved feedback others left on assets they own, and what has newly been shared with them. Delivered once, watermarked per user, for the people who never open the portal | ## The Portal A built-in web portal serves both operators and end users. Enable with `portal.enabled: true`. On a deployment that signs in through OIDC, `portal.login_redirect: true` sends a signed-out visitor straight to the identity provider instead of the login screen; `/portal/?login=api_key` keeps the API key form for administrators and service accounts unless `portal.allow_api_key_login: false` ([configuration](https://mcp-data-platform.txn2.com/server/configuration/#sending-sign-in-straight-to-the-identity-provider)). **For operators**: dashboards with activity timelines and performance percentiles, a searchable audit log, an interactive tool explorer with per-persona visibility and inline test runs, knowledge insight governance, connection and persona management, API keys, and indexing health. See the [Admin Portal guide](https://mcp-data-platform.txn2.com/portal/). ![Admin Dashboard](docs/images/screenshots/light/admin-admin-dashboard-light.webp) **For users**: AI-generated assets (reports, charts, documents) are saved from any session with the `save_asset` tool, organized into shareable [collections](https://mcp-data-platform.txn2.com/portal/collections/), and shared with teammates or through public links. A [prompt library](https://mcp-data-platform.txn2.com/portal/prompts/), [feedback threads](https://mcp-data-platform.txn2.com/portal/shared/#feedback) on any asset, and personal knowledge and activity views round out the [User Portal](https://mcp-data-platform.txn2.com/portal/). ![Collections](docs/images/screenshots/light/user-collection-view-light.webp) ## Quick Start Install (see [all methods](https://mcp-data-platform.txn2.com/server/installation/): Homebrew, Docker, source): ```bash go install github.com/txn2/mcp-data-platform/cmd/mcp-data-platform@latest ``` Create a minimal configuration. The database holds the catalog cross-enrichment reads, and the Trino connection is scanned into it; `${VAR}` references are expanded from the environment: ```yaml # platform.yaml server: name: mcp-data-platform transport: stdio database: dsn: "${DATABASE_URL}" toolkits: trino: enabled: true instances: primary: host: "${TRINO_HOST}" port: 443 user: "${TRINO_USER}" ssl: true default: primary ``` Wire it to Claude Code: ```bash claude mcp add data-platform \ -e DATABASE_URL=postgres://platform:secret@localhost:5432/platform \ -e TRINO_HOST=trino.example.com -e TRINO_USER=analyst \ -- mcp-data-platform --config platform.yaml ``` Starting without a warehouse? Swap the `toolkits:` block above for the API toolkit, and the gateways, knowledge layer, memory, portal, and `search`/`fetch` come up on PostgreSQL alone: ```yaml # platform.yaml server: name: mcp-data-platform transport: http address: ":8080" database: dsn: "${DATABASE_URL}" # API connections are authored in the admin portal, not in YAML. toolkits: api: enabled: true ``` [Deployment Shapes](https://mcp-data-platform.txn2.com/server/deployment-shapes/) covers the full configuration, what each shape includes, and what it leaves out. For a hosted deployment, run `--transport http` and enable the built-in OAuth 2.1 server so Claude and other MCP clients sign in through your identity provider. That server is a [broker, not an identity provider](#a-broker-not-an-identity-provider): it hands every human login to your IdP. See [Configuration](https://mcp-data-platform.txn2.com/server/configuration/), [Deployment](https://mcp-data-platform.txn2.com/server/deployment/) (Docker Compose, Kubernetes), and the [OAuth 2.1 Server guide](https://mcp-data-platform.txn2.com/auth/oauth-server/). ## Security The platform implements a **fail-closed** security model: missing or invalid credentials deny access, never bypass. Personas are default-deny, Trino and S3 support enforced read-only mode. See the [Auth Overview](https://mcp-data-platform.txn2.com/auth/overview/) and [MCP Defense: A Case Study in AI Security](https://imti.co/mcp-defense/) for the architecture rationale. | Transport | Authentication | TLS | |-----------|----------------|-----| | **stdio** | Not required (local execution) | N/A | | **HTTP** | Required (Bearer token or API key) | Strongly recommended | ### Access control: the connection is the boundary **The unit of access is the connection, not the end user.** A connection is a named binding to one downstream system under one credential, and several connections may front the *same* system under *different* credentials at different permission levels: a read-only Trino account and a write-capable one on the same cluster are two connections, and each persona is granted the subset it may reach. Connection rules are deny-by-default (an empty or omitted `connections.allow` grants nothing), enforced on every tool call alongside the tool-pattern check, and applied to discovery so `search`, `fetch`, `list_connections`, and the portal search do not surface entities behind a connection the persona was not granted, with `search` and `list_connections` reporting a `withheld` count and a notice naming the persona (`pkg/persona/filter.go`, `internal/platform/connscope`). `api_routes` narrows an HTTP API gateway connection further by method and path, which is how read-write and read-only access to one API are split across personas. The trade is explicit. The platform does not impersonate the caller downstream: no per-user token exchange and no session-user propagation, so everyone granted a connection acts as that connection's credential, and warehouse row policies or column masks that key off the end user do not follow a caller through. Per-person policy is expressed as connections, one per distinct outcome. Per-user attribution comes from the audit trail instead: with audit enabled, each call records `user_id`, `user_email`, `persona`, and the connection it targeted, so the platform knows who acted even where the downstream system sees only a service account. Tighten access by adding a connection with a narrower downstream account, not by adding a role that lands on the same connection. [Authorization model](https://mcp-data-platform.txn2.com/concepts/authorization/). ### A broker, not an identity provider **mcp-data-platform is an OAuth 2.1 broker, not an identity provider.** No person authenticates to it: there is no login form, no user password to verify, and no MFA. A human's identity comes from your existing IdP (Keycloak, Auth0, Okta, Azure AD) over OIDC. `/authorize` redirects the browser there and refuses the flow outright when no upstream IdP is configured, and the roles and email that person is authorized against are the ones the IdP asserts. Service accounts authenticate with API keys instead, and their roles come from local configuration. It stores no human passwords, and no migration in the tree defines a password column. The secrets it does hold are machine credentials, held the way an auditor would want: API keys and the client secrets Dynamic Client Registration issues to MCP client software are bcrypt hashes, the authorization codes and tokens the platform itself issues are SHA-256 digests, and refresh tokens for upstream services are encrypted at rest (AES-256-GCM when `ENCRYPTION_KEY` is set; the server warns loudly at startup when it is not). The platform presents an authorization server toward MCP clients because the MCP specification requires a discoverable authorization server supporting Dynamic Client Registration, which upstream IdPs generally do not expose. The broker shape is what the spec requires, not a decision to reimplement identity. The parts an auditor reaches for first: | Concern | Implementation | |---|---| | `redirect_uri` matching | Exact match for non-loopback; RFC 8252 section 7.3 handling for loopback (`pkg/oauth/storage.go`) | | DCR abuse | Plain HTTP to non-loopback hosts refused regardless of configuration; private-use schemes excluded from `AllowAllRedirectURIs`, so the unauthenticated registration endpoint never hands out scheme hijacking by default (`pkg/oauth/dcr.go`) | | Brute force and registration flood | Per-IP token-bucket limits on `/token` and `/register`, applied before the bcrypt work they would otherwise burn (`pkg/oauth/ratelimit.go`) | | Authorization | Deny-before-allow, default deny, fail-closed on unresolved persona (`pkg/persona/filter.go`) | | Prompt injection carried in catalog curation | Descriptions, tags, owners and glossary text sanitized before reaching the model and detected attempts logged (`pkg/semantic/sanitize.go`, `pkg/semantic/injection_logger.go`); curation is written only by callers holding `apply_knowledge` or the portal's curation rights, and every write is audited, versioned and reversible | ### Engineering posture More than 1.25 lines of test code per line of production Go, with the security-critical packages carrying the highest ratios in the tree: `pkg/oauth` and `pkg/middleware` are both above 2:1. Fuzz suites cover `pkg/oauth`, `pkg/auth`, `pkg/platform`, and `pkg/middleware`. Every PR passes race-detector tests, `golangci-lint`, `gosec`, and Semgrep and CodeQL SAST, under a coverage floor enforced in CI. Release artifacts are Cosign-signed with GitHub build-provenance attestations, and supply-chain posture is tracked by [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/txn2/mcp-data-platform). Those ratios are claims a reader can check, so they are kept true mechanically rather than by hand: `make posture-check` recomputes them and fails when the tree crosses a stated line. ## Ecosystem mcp-data-platform is the orchestration layer for a suite of open-source MCP servers that also run standalone: - [txn2/mcp-trino](https://github.com/txn2/mcp-trino/): Trino distributed SQL with configurable timeouts and row limits - [txn2/mcp-s3](https://github.com/txn2/mcp-s3/): S3 object storage: buckets, prefixes, objects, presigned URLs See [Ecosystem](https://mcp-data-platform.txn2.com/ecosystem/) for how they compose. ## Documentation Full documentation lives at [mcp-data-platform.txn2.com](https://mcp-data-platform.txn2.com/). - [Server Guide](https://mcp-data-platform.txn2.com/server/overview/): architecture, configuration, deployment - [Cross-Enrichment](https://mcp-data-platform.txn2.com/cross-enrichment/overview/): how automatic enrichment works - [Authentication](https://mcp-data-platform.txn2.com/auth/overview/): OIDC, API keys, OAuth 2.1 - [Knowledge Capture](https://mcp-data-platform.txn2.com/knowledge/overview/) and [Memory](https://mcp-data-platform.txn2.com/memory/overview/): the agent knowledge loop - [Go Library](https://mcp-data-platform.txn2.com/library/overview/): build custom MCP servers ([API stability policy](https://mcp-data-platform.txn2.com/library/stability/)) - [Tools API Reference](https://mcp-data-platform.txn2.com/reference/tools-api/): complete tool specifications - [Examples Gallery](https://mcp-data-platform.txn2.com/examples/): real-world configurations - [Troubleshooting](https://mcp-data-platform.txn2.com/support/troubleshooting/): common issues and debugging ## Development ```bash go build -o mcp-data-platform ./cmd/mcp-data-platform # build go test -race ./... # tests make verify # full CI-equivalent suite make vet-tags # compile the integration-tagged build (~1s) make acceptance # per-ticket acceptance suite against a running server (make dev) make acceptance ISSUE=1675 # one ticket's criteria, recorded the same way ACCEPTANCE_ENV=clickstack bash scripts/release-acceptance.sh # one environment's criteria, on the stack it needs make osv # osv-scanner over go.mod, ui/package-lock.json, docs/requirements.txt ``` `make acceptance` runs [`test/acceptance`](test/acceptance/acceptance_test.go): one file per ticket, each executing that ticket's acceptance criteria as a real MCP client against a running platform, through the tool a user calls. It targets the dev stack (`make dev`) by default and fails rather than skips when no server answers. `make acceptance-release` runs the whole suite, using a stack already answering and otherwise starting one for the run and stopping it after; the E2E Nightly runs it, and `make verify-release` does not, because the whole suite runs for well over an hour and a release requires only its own tickets' recorded runs (#2218); `make verify`'s Playwright suite, which wants port 5173, runs on the next free port when it finds a dev server there. A few criteria need a stack `make dev` does not make: a replica with the script worker off, or the ClickStack bundle beside the platform. [`test/acceptance/environments.txt`](test/acceptance/environments.txt) names each environment, its criteria and the settings its stack is started with. `make acceptance` leaves those criteria out and says which, and `ACCEPTANCE_ENV=` runs one environment's criteria alone; [`scripts/release-acceptance.sh`](scripts/release-acceptance.sh) starts the stack an environment needs, and anything it runs beside it, then stops what it started. Each such criterion fails, naming what it needs, on any other stack. The E2E Nightly runs the whole suite every night: the plain stack's criteria in eight groups at once, split by ticket number (`ACCEPTANCE_SHARD=i/8`), each on a stack of its own, and one job per other environment. A final release tag is refused while the most recent night is red (`make release-tag-check`). A pull request that changes `dev/`, the `Makefile`, `scripts/acceptance*`, `scripts/release-acceptance.sh`, the suite's harness under `test/acceptance/` (any file but a ticket's `issue__test.go`) or the acceptance workflows runs one group in CI before it merges: the `worker-off` environment with `make dev-smoke` first (`ACCEPTANCE_SMOKE=1`), from the same workflow the nightly calls ([`.github/workflows/acceptance-run.yml`](.github/workflows/acceptance-run.yml), paths in [`scripts/acceptance-pr-paths.py`](scripts/acceptance-pr-paths.py), #2274). The run is recorded, not described. `make acceptance` streams `go test -json` through [`scripts/acceptance-evidence.py`](scripts/acceptance-evidence.py), which files each ticket's events at `build//acceptance.jsonl`. `make verify` warns (`acceptance-check`) when production Go changed with no acceptance file beside it, and fails when an acceptance file changed and any `func TestIssue_` it declares has no terminal pass event in that stream — absent, skipped and failed are one verdict, so a run that did not happen cannot be explained. It fails too when the diff registers a tool name no changed acceptance file calls, and when the run transcript at `build//acceptance.md` is missing or carries no `Wire forms:` line naming every JSON form the test sent for each parameter whose schema admits more than one. `make verify-release` applies the results rule to every acceptance file changed since the last tag (`acceptance-release-check`). A file behind `//go:build integration` is read by no default build: `make lint`, `make patch-coverage`, `gosec` and `npm run build` all compile with the default tags, and `.golangci.yml` sets none. Every other consumer of the tag wants a machine — `make test-realdb` wants Docker and a migrated Postgres, `make acceptance` wants a dev stack — so a compile error in one used to surface only after that machinery was up. `make vet-tags` runs `go vet -tags=integration ./...` in about a second and sits in `make verify`'s serial preamble, ahead of the lanes, so a tagged file that does not compile is reported before the Docker daemon starts. `make verify` also runs every Go package the change touched at `go test -race -cpu=1,2 -count=5` (`make schedule-lane`, which CI runs on every pull request), because a test whose assertion depends on goroutine ordering passes one run on a many-core machine and fails a loaded CI runner; each failure prints the command that reproduces it. The frontend counterpart, `make schedule-lane-ui`, runs every changed vitest file five times beside the full suite. `make e2e-copy-check` fails when a Playwright spec asserts a string the diff removed from `ui/src` and no `ui/src` source still shows, naming the spec line, since the Playwright suite otherwise reports it only at the end of verify. `make route-manifest-check` runs the screenshot route-manifest test alone, so a portal route with no manifest entry fails the pre-gate rather than verify's UI lane (#2055). `make route-conformance-check` does the same for the MSW route conformance test, which fails a mocked route `internal/apidocs/swagger.json` does not describe. `make preverify` also runs the standalone `gosec` over the packages the diff changes (`make security-diff`) and, when the diff adds or removes a mux registration, the route-template gate alone (`make route-template-diff`) (#2186). `make dev-smoke` checks a running dev stack in under a minute with one call through the platform per service it runs, and `make preverify` runs it when the diff changes `dev/` (#2248). `make state-readers-check` lists every API contract change in the diff (a tagged field on an admin or portal response struct, a route or definition in `internal/apidocs/swagger.json`) and warns when nothing under `ui/src` changed with it; the pull request template's "Readers of this state" section names the readers checked for each. `make builtin-pages-check` warns when the diff changes source a built-in knowledge page describes (the paths `internal/platform/knowledgebuiltin/covers.txt` lists for it) and leaves the page alone, since every deployment serves those pages on its next start. The running side of the same invariant is enforced by the platform itself: after runtime wiring it compares each toolkit's tool list with the tools its MCP server holds, and refuses to start when it names one the server does not have. `make verify` runs `govulncheck`, which does reachability analysis and reports only vulnerabilities your code actually calls. A finding against a dependency with no patched release would otherwise leave every build red, so the report is judged by [`scripts/govulncheck-gate.py`](scripts/govulncheck-gate.py) against [`.govulncheck-allow.txt`](.govulncheck-allow.txt), where an accepted advisory carries the reason it is accepted. The acceptance cannot outlive its reason: the gate fails the build when an accepted advisory starts naming a fixed version, and again when one stops being reported, so a stale line is a build failure rather than a silent suppression. CI runs the same script against the same file. `make verify` also runs [osv-scanner](https://github.com/google/osv-scanner) (`make osv`) over `go.mod`, `ui/package-lock.json` and `docs/requirements.txt`. It reads the GitHub advisory database, which carries advisories the Go vulnerability database does not (15 excelize advisories in #2143 reached only the first), and it is the only scan of the UI's npm lockfile. It fails on any finding, except a Go advisory whose symbols osv-scanner's call analysis finds the module never calls. An accepted advisory is listed in [`osv-scanner.toml`](osv-scanner.toml) with its reason and an `ignoreUntil` date: past that date the entry stops applying and the advisory fails the build again, and an entry that matches no finding fails it too. CI runs the same target. The React admin portal lives under [`ui/`](ui/README.md). Its CI job runs `npm run lint`, which enforces per-function complexity budgets that mirror the Go gates (`complexity <= 10` ≈ `gocyclo <= 10`, `cognitive-complexity <= 15` ≈ `gocognit <= 15`) plus an import-cycle rule. See [`ui/README.md`](ui/README.md) for the thresholds and the ratchet baseline. One browser suite also lives outside `make verify`: `make frontend-e2e-public-viewer` renders the public share viewer's client-rendered content families — HTML, JSX, markdown, SVG and a collection item — against a live stack and fails on anything the viewer's Content-Security-Policy blocks. It needs `make frontend-build`, a running server (`make dev`) and network egress to esm.sh, so it is run on demand rather than per commit — see [`ui/e2e/public-viewer/README.md`](ui/e2e/public-viewer/README.md). Two measurement harnesses live outside `make verify` (each is its own Go module): [`test/load`](test/load/README.md) measures throughput and resource limits ("how much"), and [`bench/`](bench/README.md) measures agent effectiveness — arm-ablated accuracy and efficiency with audit-derived metrics ("how well"). Run them via `make load-*` and `make bench-*` targets. Contributions for bug fixes, tests, and documentation are welcome. Please run `make verify` (formatting, race-detected tests, coverage, linting, security scanning) before opening a pull request. ## License [Apache License 2.0](LICENSE) --- Open source by [Craig Johnston](https://imti.co/about/), sponsored by [Deasil Works, Inc.](https://deasil.works/) and [Plexara.io](https://plexara.io)