Skip to main content
Glama
aayushsinghm16

harbor-mcp-server

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.


Related MCP server: sql-explorer-mcp

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.

Available Tools

8 tools
harbor_customer_360Full customer pictureA
Read-onlyIdempotent

Everything about one customer in a single call: profile, subscription, recent invoices, open tickets and 90-day usage totals.

Use this instead of four separate queries when a human asks about an account.

Args:

  • customer_id (string): exact id, format cus_0042

Returns JSON: { "customer": { id, company_name, contact_name, email, country, industry, employee_count, signed_up_at, churned_at, churn_reason, status }, "subscription": { plan, tier, status, seats, mrr_cents, started_at, trial_ends_at, canceled_at } | null, "billing": { invoices_total: number, paid_cents: number, refunded_cents: number, failed_count: number, recent: object[] }, "support": { open_count: number, resolved_count: number, avg_csat: number | null, recent: object[] }, "usage_90d": [ { feature: string, events: number, quantity: number } ] }

Example: "Why is Kestrel Robotics unhappy?" -> find_customer, then this tool; the open tickets and failed invoices usually answer it.

Error: returns an error naming the id if no such customer exists.

ParametersJSON Schema
NameRequiredDescriptionDefault
customer_idYesExact customer id. Use harbor_find_customer if you only have a name.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Although annotations already declare readOnlyHint=true and idempotentHint=true, the description adds valuable behavioral context: it specifies the exact return structure in JSON, indicates that an error names the id if the customer does not exist, and notes that it aggregates data from multiple sources. This goes beyond the annotations and helps the agent understand side effects (none) and failure modes, which is particularly useful given the absence of an output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-organized and front-loaded: a one-sentence summary, usage guidance, args, return schema, example, and error behavior. Every section serves a purpose. The detailed JSON return structure is necessary because there is no output schema, and the example clarifies the tool's role in a broader workflow. Nothing is wasted or redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema and moderate complexity (aggregating five data areas), the description is exceptionally complete. It covers the purpose, usage context, input format, full return structure, an example workflow, and error behavior. It also integrates gracefully with sibling tools by referencing harbor_find_customer. There are no significant gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already fully describes the single parameter customer_id with a pattern and explicit instruction to use harbor_find_customer if only a name is available. The description's 'Args' section repeats the same information ('exact id, format cus_0042') without adding new meaning. With 100% schema coverage, the baseline is 3, and the description adds no additional parameter semantics.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific and comprehensive statement: 'Everything about one customer in a single call: profile, subscription, recent invoices, open tickets and 90-day usage totals.' This clearly identifies the tool as an aggregation endpoint and differentiates it from siblings like harbor_find_customer (which locates IDs) and harbor_revenue_summary (which focuses on revenue). The verb 'get' is implied by 'Everything about one customer', making the purpose unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit guidance is provided: 'Use this instead of four separate queries when a human asks about an account.' The example 'Why is Kestrel Robotics unhappy?' -> find_customer, then this tool illustrates the intended workflow and contrasts with harbor_find_customer. This clearly communicates when to use the tool and what to do before calling it, fulfilling the dimension fully.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_describe_tableDescribe a Harbor tableA
Read-onlyIdempotent

Return the column list, types, nullability and primary key for one table.

Args:

  • table (string): one of customers, subscriptions, plans, invoices, tickets, usage_events

Returns JSON: { "table": string, "note": string, "columns": [ { "name": string, "type": string, "nullable": boolean, "primary_key": boolean, "masked": boolean } ] }

"masked" marks columns whose values are partially redacted on the way out.

Example: use before writing a query that filters on a column you have not seen yet. Error: returns an error listing valid tables if the name is not readable.

ParametersJSON Schema
NameRequiredDescriptionDefault
tableYesName of the table to describe. Must be one of the readable tables.

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses important behavioral details not captured by annotations: the 'masked' field indicates partially redacted values, and errors return a list of valid tables. It also details the exact return JSON. There is no contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a one-line summary, labeled args, returns JSON, example, and error behavior. Every sentence earns its place and there is no redundant filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema in the tool definition, the description fully documents the return JSON structure, including column attributes and masking semantics. It also covers error behavior and an example usage, making it complete for a 1-parameter tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already fully describes the parameter with an enum and description. The description repeats the allowed values but adds no extra semantic meaning beyond reminding the user it is 'one table.' Schema coverage is 100%, so baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Return the column list, types, nullability and primary key for one table.' This is a specific verb+resource pairing and distinguishes the tool from siblings like harbor_list_tables and harbor_run_query.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides a clear use case: 'Example: use before writing a query that filters on a column you have not seen yet.' This gives context for when to use the tool, though it does not explicitly contrast with alternatives or mention exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_extend_trialExtend a trialA

