Skip to main content
Glama
ivantagesam

OpsBridge MCP

by ivantagesam

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.

Related MCP server: SQLite MCP Server

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.

F
license - not found
Not graded
quality - not tested
C
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.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Runtime permission, approval, and audit layer for AI agent tool execution.

  • Deterministic compliance and vertical knowledge bases for autonomous agents. Free 24hr trial.

  • Pre-action allow/deny for AI agents. 24 statutes, 13 jurisdictions: EU AI Act, GDPR, DPDP.

View all MCP Connectors

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/ivantagesam/opsbridge-mcp'

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