<!-- https://leantools.ai/doc/mcp · HTML twin of this page; the whole documentation: https://leantools.ai/llms-full.txt -->
# MCP server

Agents

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](https://leantools.ai/doc/api)).

## 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.

| Tool | Kind | Billing |
| --- | --- | --- |
| `web_search.search` | Native | One Web Search `request` per query |
| `email.send` | Native | One Email `email` per recipient (to + cc + bcc) |
| `data.list`, `data.describe` | Data | Free |
| `data.query` | Data | One `call` plus `row` per 1,000 rows, at the dataset's rate |
| `data.documents.list`, `data.documents.get` | Data | One `call` plus one `row` per summary or document |
| `data.subscribe`, `data.unsubscribe` | Data | The first month at subscribe; nothing to cancel |
| `connector.<key>.<action>` | Connector | One 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](https://leantools.ai/doc/connectors#approvals)).

### Protocol errors

| Situation | HTTP | JSON-RPC error |
| --- | --- | --- |
| Missing or invalid key | 401 (403 for a suspended workspace), with `WWW-Authenticate: Bearer realm="LeanTools", error="invalid_token"` | `-32001`, `data.code = authentication_error` |
| Unknown tool | 200 | `-32602` |
| Unknown method | 200 | `-32601` |
| Malformed JSON | 200 | `-32700` |
| Batch (array) or message without `method` | 200 | `-32600` |

## Native tools

### web_search.search

- `query`requiredWhat to search for.
- `resultCount`integer 1–50Results per page; the engine default when omitted.
- `language`stringTwo-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

- `to`requiredRecipient address.
- `subject`requiredSubject line.
- `body`requiredPlain text, or HTML when `isBodyHtml` is true.
- `from`, `fromName`, `toName`, `cc`, `bcc`, `isBodyHtml`Optional. 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](https://leantools.ai/doc/credits).
