Archive API docs
    Jump to

    Narrow by kind with a prefix — q: queries, m: mutations, t: types, g: guides, f: fields and arguments.

    Connect a client

    Any MCP client that speaks Streamable HTTP can connect. You need the endpoint and a credential. There is nothing to install and no per-client configuration on Archive’s side.

    Endpoint
    POST https://app.archive.com/api/v2/mcp

    Option 1: Bearer token

    This is the simplest option, and the right one for a server-side agent that you run yourself. Create an API token in the Archive app’s Integration tab — the same token that works against /api/v2 — and send it in the Authorization header.

    Client configuration
    {
    "mcpServers": {
      "archive": {
        "type": "http",
        "url": "https://app.archive.com/api/v2/mcp",
        "headers": {
          "Authorization": "Bearer <your-token>"
        }
      }
    }
    }

    Most clients accept this shape; check yours for the exact key names. If your client can’t send a static header, use OAuth instead.

    Option 2: OAuth 2.1

    Use OAuth for a client used by other people, where each person authorizes their own access. Archive is its own authorization server, and the whole flow is discoverable: a compliant client needs only the endpoint URL.

    1. The client calls the endpoint without credentials and gets HTTP 401 with a WWW-Authenticate challenge naming the protected-resource metadata URL.
    2. It fetches that metadata (RFC 9728), then the authorization-server metadata (RFC 8414).
    3. It registers itself if needed via Dynamic Client Registration (RFC 7591) at POST /oauth/register.
    4. It sends the person to /oauth/authorize. They log in and consent, choosing whether to grant the optional write scope.
    5. It exchanges the authorization code at /oauth/token for an access token and uses that as the bearer credential.

    Tokens are opaque and Archive-issued; there is nothing for the client to decode.

    Verify the connection

    Call getWorkspaces first to verify the connection. It doesn’t require a workspace, so it succeeds as soon as the credential is valid, and it returns the workspace IDs that every other tool needs.

    tools/call — getWorkspaces
    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "getWorkspaces",
      "arguments": {}
    }
    }

    If it returns HTTP 401, the credential is the problem. If it returns HTTP 200 with isError: true, the credential is valid but something about the request is not; read the message.

    What tools/list returns

    tools/list advertises only the tools your credential can actually call. Two things narrow the list:

    • Write capability. An OAuth connection without the write scope sees read tools only.
    • Alpha gating. Tools behind an alpha feature are omitted unless the request opts in via the X-Archive-Alpha-Features header, and calling one without opting in returns the same response as calling a tool that doesn’t exist. See Alpha features.

    If your tools/list output is missing tools that are documented here, your credential isn’t authorized for them.

    Selecting a workspace

    Most tools act on one workspace. Pass it explicitly as workspaceId on the call — a workspace UUID or the numeric ID from the app — or send a WORKSPACE-ID header. If your credential can access exactly one workspace, you can omit both. Omitting both when several are reachable returns a message listing them — or, on 2026-07-28 with an elicitation-capable client, a structured input request you can render as a picker.

    tools/call — an explicit workspace
    {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "searchItems",
      "arguments": {
        "workspaceId": "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a",
        "limit": 20
      }
    }
    }

    Protocol version

    The server speaks two families of the Streamable HTTP transport, and picks per request:

    • Handshake revisions 2025-03-26 and 2025-06-18 — you initialize once, then send calls.
    • Per-request revision 2026-07-28 — no handshake; every request carries its own version in params._meta.

    You do not choose between them in configuration. A request that carries a _meta version is answered on the newer revision; everything else takes the handshake path.

    Handshake revisions (2025-03-26, 2025-06-18)

    initialize negotiation (the protocolVersion parameter). The server echoes back the version it will speak:

    You sendServer responds with
    A supported versionThat same version
    An unsupported versionThe newest version it supports (2025-06-18) — it does not fail
    NothingThe oldest supported version (2025-03-26), the most compatible choice

    Read the protocolVersion in the initialize result and speak that version, rather than assuming your request was honored.

    The MCP-Protocol-Version HTTP header. This is recorded but not validated. An unrecognized value is accepted rather than rejected, and a request that omits the header is treated as 2025-03-26 per the transport spec’s backwards-compatibility clause.

    The per-request revision (2026-07-28)

    On this revision there is no handshake and no session. Each request declares its own version inside params._meta, and the server answers statelessly.

    tools/list on 2026-07-28
    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {
      "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
    }
    }

    Required headers on 2026-07-28

    Every request on this revision mirrors part of its body onto HTTP headers, so an intermediary routing on headers and this server executing on the body can never disagree. Send all of them:

    HeaderValueRequired on
    MCP-Protocol-VersionThe same string as params._meta["io.modelcontextprotocol/protocolVersion"]Every request that carries a _meta version
    Mcp-MethodThe JSON-RPC methodEvery request
    Mcp-Nameparams.nametools/call

    Header names are compared case-insensitively; header values are compared byte-for-byte, so TOOLS/LIST does not match tools/list. Values must contain only visible ASCII, space, or tab.

    A Mcp-Name that cannot be expressed in that character set — or that would itself look like the sentinel — is sent Base64-encoded as =?base64?<base64 of the UTF-8 name>?=. The markers are lowercase and case-sensitive. The server decodes the value before comparing it, so Mcp-Name: =?base64?Z2V0V29ya3NwYWNl?= matches a body name of getWorkspace.

    tools/call on 2026-07-28, with the required headers
    POST /api/v2/mcp HTTP/1.1
    Content-Type: application/json
    Authorization: Bearer <token>
    WORKSPACE-ID: <workspace uuid>
    MCP-Protocol-Version: 2026-07-28
    Mcp-Method: tools/call
    Mcp-Name: getWorkspace
    
    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "getWorkspace",
      "arguments": {},
      "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }
    }
    }

    A header that is missing, disagrees with the body, or carries illegal characters is answered with 400 Bad Request and JSON-RPC error -32020:

    A header mismatch
    {
    "jsonrpc": "2.0",
    "id": 1,
    "error": {
      "code": -32020,
      "message": "Header mismatch: Mcp-Name header value 'getWorkspaces' does not match body value 'getWorkspace'"
    }
    }

    Results on this revision carry resultType: "complete" and an _meta["io.modelcontextprotocol/serverInfo"] object. tools/list additionally carries ttlMs: 3600000 and cacheScope: "private" — the listing is per-credential, so cache it per credential, for at most an hour. tools/call results carry no cache hints.

    Discovering support without a credential. server/discover tells you which revisions the server speaks. It is the one method that never requires authentication, so you can probe before running an OAuth flow. Its result is cacheScope: "public" with the same one-hour ttlMs.

    server/discover — no Authorization header needed
    {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "server/discover"
    }
    The response
    {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "supportedVersions": ["2026-07-28"],
      "capabilities": { "tools": {} },
      "instructions": "Archive's workspace-scoped social-content API. …",
      "resultType": "complete",
      "_meta": {
        "io.modelcontextprotocol/serverInfo": {
          "name": "archive-sellable-api",
          "title": "Archive",
          "version": "0.2.0"
        }
      },
      "ttlMs": 3600000,
      "cacheScope": "public"
    }
    }

    Everything else about a request is unchanged from the handshake revisions: the same bearer or OAuth credential, the same WORKSPACE-ID header and workspaceId argument, the same rate-limit charging, and the same 401 discovery challenge when a credential is missing on tools/list or tools/call.

    Four differences are worth knowing:

    SituationResponse
    A required request-metadata header is missing or disagrees with the body400 with JSON-RPC error -32020, naming the header that disagreed
    _meta names a version the server does not serve400 with JSON-RPC error -32022, whose data.supported lists what it does serve
    A method the server does not implement (including initialize and ping, which belong to the handshake revisions)404 with JSON-RPC error -32601
    You send Mcp-Session-Id or Last-Event-IDIgnored. No session id is ever issued or echoed on this revision

    Answering an input request

    If your client declares that it can render an elicitation form, two situations answer with resultType: "input_required" instead of prose: a call that did not name a workspace when your credential can reach several (see Selecting a workspace), and a refetchEngagementBulk batch large enough to need cost confirmation.

    Declare the capability in params._meta. An empty elicitation object means form mode; a client that declares only { "url": {} } is never sent a form.

    Declaring the capability
    {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
    }
    }

    The result carries an inputRequests map keyed by server-assigned ids, plus an opaque requestState. Do not inspect or modify the state — echo it back verbatim.

    An input_required result
    {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "resultType": "input_required",
      "inputRequests": {
        "workspace": {
          "method": "elicitation/create",
          "params": {
            "mode": "form",
            "message": "Multiple workspaces are accessible; pass a 'workspaceId' argument …",
            "requestedSchema": {
              "type": "object",
              "properties": {
                "workspaceId": {
                  "type": "string",
                  "title": "Workspace",
                  "description": "The workspace this call should act on.",
                  "oneOf": [
                    { "const": "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a", "title": "Acme" },
                    { "const": "b1d0c2a4-1f3e-4c7b-8a29-77c1f0e4d5b6", "title": "Acme EU" }
                  ]
                }
              },
              "required": ["workspaceId"]
            }
          }
        }
      },
      "requestState": "<opaque>"
    }
    }

    Re-send the same call with requestState and inputResponses on params — siblings of name and arguments, not inside them. Use a fresh JSON-RPC id.

    The retry
    {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "getWorkspace",
      "arguments": {},
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
      },
      "requestState": "<the value from the input_required result>",
      "inputResponses": {
        "workspace": {
          "action": "accept",
          "content": { "workspaceId": "6ccefa76-e8ba-5ab0-9c60-e48a9e235a4a" }
        }
      }
    }
    }

    The answer is treated exactly as if you had passed the argument yourself: an answered workspace is still checked against what your credential can reach, and an answered confirmation still buys only the batch it was quoted for — change itemIds and you are asked again, provided the revised batch still exceeds the confirmation threshold.

    What you send backWhat you get
    action: "accept" with a usable valueThe normal resultType: "complete" result
    action: "decline" or "cancel"The prose result a client without the capability would have received — never a second ask
    A state that is missing, altered, expired (10 minutes), or minted for a different callNever an error — the call is simply re-evaluated without your answer, so you get a fresh input_required only if it still lands on an ambiguous workspace or an over-threshold batch; otherwise it just completes