# MCP Server

> Connect AI agents to Qualy through the Model Context Protocol (MCP) server.

Qualy runs a remote [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that lets AI agents work with your Qualy data through a curated set of tools. Connect a client such as Claude Desktop, Cursor, or Windsurf — or build your own agent — and it can look up contacts, create payment requests, search your data, and more, all scoped to your tenant.

## Endpoint

The server speaks the **Streamable HTTP** MCP transport at:

```
https://api.qualyhq.com/v1/mcp
```

There are two ways to authenticate:

**Sign in with Qualy (OAuth)** — for clients with a connectors UI (claude.ai, Claude Desktop, Claude Code, and any client that supports MCP authorization). Add the server URL with **no credentials**; your browser opens a Qualy sign-in (email + password, plus your usual two-factor code), you review what you're connecting, and you're done. No API key to create or store. The connection acts **as you** — it can see and do exactly what your Qualy user can, nothing more.

**API key** — for headless agents, scripts, and clients where you configure headers by hand. Pass your [Qualy API key](/docs/api-keys.md) as a bearer token:

| Header | Value |
|--------|-------|
| `Authorization` | `Bearer <your-api-key>` |

The tenant is encoded in your login or key, so MCP needs **no `X-TENANT-ID` header**. (`Authorization: ApiKey <key>` is also accepted, and if your client happens to send `X-TENANT-ID` it's cross-checked against the credential.) Either way, every tool call runs with the permissions of the connected user and is scoped to their tenant.

---

## Connect a client

### Claude (sign in — no key)

Add Qualy once as a **custom connector on your Claude account**, at [claude.ai/settings/connectors](https://claude.ai/settings/connectors) → **Add custom connector** → paste `https://api.qualyhq.com/v1/mcp`, leaving every other field empty. Your browser opens a Qualy sign-in and consent screen; approve it and the tools are available.

Because the connector lives on your account, it follows you into **Claude chat, Claude Desktop, Cowork and Claude Code** — you set it up once. On Team and Enterprise plans an Owner can add it for the whole organisation under **Admin settings → Connectors**; everyone else then only clicks **Connect** and signs in.

See [Connect Qualy to Claude](/docs/connect.md) for the step-by-step version to hand a non-technical colleague.

To revoke a connection later, remove it in your client — or sign out of all sessions in the Qualy Dashboard, which disconnects every AI client at once.

#### Claude Code: connector or `claude mcp add`?

Claude Code can reach Qualy either way, and they don't reach the same places:

```bash
claude mcp add --transport http qualy https://api.qualyhq.com/v1/mcp
```

| | Account connector | `claude mcp add` |
|---|---|---|
| Claude chat, Desktop, Cowork | yes | no |
| Claude Code | yes | yes |
| Scope | your Claude account | `local` (this folder, default), `project` (committed `.mcp.json`), or `user` (all your folders) via `-s` |
| Auth | sign in with Qualy | sign in, or an [API key](/docs/api-keys.md) header |

Reach for `claude mcp add` when you want Qualy pinned to a specific codebase — `-s project` writes a `.mcp.json` your teammates get on clone, which is useful for a repo whose whole job is talking to Qualy. For everything else the account connector is the better default: one setup, and it's there in Cowork and on your phone too.

### Other clients

Most other MCP clients connect by adding an entry to their MCP configuration. Clients that support MCP authorization will also walk you through the same browser sign-in when you omit the header; otherwise, use an API key.

#### Clients with native remote MCP support

Cursor, VS Code, Zed and similar clients can point at the remote server directly:

```json
{
  "mcpServers": {
    "qualy": {
      "url": "https://api.qualyhq.com/v1/mcp",
      "headers": {
        "Authorization": "Bearer pk_prod_your-api-key"
      }
    }
  }
}
```

> **Warning — Windsurf uses a different key**
>
> Windsurf (now Devin Desktop) expects **`serverUrl`**, not `url`. Copying the block above verbatim fails silently — the server is accepted but never connects. Use:
>
> ```json
> {
>   "mcpServers": {
>     "qualy": {
>       "serverUrl": "https://api.qualyhq.com/v1/mcp"
>     }
>   }
> }
> ```

#### Stdio-only clients

Clients that only launch local (stdio) servers can bridge to the remote server with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (drop the `--header` arguments to sign in with OAuth instead):

```json
{
  "mcpServers": {
    "qualy": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://api.qualyhq.com/v1/mcp",
        "--header",
        "Authorization: Bearer pk_prod_your-api-key"
      ]
    }
  }
}
```

After saving the configuration and restarting your client, the Qualy tools appear in its tool list.

Any MCP-compatible client connects the same way: point it at `https://api.qualyhq.com/v1/mcp` and either complete the sign-in flow or set `Authorization: Bearer <your-api-key>`. ChatGPT, Replit, Zed and others each have their own "add MCP server" / connectors screen — follow your client's MCP instructions (for example, [Cursor's MCP install links](https://cursor.com/docs/mcp/install-links)).

For clients and tools that consume the [MCP registry](https://registry.modelcontextprotocol.io) format, Qualy publishes a machine-readable entry — name, description, logo, and the remote endpoint — at [`https://docs.qualyhq.com/server.json`](https://docs.qualyhq.com/server.json).

> **Warning — Keep your key safe**
>
> If you configure an API key, your MCP configuration stores it in plain text on your machine. Treat it like any other secret — see [API key best practices](/docs/api-keys.md). Signing in with Qualy avoids this: the client holds short-lived tokens that you can revoke at any time.

### How it appears in your client

On connect, the server tells your client who it is — the name **Qualy**, a website, and the Qualy logo — so clients that support it show proper branding rather than a bare hostname. It also returns a short set of usage instructions that guide the assistant on how to use the tools (find before acting, confirm writes, and so on). You don't configure any of this; it comes from the server. Support varies by client, so exactly how much is shown (logo, description) depends on the app you connect from.

---

## Available tools

All tools are scoped to your tenant. Reads are safe to call freely; writes are rate-limited and de-duplicated.

### Contacts

| Tool | What it does |
|------|--------------|
| `contact_list` | List contacts (customers / leads / students), newest first; filter by email. |
| `contact_get` | Get one contact by id or email. |
| `contact_create` | Create a contact. Returns the existing contact if the email is already on file. |
| `contact_update` | Update a contact's name, phone, or email. |

### Payment intents

| Tool | What it does |
|------|--------------|
| `payment_intent_list` | List payment requests; filter by contact or status. |
| `payment_intent_get` | Get one payment intent, including its shareable payment link. |
| `payment_intent_create` | Create a payment request for a contact. |
| `payment_intent_cancel` | Cancel a payment intent. |
| `payment_intent_remind` | Email the contact a payment reminder — the same reminder the Dashboard sends. Customer-visible; agents confirm before sending. |

### Transactions

| Tool | What it does |
|------|--------------|
| `transaction_list` | List transactions (charges and refunds); filter by contact, payment intent, or status. |
| `transaction_get` | Get one transaction by its id. |

### Orders

| Tool | What it does |
|------|--------------|
| `order_list` | List orders; filter by contact. |
| `order_get` | Get one order by its id. |

### Partnerships

| Tool | What it does |
|------|--------------|
| `partnership_list` | List partnerships (agents / institutions / suppliers). |
| `partnership_get` | Get one partnership by id, code, or name. |
| `partnership_create` | Create a partnership. |

### Search

| Tool | What it does |
|------|--------------|
| `search` | Fuzzy search across contacts or partnerships by name, email, or keyword. |

### Analytics

Aggregations computed in the database — ask for totals, trends, and forecasts instead of having the agent page through lists. Amounts in analytics results are **integer minor units (cents)**, grouped by currency.

| Tool | What it does |
|------|--------------|
| `payment_intent_stats` | Payment analytics in 11 formats: consolidated totals, month-by-month, payer behavior, by-services, calendar-by-day, cancellations, customer payments by month, cash velocity, revenue by service, receivables forecast, and FX corridor margin. |
| `transaction_stats` | Per-currency transaction counts and totals, broken down by status or by type. |
| `payment_split_stats` | Partner-commission analytics: owed / paid / outstanding, monthly cohorts, per-partner and per-team breakdowns, reconciliation. |

### Payouts & payment splits

Payouts are read-only; splits can also be recorded.

| Tool | What it does |
|------|--------------|
| `payout_list` | List partner / supplier payouts; filter by status, partnership, method, or currency. |
| `payout_get` | Get one payout, including its FX details and failure reasons. Bank account numbers are always masked. |
| `payment_split_list` | List payment splits (partner commissions / revenue shares). |
| `payment_split_get` | Get one payment split, including amounts, approval, and payout linkage. |
| `payment_split_create` | Record a fixed-amount commission on a payment intent. Records what is owed — paying it still happens through payouts. |

### Customer subscriptions

Your customers' recurring payment schedules — not your own Qualy plan.

| Tool | What it does |
|------|--------------|
| `subscription_list` | List customer payment schedules; filter by status, mode, or contact. |
| `subscription_get` | Get one payment schedule, including cadence, amounts, and charge stats. |
| `subscription_create` | Create a recurring payment schedule for a contact — Qualy raises a payment request each cycle. |

### Refunds & disputes

Disputes are read-only. Refunds can be **started** — never executed: a created
refund is a draft that a human must approve in the Dashboard before any money
moves.

| Tool | What it does |
|------|--------------|
| `refund_intent_list` | List refund requests and their approval / settlement state. |
| `refund_intent_get` | Get one refund request with per-item amounts, fees, and taxes. |
| `refund_intent_create` | Start a refund against a settled charge, as a draft for human approval. |
| `dispute_list` | List disputes (chargebacks); `open: true` narrows to those still needing attention. |
| `dispute_get` | Get one dispute, including its reason, evidence, and due dates. |

### Your Qualy billing

Your own Qualy subscription and invoices — what you pay Qualy, not your customers' payments. Requires an admin user.

| Tool | What it does |
|------|--------------|
| `billing_invoice_list` | List your Qualy subscription invoices. |
| `billing_subscription_list` | Your Qualy subscription status and plans. |
| `billing_stats` | Account balance, currency, and the upcoming invoice preview. |

> **Note — What's not exposed**
>
> Nothing on the MCP surface **executes** money movement: no capturing charges, no approving or executing refunds, no creating or sending payouts. Creates raise requests or drafts — a payment request the payer settles through a secure link, a refund draft a human must approve. Executing operations still go through the [API](/docs/authentication.md) or the Dashboard.

---

## Smart references

Wherever a tool takes a **contact** or **partnership**, you can pass a natural identifier instead of a Mongo ObjectId — Qualy resolves it to the right record before the tool runs. An agent that only knows a student's email or a partner's name doesn't need to look up an id first. This is the same [Smart references](/docs/smart-references.md) resolution the REST API uses, applied to the MCP tools' reference fields.

| Field | Accepts |
|-------|---------|
| `contact` | a contact id, **or** the contact's email (case-insensitive) |
| `partnership` | a partnership id, code, exact name, **or** a unique name prefix |

### Examples

Get a contact by email instead of an id — `contact_get`:

```json
{ "contact": "camila@example.com" }
```

Raise a payment request straight from the payer's email, with no lookup step — `payment_intent_create`:

```json
{
  "contact": "camila@example.com",
  "currency": "AUD",
  "items": [{ "name": "Tuition — Semester 1", "amount": 12000 }]
}
```

Fetch a partner by name (or its code) — `partnership_get`:

```json
{ "partnership": "Sydney English College" }
```

The same `contact` resolution applies to the contact filter on `payment_intent_list`, `transaction_list`, and `order_list`.

### When a reference is ambiguous

If a name or prefix matches more than one record, the tool returns an `AGENT_TOOL_AMBIGUOUS_REFERENCE` error listing the candidate records, so the agent can retry with something more specific (an exact name, the partnership code, or an id). If nothing matches, it returns `AGENT_TOOL_REFERENCE_NOT_FOUND`.

For automated flows, prefer stable identifiers — email, partnership code, or id — over name prefixes, which can drift into ambiguity as new records are added.

---

## When to use MCP vs the API

The MCP server and the [REST API](/docs/authentication.md) run on the same platform, the same data, and the same API-key auth — they're two front doors to the same back office. Pick by how your integration is driven:

**Use the MCP server when an AI assistant is your interface.** A person or agent asks in natural language and the model decides which tools to call, in what order, with you approving anything that writes. It exposes a curated, tenant-scoped subset of the platform — ideal for the long tail of ad-hoc questions and tasks ("find this student, draft a plan, chase that invoice") without writing or maintaining integration code.

**Use the REST API when you're building a programmatic integration.** You get the full endpoint surface, deterministic control over every call, [webhooks](/docs/webhooks.md), [idempotency keys](/docs/idempotency.md), and conveniences like [Smart references](/docs/smart-references.md) — sending a contact email, partnership code, or service name where an endpoint expects an ObjectId. (MCP tools accept Smart references too, so this isn't a reason to choose one over the other.)

Many teams use both — an assistant for day-to-day operators, the API for their own product surface. They never conflict: every call, from either door, runs through the same tenant scoping, permissions, and platform controls.

---

## Build your own agent

To drive the server from your own agent, connect with any MCP-compatible client library. With the official TypeScript SDK:

```javascript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(
  new URL('https://api.qualyhq.com/v1/mcp'),
  {
    requestInit: {
      headers: {
        Authorization: 'Bearer pk_prod_your-api-key',
      },
    },
  },
);

const client = new Client({ name: 'my-agent', version: '1.0.0' });
await client.connect(transport);

// Discover the tools…
const { tools } = await client.listTools();

// …and call one.
const result = await client.callTool({
  name: 'contact_list',
  arguments: { limit: 5 },
});

console.log(result);
```

---

## Behavior and limits

- **Tenant scoping** — every call runs against your tenant only; the tenant is taken from your credentials.
- **Permissions** — each tool requires the matching view/edit permission on the connected user (for example, `payout_list` requires the splits view permission). A call the user isn't allowed to make returns `AGENT_TOOL_FORBIDDEN`. OAuth connections act as the signed-in user; API keys carry their own user's permissions.
- **Authoritative totals** — every `*_list` result includes `count` (the total matching your filter) and `hasMore`. Agents should read those instead of counting pages; for sums and trends, the `*_stats` tools aggregate in the database.
- **Rate limits** — calls are rate-limited per tool. Reads have a generous budget; analytics and billing are tighter. Exceeding a limit returns an `AGENT_TOOL_RATE_LIMITED` error with a retry hint.
- **Idempotency** — write tools (create / update / cancel) are idempotent: repeating the same call with the same input within 24 hours returns the original result instead of creating a duplicate.
- **Structured errors** — tool errors come back with a `code` and `message` so your agent can react — for example, retry on a rate limit or re-prompt on invalid input.
- **OAuth session lifetime** — sign-in tokens are short-lived and refresh automatically; your client re-prompts to sign in only if the connection is revoked or long unused. Signing out of all Qualy sessions disconnects AI clients too.