Push a trialing subscription's end date out by a number of days. Requires two calls.

Call 1 — omit confirm_token to get a preview and a token. Call 2 — pass the token with identical arguments to apply it.

Args:

  • customer_id (string): format cus_0042

  • days (number): 1 to 30

  • reason (string): 4-200 chars

  • confirm_token (string, optional)

Returns for preview: { "stage": "preview", "confirm_token": string, "expires_in_seconds": number, "current_trial_ends_at": string, "new_trial_ends_at": string }

Returns for execute: { "stage": "executed", "customer_id": string, "new_trial_ends_at": string }

Refuses when the customer has no subscription, or the subscription is not in 'trialing' status — you cannot start a new trial for a paying or churned account.

ParametersJSON Schema
NameRequiredDescriptionDefault
daysYesDays to add to the trial. Maximum 30.
reasonYesWhy. Stored in the audit log.
customer_idYesCustomer whose trial should be extended.
confirm_tokenNoOmit on the first call to get a preview. Pass the returned token to execute.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes beyond annotations by explaining the mandatory two-call preview/execute flow, the refusal cases (no subscription or non-trialing status), and the exact structure of preview and executed responses. It clarifies the token-based confirmation mechanism that isn't evident from annotations or schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is structured with a one-sentence purpose, a clear call-by-call breakdown, parameter list, and return formats. Every section earns its place; there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a stateful two-call tool with no output schema, the description fully covers all behavioral aspects: request flow, return shapes, validation constraints, and error conditions. This is a complete specification for the agent to invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Though the schema already covers all four parameters, the description adds crucial context: confirm_token's role in the two-step flow, the day range, reason length, and the exact format for customer_id. It explains the meaning of omitting or passing the token, which the schema only hints at.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb phrase 'Push a trialing subscription's end date out' and clearly states the resource (trial subscription) and action (extend by days). This clearly differentiates from siblings like harbor_issue_refund or harbor_run_query.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly states when the tool is applicable (trialing subscription) and provides refusal conditions, but it doesn't name any alternative tool for extending trials or handling non-trialing customers. The two-call protocol is described explicitly, which is helpful context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_find_customerFind a customerA
Read-onlyIdempotent

Search customers by company name, contact name, email or id.

Use this to turn a human's phrasing ("the Kestrel account") into a customer id before calling harbor_customer_360.

Args:

  • query (string): partial name, partial email, or exact id

  • limit (number): 1-50, default 10

Returns JSON: { "matches": [ { "id": string, "company_name": string, "contact_name": string, "email": string, "country": string, "status": "active" | "churned", "signed_up_at": string } ], "count": number }

Emails are partially masked. Matching still works on the unmasked value, so searching "priya" finds her even though the response shows "pr***@...".

Returns an empty match list, not an error, when nothing matches.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNoMaximum matches to return.
queryYesPartial company name, contact name, email, or an exact customer id like cus_0042.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint and idempotentHint, but the description adds valuable context: email masking behavior, matching still works on unmasked values, and returning an empty match list instead of an error. These are non-obvious traits beyond what annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured and front-loaded: it states purpose, usage, arguments, return format, and edge-case behavior in a logical flow. Every sentence contributes useful information without redundancy. It is appropriately sized for a lookup tool with edge cases to clarify.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (2 params, no output schema) and rich annotations, the description covers all necessary aspects: input semantics, return structure, example usage context, and error behavior. It is complete enough for an agent to invoke the tool correctly without further assumptions.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema already provides full parameter descriptions (100% coverage), including the meaning of 'query' and 'limit'. The description largely repeats this information without adding new semantic detail. For example, both describe partial matches and exact ids. Thus it meets baseline but adds little beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool searches customers by multiple criteria (company name, contact name, email, or id) with a specific verb ('Search'). It also distinguishes itself from sibling tools by explicitly positioning it as a pre-step for harbor_customer_360, making its purpose unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use: 'Use this to turn a human's phrasing ... into a customer id before calling harbor_customer_360.' This provides a clear use case and differentiates from customer_360. It also notes the empty-list behavior, setting expectations for no-match cases.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_issue_refundIssue a refundA
Destructive

