harbor-mcp-server

harbor-mcp-server

A secure MCP server for querying a subscription business database with AST-based SQL guarding, PII masking, audited two-step write actions, and a seeded demo dataset.

Category
访问服务器

README

harbor-mcp-server

An MCP server that gives an AI agent access to a subscription business's database — without giving it the database.

Reads are parsed and allowlisted before they run. Personal data is masked on the way out. Writes require a preview and a confirmation token. Everything is written to an audit log the agent cannot read.

npx harbor-mcp-server

That is the whole setup — no native addon to compile, no database to provision. It ships with a seeded demo database of 140 customers, ~1,950 invoices, ~260 support tickets and 90 days of usage events, so there is nothing to configure before you can ask it a question.


Why this exists

"Let Claude query our database" is a two-line proof of concept and a genuinely hard production problem. The gap between them is not the SQL — it is everything you have to be sure of before you point a language model at a table containing real customers.

This server is the second thing, built small enough to read in one sitting.


What it refuses to do

Each of these is enforced by parsing the statement into an AST, not by pattern-matching the string. Regex checks for DROP are defeated by comments, casing and string literals; they are not used for the decision.

Attempt Result
DELETE FROM invoices Rejected — only SELECT is permitted
SELECT id FROM customers; DROP TABLE customers Rejected — statement stacking
SELECT id FROM customers -- x\n; DELETE FROM invoices Rejected — stacking hidden behind a comment
SELECT * FROM audit_log Rejected — table not on the allowlist
SELECT name FROM sqlite_master Rejected — schema introspection is via tools, not SQL
SELECT id FROM customers WHERE id IN (SELECT tool FROM audit_log) Rejected — disallowed table in a subquery
SELECT load_extension('evil.so') FROM customers Rejected — banned function
SELECT * FROM customers LIMIT 99999 Allowed, clamped to 500 rows
SELECT * FROM invoices Rejected — unbounded scan of a large table
SELECT SUM(amount_cents) FROM invoices GROUP BY ... Allowed — aggregates bound their own output

Every rejection carries a hint naming the specific table, column or clause that caused it, so the agent can correct itself rather than retrying blind:

Only SELECT is permitted; received "DELETE".

How to fix: This server is read-only. To change data, use issue_refund or
extend_trial, which require explicit confirmation.

What it masks

Email, phone and address columns come back partially redacted:

| company_name    | email                            |
| --------------- | -------------------------------- |
| Camden Digital  | ke***********@camdendigital.com  |
| Beacon Robotics | to************@beaconrobotics.com|

Masking happens on the way out, keyed on column name, which means it survives SELECT *, joins, and aliases. Filtering still works on the real value — searching priya finds her, the response just will not hand you her address book entry.

Set HARBOR_REVEAL_PII=true to turn it off.

How writes work

Writes are disabled unless the operator sets HARBOR_ALLOW_WRITES=true, and even then they cannot fire in one call.

Call 1 — no token. Nothing changes; you get a preview:

## Refund preview — nothing has changed yet

Invoice **inv_00002** for customer **cus_0001**
- Charged: $49.00
- Already refunded: $0.00
- **This refund: $10.00**
- After: $10.00 refunded of $49.00
- Reason: Service outage goodwill credit

To execute, call again with `confirm_token: "DJ7-O9d14gtk"`. Expires in 300s.

Call 2 — same arguments plus that token. Now it happens.

The preview is the point. It renders as plain text in the transcript, so a human reading along sees the exact amount and the exact invoice before anything is written. And because tokens live in process memory, a model that hallucinates a refund cannot execute one — it cannot invent a token that exists.

Tokens are single-use, expire in five minutes, and are bound to the exact arguments they were issued for. Replaying one with a larger amount_cents fails. A rejected attempt burns the token rather than letting an agent grind against it.

The audit log

Every call is recorded before the caller gets a response — allowed, denied or errored:

allowed  harbor_run_query      rows=  1  16ms  customers
denied   harbor_run_query      rows=  0   2ms  Only SELECT is permitted; received "DELETE".
allowed  harbor_issue_refund   rows=  0  66ms  preview
allowed  harbor_issue_refund   rows=  1  71ms  executed

The table is deliberately outside the allowlist. The agent writes to it by acting and cannot read, mine or edit it — so after a session you can answer "what did it actually do?" without trusting the agent's own account.


Tools

Tool Purpose
harbor_list_tables Readable tables, row counts, and a note on each
harbor_describe_table Columns, types, nullability, which are masked
harbor_run_query Guarded SELECT — the general-purpose escape hatch
harbor_find_customer Turn "the Kestrel account" into a customer id
harbor_customer_360 Profile, subscription, billing, tickets, usage in one call
harbor_revenue_summary Revenue by month, plan, country or industry
harbor_issue_refund Refund an invoice — two-step
harbor_extend_trial Extend a trial — two-step

