company-state-mcp
by kcole16
README.md
# company-state-mcp
The single source of truth and single point of control for an agent-operated company: one MCP server fronting Postgres that carries canonical EOS-style business state (V/TO, rocks, scorecard, issues, todos, handoffs, budgets, payments, proposals), a **deterministic policy engine** (pure code — no LLM calls anywhere server-side), seat identity enforcement, and an append-only hash-chained audit trail. Agents connect as MCP clients holding per-seat tokens; nothing mutates business state except through this server. The server is business-agnostic — all business specifics live in data (jsonb/text), never in schema.
**The 8 hard invariants:**
1. **Audit everything** — every mutation (including denied calls) writes a hash-chained audit row (`prev_hash`/`row_hash`); `verify-audit` replays and validates the full chain.
2. **Classifier takes the max** — decision class is a pure function (tool, args, config) → class; the server always uses max(requested, computed), so a seat can never under-class its own action.
3. **Class ≥ 2 requires an approved proposal** — otherwise `PROPOSAL_REQUIRED` with a draft proposal skeleton in the error payload.
4. **Permission matrix is data** — spec §5 encoded as a table checked in middleware before any handler logic; one test per cell.
5. **Idempotency** — replaying any mutation's `idempotency_key` returns the original result with zero duplicate side effects.
6. **Circuit breaker** — while the pause flag is set, everything except reads and `issue_create` returns `PAUSED`.
7. **The money boundary** — `payment_intent_execute` above Class 0 limits requires co-sign approval (Cosigner interface); the server initiates but never unilaterally moves funds above Class 0.
8. **The system can't loosen its own leash** — class thresholds live in the `config` table and reject direct writes; changing them requires a Class 4 approved proposal.
## Requirements
- Node 22+
- Docker (for Postgres 16; the server itself runs on the host — see DECISIONS.md #56)
## Setup
```bash
docker compose up -d # Postgres 16 on host port 5433 (+ auto-creates the test DB)
npm install
cp .env.example .env # dev defaults work out of the box
npm run migrate # schema + genesis config (class thresholds, pause flag, charter pins)
npm run seed # demo company: V/TO, budget, rocks, metrics, issues, counterparties
```
## Minting seat tokens
Seven actors can hold a token: the six operating seats (`integrator`, `growth`, `delivery`, `customer`, `finance`, `critic`) plus `board`, which is **read-only** on the MCP surface (votes and the pause flag go through the board bridge). Tokens are short-lived HS256 JWTs signed with per-seat secrets from `.env` (`SEAT_SECRET_<SEAT>`).
```bash
npm run mint-token -- --seat finance # prints only the JWT (default TTL from JWT_TTL_SECONDS)
npm run mint-token -- --seat finance --ttl 3600
```
## Running the server
```bash
npm run dev # streamable HTTP (stateless) on :3030/mcp
npm run dev -- --stdio # stdio transport (protocol owns stdout; logs on stderr)
```
Every tool call carries a `seat_token` argument. Mutations additionally require an `idempotency_key`, and may carry a `proposal_id` linking an approved proposal (required for Class ≥ 2 actions).
## Connecting an MCP client
TypeScript (stdio, spawning the server):
```ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'npx',
args: ['tsx', 'src/index.ts', '--stdio'],
cwd: '/path/to/this/repo',
});
const client = new Client({ name: 'my-agent', version: '0.1.0' });
await client.connect(transport);
const res = await client.callTool({
name: 'get_context',
arguments: { seat_token: process.env.FINANCE_SEAT_TOKEN },
});
// The envelope is JSON in the first text content block:
const envelope = JSON.parse((res.content as Array<{ text: string }>)[0].text);
```
HTTP (stateless streamable HTTP — one JSON-RPC call per POST):
```bash
TOKEN=$(npm run -s mint-token -- --seat finance)
curl -s http://localhost:3030/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_context","arguments":{"seat_token":"'"$TOKEN"'"}}}'
```
**Envelopes.** Reads return `{result, audit_id}`; mutations return `{result, audit_id, policy: {class, decision, reason?}}`. Refusals return the §6 error envelope `{error: {code, reason?, suggested_class?, draft_proposal?, original_result?, hint?}, audit_id}` so seats can self-route instead of retrying blindly:
| Code | Meaning | Expected agent behavior |
|---|---|---|
| `POLICY_DENIED` | Failed a limit/allowlist check; includes `reason` + `suggested_class` | Create proposal at suggested class |
| `PROPOSAL_REQUIRED` | Class ≥ 2 without approved linkage; returns draft proposal skeleton | Complete and submit the proposal |
| `SEAT_FORBIDDEN` | Tool not permitted for this seat | Hand off to the permitted seat |
| `PAUSED` | Circuit breaker active | Stop; log issue if urgent |
| `BUDGET_EXCEEDED` | Line has no headroom | Request reallocation via Integrator (Class per amount) |
| `STALE_STATE` | Version conflict on the referenced object | Re-read, re-decide, retry |
| `DUPLICATE` | Idempotency key seen; original result returned | Treat as success |
## The board bridge
A separate, humans-only HTTP service (never an MCP tool surface) — the only path to votes, proposal state flips, proposal execution, payment objections, and the global pause flag:
```bash
npm run bridge # listens on :3031 (BRIDGE_PORT)
```
Endpoints: `POST /votes`, `POST /proposals/:id/state`, `POST /proposals/:id/execute`, `POST /intents/:id/object`, `POST /pause`.
Every request is signed: `X-Bridge-Signature` is the lowercase-hex HMAC-SHA256 of the **raw request body** with `BRIDGE_SHARED_SECRET`, and `X-Board-Member` names the member. Worked example (set the pause flag):
```bash
BODY='{"paused":true}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "dev-bridge-secret" -r | cut -d' ' -f1)
curl -s http://localhost:3031/pause \
-H 'Content-Type: application/json' \
-H 'X-Board-Member: alice' \
-H "X-Bridge-Signature: $SIG" \
-d "$BODY"
```
## Watchdog
```bash
npm run watchdog # one idempotent sweep: flips past-due handoffs to overdue + creates escalation issues
```
## Audit verifier
```bash
npm run verify-audit # replays the hash chain from genesis; exits non-zero on any break
```
## Full-loop example
```bash
npx tsx examples/full-loop.ts
```
Demonstrates one complete cycle end to end: Finance reports an off-target metric → issue auto-created → Integrator creates a handoff → the owning seat completes it with evidence → a Class ≥ 2 payment without a proposal is refused with `PROPOSAL_REQUIRED` (draft proposal included) → the audit verifier passes over the whole run.
## Ecosystem: sentinel + the agentgame forum
The main agent (Hermes) typically holds this server's seat tools **and** the
[investor-forum sentinel](./sentinel/)'s ten `sentinel_*` tools side by side.
Sentinel is vendored in this repo at `./sentinel` (Python; its own
[README](./sentinel/README.md) and [SETUP.md](./sentinel/SETUP.md) are the
runbook) so one clone carries the whole stack. The forum itself is a separate
deployment. The two systems are designed to compose without touching each
other:
- **Disjoint tool namespaces.** All sentinel tools are `sentinel_*`-prefixed;
none of this server's 31 tool names collide. One harness can register both.
- **Opaque refs are the bridge.** Sentinel outputs (decision-memo UUIDs,
digest cursors, suggestion ids, statement ids) flow into this server as
`source_ref` / `deliverable_refs` strings — e.g.
`issue_create(..., source_ref: "sentinel:memo:<uuid>")` or a handoff
completed with `{analysis: "sentinel:digest:p42:c17"}`. This server never
interprets them (§1.6); sentinel never sees them.
- **Governance composes.** Sentinel's LOOP_GOAL step 4 — "act through your
governed channels" — is this server: memos and `questions_to_ceo` become
issues/proposals here; a substantive public statement can be run through a
proposal first, then queued via `sentinel_statement_queue`, whose id is the
deliverable ref. Each system enforces its own side (sentinel validates
statements independently; this server enforces decision classes).
- **No environment conflicts.** Forum API :3001, this server :3030, bridge
:3031; sentinel uses SQLite + a unix socket, this server Postgres :5433;
credential env vars are disjoint (`FORUM_PRIVATE_KEY`/`OPENROUTER_API_KEY`
vs `SEAT_SECRET_*`/`BRIDGE_SHARED_SECRET`). Sentinel's MA harness denies the
forum domains to the agent — this server is a separate process and remains
reachable.
- **Wallet identity aligns.** The agentgame forum authenticates writes with
EIP-191 signatures + an on-chain FORUM-token balance gate — the same
identity model the board bridge's `WalletSignatureVerifier` stub anticipates
(board members = token-holding wallets).
**Forum delivery.** `forum_post` rows stop at the `forum_outbox` table
(transactional-outbox pattern; `ForumAdapter` interface); `npm run forum-deliver`
is the out-of-band worker that drains them. Each sweep signs every `pending`
row's post with the company wallet (EIP-191 `personal_sign` over the forum's
canonical message) and `POST`s it to `FORUM_API_BASE` (default
`http://localhost:3001/api`). Two env vars drive it: `FORUM_API_BASE` and
`COMPANY_FORUM_PRIVATE_KEY` (a 0x-prefixed 32-byte hex key, deliberately
distinct from sentinel's `FORUM_PRIVATE_KEY`; the worker exits 1 and never logs
the key if it is unset or malformed). The nonce is `md5(post_id)`, so a
crash-replayed POST hits the forum's nonce registry and is recorded as delivered
(`external_ref = dedup:<nonce>`). On failure a row's `attempts` increments and
`delivery_error` is recorded while it stays `pending`; the fifth attempt flips it
to terminal `failed`. Each attempt writes one audit row (`system:forum-deliver` /
`forum_deliver`). Caveat: sentinel ingests **all** forum content with no
self-post filter, so company posts are classified as investor signal (burning
sentinel budget) until sentinel grows a sender filter. See DECISIONS.md #57.
## Running it with a Hermes-style agent
Two drop-in prompts stand the whole thing up and then operate it:
- [docs/HERMES-SETUP.md](./docs/HERMES-SETUP.md) — a single prompt your main
agent executes to bootstrap the stack from scratch, mint seat tokens without
ever seeing a secret, register all 41 tools (31 here + sentinel's 10) under
token-bound presets, and adopt the standing rules.
- [docs/EOS-CADENCE.md](./docs/EOS-CADENCE.md) — the operating cadence:
per-seat heartbeats, the daily critic sweep, the weekly L10 ritual, and the
quarterly rock cycle, each mapped to exact tool semantics.
## Production security notes
The dev defaults are for local development only. Before any deployment that
matters:
- Rotate every `SEAT_SECRET_*` and `BRIDGE_SHARED_SECRET` to long random
values; the committed `.env.example` values are placeholders.
- `COMPANY_FORUM_PRIVATE_KEY` must be a real funded wallet you control — never
reuse the well-known Hardhat test keys that appear in this repo's test
fixtures (they are intentionally public and hold nothing).
- Keep signing secrets out of the agent's environment: the agent holds
short-lived seat JWTs only; minting secrets, the bridge secret, and wallet
keys belong to the operator/sidecar (see the isolation pattern in
`sentinel/SETUP.md`).
- Don't expose :3030 (MCP) or :3031 (bridge) publicly without a reverse proxy,
TLS, and network policy; the bridge is for your board members only.
- The audit chain's daily head-hash anchoring to an external witness (spec §7)
is not implemented in v0 — ship the head hash off-box if you need tamper
evidence against a root-level attacker.
## Testing
```bash
npm test # vitest; needs the docker Postgres — tests run against TEST_DATABASE_URL, auto-migrated
```
## Repo layout
```
src/ MCP server: entrypoint (index.ts), server assembly, db pool, auth (JWT seats)
src/tools/ tool handlers — reads, issues/todos, handoffs, forum, governance, payments, counterparties, scorecard, skills
src/policy/ permission matrix (§5), decision-class classifier (§4), mutation pipeline, error semantics (§6)
src/schemas/ zod contracts for every tool input/result and the envelopes
src/audit/ hash-chained audit append + independent verifier (§7)
src/payments/ Cosigner interface (the money boundary above Class 0)
bridge/ board bridge HTTP service (votes, proposal state/execute, objections, pause)
migrations/ node-pg-migrate schema + genesis config
scripts/ migrate / seed / mint-token / watchdog / verify-audit CLIs
examples/ full-loop.ts — end-to-end demo MCP client
test/ vitest unit + integration suites (run against the test DB)
docker/ Postgres init scripts (creates the test database)
docs/ drop-in Hermes prompts: setup + EOS operating cadence
sentinel/ the investor-forum sentinel pipeline (Python; own README/SETUP)
```
Design decisions are logged in [DECISIONS.md](./DECISIONS.md); the authoritative contract is [company-state-mcp-spec.md](./company-state-mcp-spec.md).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues