OpsBridge MCP

OpsBridge MCP

Enables AI clients to search and retrieve customer and support-ticket data from a SQLite database, and to create support tickets only when an explicit approval flag is supplied, with all actions validated and audit-logged.

Category
Visit Server

README

OpsBridge MCP

A Model Context Protocol (MCP) server that gives an AI client controlled, auditable access to a business's customer and support-ticket data — including one real write action, gated by a server-enforced approval check rather than a prompt instruction.

This is a focused technical demonstration, not a product. It's a portfolio piece built to show one thing well: a correctly-implemented MCP server in TypeScript, with the specific engineering discipline that separates a demo that merely works from one that's actually safe to point an LLM at — schema validation, parameterized SQL, an approval gate enforced in application code, and an audit trail, all verified against the real SDK and the real protocol rather than assumed. It is not deployed anywhere, has no real customers, and is not claiming production readiness — see Limitations and What I'd change for production for exactly where that line is.

What problem this solves

AI clients are increasingly expected to take real actions on real systems, not just answer questions. That creates a specific engineering problem: how do you let a model read live business data and perform a consequential action, without either (a) giving it unrestricted database access, or (b) trusting the prompt to be the only thing standing between "the model suggested this" and "this actually happened"?

OpsBridge is a small, complete answer to that problem for one concrete case: a support-ticket system. It exposes exactly the data an AI assistant needs (customers, tickets), and exactly one way to change anything (create a ticket) — and that one write path cannot execute unless the caller explicitly supplies approved: true, checked in server code that runs regardless of what the model "decides." Everything else in the project — schemas, error handling, audit logging — exists to make that one guarantee actually trustworthy.

What MCP is doing in this architecture

The Model Context Protocol is the layer that lets an AI client (Claude Code, Claude Desktop, the MCP Inspector, or anything else that speaks MCP) discover what this server can do and call it, without any custom integration code per client. Concretely, in this project MCP is responsible for:

  • Tool discovery — the server advertises search_customers, get_customer, list_customer_tickets, and create_support_ticket, each with a JSON-Schema-described input and output, generated automatically from this project's Zod schemas.
  • A structured request/response contract — every tool call is validated against its schema before this project's code ever runs, and every response is either a normal result or a well-formed isError: true result — never a raw exception or a malformed reply.
  • Transport — JSON-RPC 2.0 over stdio. The client spawns node dist/index.js as a subprocess and talks to it over stdin/stdout; there's no network port.

MCP does not do any of the actual work — it's the reason a generic AI client can use this server at all without bespoke glue code. The business logic, validation, and safety guarantees are this project's own.

Architecture

flowchart TD
    Client["Claude Code / MCP Client"]
    Protocol["MCP Protocol<br/>(JSON-RPC over stdio)"]
    Server["OpsBridge MCP Server<br/>src/server.ts · src/index.ts"]
    Tools["Tool Layer<br/>src/tools/*.ts"]
    Approval["Approval / Validation<br/>src/domain/*.ts"]
    DB[("SQLite Database<br/>src/db/*.ts")]
    Audit["Audit Log (stderr)<br/>src/lib/audit.ts"]

    Client --> Protocol --> Server --> Tools --> Approval --> DB
    Tools -.->|every call, success or failure| Audit
src/
  db/        SQLite schema, synthetic seed data, idempotent seeding
  domain/    Repository functions (customers, tickets) — plain TS, no MCP knowledge
  tools/     One file per MCP tool: Zod schema, audit-log wrapper, thin handler
  lib/       Audit logging (lib/audit.ts) and typed error classes (lib/errors.ts)
  server.ts  Builds the McpServer and registers all tools
  index.ts   Entrypoint — opens/seeds the DB, connects stdio transport

The layering is deliberate and one-directional: each layer only knows about the one below it, and domain/ has no import of anything from @modelcontextprotocol/sdk — it's plain TypeScript operating on a better-sqlite3 database. That's what lets the test suite exercise the real, end-to-end tool-call path (a real MCP Client talking to a real McpServer) instead of mocking the layer boundaries. Full write-up, including exact code paths: docs/architecture.md.

Tools exposed

Tool Type Purpose
search_customers read Find customers by name or email (partial, case-insensitive)
get_customer read Fetch one customer's details by id
list_customer_tickets read List a customer's tickets, optionally filtered by status
create_support_ticket write Create a new ticket — requires explicit approved: true

