Skip to main content
Glama
brendendearie

waylucid-agent-mcp

README.md
# waylucid-agent-mcp

Permission-aware MCP tools, a small agent harness, and CI evals for an ops desk.

This is a **sanitized public reference**. It is not LucidDock, not a customer deployment, and not an open-source of proprietary WayLucid code. Harborline is fictional seed data. The point is the shape: how you discover an ontology, ship it as MCP tools the model can actually see, and productize the boundary with evals.

Built by **Brenden Dearie** as a demoable slice for agentic-experience and FDE conversations.

## What hiring managers should look at in 5 minutes

1. **`src/mcp/tools.ts`** — tools are the product surface. The catalog is role-gated. `cases.update` is a *different schema* for operator vs supervisor, so the model cannot plan a field it is not allowed to send. The handler still enforces the matrix.
2. **`src/auth.ts` + `src/seed.ts`** — contacts / cases / tasks, plus data-plane redaction: operators get `hiddenInternalNoteCount`, supervisors get the note body.
3. **`src/eval/run.ts`** — a golden set that fails if a write leaks into the viewer catalog, if the demo path stops creating a P1 case, or if internal notes spill.
4. Run `pnpm agent --demo` then `pnpm eval`. That is the whole loop.

If you only have two minutes: switch the playground role from operator to viewer and run the same utterance. Create disappears from the plan.

## Discovery → ship → productize

```mermaid
flowchart LR
  D["Discover<br/>ontology + permissions"] --> S["Ship<br/>MCP tools + schemas"]
  S --> P["Productize<br/>harness + golden evals"]
  P --> D
```

| Stage | What we actually did here |
| --- | --- |
| **Discover** | Three objects an ops desk already has: contacts, cases, tasks. Three roles: viewer, operator, supervisor. Write down what each role must *never* do — assign, resolve, freeze an account, read internal notes — before writing a tool. |
| **Ship** | A TypeScript MCP server (`@modelcontextprotocol/server` v2) that advertises a coherent toolset over stdio and Streamable HTTP. Schemas are narrowed per role. Resources expose `ops://desk/whoami` and the catalog. Backing store is in-memory so this repo runs without a database or a paid API. |
| **Productize** | A harness that plans against the *advertised* catalog, not against a hidden admin API. A golden eval script that belongs in CI. A playground so a screen can happen without wiring Cursor or Claude Desktop first. |

The interesting failure mode for agent products is not “the model can’t call tools.” It is “the model planned a privileged write because the tool list lied.” This repo treats that as the product bug.

## Architecture

```mermaid
flowchart TB
  subgraph hosts [Hosts]
    CLI["CLI harness<br/>pnpm agent"]
    UI["Playground<br/>pnpm playground"]
    CI["Golden evals<br/>pnpm eval"]
  end

  subgraph mcp [MCP server]
    Factory["createOpsServer(role)"]
    Tools["Role-gated tools<br/>+ narrowed input schemas"]
    Resources["ops://desk/*"]
  end

  subgraph desk [Harborline desk]
    Store["In-memory OpsStore"]
    Seed["Fictional contacts / cases / tasks"]
  end

  CLI -->|stdio or in-process HTTP| Factory
  UI -->|REST + /mcp| Factory
  CI -->|in-process Client| Factory
  Factory --> Tools
  Factory --> Resources
  Tools --> Store
  Resources --> Store
  Store --> Seed
```

Stdio is what Cursor / Claude Desktop / MCP Inspector spawn. The harness tests use `createMcpHandler` + `StreamableHTTPClientTransport` with `fetch` pointed at the handler — no socket, same factory you would deploy.

## Permission matrix

| Tool | viewer | operator | supervisor |
| --- | --- | --- | --- |
| `whoami`, `*.list`, `*.get` | yes | yes | yes |
| `cases.create`, `cases.update`*, `tasks.create`, `tasks.complete` | — | yes | yes |
| `cases.assign`, `cases.resolve`, `cases.add_internal_note`, `contacts.update_status` | — | — | yes |
| Internal note bodies on `cases.get` | hidden | hidden | visible |

\*Operator `cases.update` accepts `title` / `description` / `priority`. Supervisor `cases.update` also accepts `tags`. Assignment and resolve stay on their own tools so a host can attach a confirmation to the destructive ones (`destructiveHint` is set).

The server never relies on the client to filter. If a viewer calls `cases.create` anyway, the tool is not registered on that session and MCP returns a protocol-level "not found" — the model never gets a successful write.

## How to run locally

Node 20+ (22 is what CI uses). pnpm preferred; npm works.

```bash
pnpm install          # or npm install
pnpm tools            # start the MCP factory, list the operator catalog, exit
pnpm agent --demo     # list Maya → open P1 case → create follow-up
pnpm eval             # golden set; exit 0 when green
pnpm test             # vitest, including the golden set
pnpm playground       # http://127.0.0.1:43123
```

Role is `--role viewer|operator|supervisor` or `WAYLUCID_ROLE`.

```bash
pnpm agent --role viewer "Maya's webhook is failing — open a P1 case"
pnpm agent --role supervisor "assign Maya's webhook case to Priya and resolve it"
```

### MCP Inspector / Cursor

```bash
pnpm mcp
```

Point a host at `tsx src/mcp/stdio.ts` (or `pnpm mcp`). Set `WAYLUCID_ROLE` in the server env. Logs go to stderr; stdout is JSON-RPC.

The playground also serves Streamable HTTP at `POST /mcp` with `x-waylucid-role: operator`.

### Optional live model

Default planner is a deterministic mock. That is intentional: the product surface is the tool boundary, and screens should not depend on a vendor key.

```bash
export WAYLUCID_LLM=openai
export OPENAI_API_KEY=...
# or WAYLUCID_LLM=anthropic and ANTHROPIC_API_KEY
pnpm agent --demo
```

No key? The mock planner still runs the demo path and the evals still gate quality.

## Package layout

```
src/auth.ts              roles, advertised catalog, principals
src/ontology.ts          contacts / cases / tasks
src/store.ts             in-memory desk
src/seed.ts              Harborline fixtures
src/mcp/create-server.ts factory
src/mcp/tools.ts         permission-aware tool schemas
src/mcp/stdio.ts         stdio entry
src/mcp/session.ts       in-process Client used by harness + evals
src/harness/             mock planner, optional OpenAI/Anthropic, CLI
src/eval/                golden fixtures + runner
src/web/                 playground HTTP + /mcp
playground/              Vite + React UI
tests/                   store, permissions, protocol, harness, evals
```

`npm test` and `pnpm eval` are the two commands CI runs after typecheck.

## Scope

This is a small reference, not a framework: one ontology, one server, one harness, one eval set. The store is in-memory and the default planner is a mock so the repo runs without a database or vendor key.

For a production shape, bind the same factory to a real CRM, run the golden set against recorded traces, and keep the tool schemas as the source of truth for what an agent is allowed to do.

## License

MIT. See [CONTRIBUTING.md](CONTRIBUTING.md) for the bar on PRs.