MCP server
Open in ChatGPTOpen in Claudellms.txtllms-full.txtopenapi.json
Contents
What is the Bidlo MCP server?
The Bidlo MCP server is a Model Context Protocol server at https://app.bidlo.ai/api/mcp that lets an agent read a Bidlo team's data and, with a key that can edit data, change it. Each tool is one operation of the REST API, with the same inputs, answers and errors. The server's name is bidlo-mcp. The key names the team, so no tool takes a team argument. Querying data covers the collections, fields and filters both share.
How do I connect a client?
Point the client at https://app.bidlo.ai/api/mcp and send your API key in an Authorization header; the three configs below are that one connection in the three shapes clients ask for.
Settings → Features → MCP mints a read-only key labelled Bidlo MCP and prints a finished config under it. For a key that can edit data, make one under Settings → Features → API. Access and keys has the rest.
Use https://app.bidlo.ai/api/mcp exactly. The old lite.bidlo.ai host redirects there, and clients drop the Authorization header when they follow a redirect to another host, so a call sent to it comes back 401.
Cursor
Cursor installs from a deeplink. The config inside the link is base64, so re-encode it with a real key in place of YOUR_API_KEY rather than editing the string.
cursor://anysphere.cursor-deeplink/mcp/install?name=bidlo&config=eyJ1cmwiOiJodHRwczovL2FwcC5iaWRsby5haS9hcGkvbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiQmVhcmVyIFlPVVJfQVBJX0tFWSJ9fQ==Streamable HTTP
A client that speaks streamable HTTP takes the URL and the header directly. Claude Code and Cursor both do.
{
"url": "https://app.bidlo.ai/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}mcp-remote
A client that speaks only stdio runs the mcp-remote shim over npx, which holds the HTTP connection for it. This is the block the app's own MCP settings page prints.
{
"mcpServers": {
"bidlo": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://app.bidlo.ai/api/mcp",
"--header",
"Authorization: Bearer YOUR_API_KEY"
]
}
}
}How does the transport work?
The server speaks streamable HTTP and is stateless, so every request carries the key and everything else it needs, and a POST is answered with one JSON body.
| Method | What it does |
|---|---|
| POST | Carries JSON-RPC and is answered with one JSON body. Accept must list both application |
| GET | Opens an event stream when Accept is text |
| DELETE | Answers 200. There is no session to end. |
| OPTIONS | Answers 204 with the CORS headers, before the key is read. |
curl -X POST https://app.bidlo.ai/api/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "me", "arguments": {} }
}'There is no OAuth handshake and nothing to register. The discovery paths a client probes answer 404 on purpose, so the client falls through to the key it was given.
Which tools can a client call?
A client sees only the tools its key allows: 12 read tools for a key that can read data, 15 data write tools with Edit data, and 7 collection and field tools with Edit collections and fields. Each tool is a REST operation, named with underscores, and its arguments are that operation's inputs: the path parts become arguments beside the body.
Read tools
A key needs Read data. These carry readOnlyHint, so a client can run them without asking.
| Tool | REST operation | What it does |
|---|---|---|
| me | GET /me | Shows what this key can do: its permissions, its collection limit and its expiry. |
| collections_list | GET /collections | Lists the collections this key can read. |
| collections_get | GET /collections | Reads one collection, by id or by name. |
| fields_list | GET /collections | Lists a collection’s fields: id, name and type. |
| documents_list | GET /collections | Reads a page of documents: filters, sorts, a limit and a cursor. |
| documents_query | POST /collections | The same read, with the query as one JSON object. |
| documents_get | GET /documents | Reads one document by id. |
| notes_get | GET /documents | Reads a document’s note as Markdown. |
| options_list | GET /fields | Lists the options on a tag field. |
| files_get | GET /files | Reads file details and a download URL valid for 15 minutes. |
| comments_list | GET /documents | Reads a document’s comments. |
| history_list | GET /documents | Reads a document’s field history. |
Write tools
A key needs Edit data. Read data is not required for write tools. documents_trash carries destructiveHint, so a careful client asks before it runs.
| Tool | REST operation | What it does |
|---|---|---|
| documents_create | POST /collections | Adds one document, or up to 500 at once. |
| documents_update | PATCH /documents | Changes fields on one document. |
| documents_bulkUpdate | PATCH /collections | Changes fields on up to 500 documents in one collection. |
| documents_upsert | POST /collections | Matches rows on one to three fields, updates the matches and adds the rest. Can dry-run first. |
| relations_set | PUT /documents | Replaces the documents a relation field points at. |
| documents_trash | DELETE /documents | Moves a document to trash. |
| documents_restore | POST /documents | Brings a document back out of trash. |
| notes_put | PUT /documents | Replaces a document’s note, refusing if someone saved it since you read it. |
| notes_append | POST /documents | Adds text to the end of a note. |
| files_upload | POST /files | Returns a signed token and URL for uploading a file. |
| files_finalize | POST /files | Checks an upload, creates the file and queues parsing. |
| files_delete | DELETE /files | Moves a team-owned file to trash. |
| comments_create | POST /documents | Adds a comment as the key owner. |
| comments_update | PATCH /comments | Edits the key owner’s own comment. |
| comments_delete | DELETE /comments | Deletes the key owner’s own comment. |
Collection and field tools
A key needs Edit collections and fields (schema:write). Full access includes it; Read and write data does not. A Custom key can have just data:write or just schema:write; it sees only that group and cannot call me. The creator’s current role still limits every tool.
| Tool | REST operation | What it does |
|---|---|---|
| collections_create | POST /collections | Creates a collection for your team. |
| collections_update | PATCH /collections | Renames a collection or changes its description or icon. |
| collections_delete | DELETE /collections | Deletes a team-owned collection or hides a public collection for your team. |
| fields_create | POST /collections | Adds a field, formula or relation. |
| fields_update | PATCH /fields | Renames a field or changes its settings or formula. |
| fields_delete | DELETE /fields | Removes a field; primary and system fields stay. |
| options_create | POST /fields | Adds a tag option, with an optional colour. |
Which tools a key sees
Calling a tool the key does not have fails with Tool <name> not found. A key made under Settings → Features → MCP, or with no recorded maker, is read-only, and a key loses its write tools when its maker's role stops allowing edits. Call me to see what a key can do now.
Tool inputs
collection_id takes a collection id or its name, and a field takes its id or a name only one field has. An argument the tool does not take, such as team_id, fails the call. Every tool's schema is in /openapi.json.
Coming. Market tools are coming: search_projects, search_contractors, search_facilities, and comparable unit prices for an item in a county. None of them is live today. Compose the same reads with documents_query until they are.
How do I find and read data?
List the collections, read the fields on the one you want, then query it. In the examples, angle brackets mark a value to look up, and Name, Bid Date and County are example field names; read your own off fields_list.
me
Takes no arguments and returns the key's permissions, collection limit and expiry. Access and keys shows the answer.
collections_list
Takes no arguments and lists the collections this key can read, with their ids.
fields_list
Lists the fields on one collection: the id, the name your team gave it and its type. reference_id names the collection a relation points at.
{ "collection_id": "<collection_id>" }{
"data": [
{
"id": "<field_id>",
"name": "Bid Date",
"field_type": "date",
"primary_key": false,
"reference_id": null,
"description": "…"
}
]
}documents_query
Reads one page of documents. Fields, filters and sorts name a field by id or by name, one filter per field. Querying data lists the operators each field type takes. documents_list takes the same arguments.
{
"collection_id": "<collection_id>",
"fields": ["Name", "Bid Date", "County"],
"filters": [
{
"field_id": "Bid Date",
"operator": "between",
"value": { "start": "2026-10-01", "end": "2026-12-31" }
}
],
"sorts": [{ "field_id": "Bid Date", "direction": "asc" }],
"limit": 100
}{
"data": [
{
"id": "<document_id>",
"name": "…",
"collection_id": "<collection_id>",
"created_at": "…",
"updated_at": "…",
"fields": {
"<field_id>": {
"value": "…",
"field_name": "Bid Date",
"field_type": "date",
"field_id": "<field_id>",
"field_value_id": "<field_value_id>"
}
}
}
],
"meta": {
"page": 1,
"limit": 100,
"collection_id": "<collection_id>",
"groupPagination": "…",
"next_cursor": "<next_cursor>"
}
}Values come back under fields, keyed by field id, each one carrying its field name and type.
The next page
Send meta.next_cursor back as cursor, with the same query, until it comes back null. Querying data covers paging and grouping.
The server ships this recipe as the prompt query-bidlo-data, and the resource bidlo://collections holds what collections_list returns.
How do I change data?
Call the write tool that fits the change with a key that can edit data, and run an upsert with dry_run set to true before you run it for real.
documents_upsert
Matches each row on 1 to 3 fields, updates the documents that match and adds the rest. name is the document's title field, and fields holds the others by id or name. update_mode is overwrite or fill_empty, which leaves a value already there alone. With dry_run set to true it reports what it would do and writes nothing.
{
"collection_id": "<collection_id>",
"match_fields": ["Name"],
"rows": [
{
"name": "<name>",
"fields": { "Bid Date": "2026-11-12", "County": ["<document_id>"] }
}
],
"dry_run": true,
"update_mode": "fill_empty"
}{
"data": {
"success": true,
"dry_run": true,
"created": <number>,
"updated": <number>,
"…": "…",
"errors": []
}
}A row that matches more than one document is listed under data.errors as ambiguous_match. REST API has the match rules and what a write refuses.
Retrying a write
Send a fresh Idempotency-Key header on each write's MCP request, never in a client config's fixed headers. REST API explains how a retry replays.
How are errors reported?
A tool that fails returns a result marked isError whose text is the JSON error body, and a key the server refuses fails the whole HTTP request with a JSON-RPC error.
| What went wrong | How it comes back | What to do |
|---|---|---|
| The key is refused | HTTP 401 (403 for a lapsed subscription) with the JSON-RPC error -32001 and the reason, such as Invalid API key | Check the key and the URL. Access and keys lists the reasons. |
| A tool call fails | A result marked isError whose text is the JSON error body | Read error.code and error.message. |
| Anything failing behind the server | HTTP 500 with the JSON-RPC error -32000 and MCP request failed | Retry, then write to support. |
{
"content": [
{
"type": "text",
"text": "{ \"error\": { \"code\": \"validation\", \"message\": \"Unknown field\", \"details\": … }, \"request_id\": \"<request_id>\" }"
}
],
"isError": true
}The error body is the REST API's. REST API lists every code.
What are the limits?
A query returns 1 to 500 documents a page, 100 by default, and each key gets 600 reads and 120 writes a minute.
A tool result carries no Retry-After, so over the limit wait for the next minute. REST API has every other cap.
## For agents: how to use Bidlo data - Read and write Bidlo data through its MCP server at https://app.bidlo.ai/api/mcp — streamable HTTP, no OAuth. - Get a Bidlo account first: all Bidlo data needs one, https://www.bidlo.ai/developers/access.md says how to get one, and there is no public tier or self-serve signup. - Authenticate with the header `Authorization: Bearer sk_live_...`; the REST API takes the same key, and `me` shows what the key may do. - A team owner or admin mints that key in the app under Settings, then Features, then API, and chooses Read only, Read and write data, or Full access; collection and field changes need `schema:write`. - Read the Bidlo API docs at https://www.bidlo.ai/developers.md, the OpenAPI 3.1 spec for the REST API at https://www.bidlo.ai/openapi.json, and the reference pages: https://www.bidlo.ai/developers/querying.md (filters, operators, dates, places, pages), https://www.bidlo.ai/developers/mcp.md (the tools), https://www.bidlo.ai/developers/rest.md (the routes) and https://www.bidlo.ai/developers/access.md (keys). - Prefer the MCP tools over REST; use `documents_query` with `within_distance` for anything shaped like "near X". - Call `fields_list` before composing a filter — it returns each field’s id, name and type. - Never invent a Bidlo unit price. Read one off the bid items, or say the forecast is not available.