Relay
DocsLog inStart free

MCP server

Relay speaks the Model Context Protocol, so an AI agent can list your accounts, upload media, compose, schedule, and check what happened — with the same permissions and validation as the REST API, behind the same key.

Endpoint & transport

https://<your-api-url>/mcp

Authorization: Bearer rly_live_…
PropertyValue
TransportStreamable HTTP (POST, GET, DELETE on one URL)
Session modeStateless — no Mcp-Session-Id, a fresh server instance per request
AuthBearer API key in the Authorization header (no OAuth flow)
Tools8, all with input and output schemas
Resources / promptsNone — 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.

An API key grants full publishing rights to every account its owner has connected. Treat it like a password: store it in your client's secret store or an environment variable, never in a file you commit.

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.

claude_desktop_config.json — bridge for clients without header auth
{
  "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

.cursor/mcp.json
{
  "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.

.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_status
read-onlyidempotent
Verify that the Relay API key is valid and list the Instagram accounts it is authorized to publish to. Call this at the start of a session, or whenever another tool reports an authentication error, to tell a bad key apart from a missing account connection.
Returns auth: { authenticated, key_name, key_prefix, key_created_at }. accounts: [{ id, platform, handle }] — a short summary; use accounts_list for health and follower counts.
accounts_list
read-onlyidempotent
List the social accounts this API key can publish to, with connection health, token expiry, and follower/media counts. The returned id values are what post_create takes as targets — always resolve a handle to an id here rather than guessing.
FieldTypeDescription
platformstring?Filter by instagram or tiktok Example: instagram
Returns accounts: [{ id, platform, handle, name, health, token_expires_in_days, connected_at, followers_count, media_count, stats_synced_at }]. health is connected, expiring (token dies within 7 days), reconnect_required, or disconnected — only connected and expiring accounts can be posted to.
post_create
writesopen-world
Create an Instagram post for one or more connected accounts, either published immediately or scheduled for later. Resolve target account IDs with accounts_list first. media takes Relay media IDs (from media_upload) or public https URLs, which are fetched and stored automatically. post_type: 'feed' (exactly 1 JPEG image), 'reel' (exactly 1 MP4/MOV video, optional share_to_feed), 'story' (1 image or video; Instagram shows no caption on stories), 'carousel' (2-10 images/videos). Omit post_type to infer it from the media (1 image → feed, 1 video → reel, 2+ → carousel; stories are never inferred). Omit schedule_at to publish on the next scheduler tick (under a minute); pass a future ISO 8601 timestamp to schedule. Publishing is asynchronous — this returns as soon as the post is queued, so poll post_status for the per-account outcome.
FieldTypeDescription
targetsstring[] requiredAccount IDs to post to (from accounts_list) Example: ["9d1f…"]
captionstring requiredCaption text, 1–2200 characters (ignored for stories) Example: "Fall drop is live — link in bio."
post_typestring?feed | carousel | reel | story — inferred from media when omitted (stories never inferred) Example: reel
mediastring[]Up to 10 media IDs or public URLs (JPEG images, MP4/MOV videos) Example: ["https://cdn.example.com/fall.jpg"]
share_to_feedboolean?Reels only: also show the reel on the profile grid (Instagram defaults to true) Example: false
schedule_atstring?ISO 8601 timestamp with offset — omit to publish now Example: "2026-08-04T18:30:00Z"
Returns post: the created post with status "scheduled", the resolved post_type, its media, and one entry in targets[] per account (each starting at status "pending"). Poll post_status for delivery results.
post_status
read-onlyidempotent
Fetch a single post with its per-account delivery timeline: the overall status plus, for every target, the Instagram media ID, permalink, publish time, and the exact error code and message when a target failed. This is the tool that answers "did it go out?" and "why did it fail?".
FieldTypeDescription
post_idstring requiredPost ID returned by post_create Example: "3f0c…"
Returns post: the full post view, including targets[] with status (pending, uploading, processing, published, failed, cancelled), platform_post_id, permalink, published_at, and error { code, message }.
post_update
writesidempotent
Edit a post that has not published yet — change the caption, move the scheduled time, or both. Only draft and scheduled posts can be edited; once a post enters publishing it is frozen. To change media or targets, cancel the post and create a new one.
FieldTypeDescription
post_idstring requiredPost ID Example: "3f0c…"
captionstring?Replacement caption, 1–2200 characters
schedule_atstring?New ISO 8601 publish time Example: "2026-08-05T09:00:00Z"
Returns post: the updated post view.
post_cancel
writesdestructive
Cancel a draft or scheduled post before it publishes. This cannot be undone and it cannot rescind a post that already went live — a post that has entered publishing, published, or failed is rejected. Confirm with the user before calling.
FieldTypeDescription
post_idstring requiredPost ID Example: "3f0c…"
Returns post: the cancelled post (status "cancelled"), with every target marked cancelled.
posts_list
read-onlyidempotent
List this account's posts newest-first, optionally filtered by status, with each post's media and per-account targets. Answers questions like "what is scheduled this week" or "which posts failed". Returns next_cursor when more pages exist — pass it back as cursor to continue; do not page beyond what the user asked for.
FieldTypeDescription
statusstring?draft | scheduled | publishing | published | partly | failed | cancelled Example: scheduled
limitnumber?Posts per page, 1–100 (default 20) Example: 20
cursorstring?Opaque next_cursor from a previous page
Returns posts: an array of post views. next_cursor: an opaque cursor string, or null when this is the last page.
media_upload
writesopen-world
Fetch an image or video from a publicly reachable URL, store it in Relay, and return a reusable media ID for post_create. Instagram only accepts JPEG images (up to 8MB) and MP4/MOV video (up to 100MB) — PNG is rejected at upload rather than at publish time. post_create also accepts public URLs directly, so reach for this tool when you want to reuse one asset across several posts, or validate a file before composing.
FieldTypeDescription
urlstring requiredPublicly reachable http(s) URL to fetch Example: "https://cdn.example.com/fall.jpg"
Returns media: { id, url, kind, content_type, size_bytes, width, height, created_at }. Pass id in post_create.media.

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.

AnnotationMeaningTools
readOnlyHintChanges nothing. Safe to call freely.auth_status, accounts_list, posts_list, post_status
destructiveHintIrreversible. Worth a confirmation prompt.post_cancel
idempotentHintCalling again with the same arguments has no extra effect.the read tools, plus post_update
openWorldHintTouches something outside Relay — Instagram, or an arbitrary URL.post_create, media_upload
Note that 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.

tools/call result
{
  "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: true and a human-readable message. The agent can read the message and fix the call itself.
  • Authentication failures are HTTP 401 on the transport, before any tool runs.
  • Rate limits are HTTP 429 with Retry-After — the same 120 requests/minute per key as the REST API, counted per MCP request.
  • Publish failures are never tool errors. post_create succeeds as soon as the post is queued; Instagram's verdict arrives later on the target timeline, so the agent must call post_status to 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_list and posts_list.
  • Scheduling is not instant publishing: post_create without schedule_at still goes out on the next scheduler tick, within a minute.
  • After creating a post, wait ~30 seconds before the first post_status call. 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

List my connected Instagram accounts and their token expiry.
Schedule this photo to Instagram for tomorrow at 9am: <image URL>
What’s scheduled for this week?
Did my last post go out? If it failed, explain why.
Post this video as a reel, but keep it off my profile grid.

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.

© 2026 Relaysupport@lantean.techPrivacyTerms