Protocol

MCP server

tinyposter speaks Model Context Protocol. Drop the URL + a personal access token into any MCP-aware AI client.

Get a token

A token is like a password your AI client will use to talk to tinyposter. It starts with tp_.

  1. Open your tokens page.
  2. Click Create token. Name it after the client (“Claude”, “Cursor”, etc.).
  3. Copy the token. It only shows once.
Keep this private
Anyone with this token can post for you. If it leaks, revoke it on the same page.

What is MCP?

Model Context Protocol is the standard way AI agents call external tools. Anthropic, Google, and OpenAI all support it. tinyposter exposes its full feature set as MCP tools, so any MCP-aware client (Claude Desktop, Claude Code, Cursor, Zed, etc.) can post for you with a single config snippet.

Endpoint

  • URL: https://tinyposter.app/api/mcp
  • Transport: Streamable HTTP (JSON-RPC 2.0 over POST)
  • Auth: Bearer token (paste a tp_… token) or OAuth 2.1 with PKCE + Dynamic Client Registration (RFC 7591 + 7636 + 8414 + 9728)
  • Protocol versions: 2024-11-05, 2025-03-26, 2025-06-18 (the server echoes whichever the client requests)
  • Methods: initialize, tools/list, tools/call, ping
  • Notifications: JSON-RPC messages without an id are accepted and acked with HTTP 202 (e.g. notifications/initialized)
  • Session: the server issues an Mcp-Session-Id header on every response; clients that track sessions can echo it back, stateless clients can ignore it

Bearer or OAuth?

Both work. Pick whichever your client prefers — most clients support both and will pick automatically.

  • Bearer (simplest). You generate a tp_… token at /dashboard/tokens and paste it into the client's config as a header. The token never expires until you revoke it. Best for terminal tools, CI, anything where you don't want a browser hop.
  • OAuth 2.1 (zero config). The client points itself at https://tinyposter.app/api/mcp with no token. Our 401 advertises the protected-resource metadata, the client auto-registers, opens a browser to /oauth/authorize, you click “Allow,” and the client gets a 90-day access token plus a refresh token. No copy-pasting secrets. Best for desktop clients (Claude Desktop, ChatGPT, Cursor) that already speak OAuth.
Discovery URLs
  • https://tinyposter.app/.well-known/oauth-protected-resource — points clients at the auth server
  • https://tinyposter.app/.well-known/oauth-authorization-server — lists the authorize, token, and registration endpoints
  • POST https://tinyposter.app/api/oauth/register — Dynamic Client Registration (RFC 7591)
  • https://tinyposter.app/oauth/authorize — user consent screen
  • POST https://tinyposter.app/oauth/token — exchanges code for access + refresh tokens

Setup

Claude Desktop / Claude Code

Open claude_desktop_config.json from Settings → Developer → Edit Config (Mac/Windows) and add:

claude_desktop_config.jsonjson
{
  "mcpServers": {
    "tinyposter": {
      "url": "https://tinyposter.app/api/mcp",
      "headers": {
        "Authorization": "Bearer tp_PASTE_YOUR_TOKEN_HERE"
      }
    }
  }
}

Restart Claude. Done. Detailed walkthrough on the Claude page.

Cursor

Cursor has its own MCP UI — no JSON file to edit.

  1. 01

    Open Cursor settings

    Press Cmd+, on Mac or Ctrl+, on Windows. Or click the gear icon in the sidebar.

  2. 02

    Find MCP Servers

    In the left rail of Settings, scroll to Tools & Integrations. Click MCP Servers.

    Click Add new MCP server (or just Add new server).

  3. 03

    Fill in the form

    • Name: tinyposter
    • Type / Transport: HTTP (or Streamable HTTP if shown)
    • URL: https://tinyposter.app/api/mcp
    • Headers: click Add header. Set name to Authorization and value to Bearer tp_PASTE_YOUR_TOKEN_HERE.

    Save. Open the Composer / Agent tab — tinyposter's tools appear in the tool list and Cursor can call them mid-task.

