MCP server

Bidlo runs a Model Context Protocol server at https://app.bidlo.ai/api/mcp. This page covers how to connect a client, which tools a key unlocks, and how to read and change data with them.

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

Client config
{
  "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.

Client config
{
  "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.

MethodWhat it does
POSTCarries JSON-RPC and is answered with one JSON body. Accept must list both application/json and text/event-stream or the answer is a 406, and Content-Type must be application/json or it is a 415. A body holding only notifications is answered 202 with no content.
GETOpens an event stream when Accept is text/event-stream. Nothing is pushed on it and there is no replay, so a client does not need to open one.
DELETEAnswers 200. There is no session to end.
OPTIONSAnswers 204 with the CORS headers, before the key is read.
A tool call over HTTP
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.

ToolREST operationWhat it does
meGET /meShows what this key can do: its permissions, its collection limit and its expiry.
collections_listGET /collectionsLists the collections this key can read.
collections_getGET /collections/<collection_id>Reads one collection, by id or by name.
fields_listGET /collections/<collection_id>/fieldsLists a collection’s fields: id, name and type.
documents_listGET /collections/<collection_id>/documentsReads a page of documents: filters, sorts, a limit and a cursor.
documents_queryPOST /collections/<collection_id>/documents/queryThe same read, with the query as one JSON object.
documents_getGET /documents/<document_id>Reads one document by id.
notes_getGET /documents/<document_id>/noteReads a document’s note as Markdown.
options_listGET /fields/<field_id>/optionsLists the options on a tag field.
files_getGET /files/<file_id>Reads file details and a download URL valid for 15 minutes.
comments_listGET /documents/<document_id>/commentsReads a document’s comments.
history_listGET /documents/<document_id>/historyReads 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.

ToolREST operationWhat it does
documents_createPOST /collections/<collection_id>/documentsAdds one document, or up to 500 at once.
documents_updatePATCH /documents/<document_id>Changes fields on one document.
documents_bulkUpdatePATCH /collections/<collection_id>/documentsChanges fields on up to 500 documents in one collection.
documents_upsertPOST /collections/<collection_id>/documents/upsertMatches rows on one to three fields, updates the matches and adds the rest. Can dry-run first.
relations_setPUT /documents/<document_id>/relations/<field_id>Replaces the documents a relation field points at.
documents_trashDELETE /documents/<document_id>Moves a document to trash.
documents_restorePOST /documents/<document_id>/restoreBrings a document back out of trash.
notes_putPUT /documents/<document_id>/noteReplaces a document’s note, refusing if someone saved it since you read it.
notes_appendPOST /documents/<document_id>/note/appendAdds text to the end of a note.
files_uploadPOST /files/uploadsReturns a signed token and URL for uploading a file.
files_finalizePOST /filesChecks an upload, creates the file and queues parsing.
files_deleteDELETE /files/<file_id>Moves a team-owned file to trash.
comments_createPOST /documents/<document_id>/commentsAdds a comment as the key owner.
comments_updatePATCH /comments/<comment_id>Edits the key owner’s own comment.
comments_deleteDELETE /comments/<comment_id>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.

ToolREST operationWhat it does
collections_createPOST /collectionsCreates a collection for your team.
collections_updatePATCH /collections/<collection_id>Renames a collection or changes its description or icon.
collections_deleteDELETE /collections/<collection_id>Deletes a team-owned collection or hides a public collection for your team.
fields_createPOST /collections/<collection_id>/fieldsAdds a field, formula or relation.
fields_updatePATCH /fields/<field_id>Renames a field or changes its settings or formula.
fields_deleteDELETE /fields/<field_id>Removes a field; primary and system fields stay.
options_createPOST /fields/<field_id>/optionsAdds 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.

Arguments
{ "collection_id": "<collection_id>" }
Output shape
{
  "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.

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
}
Output shape
{
  "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.

Arguments
{
  "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"
}
Output shape
{
  "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 wrongHow it comes backWhat to do
The key is refusedHTTP 401 (403 for a lapsed subscription) with the JSON-RPC error -32001 and the reason, such as Invalid API keyCheck the key and the URL. Access and keys lists the reasons.
A tool call failsA result marked isError whose text is the JSON error bodyRead error.code and error.message.
Anything failing behind the serverHTTP 500 with the JSON-RPC error -32000 and MCP request failedRetry, then write to support.
A tool that failed
{
  "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.