Skip to content

Connect an MCP client

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.

FieldValue
Server URLhttps://app.caramelme.com/api/functions/caramel-mcp
TransportStreamable HTTP, JSON-RPC 2.0, POST-only
AuthenticationOAuth 2.0 + PKCE (S256), magic-link login
Access token lifetime1 hour
Refresh token lifetime30 days, rotates on use
  • Create a Caramel account at 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.

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.

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.

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

Terminal window
curl https://app.caramelme.com/.well-known/oauth-authorization-server

Response (abridged):

{
"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)

Section titled “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.

Terminal window
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):

{
"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)

Section titled “Step 3 — Build the authorize URL (PKCE S256)”

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

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.

Terminal window
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:

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

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

Smoke test — list your businesses:

Terminal window
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:

Terminal window
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.

Terminal window
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.

Request only what you need.

ScopeWhat it allows
meta:readCapability discovery and usage counters (always granted)
forms:readList forms and read form schemas
forms:writeSubmit form data
audience:readList and read contacts (shipping soon)
audience:writeUpsert contacts
messaging:sendTrigger sends (coming soon)
provisioning:writeDomain 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.

SignalMeaningAction
JSON-RPC error code -32001Access token expiredRefresh the token and retry the call once
HTTP 401 on refreshRefresh token invalid or revokedRe-run the full authorize flow
400 invalid_redirect_uriURI not on allow-listEmail aymen@reactmotion.com to have it added
403 tier_requiredBusiness tier too lowUpgrade at caramelme.com/billing

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

const message = body.message ?? body.messsage ?? 'Unknown error';
  • 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.