Codex CLI

Codex reads MCP servers from ~/.codex/config.toml. Full walkthrough on the Codex page; the short version is:

~/.codex/config.tomltoml
[mcp_servers.tinyposter]
url = "https://tinyposter.app/api/mcp"
http_headers = { Authorization = "Bearer tp_PASTE_YOUR_TOKEN_HERE" }

Restart Codex, then run /mcp to confirm tinyposter is listed.

Continue (VS Code / JetBrains)

Edit ~/.continue/config.json and add tinyposter under mcpServers:

~/.continue/config.jsonjson
{
  "mcpServers": {
    "tinyposter": {
      "transport": {
        "type": "streamable-http",
        "url": "https://tinyposter.app/api/mcp",
        "headers": {
          "Authorization": "Bearer tp_PASTE_YOUR_TOKEN_HERE"
        }
      }
    }
  }
}

Cline (VS Code)

Open the Cline MCP panel and click Add Remote Server, or edit the JSON directly:

json
{
  "mcpServers": {
    "tinyposter": {
      "transport": "http",
      "url": "https://tinyposter.app/api/mcp",
      "headers": { "Authorization": "Bearer tp_PASTE_YOUR_TOKEN_HERE" }
    }
  }
}

Zed

In Zed's settings (cmd+,) under context_servers:

~/.config/zed/settings.jsonjson
{
  "context_servers": {
    "tinyposter": {
      "url": "https://tinyposter.app/api/mcp",
      "headers": { "Authorization": "Bearer tp_PASTE_YOUR_TOKEN_HERE" }
    }
  }
}

ChatGPT connectors

ChatGPT (Pro / Team / Enterprise) supports remote MCP servers as connectors. Walkthrough on the ChatGPT page — point it at https://tinyposter.app/api/mcp with the same Bearer token.

Anything else (stdio-only clients)

If your client only speaks stdio MCP (older agents, sandboxed runners), wrap the HTTP endpoint with mcp-remote:

json
{
  "mcpServers": {
    "tinyposter": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://tinyposter.app/api/mcp",
        "--header",
        "Authorization: Bearer tp_PASTE_YOUR_TOKEN_HERE"
      ]
    }
  }
}

Raw / curl

Want to test without an AI client? Hit it directly:

bash
# 1. Discover tools
curl https://tinyposter.app/api/mcp \
  -H "Authorization: Bearer tp_..." \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 2. Call a tool
curl https://tinyposter.app/api/mcp \
  -H "Authorization: Bearer tp_..." \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "id":2,
    "method":"tools/call",
    "params":{ "name":"view_calendar", "arguments":{ "days_ahead":7 } }
  }'

Tool reference

Each tool is also documented in the MCP tools/list response with its JSON schema.

list_brands

List the brands the user owns. Each brand maps to its own set of connected social accounts.

input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
set_active_brand

Change the user's default brand. After this, tool calls without an explicit brand_id operate on this brand. Call when the user says things like 'switch to <brand>', 'use <brand> as default', 'change active brand', or 'make <brand> my main one'. Use list_brands first to find the id.

input schema
{
  "type": "object",
  "required": [
    "brand_id"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to make active. Get from list_brands."
    }
  },
  "additionalProperties": false
}
disconnect_account

Revoke the access token currently being used to call tinyposter, disconnecting this client from the user's tinyposter account. Call when the user says 'disconnect', 'log out of tinyposter', 'switch tinyposter account', or 'use a different account'. After this runs, future tool calls will fail with 401 until the user reauthenticates from this client. Other tokens (CLI, other integrations) on the same account are NOT affected.

input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
list_accounts

List the connected social media accounts for a brand. Defaults to the user's active brand.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    }
  },
  "additionalProperties": false
}
connect_accounts

