MCP server giving AI agents safe, audited, read-mostly access to a subscription business database — SQL guardrails, PII masking, row caps and two-step confirmation on writes. Zero native dependencies.
Harbor demonstrates solid definition quality with well-structured tool schemas, clear descriptions, and thoughtful parameter constraints. All 8 tools have explicit input schemas with type definitions and descriptions. Tool names follow verb_noun convention (harbor_find_customer, harbor_issue_refund). Descriptions are substantive (100-300 chars), explaining WHAT the tool does and WHEN to use it. Parameter constraints are well-defined (regex patterns, min/max bounds, enums). Error handling includes recovery guidance ('returns an error naming the id if no such customer exists'). Two-step confirmation pattern on writes (preview/token) is excellent. Main gaps: output schemas are documented in descriptions but not as formal JSON Schema; some parameter descriptions could be more explicit about format/constraints; no batch variants for common multi-call patterns.
Retention and revenue by signup cohort (month).
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.
Extend a customer's trial period. Two-step process: 1. Call with preview=true to see what would happen and get a token 2. Call again with preview=false and the token to execute Args: - customer_id (string): exact customer id - days (number): how many days to extend, max 30 - reason (string): why the extension is being granted - preview (boolean): if true, return a preview and token; if false, execute - token (string): confirmation token from the preview (required if preview=false) Returns (preview=true): - preview: what would happen - token: use this in the confirmation call - expiresInSeconds: how long the token is valid Returns (preview=false): - success: true if the trial was extended - customer_id: the customer whose trial was extended - new_trial_end: the new trial end date Errors: - token expired, mismatch, or unknown - customer not found - customer not in trial - extension exceeds max (30 days)
Minimal descriptions for discovery tools (harbor_list_tables, harbor_revenue_summary, harbor_cohort_analysis). 'List all readable tables and their columns' and 'High-level revenue metrics' lack context for WHEN to call them or what structure they reveal. LLMs cannot infer whether to call these before or after other tools.
Output schemas documented only in prose descriptions, not as formal JSON Schema. Descriptions state 'Returns JSON { ... }' but no structured schema object is provided. LLMs cannot programmatically validate or plan downstream calls based on return types.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | D | 53 | 2025-06-18+ | v2 |
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.
Issue a refund for an invoice. Two-step process: 1. Call with preview=true to see what would happen and get a token 2. Call again with preview=false and the token to execute Args: - invoice_id (string): exact invoice id - amount_cents (number): refund amount in cents, max 50000 (USD 500) - reason (string): why the refund is being issued - preview (boolean): if true, return a preview and token; if false, execute - token (string): confirmation token from the preview (required if preview=false) Returns (preview=true): - preview: what would happen - token: use this in the confirmation call - expiresInSeconds: how long the token is valid Returns (preview=false): - success: true if the refund was issued - invoice_id: the invoice that was refunded - refunded_cents: amount refunded Errors: - token expired, mismatch, or unknown - invoice not found - refund amount exceeds max or invoice amount
List all readable tables and their columns.
Run a SELECT query against the database. The query is parsed and inspected before execution: - only SELECT is allowed (no INSERT, UPDATE, DELETE, DROP, etc.) - only readable tables are accessible - a LIMIT is enforced to cap output - PII columns are masked by default Args: - sql (string): a SELECT statement - limit (number): optional, 1-500, default 50 Returns: - rows: array of result objects - columns: column names - truncated: true if the row limit was reached - durationMs: query execution time Example: "SELECT * FROM customers WHERE country = 'US' LIMIT 10" Errors: - SqlGuardError: query rejected by the guard (e.g. disallowed table, banned function) - other errors: query syntax or execution failure
High-level revenue metrics: MRR, ARR, churn rate, ARPU.
harbor_query accepts free-form SQL with only guard-rail validation. While SqlGuardError is documented, the description does not explain what queries are rejected or provide examples of valid vs invalid queries. LLMs may attempt disallowed operations (INSERT, DROP, etc.) without clear guidance on constraints.
No batch variants for tools agents call in loops. If an agent needs to refund 5 invoices or extend trials for 10 customers, it must call harbor_issue_refund and harbor_extend_trial 5-10 times sequentially, wasting tokens and latency. A batch_issue_refunds or batch_extend_trials would be more efficient.
harbor_find_customer returns email addresses 'partially masked' (pr***@...) but description states 'Matching still works on the unmasked value'. This is confusing, LLMs may not understand that masked output still enables matching, or may attempt to unmask the email themselves.