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://api.relay.lantean.tech/mcp

Authorization: Bearer rly_live_…
PropertyValue
TransportStreamable HTTP (POST, GET, DELETE on one URL). POST responses are plain JSON; the GET stream exists for protocol compliance but carries no server-initiated messages on this stateless server.
Session modeStateless — no Mcp-Session-Id, a fresh server instance per request
AuthBearer API key in the Authorization header (no OAuth flow)
Tools10, all with output schemas; every tool except auth_status also declares an input schema
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://api.relay.lantean.tech/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://api.relay.lantean.tech/mcp",
        "--header", "Authorization: Bearer ${RELAY_API_KEY}"
      ],
      "env": { "RELAY_API_KEY": "rly_live_…" }
    }
  }
}

Cursor

.cursor/mcp.json
{
  "mcpServers": {
    "relay": {
      "url": "https://api.relay.lantean.tech/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://api.relay.lantean.tech/mcp",
      "headers": { "Authorization": "Bearer ${input:relay-key}" }
    }
  }
}

Any other MCP client

{
  "mcpServers": {
    "relay": {
      "type": "streamable-http",
      "url": "https://api.relay.lantean.tech/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 plain JSON.

curl -X POST https://api.relay.lantean.tech/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

10 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 and TikTok 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, can_comment, can_direct_post, can_upload, 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. can_comment is false when an Instagram account has not granted the permission first comments need (always false on TikTok, which has no first comments); posting still works, the first comment is what gets skipped, and the user fixes it by reconnecting the account in the Relay dashboard. can_direct_post / can_upload say whether a TikTok account granted posting straight to its profile (tiktok.mode "direct") and sending to its inbox (tiktok.mode "inbox"); both are false on Instagram.
post_create
writesopen-world
Create a post for one or more connected Instagram and/or TikTok accounts: published immediately, scheduled for later, or staged as a draft for a human to approve. 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. A carousel slide can carry its own caption: instead of a bare string, pass { id, caption } for a stored media ID or { url, caption } for a public link (exactly one of id or url). Instagram shows that text on that slide; the post's own caption still applies to the whole post. Per-item captions are carousel-only — a feed post, reel, or story has nowhere to show one and is rejected, so build a carousel in a single call rather than one slide at a time. 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 on Instagram). Omit post_type to infer it from the media (1 image → feed, 1 video → reel, 2+ → carousel; stories are never inferred). On TikTok a reel becomes a video post and feed/carousel become a photo post, which takes images only — 1 to 35 of them when no Instagram account is targeted; stories are rejected for TikTok targets. Every TikTok target needs the tiktok object: mode 'direct' (default) posts straight to the profile and requires privacy_level, which must be one the creator allows (check tiktok_creator_info) and has no default — ask the user; mode 'inbox' sends it to the creator's TikTok inbox to finish in the app. tiktok.caption overrides the caption on TikTok only; tiktok.title is a photo post's title (≤90). first_comment is posted as a comment on the post as soon as it goes live, once per Instagram target account — the usual place for a hashtag block you would rather keep out of the caption; stories take none, since Instagram has no comments there, and TikTok targets never get one. Omit schedule_at to publish on the next scheduler tick (under a minute); pass a future ISO 8601 timestamp to schedule. Pass draft: true to publish nothing yet — the post is saved as a draft and the returned preview_url shows the user exactly how Instagram and TikTok will render it, with Publish and Schedule buttons on the page. Every call returns preview_url, so you can always hand the user a link to look at. 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), up to 10 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
media(string | object)[]Media IDs or public URLs (JPEG images, MP4/MOV videos) — up to 10 when an Instagram account is targeted, up to 35 images for a TikTok-only photo post. Required unless draft is true, since neither platform has text-only posts. For a per-slide caption on an Instagram carousel, pass { id, caption } or { url, caption } instead of a bare string — exactly one of id or url Example: ["https://cdn.example.com/fall.jpg", { "id": "a1b2…", "caption": "Sand, our best seller." }]
share_to_feedboolean?Reels only: also show the reel on the profile grid (Instagram defaults to true) Example: false
first_commentstring?Text posted as a comment on the post the moment it goes live, 1–2200 characters — the usual home for a hashtag block you want out of the caption. Posted once per Instagram target account (TikTok has no first comments, so it needs at least one Instagram target). Not valid on stories, which have no comments Example: "#fallcollection #newin #slowfashion"
tiktokobject?TikTok settings, required when any target is a TikTok account: { mode: "direct" | "inbox" (default direct), privacy_level: PUBLIC_TO_EVERYONE | MUTUAL_FOLLOW_FRIENDS | FOLLOWER_OF_CREATOR | SELF_ONLY (required for direct, no default), allow_comment, allow_duet, allow_stitch (all default false; duet/stitch are video only), brand_organic (promotes your own brand), brand_content (paid partnership — never SELF_ONLY), is_aigc, caption (TikTok-only caption override), title (photo posts, ≤90), cover_timestamp_ms (video), photo_cover_index (photo), auto_add_music (photo) } Example: { "privacy_level": "PUBLIC_TO_EVERYONE", "allow_comment": true }
schedule_atstring?ISO 8601 timestamp with offset — omit to publish now Example: "2026-08-04T18:30:00Z"
draftboolean?Save as a draft instead of queueing it — nothing publishes until the user approves it on the preview page Example: true
Returns post: the created post — status "draft" when draft was true, otherwise "scheduled" — with the resolved post_type, first_comment, tiktok settings, its media (each entry carrying its own caption, or null when the slide has none), and one entry in targets[] per account (each starting at status "pending"). preview_url: a dashboard link showing the post exactly as Instagram and TikTok will render it; on a draft that page is also where the user publishes or schedules it. 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 platform's post 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, error { code, message }, and first_comment_status (pending, posted, or failed; null when the post has no first comment). A first comment that failed does not make the target any less published — the post is live either way, and the reason is in error. A TikTok target sent to the inbox reads published with error.code tiktok_inbox: it is waiting in the creator's TikTok inbox for them to finish. A public TikTok post can read published before its permalink exists — TikTok assigns the link after moderation, and Relay fills it in when TikTok reports it. preview_url: the same dashboard preview link post_create returns, so you can re-share it without having kept it.
post_update
writesidempotent
Edit a post that has not published yet — change the caption, replace or remove the first comment, replace the TikTok settings, move the scheduled time, or any combination. Only draft and scheduled posts can be edited; once a post enters publishing it is frozen. This changes the post's own caption, not the per-slide captions on a carousel. To change media, per-item captions, or targets via MCP, cancel the post and create a new one (the Relay dashboard can edit media and targets directly).
FieldTypeDescription
post_idstring requiredPost ID Example: "3f0c…"
captionstring?Replacement caption for the post itself, 1–2200 characters — not a carousel slide's own caption
first_commentstring?Replacement first comment, 1–2200 characters. Pass null to remove one that was set; omit the field to leave it as it is Example: "#fallcollection #newin"
tiktokobject?Replacement TikTok settings — the whole object, same shape as post_create's tiktok. Omit to leave them as they are Example: { "privacy_level": "SELF_ONLY" }
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.
post_retry
writesopen-world
Retry a post whose status is failed or partly. Failed targets reset to pending and the post re-enters the publish queue on the next scheduler tick; targets that already published are untouched, so a partly post republishes only what failed. Fix the underlying cause first — post_status shows each failed target's error code and message (an expired account needs reconnecting, a quota window needs to pass) — or the retry will fail the same way. The post keeps its ID; poll post_status for the outcome.
FieldTypeDescription
post_idstring requiredID of a failed or partly-published post Example: "3f0c…"
Returns post: the re-queued post (status "scheduled"), with previously failed targets reset to "pending" and their errors cleared. Already-published targets keep their state.
posts_list
read-onlyidempotent
List this account's posts newest-first, optionally filtered by status and/or a schedule_at time range. Answers questions like "what is scheduled this week" (pass from/to bounding the week) or "which posts failed". Drafts have no schedule time and never match a ranged query. 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
fromstring?Only posts with schedule_at at or after this ISO 8601 timestamp Example: "2026-08-03T00:00:00Z"
tostring?Only posts with schedule_at at or before this ISO 8601 timestamp Example: "2026-08-09T23:59:59Z"
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
Store an image or video in Relay and return a reusable media ID for post_create. Two input modes — pass exactly one: url (a publicly reachable http(s) URL that Relay fetches) or data (the file content itself, as raw base64 or a data: URI). For a local file or blob on your side, read the bytes and base64-encode them into data — the server cannot read your filesystem. Relay stores JPEG images (up to 8MB) and MP4/MOV video (up to 100MB) — what both Instagram and TikTok accept — and rejects PNG at upload rather than at publish time. The content type is taken from content_type, the data: URI, or sniffed from the bytes, in that order. Inline data is for small files only: the whole MCP request is capped at 1MB and base64 costs a third on top, so data tops out around 700KB of file — a modest JPEG, never video. (The REST twin POST /v1/media is not behind that cap, so data there is bounded only by the 8MB/100MB limits above.) For anything bigger, POST the file bytes to /v1/uploads — same host as this server, API key in the Authorization header, the file as the request body, Content-Type set, and Content-Length set to the exact byte count, which is required or you get a 411. It answers 201 with upload.url, which you pass here as url or straight to post_create, and upload.expires_at, usually an hour out. post_create also accepts public URLs directly, so reach for this tool when you want to reuse one asset across several posts, upload local content, or validate a file before composing.
FieldTypeDescription
urlstring?Publicly reachable http(s) URL to fetch — pass exactly one of url or data Example: "https://cdn.example.com/fall.jpg"
datastring?File content as raw base64 or a data: URI — pass exactly one of url or data Example: "data:image/jpeg;base64,/9j/4AAQ…"
content_typestring?MIME type of data when it is raw base64 — image/jpeg, video/mp4, or video/quicktime (otherwise taken from the data: URI or sniffed from the bytes) Example: video/mp4
Returns media: { id, url, kind, content_type, size_bytes, width, height, duration_ms, created_at }. duration_ms is set for videos whose header could be read (TikTok checks it against the creator's limit). Pass id in post_create.media.
tiktok_creator_info
read-onlyidempotentopen-world
Ask TikTok what a connected TikTok account allows at this moment: the privacy levels it can post with (privacy_level_options — the only valid values for post_create's tiktok.privacy_level), whether the creator has turned off comments, duets or stitches in their TikTok settings (a disabled interaction is forced off), the longest video they may post, and whether they can post at all right now (TikTok caps every creator's daily posts). Call this before creating a post for a TikTok account, and show the user the creator's nickname so they know which TikTok account it goes to.
FieldTypeDescription
account_idstring requiredA TikTok account ID from accounts_list Example: "7c2a…"
Returns creator: { username, nickname, avatar_url, privacy_level_options, comment_disabled, duet_disabled, stitch_disabled, max_video_post_duration_sec, can_post, cannot_post_reason }.

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.
  • Input-validation failures (arguments that don't match a tool's schema) arrive the same way — an isError result describing the mismatch, not a protocol error. Unexpected server failures also come back as isError with a generic Internal error — try again; the details are logged server-side, never forwarded.
  • Authentication failures are HTTP 401 on the transport, before any tool runs.
  • Rate limits are HTTP 429 with Retry-After — the same budgets as the REST API (120/minute per key, 300/minute per user), counted per JSON-RPC message: a batched request of N tool calls costs N, and batches are capped at 20 messages per request.
  • Oversized requests are HTTP 413 before any tool runs: the whole JSON-RPC body is capped at 1 MB. That cap is what bounds media_upload's inline data to roughly 700 KB of file once base64 has added its third — anything larger has to go through POST /v1/uploads, whose URL you then pass as a media entry.
  • 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 server delivers this guidance to agents automatically via its initialize instructions — what you see below is exactly what a connected agent receives:

Relay publishes posts to Instagram and TikTok. 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 (each has a platform)
tiktok_creator_info  → TikTok targets only: which privacy levels and interactions that creator allows right now
media_upload         → optional: store a file once (public URL, or small inline base64), reuse it across posts
post_create          → queue the post (targets + caption + media [+ tiktok] [+ schedule_at]), or stage it with draft: true
post_status        → poll until every target is published or failed
post_retry         → re-queue a failed or partly-published post after fixing the cause

Rules:
- 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.
- TikTok targets need a tiktok object. For mode "direct" (the default) tiktok.privacy_level is required and has no default: ask the user who should see the post, and offer only the levels tiktok_creator_info returns. Comments, duets and stitches stay off unless the user turns them on. Say whether the post promotes the user's own brand (brand_organic) or a third party (brand_content) — branded content cannot be SELF_ONLY. mode "inbox" sends it to the creator's TikTok inbox to finish in the app instead.
- TikTok takes one video, or 1–35 JPEG images as a photo post; it has no stories and no first comments. A post that targets Instagram as well is held to Instagram's limits too (at most 10 items).
- Posting to TikTok means the user agrees to TikTok's Music Usage Confirmation (and its Branded Content Policy when brand_content is set) — make sure they know before you publish.
- Media over ~700KB cannot go inline: the whole MCP request is capped at 1MB and base64 costs a third on top. POST the raw bytes to /v1/uploads on the same host as this endpoint instead — your API key, the file's Content-Type (image/jpeg, video/mp4, or video/quicktime), and Content-Length set to the exact byte count, which is required. It answers 201 with upload.url, a public link you pass as a media entry to post_create or as media_upload's url, and upload.expires_at, usually an hour out. Create the post before then; the post keeps its own permanent copy, so scheduling weeks ahead is fine.
- Do not publish without an explicit go-ahead. To get one, call post_create with draft: true and give the user the preview_url it returns — that page shows the post exactly as Instagram and TikTok will render it, with Publish and Schedule buttons. A draft never publishes on its own, so this is the safe default when the user has not already told you to send it.

Prompts to try

List my connected Instagram and TikTok 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://api.relay.lantean.tech/v1/posts.

© 2026 Relaysupport@lantean.techPrivacyTerms