Skip to main content
Glama
aayushsinghm16

harbor-mcp-server

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
HARBOR_DB_PATHNoDatabase location. Use ':memory:' for a throwaway../harbor.db
HARBOR_REVEAL_PIINoReturns email, phone and address unmasked.false
HARBOR_ALLOW_WRITESNoEnables issue_refund and extend_trial.false

Capabilities

Features and capabilities supported by this server

CapabilityDetails
tools
{
  "listChanged": true
}

Tools

Functions exposed to the LLM to take actions

NameDescription
harbor_list_tablesA

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.

harbor_describe_tableA

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.

harbor_run_queryA

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.

harbor_find_customerA

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.

harbor_customer_360A

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.

harbor_revenue_summaryA

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'

harbor_issue_refundA

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.

harbor_extend_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.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

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/aayushsinghm16/harbor-mcp-server'

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