Skip to main content
Glama

tessera-mcp

A Model Context Protocol (MCP) server that gives an AI agent seven support-ops tools for Tessera, a fictional B2B workspace SaaS. It runs over stdio with a bundled SQLite database — no hosted runtime, no API keys, no LLM calls anywhere in this repo.

Why this exists (60-second pitch). Wiring an LLM to tools is easy; wiring it safely is the hard part. This server demonstrates the pattern I ship for agentic work: typed tools with LLM-readable descriptions, a dry-run/confirm human-in-the-loop gate on every mutation, and a deterministic test suite that proves the tools behave — 20/20, no model in the loop. Point Claude Code at it and watch it run a realistic morning ops workflow: spot a double-billed customer, investigate, refund with approval, rescue a churning trial.


Quickstart

# 1. Create + seed the local database (idempotent — safe to re-run)
npx tessera-mcp --seed

# 2. Register the server with Claude Code
claude mcp add tessera -- npx tessera-mcp

That's it. Claude Code now has the seven tools below. No environment variables required — the server uses a local SQLite file at ~/.tessera-mcp/data.db. The path is home-anchored (not relative to the current directory) so the server finds the same database no matter where the host launches it from, and it auto-seeds on first run if you skip step 1.

Requires Node 20+.

Claude Desktop

Add this to your claude_desktop_config.json (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "tessera": { "command": "npx", "args": ["tessera-mcp"] }
  }
}

Claude Desktop launches MCP servers from the filesystem root, so a working-directory-relative database would be empty. This server sidesteps that by using the home-anchored ~/.tessera-mcp/data.db and seeding it on first run — no --seed step required.


Related MCP server: CRMy

The tools

Tool

Kind

What it does

list_open_tickets({ priority?, limit? })

read

Open tickets, worst priority first; optional priority filter + limit

get_customer({ idOrEmail })

read

One customer by id or email, with their invoices + tickets summary

search_invoices({ customerId?, status?, minAmount? })

read

Filter invoices — spot duplicates, overdue balances, history

daily_summary()

read

Start-of-day snapshot: open tickets by priority, overdue invoices ($), trials expiring ≤7d

update_ticket_status({ ticketId, status, confirm? })

mutation

Move a ticket to open / pending / solved

extend_trial({ customerId, days, confirm? })

mutation

Push a trial customer's end date out by N days

issue_refund({ invoiceId, reason, confirm? })

mutation

Refund a paid invoice (rejects already-refunded / non-paid)

Every input field carries a description — that text is what the client LLM reads to call the tool correctly. Results come back as concise text tables, not JSON dumps. Invalid ids return clear error strings, never exceptions.


The confirm / dry-run pattern (human-in-the-loop)

MCP has no built-in "approve this action" UI — the protocol hands a tool call straight to your code. So every mutation here is gated by a confirm flag that defaults to false:

  • Without confirm: true the tool changes nothing and returns a preview:

    DRY RUN — would refund INV-1013 ($2,400.00, Atlas Freight) — reason: "double billed".
    Re-call with confirm:true to execute.
  • With confirm: true it executes atomically (a single write batch that also appends an audit-log row) and reports what changed:

    Done — refunded INV-1013 ($2,400.00, Atlas Freight). Reason: "double billed".

The agent shows the human the dry-run preview, waits for a yes, then re-calls with confirm:true. It turns an irreversible action into a two-step, reviewable one. See DECISIONS.md for why this beats an ungated tool.


Example Claude Code session (illustrative)

Transcript below is an illustration of the intended workflow, lightly trimmed. The deterministic suite (npm run eval) is the actual correctness metric.

You:  Good morning — anything on fire?

Claude:  [calls daily_summary]
  Open tickets: 10 (3 urgent). 4 overdue invoices ($2,847.00). 2 trials expiring within 7 days.
  One thing stands out: Atlas Freight has two identical $2,400 invoices issued the same day.

You:  Dig into Atlas Freight.

Claude:  [calls get_customer({ idOrEmail: "CU-1010" })]
  Atlas Freight (CU-1010), enterprise. INV-1013 and INV-1014 are both $2,400.00,
  both issued Jul 14, 2026 — that's a double charge.

You:  Refund one of them.

Claude:  [calls issue_refund({ invoiceId: "INV-1013", reason: "duplicate charge — double billed" })]
  DRY RUN — would refund INV-1013 ($2,400.00, Atlas Freight). Re-call with confirm:true to execute.
  Want me to go ahead?

You:  Yes.

Claude:  [calls issue_refund({ ..., confirm: true })]
  Done — refunded INV-1013 ($2,400.00, Atlas Freight).

You:  Also, Paperkite's trial is about to lapse and they're angry — give them two more weeks and close their ticket.

Claude:  [extend_trial CU-1003 +14 (dry-run → confirm), then update_ticket_status TK-1003 solved (dry-run → confirm)]
  Done — Paperkite's trial now ends Aug 3, 2026, and TK-1003 is solved.

You:  Re-run the summary.

Claude:  [calls daily_summary]
  Open tickets down to 9 (2 urgent). Overdue unchanged. Trials expiring within 7 days: down to 1.

Eval — the honest metric

npm run eval connects an in-process MCP client to the server over the SDK's linked in-memory transport (no child process, no network, no LLM) and replays 23 scripted cases from data/eval-cases.json against a freshly seeded, isolated database:

  • valid reads, filtered reads, and invalid-id errors

  • each mutation as dry-run and then confirmed

  • post-mutation state assertions (a re-read proves the change landed)

  • a same-status no-op that returns success, not an error

  • direct audit_log assertions (exactly one row per confirmed mutation)

  • refund-twice rejection (a second refund on an already-refunded invoice errors)

  • a concurrency check: two overlapping confirmed refunds race via Promise.all; exactly one wins and exactly one audit row is written (proving the TOCTOU fix)

The eval is isolated — it runs against its own file:eval.db and never inherits TURSO_DATABASE_URL, so npm run eval can never touch your real or hosted data.

Because there is no model in the loop, the suite is fully deterministic: the gate is 100%. Anything less is a real bug and the process exits non-zero.

23/23 cases passed.
All 23 eval cases + concurrency check passed (deterministic gate: 100%).

Configuration

Zero config by default. Optional environment variables:

Variable

Effect

TURSO_DATABASE_URL

Use a hosted libSQL/Turso database instead of the default ~/.tessera-mcp/data.db

TURSO_AUTH_TOKEN

Auth token for the hosted database above

TESSERA_NOW

Override the demo's fixed reference clock (ISO 8601); defaults to 2026-07-16T12:00:00Z so the eval stays deterministic. An invalid value is ignored with a stderr warning

EVAL_DB_URL

Database the eval uses (default file:eval.db). The eval ignores TURSO_DATABASE_URL entirely so it can never wipe real data

EVAL_ALLOW_REMOTE

Set to 1 to allow the eval to target a remote (non-file:) URL; otherwise remote eval URLs are refused

CLI flags: --seed (create + seed, then exit), --db <path> (use a specific SQLite file), --help. The default database is ~/.tessera-mcp/data.db, auto-seeded on first run.

See env.example. This project never reads or writes .env files.


The Tessera universe

Tessera is a fictional B2B workspace SaaS used across a family of portfolio demos. Sibling repos:

All data here is synthetic. See DECISIONS.md for architecture rationale and VIDEO-SCRIPT.md for the demo narration.

License

MIT

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NDilanka/tessera-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server