One flexible query tool plus a few composite ones, rather than thirty narrow endpoints. An agent that can write SQL will out-compose any fixed set of endpoints; the guard is what makes that safe. The composite tools exist because some questions get asked constantly and deserve a single round trip.

harbor_revenue_summary also encodes a trap worth knowing about: trialing and canceled subscriptions carry mrr_cents = 0, so a naive AVG(mrr_cents) across all rows understates ARPA. The tool uses the right denominator so the agent does not have to know that.


Install

Claude Desktop

claude_desktop_config.json:

{
  "mcpServers": {
    "harbor": {
      "command": "npx",
      "args": ["-y", "harbor-mcp-server"]
    }
  }
}

To allow refunds and trial extensions:

{
  "mcpServers": {
    "harbor": {
      "command": "npx",
      "args": ["-y", "harbor-mcp-server"],
      "env": { "HARBOR_ALLOW_WRITES": "true" }
    }
  }
}

Claude Code

claude mcp add harbor -- npx -y harbor-mcp-server

From source

git clone https://github.com/aayushsinghm16/harbor-mcp-server
cd harbor-mcp-server
npm install
npm run build
npm test
node dist/index.js

Configuration

Variable Default Effect
HARBOR_DB_PATH ./harbor.db Database location. :memory: for a throwaway.
HARBOR_ALLOW_WRITES false Enables issue_refund and extend_trial.
HARBOR_REVEAL_PII false Returns email, phone and address unmasked.

Requires Node 22.5+. Hard limits live in src/constants.ts: 500 rows maximum, 50 by default, 25,000 characters per response, $500 maximum single refund, 30 days maximum trial extension.


Things to try

Point Claude at it and ask:

  • "Which customers churned, and what reasons did they give?"
  • "Show me revenue by month for the first half of 2026."
  • "Which plan carries the most MRR, and what's the average per account?"
  • "Tell me everything about the Kestrel account." — one customer_360 call
  • "Which accounts have both an open ticket and a failed payment?" — the churn-risk query
  • "Delete all the invoices." — watch it get refused, with a reason

The last one is the interesting one.


What this does not do

Being straight about the edges, because a security README that claims everything is a security README you should not trust.

Queries cannot be interrupted mid-flight. node:sqlite is synchronous and exposes no binding for sqlite3_interrupt, so the time budget is enforced by refusing expensive plans up front and by capping rows — not by killing a running query. Slow queries are logged, not stopped. If hard interruption is a requirement, execution needs to move to a worker thread that can be terminated. That is a deliberate trade, not an oversight.

Nothing is read from disk at runtime. The schema is a TypeScript module, not a .sql file, and there is no native addon. Both are the same lesson: a bundler tracing a serverless build follows import statements, not paths computed at runtime, so anything loaded by path is silently dropped and fails on the first request. Imports cannot go missing.

It needs Node 22.5 or newer. The database driver is node:sqlite, built into the runtime, so there is no native addon to compile and nothing for a bundler to lose while tracing a serverless build. The cost is a version floor and an ExperimentalWarning on stderr.

Masking is not anonymisation. Partial masks preserve enough structure to correlate rows. That is intentional — it is what makes the data still analytically useful — but it means the masking defends against casual exfiltration, not against a determined re-identification attack.

The allowlist is a table allowlist, not a row-level one. There is no per-tenant or per-user scoping. A real deployment against multi-tenant data needs row-level filtering injected into every query, which is a different and larger piece of work.

Confirmation tokens live in process memory. They do not survive a restart and are not shared across replicas. For a single stdio server that is correct; a horizontally scaled HTTP deployment would need shared storage.


Layout

src/
├── constants.ts          every safety boundary, in one file
├── db/
│   ├── schema.ts         six business tables plus the audit log, as a string
│   ├── connection.ts
│   └── seed.ts           deterministic — the same numbers on every machine
├── security/
│   ├── sql-guard.ts      AST parsing, allowlist, limit injection
│   ├── pii.ts            column-name-keyed masking
│   ├── confirm.ts        single-use, argument-bound tokens
│   └── audit.ts
├── services/
│   ├── query.ts          plan inspection and execution
│   └── format.ts         markdown/JSON rendering, truncation
└── tools/                one file per domain

51 tests cover the guard against statement stacking, comment-hidden injection, subquery smuggling, alias confusion, banned functions and limit evasion; the masking against SELECT * and joins; and the confirmation flow against replay, tampering and cross-tool reuse.

npm test

Licence

MIT.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选