# Connect an MCP client

> Connect Claude, Cursor, Windsurf, Lovable, or a custom client to the Caramel MCP server using OAuth 2.0 + PKCE.

The Caramel MCP server lets AI clients call Caramel tools — list businesses, submit forms, upsert contacts, and more — directly from a chat or coding assistant. The server speaks Streamable HTTP + JSON-RPC 2.0.

| Field | Value |
|---|---|
| **Server URL** | `https://app.caramelme.com/api/functions/caramel-mcp` |
| **Transport** | Streamable HTTP, JSON-RPC 2.0, POST-only |
| **Authentication** | OAuth 2.0 + PKCE (S256), magic-link login |
| **Access token lifetime** | 1 hour |
| **Refresh token lifetime** | 30 days, rotates on use |

## Before you begin

- Create a Caramel account at [caramelme.com](https://caramelme.com). The free Starter tier is enough to test the connection and call read-only tools.
- Confirm your client can handle an OAuth redirect. Claude, Cursor, Windsurf, and Lovable handle this for you. Custom server-side clients need to host a redirect handler.

## Connect from Claude

Claude.ai, Claude Desktop, and Claude Code support MCP natively. No code required.

1. Open connector or integration settings in your Claude client.
2. Add a new MCP server.
3. Enter the URL: `https://app.caramelme.com/api/functions/caramel-mcp`
4. Click **Connect**. Your browser opens Caramel's login page.
5. Enter your Caramel account email. Caramel sends a magic link.
6. Click the link. The browser returns to Claude and the connection is live.

To verify, ask: "What can Caramel do?" or "List my Caramel businesses." Claude calls `caramel.v1.meta.capabilities` and lists the available tools.

> **Note** Dynamic Client Registration is pre-approved for Claude.ai and loopback redirect URIs (`http://localhost:<port>/callback`). Claude self-registers with no manual step.

## Connect from Cursor, Windsurf, or Lovable

All three use the same MCP server settings flow.

1. Open **Workspace Settings → Connectors → Add MCP server**.
2. Enter the URL: `https://app.caramelme.com/api/functions/caramel-mcp`
3. Click **Connect** and complete the magic-link OAuth flow.

Once connected, describe what you want in plain language. The assistant writes the JSON-RPC tool calls for you.

## Connect a custom client (OAuth by hand)

Use this path if you're building your own MCP client or calling the server from server-side code.

### Step 1 — Discover OAuth endpoints

```bash
curl https://app.caramelme.com/.well-known/oauth-authorization-server
```

Response (abridged):

```json
{
  "issuer":                 "https://api.caramelme.com",
  "authorization_endpoint": "https://api.caramelme.com/functions/v1/mcp-oauth",
  "token_endpoint":         "https://api.caramelme.com/functions/v1/mcp-oauth",
  "registration_endpoint":  "https://api.caramelme.com/functions/v1/mcp-oauth/register",
  "response_types_supported":            ["code"],
  "grant_types_supported":               ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported":    ["S256"],
  "token_endpoint_auth_methods_supported": ["none"]
}
```

### Step 2 — Register your client (Dynamic Client Registration)

POST your redirect URI to get a `client_id`. Skip this step if you were issued one manually.

```bash
curl -X POST https://api.caramelme.com/functions/v1/mcp-oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name":                "Your app",
    "redirect_uris":              ["http://localhost:3000/callback"],
    "grant_types":                ["authorization_code", "refresh_token"],
    "response_types":             ["code"],
    "token_endpoint_auth_method": "none"
  }'
```

Response (`201 Created`):

```json
{
  "client_id":    "claude-caramel",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types":  ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}
```

Registration only accepts redirect URIs on the server allow-list: `https://claude.ai/api/mcp/auth_callback` and `http://localhost:<port>/callback`. Any other hosted redirect URI returns `400 invalid_redirect_uri` — email `aymen@reactmotion.com` to have it added.

### Step 3 — Build the authorize URL (PKCE S256)

Generate a code verifier and challenge, then redirect the user:

```js
async function buildAuthorizeUrl(clientId, redirectUri, scope) {
  // Generate verifier — store in server-side session, NOT localStorage
  const verifier = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
  const hash     = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  const challenge = base64UrlEncode(new Uint8Array(hash));
  const state     = crypto.randomUUID();

  session.set('pkce_verifier', verifier);
  session.set('oauth_state',   state);

  const url = new URL('https://api.caramelme.com/functions/v1/mcp-oauth');
  url.searchParams.set('response_type',         'code');
  url.searchParams.set('client_id',             clientId);
  url.searchParams.set('redirect_uri',          redirectUri);
  url.searchParams.set('code_challenge',        challenge);
  url.searchParams.set('code_challenge_method', 'S256');
  url.searchParams.set('state',                 state);
  url.searchParams.set('scope',                 scope);
  return url.toString();
}

function base64UrlEncode(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
```

The user enters their email, receives a magic link, and is redirected to `REDIRECT_URI?code=AUTH_CODE&state=…`.

> **Important** Always verify the returned `state` matches what you sent. A mismatch is a CSRF attempt — discard the code.

### Step 4 — Exchange the code for tokens

```bash
curl -X POST https://api.caramelme.com/functions/v1/mcp-oauth \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$AUTH_CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "client_id=$CLIENT_ID" \
  -d "code_verifier=$VERIFIER"
```

Response:

```json
{
  "access_token":  "eyJ…",
  "refresh_token": "rt_…",
  "expires_in":    3600,
  "token_type":    "Bearer",
  "scope":         "meta:read forms:write audience:write"
}
```

### Step 5 — Call MCP tools

The server accepts JSON-RPC 2.0 POST requests. Pass the access token as a Bearer header.

**Smoke test — list your businesses:**

```bash
curl -X POST https://app.caramelme.com/api/functions/caramel-mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method":  "tools/call",
    "id":      1,
    "params":  { "name": "caramel.v1.business.list", "arguments": {} }
  }'
```

**Discover all available tools:**

```bash
curl -X POST https://app.caramelme.com/api/functions/caramel-mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method":  "tools/call",
    "id":      2,
    "params":  { "name": "caramel.v1.meta.capabilities", "arguments": {} }
  }'
```

> **Tip** Fetch the tool list at runtime via `caramel.v1.meta.capabilities` rather than hardcoding it. New tools appear automatically.

### Step 6 — Refresh before expiry

```bash
curl -X POST https://api.caramelme.com/functions/v1/mcp-oauth \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=$CLIENT_ID"
```

> **Important** Refresh tokens rotate on every use. Persist the new `refresh_token` from the response and discard the previous one. Using a stale refresh token invalidates the session — the user must re-authorize.

## Scopes

Request only what you need.

| Scope | What it allows |
|---|---|
| `meta:read` | Capability discovery and usage counters (always granted) |
| `forms:read` | List forms and read form schemas |
| `forms:write` | Submit form data |
| `audience:read` | List and read contacts (shipping soon) |
| `audience:write` | Upsert contacts |
| `messaging:send` | Trigger sends (coming soon) |
| `provisioning:write` | Domain and sender management |

A tool call that needs a scope your token lacks returns `403 scope_required`. There is no in-place upgrade — re-run the authorize flow with the additional scope added.

## Error reference

| Signal | Meaning | Action |
|---|---|---|
| JSON-RPC error code `-32001` | Access token expired | Refresh the token and retry the call once |
| HTTP `401` on refresh | Refresh token invalid or revoked | Re-run the full authorize flow |
| `400 invalid_redirect_uri` | URI not on allow-list | Email `aymen@reactmotion.com` to have it added |
| `403 tier_required` | Business tier too low | Upgrade at `caramelme.com/billing` |

Some error responses use the misspelled key `messsage` (three s's) instead of `message`. Normalize both in your client:

```js
const message = body.message ?? body.messsage ?? 'Unknown error';
```

## Token storage checklist

- Encrypt refresh tokens at rest. Never log them.
- Store PKCE verifiers and `state` in server-side session storage (encrypted cookies, etc.) — not `localStorage`.
- Discard verifiers and state parameters after a single use.
- Treat access tokens with the same care as refresh tokens if you persist them.

## Next steps

- [Build with Lovable](build-with-lovable) — use the MCP connection inside a Lovable app.
- [Connect a ChatGPT Custom GPT](connect-chatgpt-actions) — use the REST surface instead of MCP.
- [Tool reference](../reference/tools) — every tool name, parameters, and error codes.
- [Rate limits](../reference/rate-limits) — per-bearer caps and backoff guidance.