Refund part or all of a paid invoice. Requires two calls.

Call 1 — omit confirm_token. Returns a preview of exactly what would change, plus a token valid for 5 minutes. Call 2 — pass that token with identical arguments. The refund is written.

Args:

  • invoice_id (string): exact id, format inv_00123

  • amount_cents (number): 1 to 50000

  • reason (string): 4-200 chars, stored in the audit log

  • confirm_token (string, optional): omit first, then supply

Returns for the preview call: { "stage": "preview", "confirm_token": string, "expires_in_seconds": number, "invoice": { id, customer_id, amount_cents, already_refunded_cents, status }, "would_refund_cents": number, "resulting_refunded_cents": number }

Returns for the execute call: { "stage": "executed", "invoice_id": string, "refunded_cents": number, "total_refunded_cents": number }

Refuses when: the invoice does not exist, is not paid, or the refund would exceed the amount actually charged.

ParametersJSON Schema
NameRequiredDescriptionDefault
reasonYesWhy this refund is being issued. Stored in the audit log.
invoice_idYesExact invoice id to refund against.
amount_centsYesAmount to refund, in cents. Maximum 50000.
confirm_tokenNoOmit on the first call to get a preview. Pass the returned token to execute.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare destructiveHint=true and readOnlyHint=false, but the description goes far beyond that by explaining the preview/execute two-step flow, token expiry (5 minutes), audit log storage, return shapes for both stages, and refusal rules. This adds substantial behavioral context that the annotations alone do not convey, with no contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-organized with clear call labels, an Args list, return format examples, and refusal conditions. Every sentence conveys necessary information—no fluff or redundancy. The front-loaded 'Requires two calls' immediately signals the key complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (two-call flow, no output schema), the description covers all essential aspects: purpose, step-by-step usage, parameter semantics, return values for both preview and execution, and failure conditions. It is self-contained enough for an agent to invoke correctly without additional external docs.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although the input schema already covers all parameters at 100%, the description enriches them with practical formats (e.g., 'inv_00123'), ranges (1–50000 cents, 4–200 chars), and the crucial two-call semantics of confirm_token (omit, then supply). It clarifies the exact role of each parameter in the workflow, adding procedural meaning beyond the schema definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'Refund part or all of a paid invoice.' The specific verb 'refund' paired with the resource 'paid invoice' distinguishes it from all siblings (list, query, extend_trial, etc.). It also outlines the two-call execution model, making the purpose unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides strong contextual guidance by detailing the two-call sequence, explicit refusal conditions (invoice not found, not paid, or would exceed amount), and parameter constraints. However, it does not explicitly mention when to use this tool over alternatives, though no sibling serves a similar purpose. This is a minor gap, hence 4 rather than 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_list_tablesList Harbor tablesA
Read-onlyIdempotent

List every table an agent may read, with row counts and a one-line note on each.

Call this first when you do not already know the schema. It is cheap and it prevents guessing.

Args: none.

Returns JSON: { "tables": [ { "name": string, "rows": number, "note": string } ], "pii_masked": boolean // true when email/phone/address columns come back partially redacted }

Tables not listed here cannot be queried — harbor_run_query will reject them.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint, idempotentHint, etc.), the description discloses the return format (JSON with tables array and pii_masked flag), notes that PII may be partially redacted, and states that unlisted tables will be rejected. This important behavioral context is not present in the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise and well-structured: core purpose in the first line, then usage guidance, args, return JSON example, and a restriction note. Every sentence contributes meaningful information without redundancy, and the JSON example is compact.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Although no output schema is provided, the description fully documents the return structure with an inline JSON example, including the pii_masked flag. It also explains the closed-world behavior that unlisted tables cannot be queried. Combined with strong annotations, all aspects of the tool's behavior are specified.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema is empty, and the description explicitly states 'Args: none,' confirming there are no parameters. With zero parameters, the baseline is 4, and the description adds no further parameter details because none are needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with 'List every table an agent may read, with row counts and a one-line note on each,' which clearly identifies the action (list) and scope (all tables). It distinguishes itself from siblings like harbor_describe_table by focusing on all tables rather than a specific one.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly instructs 'Call this first when you do not already know the schema. It is cheap and it prevents guessing.' This is a clear when-to-use directive. It also mentions that tables not listed here will be rejected by harbor_run_query, which proactively warns against querying unknown tables.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_revenue_summaryRevenue summaryA
Read-onlyIdempotent

