MCP server
Endpoint & transport
https://<your-api-url>/mcp Authorization: Bearer rly_live_…
| Property | Value |
|---|---|
| Transport | Streamable HTTP (POST, GET, DELETE on one URL) |
| Session mode | Stateless — no Mcp-Session-Id, a fresh server instance per request |
| Auth | Bearer API key in the Authorization header (no OAuth flow) |
| Tools | 8, all with input and output schemas |
| Resources / prompts | None — the surface is tools only |
Because the server is stateless, there's no session to keep alive and no reconnect dance: every request is independently authenticated and independently isolated. Clients that only speak the deprecated HTTP+SSE transport, or that can't send a custom header, need the mcp-remote bridge shown under Claude apps below.
Connect a client
Create a key at API keys first — it's shown once. Every snippet below assumes it's in $RELAY_API_KEY.
Claude Code
claude mcp add --transport http relay https://<your-api-url>/mcp \ --header "Authorization: Bearer $RELAY_API_KEY" claude mcp list # should report: relay ✔ Connected
Claude apps (claude.ai and Claude Desktop)
Add Relay as a custom connector: Settings → Connectors → Add custom connector, paste the endpoint URL, and put Bearer $RELAY_API_KEY in the request-header field. Header authentication for custom connectors is rolling out gradually; if your account doesn't have it, use the local bridge below instead.
{
"mcpServers": {
"relay": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://<your-api-url>/mcp",
"--header", "Authorization: Bearer ${RELAY_API_KEY}"
],
"env": { "RELAY_API_KEY": "rly_live_…" }
}
}
}Cursor
{
"mcpServers": {
"relay": {
"url": "https://<your-api-url>/mcp",
"headers": { "Authorization": "Bearer rly_live_…" }
}
}
}VS Code
Put this in .vscode/mcp.json — VS Code prompts for the key on first use and keeps it out of the file. Headers set in a workspace .mcp.json are ignored, so use .vscode/mcp.json.
{
"inputs": [
{ "type": "promptString", "id": "relay-key", "description": "Relay API key", "password": true }
],
"servers": {
"relay": {
"type": "http",
"url": "https://<your-api-url>/mcp",
"headers": { "Authorization": "Bearer ${input:relay-key}" }
}
}
}Any other MCP client
{
"mcpServers": {
"relay": {
"type": "streamable-http",
"url": "https://<your-api-url>/mcp",
"headers": { "Authorization": "Bearer rly_live_…" }
}
}
}Verify it by hand
Because the server is stateless, you can list tools with one request — no initialize handshake required. Responses come back as a server-sent event stream.
curl -N -X POST https://<your-api-url>/mcp \
-H "Authorization: Bearer $RELAY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'For interactive debugging, point the official inspector at the same URL: npx @modelcontextprotocol/inspector.
Tool reference
8 tools. Each declares a JSON Schema for its input and its output, so clients validate structured results rather than parsing prose.
auth_statusaccounts_list| Field | Type | Description |
|---|---|---|
| platform | string? | Filter by instagram or tiktok Example: instagram |
post_create| Field | Type | Description |
|---|---|---|
| targets | string[] required | Account IDs to post to (from accounts_list) Example: ["9d1f…"] |
| caption | string required | Caption text, 1–2200 characters (ignored for stories) Example: "Fall drop is live — link in bio." |
| post_type | string? | feed | carousel | reel | story — inferred from media when omitted (stories never inferred) Example: reel |
| media | string[] | Up to 10 media IDs or public URLs (JPEG images, MP4/MOV videos) Example: ["https://cdn.example.com/fall.jpg"] |
| share_to_feed | boolean? | Reels only: also show the reel on the profile grid (Instagram defaults to true) Example: false |
| schedule_at | string? | ISO 8601 timestamp with offset — omit to publish now Example: "2026-08-04T18:30:00Z" |
post_status| Field | Type | Description |
|---|---|---|
| post_id | string required | Post ID returned by post_create Example: "3f0c…" |
post_update| Field | Type | Description |
|---|---|---|
| post_id | string required | Post ID Example: "3f0c…" |
| caption | string? | Replacement caption, 1–2200 characters |
| schedule_at | string? | New ISO 8601 publish time Example: "2026-08-05T09:00:00Z" |
post_cancel| Field | Type | Description |
|---|---|---|
| post_id | string required | Post ID Example: "3f0c…" |
posts_list| Field | Type | Description |
|---|---|---|
| status | string? | draft | scheduled | publishing | published | partly | failed | cancelled Example: scheduled |
| limit | number? | Posts per page, 1–100 (default 20) Example: 20 |
| cursor | string? | Opaque next_cursor from a previous page |
media_upload| Field | Type | Description |
|---|---|---|
| url | string required | Publicly reachable http(s) URL to fetch Example: "https://cdn.example.com/fall.jpg" |
Behavior annotations
Every tool states all four MCP behavior hints explicitly. The protocol's defaults are pessimistic — an unannotated tool is assumed to be a destructive, non-idempotent write — so declaring them lets a client run the reads unattended and save confirmation prompts for the one tool that deserves it.
| Annotation | Meaning | Tools |
|---|---|---|
| readOnlyHint | Changes nothing. Safe to call freely. | auth_status, accounts_list, posts_list, post_status |
| destructiveHint | Irreversible. Worth a confirmation prompt. | post_cancel |
| idempotentHint | Calling again with the same arguments has no extra effect. | the read tools, plus post_update |
| openWorldHint | Touches something outside Relay — Instagram, or an arbitrary URL. | post_create, media_upload |
post_create is deliberately not idempotent: two identical calls create two posts. Retry-safety is available on the REST API through the Idempotency-Key header, which an MCP tool call has nowhere to carry.Results & errors
Every tool returns both a JSON text block and structuredContent matching its declared output schema — so a client can render the text and a program can read the fields.
{
"content": [{ "type": "text", "text": "{ \"post\": { … } }" }],
"structuredContent": {
"post": { "id": "3f0c…", "status": "scheduled", "targets": [ … ] }
}
}- Domain errors — an unknown account ID, a caption over 2,200 characters, the wrong media count for a post type — come back as a tool result with
isError: trueand a human-readable message. The agent can read the message and fix the call itself. - Authentication failures are HTTP
401on the transport, before any tool runs. - Rate limits are HTTP
429withRetry-After— the same 120 requests/minute per key as the REST API, counted per MCP request. - Publish failures are never tool errors.
post_createsucceeds as soon as the post is queued; Instagram's verdict arrives later on the target timeline, so the agent must callpost_statusto know the outcome.
Agent workflow
The path that works, in order:
auth_status → confirm the key works and something is connected accounts_list → resolve a handle like "@northlight.co" to an account id media_upload → optional: store a file once, reuse it across posts post_create → queue the post (targets + caption + media [+ schedule_at]) post_status → poll until every target is published or failed
- Never guess an account ID or a post ID — resolve them with
accounts_listandposts_list. - Scheduling is not instant publishing:
post_createwithoutschedule_atstill goes out on the next scheduler tick, within a minute. - After creating a post, wait ~30 seconds before the first
post_statuscall. Video takes longer than images — minutes, not seconds. - Confirm with the user before
post_cancel, and before publishing anything without an explicit schedule.
Prompts to try
Posts an agent creates are tagged source: "mcp", so they're easy to spot in the dashboard and in GET https://<your-api-url>/v1/posts.