Servidor MCP
Abrir en ChatGPTAbrir en Claudellms.txtllms-full.txtopenapi.json
Contenido
Documentación para desarrolladores
¿Qué es el servidor MCP de Bidlo?
El servidor MCP de Bidlo es un servidor Model Context Protocol de solo lectura en https://lite.bidlo.ai/api/mcp que le da a un agente cinco herramientas para leer las obras, las partidas, los contratistas y las plantas de un equipo de Bidlo. Son los mismos datos que tu equipo ve en la app, leídos por un agente y no por una persona.
El servidor ofrece cinco herramientas, dos recursos y un prompt, y ninguna herramienta de escritura. Su nombre es bidlo-mcp. Una sola clave sostiene toda la conexión: la clave identifica al equipo, así que ninguna herramienta lleva un parámetro de equipo y no hay nada más que configurar. La misma clave lee la REST API, and Consultar datos describe las colecciones, los campos y los filtros que comparten las dos vías.
¿Cómo conecto un cliente?
Apunta el cliente a https://lite.bidlo.ai/api/mcp y manda tu clave de API en un encabezado Authorization; las tres configuraciones de abajo son esa misma conexión en las tres formas que piden los clientes.
Se necesita una cuenta de Bidlo. Cualquiera puede conseguir una agendando una llamada en https://cal.com/matt-wolfe-yecrho/30min — Bidlo prepara el equipo, y luego un dueño o administrador del equipo crea la clave de API en Settings → Features → API. No hay un plan público ni se puede abrir una cuenta por cuenta propia.
Settings → Features → MCP crea la misma clave con la etiqueta Bidlo MCP con acceso de lectura y muestra debajo una configuración lista. Acceso y claves cubre cómo crear una, rotarla y revocarla.
Cursor
Cursor se instala con un enlace directo. La configuración que lleva el enlace está en base64, así que vuelve a codificarla con una clave real en lugar de YOUR_API_KEY en vez de editar el texto.
cursor://anysphere.cursor-deeplink/mcp/install?name=bidlo&config=eyJ1cmwiOiJodHRwczovL2xpdGUuYmlkbG8uYWkvYXBpL21jcCIsImhlYWRlcnMiOnsiQXV0aG9yaXphdGlvbiI6IkJlYXJlciBZT1VSX0FQSV9LRVkifX0=HTTP con transmisión continua
Un cliente que habla streamable HTTP toma la URL y el encabezado directamente. Claude Code y Cursor lo hacen.
{
"url": "https://lite.bidlo.ai/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}mcp-remote
Un cliente que solo habla stdio corre el mcp-remote como adaptador sobre npx, que mantiene la conexión HTTP por él. Este es el bloque que imprime la propia página de MCP en la app.
{
"mcpServers": {
"bidlo": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://lite.bidlo.ai/api/mcp",
"--header",
"Authorization: Bearer YOUR_API_KEY"
]
}
}
}¿Cómo funciona el transporte?
El servidor habla HTTP con transmisión continua y no guarda estado, así que cada solicitud lleva la clave y todo lo demás que necesita, y un POST se responde con un solo cuerpo JSON.
| Método | Lo que hace |
|---|---|
| POST | Lleva JSON-RPC y se responde con un solo cuerpo JSON. Accept debe listar los dos: application/json and text/event-stream o la respuesta es un 406, and Content-Type debe ser application/json o es un 415. Un cuerpo que solo lleva notificaciones se responde con 202 sin contenido. |
| GET | Abre un flujo de eventos cuando Accept is text/event-stream. No se envía nada por ahí y no hay reenvío, así que un cliente no necesita abrir una. |
| DELETE | Respuestas 200. No hay sesión que cerrar. |
| OPTIONS | Respuestas 204 con los encabezados CORS, antes de leer la clave. |
curl -X POST https://lite.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": "get_team_collections", "arguments": {} }
}'Sin estado quiere decir justo eso. No se emite ningún id de sesión, cada solicitud la responde un servidor nuevo y nada de lo que fijes en una solicitud pasa a la siguiente, así que manda la clave y los argumentos completos cada vez. Las versiones de protocolo de 2024-11-05 to 2025-11-25 se aceptan en el encabezado Mcp-Protocol-Version ; una versión que el servidor no conoce es un 400.
No hay intercambio OAuth ni nada que dar de alta. Las rutas de descubrimiento que un cliente prueba bajo el servidor responden 404 a propósito, así que el cliente pasa a usar el token bearer que recibió. Las respuestas llevan Access-Control-Allow-Origin: *, and authorization, content-type, mcp-session-id, mcp-protocol-version and last-event-id son los encabezados permitidos en la solicitud.
¿Qué herramientas puede llamar un cliente?
Hay cinco herramientas, todas de solo lectura, y cada una responde con un solo bloque de texto con JSON con formato. En las formas de abajo, un nombre entre corchetes angulares representa un valor que te toca buscar y los puntos suspensivos representan un valor que se omitió.
| Herramienta | Lo que hace |
|---|---|
| get_team_collections | Lista las colecciones que esta clave puede leer. |
| get_collection_fields | Lista los campos de una o más colecciones, con los operadores que acepta cada campo. |
| resolve_documents_by_name | Convierte un nombre en el id que necesita un filtro. |
| query_database_data | Lee una página de documentos: filtros, orden, un límite y una página. |
| query_mentioned_document | Lee un documento por id. |
get_team_collections
Lista las colecciones que esta clave puede leer. No recibe argumentos. La clave identifica al equipo, así que no hay nada que acotar ni un parámetro de equipo en esta ni en ninguna otra herramienta.
{
"collections": [
{
"collection_id": "<collection_id>",
"name": "Project",
"description": "…",
"mutable": true
}
]
}Las colecciones regresan ordenadas por nombre. mutable dice si tu equipo puede editar la colección en la app; no tiene efecto en este servidor, que nunca escribe.
Nada de esto falla por sí solo. Una clave ausente o mala es el 401 en la sección de errores de abajo.
get_collection_fields
Lista los campos de una o más colecciones, con los operadores que acepta cada campo. Dale ids de colección, no nombres. Lee los ids en get_team_collections primero; se usan los primeros 20 ids de la lista y el resto se descarta.
| Parámetro | Type | Obligatorio | Notes |
|---|---|---|---|
| collection_ids | array of strings | Yes | Ids de colección. Se usan los primeros 20. |
{
"collections": [
{
"collection_id": "<collection_id>",
"collection_name": "Project",
"field_count": <number>,
"fields": [
{
"field_id": "<field_id>",
"name": "Bid Date",
"field_type": "date",
"filter_field_type": "date",
"primary_key": false,
"reference_collection_id": null,
"description": "…",
"aka": ["…"]
}
]
}
],
"unknown_collection_ids": [],
"operators_by_filter_field_type": { "date": ["eq", "gt", "lt", "between", "…"] },
"filter_value_shapes": { "date_between": { "start": "ISO date string", "end": "ISO date string" } },
"formula_expression_syntax": "…"
}{
"collection_ids": ["<collection_id>", "<collection_id>"]
}field_id es como la app distingue dos campos, y es lo que nombra un filtro REST. name es como tu equipo llama al campo: un campo que viene de fábrica se puede renombrar, así que dos equipos pueden tener nombres distintos para el mismo campo, y por eso en esta página no se imprime ningún id ni ningún nombre de campo como si fuera un hecho. filter_field_type es el tipo con el que se filtra el campo, y es la clave con la que está escrita la tabla de operadores. reference_collection_id nombra la colección a la que apunta una relación, que es la colección donde buscar con resolve_documents_by_name. La respuesta también repite los operadores y los formatos de valor, así que un cliente puede leerlos en el momento en vez de cargar una copia.
Un id de colección que no es de tu equipo regresa en unknown_collection_ids en vez de fallar la llamada. Una colección con todos los campos ocultos falla con Collection has no visible fields.
resolve_documents_by_name
Convierte un nombre en el id que necesita un filtro. Un filtro sobre un contratista, un condado o una agencia quiere el id del documento, no su nombre. Esta herramienta busca por nombre en una colección y devuelve los ids.
| Parámetro | Type | Obligatorio | Notes |
|---|---|---|---|
| collection_id | string | Yes | La colección donde buscar, por id. |
| query | string | No | Texto libre. Cuando está puesto, names se ignora. |
| names | array of strings | No | Se usan los primeros 10. |
| limit | integer | No | Coincidencias por entrada. De 1 a 25, 10 por defecto. |
{
"collection_id": "<collection_id>",
"collection_name": "Counties",
"lookups": [
{
"input": "Travis County",
"exactMatches": [
{ "id": "<document_id>", "name": "Travis County", "document_mention": "…" }
],
"candidates": [],
"unresolved": false
}
],
"unresolved_names": [],
"summary": "…"
}{
"collection_id": "<collection_id>",
"names": ["Travis County", "Bexar County"],
"limit": 10
}Una coincidencia exacta es aquella cuyo nombre coincide después de normalizar mayúsculas y espacios. Todo lo demás que regresó es un candidato, y un nombre que no coincidió con nada aparece en unresolved_names. document_mention es un enlace a la app de Bidlo, no una URL pública.
Manda uno de query or names, o la llamada falla con el mensaje Provide `query` or `names` for document lookup. Un id de colección que no es de tu equipo falla con Collection is not available for this team.
query_database_data
Lee una página de documentos: filtros, orden, un límite y una página. Esta es la herramienta que lee datos. Recibe una colección por nombre y referencias de campos por nombre, las filtra y las ordena, y devuelve una página de documentos.
| Parámetro | Type | Obligatorio | Notes |
|---|---|---|---|
| collection_name | string | Yes | Un nombre, no un id. La coincidencia ignora mayúsculas, espacios y el plural al final. |
| filters | array | No | Cada uno un nombre de campo, un operador y un valor. Se combinan con AND. |
| sorts | array | No | Cada uno un nombre de campo y una dirección, aplicados en el orden del arreglo. |
| return_fields | array of strings | No | Nombres de campo. Pide los campos que quieres. |
| limit | integer | No | De 1 a 100, 25 por defecto. |
| page | integer | No | Desde 1, con tope en 20. |
| query | string | No | Texto libre que se busca en toda la colección. |
{
"collection_id": "<collection_id>",
"collection_name": "Project",
"page": 1,
"query": null,
"grouped": false,
"selected_fields": [
{ "field_id": "<field_id>", "field_name": "Bid Date" }
],
"documents": [
{
"document_id": "<document_id>",
"document_name": "…",
"document_mention": "…",
"fields": {
"Bid Date": "…",
"County": { "id": "<document_id>", "name": "…", "document_mention": "…" },
"Engineer's Estimate": "…"
}
}
],
"returned_row_count": <number>,
"has_more": true,
"next_page": 2,
"warnings": ["…"]
}{
"collection_name": "Project",
"filters": [
{
"field_name": "Bid Date",
"operator": "between",
"value": { "start": "2026-10-01", "end": "2026-12-31" }
},
{ "field_name": "County", "operator": "contains", "value": "Travis County" }
],
"sorts": [{ "field_name": "Bid Date", "direction": "asc" }],
"return_fields": ["Name", "Bid Date", "County", "Agency", "Engineer's Estimate"],
"limit": 25,
"page": 1
}Los operadores son is_null, is_not_null, eq, ne, contains, not_contains, starts_with, ends_with, gt, lt, gte, lte, between, relative_range, within_distance, outside_distance. Cuáles acepta un campo dado dependen del tipo con el que se filtra, y Consultar datos tiene esa tabla junto con la forma de valor que pide cada uno. Hay cuatro comportamientos que conviene conocer antes de la primera llamada. Sin return_fields vuelve un conjunto predeterminado de hasta 8 campos con un aviso que lo dice, así que nombra los campos que quieres. Un segundo filtro en un campo que ya tiene uno lo reemplaza en vez de fallar, que es lo contrario de lo que hace REST en la REST API. Un valor de relación o de etiqueta dado como nombre se busca por ti, y un valor dado como id se usa tal cual. Y la respuesta no trae un conteo total: has_more and next_page son la forma de saber que hay otra página.
Una entrada que la validación rechaza hace fallar la llamada: un limit mayor que 100, un operador fuera de la lista de arriba, una dirección que no sea asc or desc. Una colección, un campo de retorno, un campo de filtro o un campo de orden que el servidor no encuentra no hace fallar la llamada; regresa como un resultado normal cuyo JSON lleva un error como clave, y el mensaje nombra la herramienta que sigue. Lo mismo pasa con un nombre ambiguo en un filtro de relación, que apunta a resolve_documents_by_name.
query_mentioned_document
Lee un documento por id. Úsala cuando ya tienes el id de un documento y quieres esa obra, ese contratista o esa planta completos en vez de una página de filas.
| Parámetro | Type | Obligatorio | Notes |
|---|---|---|---|
| document_id | string | Yes | From resolve_documents_by_name, o de document_id en un resultado de consulta. |
| fields_to_fetch | array of strings | No | Nombres de campo o ids de campo. |
{
"_document_id": "<document_id>",
"_document_name": "…",
"document_mention": "…",
"_collection": "Project",
"document_profile": "…",
"Bid Date": "…",
"County": { "id": "<document_id>", "name": "…", "document_mention": "…" },
"Engineer's Estimate": "…",
"omitted_fields": { "field_names": ["…"], "note": "…" }
}{
"document_id": "<document_id>",
"fields_to_fetch": ["Name", "Bid Date", "Agency", "Low Bid"]
}Los valores de los campos van en el nivel superior del objeto con sus propios nombres, así que el cliente los lee por nombre. Sin fields_to_fetch, los campos vacíos se descartan y regresan 40 campos como máximo; el resto se nombran en omitted_fields, y una segunda llamada que los nombre los carga.
Un id de documento que no es de tu equipo regresa como un resultado normal que lleva error y el mensaje Document <document_id> not found or access denied.
Próximamente. Vienen herramientas de mercado: search_projects, search_contractors, search_facilities, y precios unitarios comparables de una partida en un condado. Ninguna está disponible hoy. Arma las mismas consultas con query_database_data hasta que lo estén.
¿En qué orden conviene llamarlas?
Lista las colecciones, lee los campos de la que quieres, convierte los nombres a ids, luego consúltala, y lee un solo documento por id cuando necesites todo lo que tiene.
- get_team_collections para las colecciones que esta clave puede leer, con sus ids.
- get_collection_fields para los campos de la colección que quieres, con los nombres de campo por los que filtrar y la colección a la que apunta una relación.
- resolve_documents_by_name para cualquier nombre escrito en palabras que vaya a ser valor de un filtro, como un condado o un contratista. Sáltala cuando ya tienes el id.
- query_database_data para la página de obras, partidas o contratistas en sí.
- query_mentioned_document cuando una de esas filas es la respuesta y quieres todo lo que trae.
El servidor trae esa receta como prompt, query-bidlo-data, así que un cliente puede cargarlo en vez de que se lo digan. Dos recursos ahorran una llamada en el primer paso.
| Recurso | Qué contiene |
|---|---|
| bidlo://schema | Un resumen en texto plano de las colecciones de tu equipo y de los campos de cada una. |
| bidlo://collections | El mismo JSON que devuelve get_team_collections. |
¿Cómo se reportan los errores?
Por dos canales: la entrada que el servidor no puede aceptar hace fallar la llamada y se marca como error, y una solicitud que entendió pero no pudo responder regresa como un resultado normal cuyo JSON lleva una clave error.
| Qué falló | Cómo regresa | Qué hacer |
|---|---|---|
| Una clave ausente, mal formada o desconocida | HTTP 401 con el error JSON-RPC -32001 y un mensaje que empieza con Unauthorized: | Revisa que el encabezado diga Authorization: Bearer sk_live_… y crea una clave nueva si la anterior se borró. |
| Una suscripción de equipo vencida | The same 401, with Unauthorized: Team subscription expired | La clave está bien. El equipo necesita renovar su suscripción. |
| Entrada que la validación rechaza | La llamada falla y el resultado se marca como error, con un texto que empieza con Input validation error: | Lee el mensaje: nombra el argumento. Un limit demasiado grande se rechaza, no se recorta. |
| Un nombre de herramienta que el servidor no tiene | La llamada falla y el resultado se marca como error, con Tool <name> not found | Llama a las cinco de arriba. |
| Una colección, un campo o un campo de orden desconocidos | Un resultado normal cuyo JSON lleva un error como clave | El mensaje nombra la herramienta que sigue y lista lo que hay disponible. |
| Un nombre ambiguo en un filtro de relación | Un resultado normal cuyo JSON lleva un error como clave | Resuelve el nombre con resolve_documents_by_name y filtra por el id. |
| Cualquier falla detrás del servidor | HTTP 500 con el error JSON-RPC -32000 and MCP request failed | Reintenta y luego escribe a soporte. |
El segundo canal es el que agarra desprevenidos a los clientes. Un resultado que no está marcado como error no es por fuerza una respuesta, así que lee el JSON del bloque de texto y busca un error como clave antes de leer las filas. Esos mensajes están escritos para actuar sobre ellos: cada uno nombra la herramienta que lo arregla.
¿Cuáles son los límites?
Una consulta devuelve como máximo 100 renglones por página y se detiene en la página 20, y los demás topes están en la tabla de abajo.
| Tope | Value | Lo que significa |
|---|---|---|
| limit on query_database_data | De 1 a 100, 25 por defecto | Filas en una página. |
| page on query_database_data | De 1 a 20 | Cierra más los filtros en vez de paginar más allá del final. |
| collection_ids on get_collection_fields | 20 | Los ids que pasan del tope se descartan. |
| names on resolve_documents_by_name | 10 | Los nombres que pasan del tope se descartan. |
| limit on resolve_documents_by_name | De 1 a 25, 10 por defecto | Coincidencias por nombre. |
| Los campos sin return_fields | 8 | Un conjunto predeterminado, con una advertencia. Nombra los campos que quieres. |
| Los campos sin fields_to_fetch | 40 | Primero se descartan los campos vacíos; el resto se nombran en omitted_fields. |
| Un valor de texto en un resultado | 400 caracteres | El texto más largo se corta, con puntos suspensivos al final. |
Ninguna respuesta trae un conteo total. Pagina hasta que has_more sea false, o filtra más fino.
¿A dónde voy después?
Lee Consultar datos para los operadores y las formas de valor que comparten las dos vías, lee la página REST API si quieres HTTP simple en vez de MCP, y escribe a support@bidlo.ai con lo que ninguna de las dos resuelva.
- Consultar datos. Las colecciones, los tipos de campo, los operadores que acepta cada uno y la forma de cada valor.
- REST API. Los mismos datos con GET and POST, para tres colecciones.
- Acceso y claves. Conseguir una clave, guardarla, rotarla.
- /openapi.json. La especificación legible por máquina.
Lo que estas no respondan, escribe a support@bidlo.ai.
## For agents: how to use Bidlo data - Read Bidlo data through its MCP server at https://lite.bidlo.ai/api/mcp — streamable HTTP, no OAuth. - Get a Bidlo account first: all Bidlo data needs one, anyone can get one by booking a call at https://cal.com/matt-wolfe-yecrho/30min, 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. - A team owner or admin mints that key in the app under Settings, then Features, then API. - Read the REST reference at https://www.bidlo.ai/openapi.json, the guide at https://www.bidlo.ai/developers, and its reference pages: https://www.bidlo.ai/developers/querying (filters, operators, dates, places, pages), https://www.bidlo.ai/developers/mcp (the five tools), https://www.bidlo.ai/developers/rest (the route) and https://www.bidlo.ai/developers/access (keys). - Prefer the MCP tools over REST; use `query_database_data` with `within_distance` for anything shaped like "near X". - Call `get_collection_fields` before composing a filter — it returns the operators each field takes. - Never invent a Bidlo unit price. Read one off the bid items, or say the forecast is not available.