Revenue broken down by month, plan, country or industry.

group_by='month' reads collected revenue from paid invoices, net of refunds. The other groupings read current MRR from live subscriptions, which is a different question — one is history, the other is run-rate.

Args:

  • group_by ('month' | 'plan' | 'country' | 'industry'): default 'month'

  • from (string, YYYY-MM-DD, optional): only for group_by='month'

  • to (string, YYYY-MM-DD, optional): only for group_by='month'

Returns JSON for group_by='month': { "basis": "collected_revenue", "rows": [ { "month": "2026-07", "invoices": number, "gross_cents": number, "refunded_cents": number, "net_cents": number } ], "totals": { "gross_cents": number, "refunded_cents": number, "net_cents": number } }

Returns JSON for the other groupings: { "basis": "current_mrr", "rows": [ { "": string, "customers": number, "mrr_cents": number, "arpa_cents": number } ], "totals": { "customers": number, "mrr_cents": number } }

Examples:

  • "How much did we collect in Q2?" -> group_by='month', from='2026-04-01', to='2026-06-30'

  • "Which plan carries most revenue?" -> group_by='plan'

  • "Where are our customers?" -> group_by='country'

ParametersJSON Schema
NameRequiredDescriptionDefault
toNoEnd of the window, inclusive. Only applies to group_by='month'.
fromNoStart of the window, inclusive. Only applies to group_by='month'.
group_byNoDimension to break revenue down by: month (from invoices), or plan/country/industry (from live MRR).month

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and idempotentHint=true, and the description adds substantial behavioral context by detailing the two distinct data sources (collected revenue vs. current MRR), the return JSON structures for each mode, and the 'basis' field that indicates which metric is being reported. It also notes that from/to only apply to group_by='month', preventing misuse.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than typical but well-structured with clear sections (main line, mode clarification, Args, return formats, examples). It front-loads the core purpose and uses bullet-style lists. Some redundancy exists with the schema parameter descriptions, but the examples and return format details justify the length.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite having no output schema, the description fully documents the return JSON for both modes, including field names and types. It provides three usage examples covering the main group_by options. It is self-contained and sufficiently complete for an AI agent to select parameters and interpret results without external context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% per the provided context, so the baseline is 3. The description adds value by explaining the semantic difference between group_by values (historical invoices vs. live MRR), clarifying the default 'month', and providing concrete examples of using from/to with YYYY-MM-DD format. This goes slightly beyond the schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Revenue broken down by month, plan, country or industry', which immediately states the verb (broken down) and resource (revenue). It clearly distinguishes between the two modes: month (collected revenue from invoices) and plan/country/industry (current MRR), differentiating this tool from sibling tools like harbor_run_query and harbor_customer_360.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use each grouping: 'group_by='month' reads collected revenue from paid invoices' versus 'the other groupings read current MRR from live subscriptions', and explains these answer different questions. Examples like 'How much did we collect in Q2?' map to group_by='month' with date range. It does not explicitly name alternative sibling tools, but it clearly contextualizes when to invoke this tool and which parameters to choose.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

harbor_run_queryRun a guarded SQL queryA
Read-onlyIdempotent

Run a read-only SQL query against the Harbor business database.

Every query is parsed and checked before execution. It will be rejected if it:

  • is not a SELECT (no INSERT, UPDATE, DELETE, DROP, ATTACH, PRAGMA)

  • contains more than one statement

  • touches a table outside: customers, subscriptions, plans, invoices, tickets, usage_events

  • calls a file or extension function

  • would scan the whole invoices or usage_events table without an index

A LIMIT is added if you omit one, and lowered if you exceed 500.

Args:

  • sql (string): one SQLite SELECT statement

  • limit (number, optional): row cap, 1-500, default 50

  • response_format ('markdown' | 'json'): default 'markdown'

