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.
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.
{
"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.
- The client calls the endpoint without credentials and gets HTTP 401 with a
WWW-Authenticatechallenge naming the protected-resource metadata URL. - It fetches that metadata (RFC 9728), then the authorization-server metadata (RFC 8414).
- It registers itself if needed via Dynamic Client Registration
(RFC 7591) at
POST /oauth/register. - It sends the person to
/oauth/authorize. They log in and consent, choosing whether to grant the optional write scope. - It exchanges the authorization code at
/oauth/tokenfor 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.
{
"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-Featuresheader, 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.
{
"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-26and2025-06-18— youinitializeonce, then send calls. - Per-request revision
2026-07-28— no handshake; every request carries its own version inparams._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 send | Server responds with |
|---|---|
| A supported version | That same version |
| An unsupported version | The newest version it supports (2025-06-18) — it does not fail |
| Nothing | The 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.
{
"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:
| Header | Value | Required on |
|---|---|---|
MCP-Protocol-Version | The same string as params._meta["io.modelcontextprotocol/protocolVersion"] | Every request that carries a _meta version |
Mcp-Method | The JSON-RPC method | Every request |
Mcp-Name | params.name | tools/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.
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:
{
"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.
{
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover"
} {
"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:
| Situation | Response |
|---|---|
| A required request-metadata header is missing or disagrees with the body | 400 with JSON-RPC error -32020, naming the header that disagreed |
_meta names a version the server does not serve | 400 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-ID | Ignored. 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.
{
"_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.
{
"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.
{
"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 back | What you get |
|---|---|
action: "accept" with a usable value | The 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 call | Never 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 |