Get a URL the user opens in their browser to connect (or reconnect) a social account on tinyposter.app. Use this whenever the user asks to connect/add/link a platform, when list_accounts shows zero accounts, or when a post fails with platform_not_connected. Always present the returned URL as a clickable link in your reply — agents cannot complete OAuth on the user's behalf.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "platform": {
      "type": "string",
      "enum": [
        "TWITTER",
        "INSTAGRAM",
        "FACEBOOK",
        "LINKEDIN",
        "TIKTOK",
        "YOUTUBE",
        "PINTEREST",
        "BLUESKY",
        "THREADS",
        "REDDIT",
        "MASTODON"
      ],
      "description": "Optional. Pre-selects the platform on the connect page."
    }
  },
  "additionalProperties": false
}
get_usage

Get the user's plan, post quota, and how many posts they've used this billing period. Quota is shared across all brands.

input schema
{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}
start_upload_session

Create a one-time drop URL the user opens in their browser to upload images that are attached to the current ChatGPT chat. ChatGPT does NOT let actions read user-attached image bytes, so this is the only way to bring a chat-uploaded image into TinyPoster. Use when the user says 'post this image' (where 'this' is an image they attached to chat) or 'I uploaded a photo, schedule it'. Flow: call this → get drop_url → tell the user to open the link and drop the file → call get_upload_session repeatedly (every ~3 seconds is fine) until status is 'closed' or 'active' with uploads.length > 0 → take each upload.id from the response and pass them as media_upload_ids to post_now/schedule_post. For images you generated yourself with DALL-E, do NOT use this — call upload_media directly with the DALL-E file URL instead.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "ttl_minutes": {
      "type": "number",
      "description": "How long the drop link is valid. Default 30, max 120."
    },
    "max_uploads": {
      "type": "number",
      "description": "Max files the user can drop. Default 4, max 10."
    }
  },
  "additionalProperties": false
}
get_upload_session

Poll an upload session to see whether the user has dropped their image(s) yet. Returns the same shape as start_upload_session. When uploads.length > 0, take the upload.id values and pass them as media_upload_ids to post_now/schedule_post.

input schema
{
  "type": "object",
  "required": [
    "session_id"
  ],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "The id returned by start_upload_session."
    }
  },
  "additionalProperties": false
}
upload_media

Upload an image or video into TinyPoster's media bridge. Returns an upload id (pass to post_now/schedule_post as media_upload_ids) and a public URL. Use this when the user has an image to post but no public URL yet — including DALL-E images generated in ChatGPT (pass the file URL from the image generation as `url`). Provide exactly one of `url` or `data_base64`.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "url": {
      "type": "string",
      "description": "Public http(s) URL to fetch and re-host. Best path for ChatGPT-generated DALL-E images and any externally-hosted file."
    },
    "data_base64": {
      "type": "string",
      "description": "Base64-encoded bytes (raw or `data:image/png;base64,...`). For raw uploads."
    },
    "mime": {
      "type": "string",
      "description": "MIME type for data_base64, e.g. image/png."
    },
    "filename": {
      "type": "string",
      "description": "Optional filename to associate with the upload."
    }
  },
  "additionalProperties": false
}
post_now

Publish a post immediately to one or more connected social platforms on a brand. The post is queued for publishing in the next minute. Use this when the user says 'post now', 'publish', 'tweet this', or gives no time. If the user gives a date/time use schedule_post instead. If they say 'queue' / 'add to my queue' use add_to_queue instead.

input schema
{
  "type": "object",
  "required": [
    "text",
    "platforms"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "text": {
      "type": "string",
      "description": "The post text/caption."
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      },
      "description": "Which connected platforms to publish to. Use list_accounts to see what's available.",
      "minItems": 1
    },
    "title": {
      "type": "string",
      "description": "Optional internal title (used for video titles on YouTube/TikTok)."
    },
    "media_urls": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional public URLs of images or videos to attach. TinyPoster fetches and re-hosts."
    },
    "media_upload_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional upload ids returned by the upload_media tool. Faster than media_urls."
    }
  },
  "additionalProperties": false
}
schedule_post