Returns JSON: { "rows": object[], // result rows, PII columns partially masked "columns": string[], "row_count": number, "tables_read": string[], "limit_applied": number, "limit_adjusted": boolean, // true if we added or lowered your LIMIT "truncated": boolean, // true if the cap was hit and more data exists "masked_columns": string[], "duration_ms": number }

Examples:

  • "How many customers churned?" -> SELECT COUNT(*) FROM customers WHERE churned_at IS NOT NULL

  • "Revenue by plan" -> SELECT p.name, SUM(s.mrr_cents) FROM subscriptions s JOIN plans p ON p.id = s.plan_id GROUP BY p.name

  • Don't use for: changing data. Use harbor_issue_refund or harbor_extend_trial.

Errors are returned with a "How to fix" line. Read it — it names the specific column, table or clause that caused the rejection.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYesA single SQLite SELECT statement. WITH ... SELECT is allowed. Anything else is rejected.
limitNoRow cap for this call. Defaults to 50, hard maximum 500.
response_formatNo'markdown' for a readable table, 'json' for machine-readable rows.markdown

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint/idempotent/destructive annotations, the description discloses rejection rules, automatic LIMIT adjustments, allowed tables, PII masking, truncation behavior, and the exact JSON return structure. This is rich behavioral context that the annotations alone do not provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average, but every section earns its place: bulleted rejection rules, parameter definitions, return JSON, examples, and error guidance. It is front-loaded with the core purpose and uses clear structure for readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex guarded SQL tool with no output schema, the description fully compensates by specifying the JSON return shape, error remediation, allowed tables, and query constraints. The agent has enough information to select and invoke the tool correctly and interpret results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Even though schema coverage is 100%, the description adds meaningful semantics: explains LIMIT adjustment behavior, default row cap, response_format effects, and shows example SQL strings. It clarifies implication of parameters beyond schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Run a read-only SQL query against the Harbor business database', using a specific verb and resource and making the guarded, read-only nature explicit. It also distinguishes itself from sibling write tools by explicitly directing mutation use to harbor_issue_refund or harbor_extend_trial.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides concrete examples of appropriate queries ('How many customers churned?', 'Revenue by plan') and explicit when-not guidance: 'Don't use for: changing data. Use harbor_issue_refund or harbor_extend_trial instead.' This gives the agent clear criteria for selecting this tool over alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 8 tool updatesv1.0.0
    • First observedharbor_customer_360
    • First observedharbor_describe_table
    • First observedharbor_extend_trial
    • First observedharbor_find_customer
    • First observedharbor_issue_refund
    • First observedharbor_list_tables
    • First observedharbor_revenue_summary
    • First observedharbor_run_query

TDQS

A4.6/5.0

Scored across 8 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: schema discovery, query execution, customer lookup, customer snapshot, revenue reporting, and write actions. Even the two write tools are unambiguous—one targets invoices, the other targets subscriptions.

Naming Consistency4/5

All tools share the consistent 'harbor_' prefix and snake_case convention, with most following verb_noun naming (list_tables, describe_table, run_query, find_customer, issue_refund, extend_trial). 'customer_360' and 'revenue_summary' deviate slightly from the verb-first pattern but remain clear and readable.

Tool Count5/5

Eight tools is well within the ideal range and each earns its place. The set covers schema discovery, querying, customer-specific lookups, a 360 view, revenue reporting, and two common business actions without excess.

Completeness4/5

The tool surface covers the core read/query workflow plus the most needed write operations (refund and trial extension). Minor gaps exist—there are no direct ticket management or subscription modification tools—but the run_query tool can access underlying tables and the 360 view provides recent support/invoice context.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    SQL Server MCP server with AST-based query validation, read-only safety, schema exploration, ER diagram generation, and DBA toolkit integration (First Responder Kit, DarlingData, sp_WhoIsActive).
    12
    6
    MIT
  • A
    license
    Not graded
    quality
    F
    maintenance
    Read-only MCP server for SQL databases (SQL Server, Postgres, SQLite) with multi-server support and three-layer safety using AST validation and linting.
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    A read-only MCP server for SQL Server that exposes metadata and guarded SELECT queries with PII masking and five-layer guards.
    5
    54 npm
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    MCP server for running read-only SQL queries across MySQL and PostgreSQL databases with safety guardrails, credentials stored outside source, and support for multiple databases.
    2
    MIT