Build a client
Protocol reference
Transport, discovery, registration and the token exchange, for anyone building or debugging an MCP client.
You do not need this page to use the MCP server from an existing client. It is for people building their own client, or checking why one does not connect.
Endpoints
Everything the server exposes, all on https://api.olee.ai except the approval page:
| Method | Path | Authentication | Purpose |
|---|---|---|---|
POST | /mcp | Bearer | MCP messages, JSON-RPC 2.0 |
GET | /mcp | None | A short description of the server |
GET | /.well-known/oauth-protected-resource | None | Protected resource metadata, RFC 9728 |
GET | /.well-known/oauth-protected-resource/mcp | None | The same document, at the path-inserted location |
GET | /.well-known/oauth-authorization-server | None | Authorization server metadata, RFC 8414 |
POST | /oauth/register | None | Dynamic client registration, RFC 7591 |
POST | /oauth/token | PKCE | Authorization code exchange |
GET | https://oleon.io/mcp-authorize | Browser session | Where a person approves the connection |
Transport
The server implements the MCP Streamable HTTP transport in stateless mode.
- Send every message as a
POSTto/mcp. - Send
Content-Type: application/jsonandAccept: application/json, text/event-stream. A request that does not accept both is refused with406. - No
Mcp-Session-Idis issued. Each request is authenticated and answered on its own, so the server never pushes notifications and there is no stream to resume. - Replies arrive as a server-sent event stream carrying the JSON-RPC response.
The server advertises the tools capability only, and returns usage instructions for the model in its initialize result.
curl https://api.olee.ai/mcp \
-H "Authorization: Bearer $OLEON_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}'event: message
data: {"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"oleon-workspace","version":"0.1.0"},"instructions":"Read access to an Oleon Workspace account…"},"jsonrpc":"2.0","id":1}Calling a tool follows the same pattern:
curl https://api.olee.ai/mcp \
-H "Authorization: Bearer $OLEON_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": { "name": "list_orders", "arguments": { "project_slug": "acme-store", "status": "new", "limit": 10 } }
}'GET /mcp needs no credential and describes the server:
{
"name": "oleon-workspace",
"version": "0.1.0",
"transport": "streamable-http",
"endpoint": "https://api.olee.ai/mcp",
"note": "POST JSON-RPC here with an Authorization: Bearer header."
}Versioning and change
Three versions matter here, and they move independently of each other.
| Version | Where it appears | What it tells you |
|---|---|---|
| Protocol version | initialize, as protocolVersion | The MCP revision the server speaks, currently 2025-06-18. Send the revision your client implements; the server answers with the one it will use. |
| Server version | initialize, as serverInfo.version | Which build you reached, currently 0.1.0. |
| Tool set | tools/list | The tools available to the credential you sent. |
Treat tools/list as the source of truth rather than this page. Call it after initialize and work from what comes back. A client that hardcodes a tool list copied out of documentation is a client that breaks the day a tool is added.
What changes without notice
- A new tool.
- A new optional argument on an existing tool.
- A new field in a result, including a new key in
_meta. - A field you have seen before being absent. Empty and null fields are left out of results entirely, so treat every field as optional and never assume a key is present because it was last time.
Ignore what you do not recognise and none of these reaches you as a break.
What we give notice for
Removing a tool, removing or renaming an argument, changing what an argument means, or changing a default. Account holders are told by email before it takes effect, the same way a material change to the terms is announced.
The server is at 0.1.0 and the tool set is still growing. If you are building something other people will depend on, tell us through support, so a change that affects you reaches you directly rather than as a broadcast.
Authorization
OAuth 2.1 authorization code flow with PKCE, for public clients. There are no client secrets, no scopes and no refresh tokens: the credential issued stands for the person who approved it, reaches what they can reach, and lasts until they revoke it.
1. Discover
A POST /mcp without a valid credential answers 401 with a pointer to the resource metadata:
HTTP/2 401
WWW-Authenticate: Bearer resource_metadata="https://api.olee.ai/.well-known/oauth-protected-resource"{
"resource": "https://api.olee.ai/mcp",
"authorization_servers": ["https://api.olee.ai"],
"bearer_methods_supported": ["header"],
"resource_name": "oleon-workspace"
}{
"issuer": "https://api.olee.ai",
"authorization_endpoint": "https://oleon.io/mcp-authorize",
"token_endpoint": "https://api.olee.ai/oauth/token",
"registration_endpoint": "https://api.olee.ai/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}The authorization endpoint is on oleon.io, not on the API. That is where people sign in, so passkeys and two-factor authentication work during approval.
2. Register
curl https://api.olee.ai/oauth/register \
-H "Content-Type: application/json" \
-d '{ "client_name": "My Client", "redirect_uris": ["http://127.0.0.1:33418/callback"] }'redirect_urisstring[]requiredFrom 1 to 10 addresses. Each must use https, or plain http on a loopback host (localhost, 127.0.0.1 or [::1]). Custom schemes are refused.
client_namestringUp to 200 characters. Shown to the person on the approval page.
A successful registration answers 201:
{
"client_id": "…",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"client_name": "My Client",
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code"],
"response_types": ["code"]
}A rejected one answers 400 with invalid_client_metadata or invalid_redirect_uri. Registration is limited to 30 per hour from one address.
3. Send the person to approve
Open the authorization endpoint in the person's browser:
https://oleon.io/mcp-authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
&state=RANDOM_STATEredirect_uri must match one registered for the client exactly, and code_challenge_method must be S256. If the person is signed out, they sign in first and return to the same request.
| Outcome | Redirect |
|---|---|
| The person presses Connect | redirect_uri?code=CODE&state=RANDOM_STATE |
| The person presses Cancel | redirect_uri?error=access_denied&state=RANDOM_STATE |
A code can be exchanged once, within five minutes.
4. Exchange the code
curl https://api.olee.ai/oauth/token \
-d grant_type=authorization_code \
-d code=CODE \
-d redirect_uri=http://127.0.0.1:33418/callback \
-d client_id=CLIENT_ID \
-d code_verifier=VERIFIERThe body may be form encoded, as above, or JSON. A successful exchange answers 200:
{
"access_token": "mcp_sk_…",
"token_type": "Bearer"
}There is no expires_in and no refresh_token. Send the token as Authorization: Bearer on every POST /mcp. When it stops working, the person revoked it or lost access, and a 401 sends your client back to step 1.
| Status | Error | Cause |
|---|---|---|
400 | unsupported_grant_type | grant_type was not authorization_code |
400 | invalid_request | A required field was missing |
401 | invalid_client | The client_id is not registered |
400 | invalid_grant | The code is unknown, used, expired, or was issued for another client or redirect_uri, or the PKCE verifier does not match |
invalid_grant deliberately does not say which of those happened. Token requests are limited to 60 per minute from one address.
