Hemos construido una aplicación en Python que consulta los gastos de un viaje, comprueba las reglas de la empresa y guarda un informe en borrador. Queremos utilizarla desde un asistente de IA, pidiendo algo como esto:
Prepara los gastos de mi viaje a Madrid y guarda un borrador. Dime qué tengo que revisar.
El asistente envía a su LLM —el modelo de IA— nuestra petición y las descripciones de las funciones que ofrece el servidor MCP de gastos. El LLM propone una llamada; el asistente la solicita al servidor y devuelve el resultado al LLM, que puede pedir otra operación o redactar la respuesta.
MCP, Model Context Protocol, establece una forma común de ofrecer esas funciones y solicitarlas. Nuestro servidor MCP es la entrada a las operaciones de la aplicación de gastos. El asistente se comunica con él mediante un componente llamado cliente MCP.
Vamos a seguir esa petición con nuestra aplicación, expense-report-mcp, para ver qué recibe cada parte y quién hace el trabajo. El README del repositorio explica cómo instalarla y probarla en local.
Las funciones que presentamos al modelo
Para elegir una operación, el LLM necesita saber qué puede pedir. Al conectar con nuestro servidor MCP, la aplicación del asistente consulta las herramientas disponibles. En este contexto, una herramienta es una función que el servidor permite ejecutar.
El servidor devuelve una descripción de cada función y los datos que necesita. En nuestro ejemplo ofrece estas cuatro:
| Herramienta | Dato de entrada | Qué hace |
|---|---|---|
list_trips |
Ninguno. | Devuelve los viajes del empleado. |
preview_expense_report |
trip_id: identificador del viaje. |
Comprueba los gastos sin guardar el informe. |
create_expense_draft |
trip_id: identificador del viaje. |
Guarda el informe como borrador. |
get_expense_draft |
draft_id: identificador del borrador. |
Recupera un informe guardado. |
La aplicación del asistente envía al LLM nuestra petición junto con las descripciones y parámetros de esas herramientas. El formato concreto depende de la API del modelo, pero la información importante es la de esta tabla: qué operaciones existen y qué datos hay que proporcionar.
El modelo recibe esa información, no el código Python que calcula los gastos. La documentación de MCP explica cómo se anuncian y solicitan estas herramientas.
La primera llamada permite encontrar el viaje
Nuestra petición menciona Madrid, pero todavía no tenemos un identificador para pasarlo a las funciones de gastos. El LLM puede proponer primero una llamada a list_trips:
{
"name": "list_trips",
"arguments": {}
}
Esta muestra abrevia la solicitud de herramienta que recibe la aplicación del asistente. No es todavía el resultado de consultar los viajes: indica qué operación se propone ejecutar.
La aplicación utiliza su cliente MCP para solicitarla al servidor. Nuestro código de gastos consulta los viajes del empleado configurado. Entre ellos aparece Madrid con el identificador trip-madrid.
La aplicación envía esa lista al LLM como resultado de la llamada anterior. Ahora el modelo dispone de un dato que antes no tenía: sabe qué identificador corresponde al viaje a Madrid. Si aparecieran varios viajes posibles, tendría que concretar cuál queremos antes de seguir.
Con ese resultado, el modelo puede pedir la comprobación
El LLM puede solicitar ahora preview_expense_report con trip_id igual a trip-madrid. La aplicación vuelve a enviar la operación al servidor, que ejecuta las reglas de gastos.
En nuestro proyecto, el servidor MCP y esas reglas forman parte del mismo programa Python. Los gastos ya están preparados en un archivo JSON. Para este viaje hay seis registros ficticios:
| Gasto | Importe | Información relevante |
|---|---|---|
| Tren | 89,90 € | Datos completos y fecha válida. |
| Hotel | 160,00 € | Datos completos y dentro del límite. |
| Dos comidas del mismo día | 22 € y 48 € | Se suman para comprobar el límite diario. |
| Copia de la primera comida | 22 € | Repite proveedor, número de factura y divisa. |
| Taxi | 12 € | Sin número de factura ni referencia al justificante. |
La política ficticia permite 35 € diarios en comidas y 180 € por justificante de hotel. Exige fechas dentro del viaje, número de factura y referencia al justificante. El transporte no tiene un límite monetario en esta demo.
El código excluye primero la copia, para no contar dos veces la misma comida. Las otras comidas suman 22 + 48 = 70 €, por encima de los 35 €: ambas necesitan revisión. El taxi también necesita revisión por sus datos incompletos. Tren y hotel pasan las comprobaciones.
El servidor devuelve un informe con los totales y el detalle de cada gasto. Esta muestra recoge sus campos principales:
{
"trip_id": "trip-madrid",
"eligible_eur": "249.90",
"review_eur": "82.00",
"excluded_duplicate_eur": "22.00"
}
Son 249,90 € de tren y hotel, 82 € que revisar y 22 € duplicados. Los importes y los motivos los obtiene nuestro código de gastos. El LLM recibe ese resultado a través de la aplicación del asistente; no tiene que deducir los importes a partir de una descripción de la herramienta.
El diagrama muestra este intercambio de comprobación. La aplicación ya conoce el viaje y las herramientas disponibles. El tiempo avanza hacia abajo: vemos la petición al modelo, la operación que propone, su ejecución y el resultado que vuelve al modelo para decidir cómo continuar.

