Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

Connect any MCP client

The HTTP contract of an AgentKit Virtual MCP server for clients that don't use the Scalekit SDK: URL, bearer token, transport, requests, responses, token lifetime and errors.

A Virtual MCP server is a standard MCP endpoint, so any client that speaks streamable HTTP can call it: an agent runtime, a hosted platform, your own HTTP code or curl. Your backend needs the Scalekit API only to mint session tokens; the client never needs the SDK. This page is the contract that client works against.

EndpointThe server’s mcp_server_url, from Create a Virtual MCP server. Send every request as POST.
AuthenticationAuthorization: Bearer <session token>. A token is for one user on one server. Mint session tokens shows how.
Request headersContent-Type: application/json and Accept: application/json, text/event-stream
TransportMCP streamable HTTP. Each response is a server-sent event stream with one message event, whose data line is the JSON-RPC response.
SessionNone to manage. tools/list and tools/call work without an initialize call first, and the server sends no Mcp-Session-Id. Clients that do send initialize and notifications/initialized get the normal responses.
Methodsinitialize, tools/list and tools/call. GET on the URL returns 405: the server never opens a stream of its own.
ToolsThe tools you chose for the server. By default, also connect_account: when the user still needs to connect an app, it returns an authorization link for that connection.
Token lifetime1 hour by default, from 60 seconds to 24 hours. A token can’t be refreshed: mint a new one. Tokens are independent, so minting one doesn’t revoke the others.
  1. Your backend mints the token for the user, as in Mint session tokens, and hands the client the server URL and the token. Keep them in MCP_SERVER_URL and SESSION_TOKEN.

  2. Terminal window
    curl -sS "$MCP_SERVER_URL" \
    -H "Authorization: Bearer $SESSION_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'

    The response is one event. Its data line holds each tool’s name, description and inputSchema:

    event: message
    data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"connect_account","description":"Connect (or RE-connect) the user's account…","inputSchema":{…}},{"name":"github_user_repos_list",…}]}}
  3. Send the tool’s name and arguments that match its inputSchema:

    Terminal window
    curl -sS "$MCP_SERVER_URL" \
    -H "Authorization: Bearer $SESSION_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {"name": "github_user_repos_list", "arguments": {"per_page": 5}}
    }'

    The result is MCP text content. Its text is a JSON string, and the app’s response is under data:

    event: message
    data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\n \"data\": [ … ]\n}"}]}}

    To read it in a shell, keep the data: line and parse the text twice:

    Terminal window
    … | sed -n 's/^data: //p' | jq -r '.result.content[0].text' | jq '.data'

If a tool call fails because the user hasn’t authorized that connection, or tools you expect are missing from tools/list, call connect_account:

  1. Call it with no arguments. The response lists connections_requiring_authorization and connections_already_authorized, each with a status and a reason. Nothing is created, so this is always safe.
  2. Call it again with connection_name set to one of the connections that needs authorization. The response has a time-limited magic_link_url.
  3. Show the link to the user. The agent shouldn’t open it itself. Once the user signs in, retry the tool call with the same token.

A connection that uses an organization-wide credential has no user step: the response says auth_mode is ORG_WIDE and has no link.

The client can’t renew a token. Before it expires, your backend mints a new one and the client switches to it. A host with an expired token fails at its next request, not at startup. Choose when to mint covers single runs, chat sessions, scheduled jobs, concurrent runs and long-running hosts.

A rejected token gets HTTP 401 with {"error":"unauthorized","error_description":"invalid or expired bearer token"}. Errors inside a request that got through come back as JSON-RPC errors in the message event.

What you seeCauseFix
401, invalid or expired bearer tokenNo Authorization header, an expired token, a token minted for another server, or a token that was copied incompletelyMint a new token for this server and user.
405A GET requestSend JSON-RPC requests as POST.
JSON-RPC error -32602 with unknown toolThe tool isn’t on this server, or the name is misspelledUse a name from tools/list. To add a tool, update the server.
A tool call fails, or a connection’s tools are missingThe user hasn’t authorized that connectionCall connect_account, as above.