loudly MCP server — connection guide
loudly exposes an MCP server (Model Context Protocol) so an AI agent can drive the daily
loop — read the stack, propose and edit cards, render assets, publish, and set up brand kits —
bound by exactly the same plan, role, severity, and credit rules as the app and the public API
(Rule 11 / PRD §12). It's the fourth thin adapter over the one service layer (lib/services/).
- Endpoint:
https://<your-domain>/api/mcp/mcp(Streamable HTTP). Local dev:http://localhost:3000/api/mcp/mcp. - Transport: Streamable HTTP only (SSE is dropped from the MCP spec since 2025-03-26; no Redis/KV needed).
- Source:
app/api/mcp/[transport]/route.ts; tools inapp/api/mcp/_lib/.
Two ways to authenticate
| Mode | For | How |
|---|---|---|
| API key (bearer) | Claude Code, Cursor, the Claude API mcp_servers connector, scripts |
mint an sk_live_… key in Settings → Developers, send it as Authorization: Bearer … |
| Clerk OAuth | the Claude app, ChatGPT, other consumer connector UIs | add a custom connector at the MCP URL and sign in — the client discovers Clerk and runs the OAuth flow |
Whatever the mode, the request resolves to a non-user mcp actor (it carries no user id),
so owner-gated capabilities (team / billing / settings-as-owner) are unreachable by
construction, and there is no approval-decision tool.
Why two modes: the Claude app's connector UI accepts OAuth fields only — it has no field for a pasted bearer token — so OAuth is required to reach it. Dev clients and scripts are simplest with a key. (Unkey is not involved; Clerk is the OAuth authorization server.)
Connect from Claude Code (API key)
# mint a key first: Settings → Developers → Generate key (shown once)
claude mcp add --transport http loudly \
https://<your-domain>/api/mcp/mcp \
--header "Authorization: Bearer sk_live_YOURKEY" \
--scope local
- Use
http://localhost:3000/api/mcp/mcpfor local dev. --scope localwrites the config (with the key) to your local~/.claude.json, not the committed repo. Don't use--scope projectwith a key in the header — that would commit the secret.- Restart Claude Code (exit and relaunch
claudein the repo) so themcp__loudly__*tools load. A server added mid-session is written to config but is not picked up by/mcpin the already-running session —/mcponly reflects servers loaded at startup. A detached dev server survives the restart. - Verify:
claude mcp list(orclaude mcp get loudly) →loudly … ✔ Connected, then callwhoami.
Connect from Cursor / other dev MCP clients (API key)
Add an HTTP (Streamable HTTP) MCP server pointing at the endpoint, with a header
Authorization: Bearer sk_live_…. Example mcp.json-style entry:
{
"mcpServers": {
"loudly": {
"type": "http",
"url": "https://<your-domain>/api/mcp/mcp",
"headers": { "Authorization": "Bearer sk_live_YOURKEY" }
}
}
}
Connect from the Claude app / ChatGPT (Clerk OAuth)
- In the app, add a custom connector and paste the MCP URL
(
https://<your-domain>/api/mcp/mcp). - The client fetches
/.well-known/oauth-protected-resource, discovers Clerk as the authorization server, and runs OAuth 2.1 (auth-code + PKCE). You'll be redirected to sign in and consent; the app stores and refreshes the token. - The tools appear once connected.
One-time setup (admin):
- The MCP server must be deployed at a public https URL so the
/.well-known/*discovery documents are reachable (they're served fromapp/.well-known/oauth-protected-resourceand…/oauth-authorization-server, already proxy-public). - In the Clerk Dashboard → OAuth applications, enable Dynamic Client Registration — consumer MCP clients register themselves at runtime and won't connect without it.
Connect from a script (raw JSON-RPC)
KEY=sk_live_YOURKEY
curl -s -X POST https://<your-domain>/api/mcp/mcp \
-H "authorization: Bearer $KEY" \
-H "content-type: application/json" \
-H "accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
Responses are Streamable-HTTP framed (event: message\ndata: {…}).
Tools (24)
Each is a thin wrapper over an existing service use-case. Brand-scoped tools take an optional
brandId; omit it to use the workspace's default brand.
Read (read scope) — whoami · list_brands · get_today_stack · get_calendar ·
list_cards · get_card · get_performance · get_post_metrics · list_channels ·
get_credits · get_brand_kit
Write — card lifecycle (write scope) — propose_card (AI-draft a brand-grounded card) ·
edit_card_copy · set_card_channels (channels + optionally what each publishes as: a
formats map of per-platform format ids, e.g. { "facebook": "fb-story" } — feed/Story/Reel
on FB/IG; Reels need a video creative) · schedule_card · reschedule_card ·
unschedule_card · dismiss_card (the reason trains the next stack)
Write — generation + publish (write scope) — render_card_asset (with no steer and no style
it's the idempotent ensure-rendered call, 0 credits on a repeat; a steer and/or a style override
is a deliberate paid regen, 1 credit per call) · publish_card (runs the pre-publish severity
check first; exactly-once per channel)
Write — brand-kit setup (write scope) — create_brand_kit (plan-capped) ·
update_brand_kit (includes visualStyle — its optional preset picks the image-generation
style: one of the eight registry styles — editorial-photo, flat-typographic-poster, flat-illustration, minimal-product, warm-lifestyle, matte-3d, bold-poster, paper-collage — or custom, the derived-from-references contract) ·
set_product_override · remove_reference_image
Call whoami first to confirm the connection and see the resolved workspace, plan, active
brand, and granted scopes.
Scopes + guardrails
- Keys carry
readand/orwrite. Read tools needread; every mutation needswrite. - Plan gating (Solo/Growth), the pre-publish severity gate, the credit ledger, and tenant
isolation are enforced in the service layer — an agent is bound by the same rules as a person
in the UI. A blocked publish returns
blockedwith a report rather than a silent success.
Rate limits
Every tool call spends the caller's per-minute budget — the same fixed-window limiter as the
REST API: reads 120, writes 30, sensitive
(render_card_asset + publish_card) 10, scaled by plan (Growth ×2). An sk_live_ key
spends one blended budget across /v1 and MCP; an OAuth connection spends a per-workspace
budget. Over the ceiling, a tool returns an error result instead of data:
{ "error": "rate_limited", "retryAfter": 34 }
Back off retryAfter seconds (the window is one minute); there are no X-RateLimit-* headers
at the tool layer. The credit ledger and idempotency are the real cost guards — this limiter is
the secondary abuse guard.
Troubleshooting
- 401 with a
WWW-Authenticatechallenge — no/invalid token. For the bearer path, check the key; for OAuth, follow theresource_metadataURL in the challenge. - Tools don't appear after
claude mcp add/ not shown by/mcp— MCP servers load at session start, so a mid-session add isn't reflected by/mcpin the running session. Restart Claude Code (exit and relaunch). Confirm the config landed withclaude mcp get loudly; a detached dev server survives the restart. - OAuth connect fails in the Claude app — confirm Dynamic Client Registration is enabled in the Clerk Dashboard and the server is on a public https URL.
Security
- Keys carry your workspace's full access — treat them like passwords, store them outside the
repo (
--scope local/ a secret manager), and revoke leaked keys in Settings → Developers. - OAuth tokens are issued and refreshed by the connector; revoke a connection from the app or the Clerk Dashboard.