Skip to content
Docs

Agents

MCP server

LeanTools speaks the Model Context Protocol over Streamable HTTP. One endpoint, /mcp, exposes the tools the workspace has switched on: the native web search and email tools, LeanData datasets and LeanConnect connector actions. Each tools/call is billed like the REST call it wraps and admitted by the same rules. LeanBrowser, LeanModel, LeanMachine, LeanVision and LeanMemory are not MCP tools; they take the same key over REST and WebSocket (see the reference).

Transport#

  • POST /mcp with one JSON-RPC 2.0 message per request. Stateless: no SSE stream, no session id, no batching.
  • Authenticate with the workspace API key as a bearer: Authorization: Bearer lt-….
  • Protocol versions 2025-06-18, 2025-03-26 and 2024-11-05 are negotiated; an unknown version is answered with the latest.
  • Accept: text/event-stream is tolerated but the answer is always a single JSON document. Notifications (no id) are acknowledged with 202 and no body. GET and DELETE /mcp answer 405. Bodies over LeanTools.Mcp.MaxBodyBytes (1 MiB) are 413.

Initialize and ping#

request
curl -s https://YOUR-HOST/mcp -H "Authorization: Bearer lt-..." -H "Content-Type: application/json" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "my-agent", "version": "1.0" } } }'
response
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": false } },
  "serverInfo": { "name": "LeanTools", "version": "1.0.0" }, "instructions": "LeanTools tools server. Every tool call is billed in credits ..." } }

ping answers {}.

The catalogue: tools/list#

The list is computed per workspace and per key: it holds only the tools the workspace has enabled and the key is scoped to. Names use dots; the underscore form (data_query) is accepted on calls for clients that cannot send dots.

ToolKindBilling
web_search.searchNativeOne Web Search request per query
email.sendNativeOne Email email per recipient (to + cc + bcc)
data.list, data.describeDataFree
data.queryDataOne call plus row per 1,000 rows, at the dataset's rate
data.documents.list, data.documents.getDataOne call plus one row per summary or document
data.subscribe, data.unsubscribeDataThe first month at subscribe; nothing to cancel
connector.<key>.<action>ConnectorOne connector call at the connector's rate, only when the action completed
tools/list (excerpt)
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [
  { "name": "web_search.search", "title": "Web Search: search",
    "description": "Searches the web through the platform's search engine and returns ranked results (title, url, description). Billed 0.005 credits per query.",
    "inputSchema": { "type": "object", "properties": { "query": { "type": "string" }, "resultCount": { "type": "integer", "minimum": 1, "maximum": 50 }, "language": { "type": "string" } }, "required": ["query"], "additionalProperties": false },
    "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": true }, "kind": "NativeSearch" },
  { "name": "data.query", "title": "Query a dataset", "description": "Runs a filtered, ordered, paged query ... Billed per call plus per 1,000 rows returned ...",
    "inputSchema": { "type": "object", "properties": { "dataset": { "type": "string", "enum": ["leantools-usage.service-usage"] }, "filters": { "type": "array" }, "take": { "type": "integer" } }, "required": ["dataset"] },
    "annotations": { "readOnlyHint": true, "destructiveHint": false, "idempotentHint": true, "openWorldHint": false }, "kind": "DataQuery" },
  { "name": "connector.twilio.sms.send", "title": "Twilio: Send SMS", "description": "... Billed 1 credits per completed call. Needs a Twilio credential.",
    "inputSchema": { "type": "object", "properties": { "input": { "type": "object" }, "credentialId": { "type": "string" }, "credential": { "type": "object" } } },
    "annotations": { "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false, "openWorldHint": true }, "kind": "ConnectorAction", "itemKey": "Twilio", "actionKey": "sms.send" }
] } }

Annotations follow the MCP specification so harnesses can ask before destructive calls. Connector actions a workspace has disabled in the console are absent from the list; a connector's default credential is used when a call names none.

Calling a tool: tools/call#

request
{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
  "params": { "name": "data.query", "arguments": { "dataset": "leantools-usage.service-usage", "take": 50, "orderBy": "day", "descending": true } } }
response
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text",
  "text": "{\"dataset\":\"leantools-usage.service-usage\",\"columns\":[...],\"rows\":[[...]],\"nextPageToken\":\"NTA=\",\"totalCount\":null,\"truncated\":false,\"usage\":{\"credits\":0.105,\"balance\":1868.05,\"rows\":50}}" } ], "isError": false } }

The text is the JSON the REST route would answer plus its usage block. Results over LeanTools.Mcp.MaxResultChars (200,000 characters) drop trailing rows and say so (truncated: true, rowsShown, rowsFetched) while keeping nextPageToken; the rows fetched were billed, so ask for smaller pages.

Refusals and failures#

A refused or failed call is a result with isError: true whose text carries the same code and status the REST API would answer, so the agent can read the reason and react:

{ "jsonrpc": "2.0", "id": 4, "result": { "content": [ { "type": "text",
  "text": "{\"error\":{\"code\":\"insufficient_credits\",\"message\":\"The account has no credits left. Top up or renew the subscription to continue.\",\"status\":402}}" } ], "isError": true } }

A connector write held for approval is such a result too: approval_pending, status 202, and error.details with approvalId, expiresUtc and pollUrl the agent can poll (see approvals).

Protocol errors#

SituationHTTPJSON-RPC error
Missing or invalid key401 (403 for a suspended workspace), with WWW-Authenticate: Bearer realm="LeanTools", error="invalid_token"-32001, data.code = authentication_error
Unknown tool200-32602
Unknown method200-32601
Malformed JSON200-32700
Batch (array) or message without method200-32600

Native tools#

  • queryrequiredWhat to search for.
  • resultCountinteger 1–50Results per page; the engine default when omitted.
  • languagestringTwo-letter language code, e.g. en.

Returns { query, totalResultCount, items[{ position, title, url, description }] }. The search engine is chosen by the host; the caller never picks it.

email.send#

  • torequiredRecipient address.
  • subjectrequiredSubject line.
  • bodyrequiredPlain text, or HTML when isBodyHtml is true.
  • from, fromName, toName, cc, bcc, isBodyHtmlOptional. Omit from to send from the platform address.

Returns { isSuccess, provider }. Because sending mail is a write, harnesses that honour annotations ask before calling it (readOnlyHint: false).

Bring your own server#

Any remote MCP server can be registered as a connector (the Generic MCP connector type): give it a URL and a credential, and its tools appear in the catalogue as connector.<key>.<tool>, metered like every other connector call and subject to the same guardrails. Stdio transports are not accepted by a hosted gateway (403 transport_not_allowed).

Limits#

LeanTools.Mcp.MaxBodyBytes
Request body limit, 1 MiB by default (413).
LeanTools.Mcp.MaxResultChars
Result text limit, 200,000 characters by default; longer results are truncated with a page token.
Per-connector rate limit and daily cap
Set in the console; 429 rate_limited / 429 daily_cap_reached.
In-flight reserve
See Credits and billing.

Updated September 30, 2026 · This page as Markdown · All docs as one file