Schedule a post to publish at a specific date/time on a brand. Use this when the user names a date or time ('tomorrow at 9am', 'next Friday', 'in 2 hours'). If they don't specify a time but want it spread across a recurring schedule, use add_to_queue. If they say 'now', use post_now.

input schema
{
  "type": "object",
  "required": [
    "text",
    "platforms",
    "scheduled_at"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "text": {
      "type": "string",
      "description": "The post text/caption."
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      },
      "description": "Which connected platforms to publish to. Use list_accounts to see what's available.",
      "minItems": 1
    },
    "scheduled_at": {
      "type": "string",
      "description": "ISO 8601 datetime for when to publish (e.g. 2026-01-15T14:30:00-05:00)."
    },
    "title": {
      "type": "string",
      "description": "Optional internal title (used for video titles on YouTube/TikTok)."
    },
    "media_urls": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional public URLs of images or videos to attach. tinyposter fetches and re-hosts."
    },
    "media_upload_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional upload ids returned by upload_media. Faster than media_urls."
    }
  },
  "additionalProperties": false
}
list_posts

List posts for a brand (scheduled, publishing, published, failed). Defaults to the active brand.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "from": {
      "type": "string",
      "description": "ISO datetime; only posts scheduled at or after this."
    },
    "to": {
      "type": "string",
      "description": "ISO datetime; only posts scheduled at or before this."
    },
    "status": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "draft",
          "scheduled",
          "publishing",
          "published",
          "failed",
          "canceled"
        ]
      },
      "description": "Filter by these statuses."
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 50
    }
  },
  "additionalProperties": false
}
view_calendar

Get a quick view of upcoming scheduled posts for a brand grouped by date.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "days_ahead": {
      "type": "integer",
      "minimum": 1,
      "maximum": 60,
      "default": 14
    }
  },
  "additionalProperties": false
}
cancel_post

Cancel a scheduled post by id. Cannot cancel posts that have already been published.

input schema
{
  "type": "object",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
get_queue_schedule

Get the recurring posting slots for a brand. Slots are weekday + HH:MM in the user's timezone and define when add_to_queue will publish. Empty schedule means the queue is unusable.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    }
  },
  "additionalProperties": false
}
set_queue_schedule

Replace the recurring posting slots for a brand. Send the full desired schedule; existing slots are wiped first. weekday: 0=Sunday..6=Saturday. time_local: HH:MM 24h.

input schema
{
  "type": "object",
  "required": [
    "slots"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "slots": {
      "type": "array",
      "maxItems": 168,
      "items": {
        "type": "object",
        "required": [
          "weekday",
          "time_local"
        ],
        "properties": {
          "weekday": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "0=Sunday through 6=Saturday."
          },
          "time_local": {
            "type": "string",
            "description": "HH:MM 24h, e.g. 09:00"
          }
        },
        "additionalProperties": false
      }
    }
  },
  "additionalProperties": false
}
add_to_queue

Add a post to the next open recurring queue slot for a brand. Returns the resolved scheduled_at. Requires the brand to have at least one slot in get_queue_schedule. Use this when the user says 'add to queue', 'queue this', or wants the post spread out without naming a specific time. If they pick a specific date/time use schedule_post; for evergreen recurring posts use add_to_autolist.

input schema
{
  "type": "object",
  "required": [
    "text",
    "platforms"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "text": {
      "type": "string",
      "description": "The post text/caption."
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      },
      "description": "Which connected platforms to publish to. Use list_accounts to see what's available.",
      "minItems": 1
    },
    "title": {
      "type": "string",
      "description": "Optional internal title (used for video titles on YouTube/TikTok)."
    },
    "media_urls": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional public URLs of images or videos to attach."
    },
    "media_upload_ids": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "Optional upload ids returned by upload_media. Faster than media_urls."
    }
  },
  "additionalProperties": false
}
list_autolists