Backed by SQLite with synthetic, fictional data: 10 customers, 18 seeded support tickets.

Technology stack

Layer Choice Why
Language TypeScript, strict mode + noUncheckedIndexedAccess / exactOptionalPropertyTypes Catches real bugs at the layer boundaries this project cares about (optional fields, indexed access)
MCP SDK @modelcontextprotocol/sdk 1.30.0 Current published major version — there is no v2 as of this writing; verified against the installed package's own .d.ts files rather than tutorials
Schema validation zod ^4 Single source of truth for both runtime validation and the JSON Schema sent to clients
Database better-sqlite3 ^12 (synchronous) No async driver/pool complexity for a single-process local server; ^12, not the newer 13.x, because 13.x requires Node 22+ and this project targets Node 20+
Runtime Node.js 20+ Stated project baseline
Tests vitest ^4 Connects a real MCP Client to a real McpServer over InMemoryTransport — see Testing
Lint eslint ^10 + typescript-eslint ^8 typescript-eslint doesn't yet support TypeScript 7 (the new Go-based compiler), so TypeScript is pinned to the 5.9.x line — a deliberate compatibility choice, not an oversight
Dev runner tsx Runs src/index.ts directly without a build step during development

Approval mechanism

create_support_ticket is the one consequential action in the system, so it's the one place this project adds a hard gate:

// src/domain/tickets.ts
export function createSupportTicket(db, input: CreateTicketInput): Ticket {
  if (input.approved !== true) {
    throw new ApprovalRequiredError(
      "Ticket creation was not approved. Set approved=true to confirm this action before it is created.",
    );
  }
  // ... only reaches the INSERT after this point
}

Two things make this an actual enforcement mechanism rather than a suggestion:

  1. It runs in the domain layer, below the MCP tool layer, before any SQL executes — there is no code path from the tool handler to the database INSERT that skips it.
  2. approved is a required boolean in the tool's input schema, not optional. Omit it and the call fails schema validation before this code even runs; pass false and it's rejected here.

The tool description also asks the model to confirm with the user first — but that's advisory text for the model's behavior, not what makes the system safe. The guarantee holds even if a model ignores the description and calls the tool directly; the server, not the prompt, is the last line of defense.

What this does not guarantee: that a human actually set the flag — approved: true is just another argument a model could supply on its own initiative, with no human ever seeing the request. Closing that gap fully would require the server to force an interactive confirmation round-trip back to a human (MCP elicitation); this project deliberately doesn't add that, since it's a real interaction-model change for a guarantee this project doesn't claim to provide. See Limitations.

Security considerations

  • Approval is enforced in application code, not the prompt — see above.
  • Every tool call is audit-logged to stderr (src/lib/audit.ts, applied at the tool layer via a withAudit() wrapper around all four tools): tool name, timestamp, success/failure, and a non-sensitive identifier (customer_id where applicable); create_support_ticket lines also record whether the call was approved. Never the sensitive content of a call — no ticket subjects/descriptions, no raw search query text, no email/phone/name.
  • All SQL is parameterized via better-sqlite3 prepared statements — no string concatenation, so there's no SQL injection surface even though input ultimately originates from an LLM. search_customers' LIKE pattern also escapes %/_ so search text is matched literally, not as a wildcard (otherwise a query of just "%" would return every row).
  • Input is validated with Zod before it reaches any business logic — length limits, enum constraints on priority/status — rejecting malformed input with a clear error instead of passing it through.
  • Stored ticket text is framed as data, not instructions. subject/description are free-text, and a ticket created now is read back verbatim by a later list_customer_tickets call — a second-order prompt-injection vector. Response text explicitly notes that this content is stored customer input, not directives. This is a mitigation, not a guarantee.
  • No authentication or authorization. This is a local, single-user demo — anyone who can spawn the process has full access to every tool, including full customer PII. Explicitly out of scope here; would have to change before this pattern touched real, multi-tenant data.
  • No secrets anywhere in the project. No API keys, tokens, or credentials; the only external dependency is the local SQLite file, which is gitignored.

Example Claude interactions

Read-path prompts, once connected:

  • "Search for a customer named Chen."
  • "Get full details for customer cust_004."
  • "What open tickets does cust_005 have?"

The interesting one is the write path:

You: "Create a high-priority support ticket for cust_002 about their tracking numbers not syncing — but check with me before you actually create it."