Guardar el borrador completa la petición
Habíamos pedido también guardar el informe. Con el resultado de la comprobación, el LLM puede proponer create_expense_draft para ese mismo viaje. La aplicación solicita la operación al servidor, que guarda el informe en SQLite, una base de datos local, y devuelve el identificador del borrador.
La aplicación entrega también esa respuesta al LLM. Con el guardado confirmado y los resultados anteriores, puede redactar una respuesta como esta:
He guardado el borrador. El tren y el hotel cumplen las comprobaciones. Debes revisar las comidas y los datos del taxi; he excluido un justificante repetido de 22 €.
El recorrido ha necesitado varias llamadas porque cada resultado aportaba información para la siguiente decisión. El modelo ha propuesto las operaciones y redactado la respuesta; la aplicación del asistente las ha solicitado; el servidor ha consultado, calculado y guardado.
Por qué utilizar MCP para esta aplicación
Podemos ofrecer las mismas operaciones a un asistente para empleados y a otro para administración. El primero prepara el borrador; el segundo puede consultarlo para revisarlo. En nuestro proyecto, el perfil --read-only deja fuera la herramienta que crea borradores.
El beneficio es compartir esas capacidades mediante un protocolo común con distintas aplicaciones de IA compatibles. Para un backend que siempre ejecuta una validación fija, una API convencional puede ser suficiente. MCP no cambia el cálculo de los gastos.
Con el recorrido completo, podemos separar los dos trabajos que hay detrás: conectar un asistente a un servidor que ya existe e implementar un servidor para ofrecer nuestras propias funciones.
Utilizar un servidor que alguien ya ofrece
Hasta aquí hemos seguido la tarea como usuarios. Si otra empresa nos proporciona la aplicación de gastos y su servidor MCP, necesitamos un asistente compatible, los datos de conexión y los permisos adecuados. Las funciones ya están implementadas por ese proveedor.
Un servidor remoto suele proporcionarnos una dirección y una forma de autorizar el acceso. Un servidor local se inicia mediante un comando en nuestro ordenador. La arquitectura oficial de MCP describe ambas opciones.
Nuestro proyecto funciona localmente. Primero preparamos sus dependencias desde la carpeta del proyecto con uv, una herramienta para gestionar el entorno de Python:
uv sync --frozen
Después añadimos un servidor local en la configuración MCP del asistente. Este es un ejemplo de comando y argumentos, sustituyendo las rutas por las nuestras:
Comando: uv
Argumentos:
run
--directory
C:/projects/expense-report-mcp
expense-server
--fixtures
C:/projects/expense-report-mcp/fixtures/travel.json
--database
C:/projects/expense-report-mcp/data/expenses.db
El asistente inicia ese proceso y se comunica con él por entrada y salida estándar, un transporte llamado stdio. --fixtures indica dónde están los datos de ejemplo; --database, dónde guardar los borradores.
La pantalla de configuración depende del asistente. Tras conectar, comprobamos que aparecen las cuatro herramientas. La aplicación del asistente se encargará de presentarlas al modelo y gestionar las llamadas durante la conversación, según sus permisos y opciones de confirmación.
Crear el servidor para ofrecer nuestras funciones
Si somos quienes desarrollan la aplicación de gastos, nos corresponde implementar el otro lado: las funciones que ejecutarán las operaciones y el servidor MCP que las ofrece.
En nuestro código, el objeto service reúne las operaciones de gastos: su método preview(trip_id) comprueba un viaje y devuelve un informe. ReportSnapshot describe los campos de ese informe. Para ofrecer esta operación utilizamos el SDK oficial de Python, una biblioteca que implementa el protocolo MCP.
Con un objeto servidor llamado server, esta versión simplificada registra la herramienta:
@server.tool()
def preview_expense_report(trip_id: str) -> ReportSnapshot:
"""Assess receipts without saving a draft."""
return service.preview(trip_id)
@server.tool() registra la función para que pueda anunciarse y solicitarse. Su nombre, descripción y parámetro permiten construir la información que recibirá el asistente. Cuando llega una llamada, el servidor ejecuta la función y devuelve el informe producido por service.preview.
El adaptador completo añade validación de respuestas y tratamiento de errores. Las reglas de comidas, fechas y duplicados permanecen separadas del protocolo. La documentación del SDK desarrolla ese registro.
Además de las herramientas, nuestro servidor ofrece un recurso, expense-policy://current, para leer los límites de la política, y un prompt, prepare_expense_report, que devuelve instrucciones reutilizables para preparar un viaje. El asistente puede incorporar esa información a lo que recibe el modelo. Las operaciones de consulta y guardado siguen correspondiendo a las herramientas que hemos recorrido.
Ese servidor puede ser personal o interno; no exige publicarlo en Internet. Al desarrollarlo decidimos qué funciones y datos ofrecer. Al utilizarlo desde un asistente, elegimos cómo incorporarlos a nuestra conversación.
Probar el proyecto con y sin un modelo
Para comprobar el proyecto sin una cuenta de IA podemos ejecutar:
uv run expense-demo
uv run expense-demo --role reviewer
Estas demos son clientes MCP con una secuencia programada, sin LLM: solicitan operaciones reales y muestran sus resultados. Sirven para comprobar la consulta, el cálculo y el guardado. Para probar el recorrido conversacional del artículo, conectamos un asistente con modelo mediante la configuración anterior y le hacemos la petición del viaje a Madrid.
La aplicación parte de datos ya estructurados; no extrae información de PDF o fotografías. Tampoco aprueba ni paga gastos. Su resultado es un borrador guardado con importes y motivos concretos: la información que vuelve al modelo para que pueda ayudarnos a revisar el viaje.