List autolists (evergreen post buckets) for a brand. Use to find existing lists before adding items.

input schema
{
  "type": "object",
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    }
  },
  "additionalProperties": false
}
create_autolist

Create an autolist (evergreen post bucket) on a brand. The cron worker pushes one item into the queue every cadence_hours. Loop=true wraps to position 0 forever; loop=false pauses after the last item.

input schema
{
  "type": "object",
  "required": [
    "name",
    "platforms"
  ],
  "properties": {
    "brand_id": {
      "type": "string",
      "description": "Brand id to operate on. Defaults to the user's currently active brand. Use list_brands to discover ids."
    },
    "name": {
      "type": "string"
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      },
      "minItems": 1
    },
    "cadence_hours": {
      "type": "integer",
      "minimum": 1,
      "maximum": 720,
      "default": 24
    },
    "loop_items": {
      "type": "boolean",
      "default": true
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "paused"
      ],
      "default": "active"
    }
  },
  "additionalProperties": false
}
get_autolist

Fetch an autolist with all its items in order.

input schema
{
  "type": "object",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
update_autolist

Update an autolist (rename, pause/resume, change cadence, change default platforms, toggle loop).

input schema
{
  "type": "object",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "string"
    },
    "name": {
      "type": "string"
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      }
    },
    "status": {
      "type": "string",
      "enum": [
        "active",
        "paused"
      ]
    },
    "cadence_hours": {
      "type": "integer",
      "minimum": 1,
      "maximum": 720
    },
    "loop_items": {
      "type": "boolean"
    }
  },
  "additionalProperties": false
}
delete_autolist

Delete an autolist and all its items.

input schema
{
  "type": "object",
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
add_to_autolist

Append a new evergreen item to an autolist. Items are posted in order; if the list loops, it wraps after the last item. Use list_autolists first to find the autolist id.

input schema
{
  "type": "object",
  "required": [
    "autolist_id",
    "text"
  ],
  "properties": {
    "autolist_id": {
      "type": "string"
    },
    "text": {
      "type": "string"
    },
    "platforms": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "TWITTER",
          "INSTAGRAM",
          "FACEBOOK",
          "LINKEDIN",
          "TIKTOK",
          "YOUTUBE",
          "PINTEREST",
          "BLUESKY",
          "THREADS",
          "REDDIT",
          "MASTODON"
        ]
      },
      "description": "Optional override; null/missing uses the autolist's default platforms."
    },
    "media_urls": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "additionalProperties": false
}
remove_autolist_item

Remove an item from an autolist.

input schema
{
  "type": "object",
  "required": [
    "autolist_id",
    "item_id"
  ],
  "properties": {
    "autolist_id": {
      "type": "string"
    },
    "item_id": {
      "type": "string"
    }
  },
  "additionalProperties": false
}

Things to ask

  • “Post ‘Friday recap incoming’ to X and LinkedIn.”
  • “Schedule a Threads post for Sunday at 9am ET that says...”
  • “What's on my calendar next week?”
  • “Cancel the post I scheduled for Saturday.”
  • “Which platforms am I connected to?”
  • “How many posts do I have left this month?”

Troubleshooting

Claude doesn't list tinyposter as a tool

Quit Claude fully (Cmd+Q) and re-open. Config is only read on launch.

I get rate limited

The MCP endpoint allows 120 calls/min/token. If you're hitting that, batch the work or move to the REST API (240/min) for bulk operations.

Can I have two clients use the same token?

You can, but if one leaks, both stop working when you revoke. Better practice: one token per client (“Claude Desktop”, “Cursor”, “CI”, etc.) so you can revoke surgically.

Same actions, two protocols
Anything you can do via MCP, you can do via the REST API. They use the same tokens, the same data, and the same Bundle.social pipeline behind the scenes.