Expected behavior: the model calls search_customers/get_customer as needed, then either asks you to confirm before calling create_support_ticket, or calls it once with approved false/omitted, gets rejected, and surfaces the proposed ticket back to you. Either way, nothing is written until you've actually agreed and the model calls it again with approved: true.

More scripted walkthroughs, including forcing the rejection path directly to see the raw enforcement message: docs/demo-script.md.

Local setup

Requires Node.js 20+.

npm install
npm run db:seed     # creates and seeds data/opsbridge.db (10 customers, 18 tickets)
npm run build        # compiles TypeScript to dist/
npm run dev           # runs src/index.ts directly with tsx (auto-seeds on first run)
# or, after `npm run build`:
npm start              # runs dist/index.js

The server communicates over stdio — no HTTP port, nothing to browse to directly.

Connecting to Claude Code: this repo includes a project-scoped .mcp.json (generated via claude mcp add opsbridge --scope project -- node dist/index.js, so it's exactly what the CLI itself produces, not hand-written). Build first, then approve it once:

npm run build
claude          # prompts to trust this project's .mcp.json server on first run — approve it
claude mcp list # should show: opsbridge: node dist/index.js - ✔ Connected

Connecting any other MCP client (Claude Desktop, etc.) — most read a JSON config with a command/args pair:

{
  "mcpServers": {
    "opsbridge": {
      "command": "node",
      "args": ["/absolute/path/to/opsbridge-mcp/dist/index.js"]
    }
  }
}

Poking at it manually without a full client — the MCP Inspector, version pinned deliberately (an unversioned npx @modelcontextprotocol/inspector can resolve to a stale cached build instead of the current release):

npx @modelcontextprotocol/inspector@2.3.0 node dist/index.js       # web UI
npx @modelcontextprotocol/inspector@2.3.0 --cli node dist/index.js -- --method tools/list   # headless

Testing

npm test        # vitest — 33 tests across 6 files
npm run typecheck
npm run lint

Tests connect a real MCP Client to a real McpServer over the SDK's InMemoryTransport, backed by a fresh in-memory SQLite database per test (tests/helpers.ts) — exercising the actual request → Zod validation → tool handler → response path a real client goes through, not just the domain functions in isolation. Coverage includes: successful and empty-result search, customer not found, ticket listing with/without a status filter, invalid input across every tool, ticket creation rejected both with approved: false and with approved omitted entirely, successful creation, duplicate-submission safety, LIKE-wildcard escaping, prompt-injection framing text, and audit-log content (including that PII never appears in a log line) for every tool.

Limitations

Deliberate scope cuts for a focused demo, not oversights:

  • No authentication, authorization, or per-user data scoping — see Security considerations.
  • The approval flag isn't a verified human signal — it's a boolean a model could set on its own initiative; see Approval mechanism.
  • No pagination — search is capped at 10 results; ticket lists are unbounded but the dataset is tiny.
  • No update or delete tools — only ticket creation is a write action.
  • stdio transport only — no HTTP/SSE, no remote deployment story.
  • No rate limiting or idempotency key on create_support_ticket — a retried call creates a second, independent ticket rather than being deduplicated.
  • SQLite, single process — no connection pooling, no migration tooling beyond CREATE TABLE IF NOT EXISTS.
  • Audit log is a local stderr stream — not shipped anywhere, not queryable, no retention policy.

What I'd change for production

If this pattern were ever pointed at real customers instead of synthetic demo data:

  • Move off stdio to Streamable HTTP with OAuth bearer auth, scoped per tenant/customer — the SDK already supports this transport; today's stdio model implicitly trusts whoever can spawn the process, which is fine for a local demo and nowhere else.
  • Add real authorization mapping the authenticated caller to which customers/tickets they may touch — every tool is currently unscoped.
  • Make approval verifiable, not just present — use MCP elicitation to force a real round-trip confirmation back to a human, or require a short-lived token minted by a separate confirmation step outside the model's control.
  • Swap SQLite for Postgres with pooled connections and a real migration tool.
  • Ship the audit log somewhere durable and queryable (not stderr) with retention and access controls appropriate for what it's auditing.
  • Add rate limiting and an idempotency key on the write path.
  • Add pagination to search_customers and list_customer_tickets.
  • Add observability — latency, error rate, and call volume per tool.
  • Run typecheck/test/lint in CI on every change, not just locally on demand.

None of this is implemented here — the point of this project is to demonstrate the pattern correctly at small scale, not to pre-build infrastructure a real deployment would need but a demo doesn't.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured