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.
The contract
Section titled “The contract”| Endpoint | The server’s mcp_server_url, from Create a Virtual MCP server. Send every request as POST. |
| Authentication | Authorization: Bearer <session token>. A token is for one user on one server. Mint session tokens shows how. |
| Request headers | Content-Type: application/json and Accept: application/json, text/event-stream |
| Transport | MCP streamable HTTP. Each response is a server-sent event stream with one message event, whose data line is the JSON-RPC response. |
| Session | None 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. |
| Methods | initialize, tools/list and tools/call. GET on the URL returns 405: the server never opens a stream of its own. |
| Tools | The 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 lifetime | 1 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. |
Call the server
Section titled “Call the server”-
Mint a session token
Section titled “Mint a session token”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_URLandSESSION_TOKEN. -
List the tools
Section titled “List the tools”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
dataline holds each tool’sname,descriptionandinputSchema:event: messagedata: {"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",…}]}} -
Call a tool
Section titled “Call a tool”Send the tool’s
nameandargumentsthat match itsinputSchema: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
textis a JSON string, and the app’s response is underdata:event: messagedata: {"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'
Connect a user’s account from the agent
Section titled “Connect a user’s account from the agent”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:
- Call it with no arguments. The response lists
connections_requiring_authorizationandconnections_already_authorized, each with a status and a reason. Nothing is created, so this is always safe. - Call it again with
connection_nameset to one of the connections that needs authorization. The response has a time-limitedmagic_link_url. - 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.
Renew the token
Section titled “Renew the token”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.
Errors
Section titled “Errors”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 see | Cause | Fix |
|---|---|---|
401, invalid or expired bearer token | No Authorization header, an expired token, a token minted for another server, or a token that was copied incompletely | Mint a new token for this server and user. |
405 | A GET request | Send JSON-RPC requests as POST. |
JSON-RPC error -32602 with unknown tool | The tool isn’t on this server, or the name is misspelled | Use a name from tools/list. To add a tool, update the server. |
| A tool call fails, or a connection’s tools are missing | The user hasn’t authorized that connection | Call connect_account, as above. |