Durante las últimas semanas, he estado profundizando en MCP ( Protocolo de Contexto de Modelo ) para entender de qué se trataba todo ese revuelo en torno a él.
En resumen, es bastante simple, pero aún así bastante potente: MCP es una API estándar para exponer conjuntos de herramientas que se pueden conectar a LLM.
Es bastante sencillo ampliar un cliente de inferencia (en HF tenemos dos SDK de cliente oficiales: @huggingface/inferenceen JS y huggingface_huben Python) para que también actúe como un cliente MCP y conecte las herramientas disponibles de los servidores MCP a la inferencia LLM.
Pero mientras hacía eso, llegó mi segunda revelación:
Una vez que tienes un cliente MCP, un agente es literalmente solo un bucle while encima de él.
En este breve artículo, le mostraré cómo lo implementé en Typescript (JS), cómo puede adoptar MCP también y cómo hará que Agentic AI sea mucho más simple en el futuro.
Crédito de la imagen: https://x.com/adamdotdev
Cómo ejecutar la demostración completa
Si tienes NodeJS (con pnpmo npm), simplemente ejecuta esto en una terminal:
npx @huggingface/mcp-client
o si usas pnpm:
pnpx @huggingface/mcp-client
Esto instala mi paquete en una carpeta temporal y luego ejecuta su comando.
Verá que su Agente simple se conecta a dos servidores MCP distintos (que se ejecutan localmente), carga sus herramientas y luego le solicita una conversación.
De forma predeterminada, nuestro agente de ejemplo se conecta a los siguientes dos servidores MCP:
- El servidor del sistema de archivos «canónico» , que obtiene acceso a su escritorio,
- y el servidor Playwright MCP , que sabe cómo utilizar un navegador Chromium en espacio aislado para usted.
Nota: esto es un poco contra-intuitivo pero actualmente todos los servidores MCP son en realidad procesos locales (aunque pronto habrá servidores remotos).
Nuestra aportación para este primer vídeo fue:
Escribe un haiku sobre la comunidad Hugging Face y escríbelo en un archivo llamado «hf.txt» en mi escritorio.
Ahora probemos este mensaje que implica algo de navegación web:
Realice una búsqueda web de proveedores de inferencia de HF en Brave Search y abra los primeros 3 resultados
Modelo y proveedor predeterminados
En términos de par modelo/proveedor, nuestro Agente de ejemplo utiliza por defecto:
- «Qwen/Qwen2.5-72B-Instrucciones»
- corriendo en Nebius
Todo esto se puede configurar mediante variables de entorno. Ver:
const agent = new Agent({
provider: process.env.PROVIDER ?? "nebius",
model: process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct",
apiKey: process.env.HF_TOKEN,
servers: SERVERS,
});
¿Dónde vive el código?
El código de Tiny Agent se encuentra en el mcp-clientsubpaquete del huggingface.jsrepositorio mono, que es el repositorio mono de GitHub en el que residen todas nuestras bibliotecas JS.
El código base utiliza funciones modernas de JS (en particular, generadores asíncronos) que facilitan enormemente la implementación, especialmente de eventos asíncronos como las respuestas LLM. Si aún no está familiarizado con estas funciones de JS, puede que necesite consultar con un LLM.
La base de esto: una herramienta que invoca soporte nativo en los LLM.
Lo que hará que esta entrada de blog sea muy sencilla es que la generación reciente de LLM (tanto cerrados como abiertos) han sido entrenados para llamar a funciones, es decir, usar herramientas.
Una herramienta se define por su nombre, una descripción y una representación JSONSchema de sus parámetros. En cierto sentido, es una representación opaca de la interfaz de cualquier función, vista desde fuera (es decir, al LLM no le importa cómo se implementa realmente la función).
const weatherTool = {
type: "function",
function: {
name: "get_weather",
description: "Get current temperature for a given location.",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "City and country e.g. Bogotá, Colombia",
},
},
},
},
};
La documentación canónica a la que enlazaré aquí es la documentación de llamadas a funciones de OpenAI . (Sí… OpenAI prácticamente define los estándares LLM para toda la comunidad 😅).
Los motores de inferencia permiten pasar una lista de herramientas al llamar al LLM, y este puede llamar a ninguna, una o más de estas herramientas. Como desarrollador, ejecuta las herramientas y envía sus resultados al LLM para continuar la generación.
Tenga en cuenta que, en el backend (a nivel del motor de inferencia), las herramientas simplemente se pasan al modelo en un formato especial
chat_template, como cualquier otro mensaje, y luego se extraen de la respuesta (utilizando tokens especiales específicos del modelo) para exponerlas como llamadas a herramientas. Vea un ejemplo en nuestro entorno de chat-template .
Implementación de un cliente MCP sobre InferenceClient
Ahora que sabemos qué es una herramienta en los LLM recientes, implementemos el cliente MCP real.
La documentación oficial en https://modelcontextprotocol.io/quickstart/client está bastante bien escrita. Solo tienes que reemplazar cualquier mención del SDK de cliente de Anthropic por cualquier otro SDK de cliente compatible con OpenAI. (También hay un archivo llms.txt que puedes usar en tu LLM para ayudarte con la programación).
Como recordatorio, utilizamos HF InferenceClientpara nuestro cliente de inferencia.
El archivo de código completo
McpClient.tsestá aquí si quieres seguir usando el código real 🤓
Nuestra McpClientclase tiene:
- un cliente de inferencia (funciona con cualquier proveedor de inferencia y
huggingface/inferenceadmite puntos finales tanto remotos como locales) - un conjunto de sesiones de cliente MCP, una para cada servidor MCP conectado (sí, queremos admitir varios servidores)
- y una lista de herramientas disponibles que se completará desde los servidores conectados y se reformateará ligeramente.
export class McpClient {
protected client: InferenceClient;
protected provider: string;
protected model: string;
private clients: Map<ToolName, Client> = new Map();
public readonly availableTools: ChatCompletionInputTool[] = [];
constructor({ provider, model, apiKey }: { provider: InferenceProvider; model: string; apiKey: string }) {
this.client = new InferenceClient(apiKey);
this.provider = provider;
this.model = model;
}
// [...]
}
Para conectarse a un servidor MCP, el @modelcontextprotocol/sdk/clientSDK oficial de TypeScript proporciona una Clientclase con un listTools()método:
async addMcpServer(server: StdioServerParameters): Promise<void> {
const transport = new StdioClientTransport({
...server,
env: { ...server.env, PATH: process.env.PATH ?? "" },
});
const mcp = new Client({ name: "@huggingface/mcp-client", version: packageVersion });
await mcp.connect(transport);
const toolsResult = await mcp.listTools();
debug(
"Connected to server with tools:",
toolsResult.tools.map(({ name }) => name)
);
for (const tool of toolsResult.tools) {
this.clients.set(tool.name, mcp);
}
this.availableTools.push(
...toolsResult.tools.map((tool) => {
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
} satisfies ChatCompletionInputTool;
})
);
}
StdioServerParameterses una interfaz del MCP SDK que le permitirá generar fácilmente un proceso local: como mencionamos anteriormente, actualmente, todos los servidores MCP son en realidad procesos locales.
Para cada servidor MCP al que nos conectamos, reformateamos ligeramente su lista de herramientas y las agregamos a this.availableTools.
Cómo utilizar las herramientas
Fácil, simplemente pasas this.availableToolsa tu chat de finalización de LLM, además de tu conjunto habitual de mensajes:
const stream = this.client.chatCompletionStream({
provider: this.provider,
model: this.model,
messages,
tools: this.availableTools,
tool_choice: "auto",
});
tool_choice: "auto"es el parámetro que se pasa para que el LLM genere cero, una o múltiples llamadas de herramientas.
Al analizar o transmitir la salida, el LLM generará algunas llamadas a herramientas (es decir, un nombre de función y algunos argumentos codificados en JSON), que usted (como desarrollador) debe calcular. El SDK del cliente MCP lo simplifica enormemente; cuenta con un client.callTool()método:
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
const toolMessage: ChatCompletionInputMessageTool = {
role: "tool",
tool_call_id: toolCall.id,
content: "",
name: toolName,
};
/// Get the appropriate session for this tool const client = this.clients.get(toolName);
if (client) {
const result = await client.callTool({ name: toolName, arguments: toolArgs });
toolMessage.content = result.content[0].text;
} else {
toolMessage.content = `Error: No session found for tool: ${toolName}`;
}
Finalmente, agregará el mensaje de la herramienta resultante a su messagesmatriz y nuevamente al LLM.
Nuestro agente de 50 líneas de código 🤯
Ahora que tenemos un cliente MCP capaz de conectarse a servidores MCP arbitrarios para obtener listas de herramientas y capaz de inyectarlas y analizarlas desde la inferencia LLM, bueno… ¿qué es un Agente?
Una vez que tienes un cliente de inferencia con un conjunto de herramientas, entonces un Agente es simplemente un bucle while encima de él.
En más detalle, un Agente es simplemente una combinación de:
- un aviso del sistema
- un cliente de inferencia LLM
- Un cliente MCP para conectarle un conjunto de herramientas desde un grupo de servidores MCP
- Algunos flujos de control básicos (ver más abajo el bucle while)
El
Agent.tsarchivo de código completo está aquí .
Nuestra clase Agente simplemente extiende McpClient:
export class Agent extends McpClient {
private readonly servers: StdioServerParameters[];
protected messages: ChatCompletionInputMessage[];
constructor({
provider,
model,
apiKey,
servers,
prompt,
}: {
provider: InferenceProvider;
model: string;
apiKey: string;
servers: StdioServerParameters[];
prompt?: string;
}) {
super({ provider, model, apiKey });
this.servers = servers;
this.messages = [
{
role: "system",
content: prompt ?? DEFAULT_SYSTEM_PROMPT,
},
];
}
}
De forma predeterminada, utilizamos un indicador de sistema muy simple inspirado en el compartido en la guía de indicadores de GPT-4.1 .
Aunque esto proviene de OpenAI 😈, esta frase en particular se aplica a cada vez más modelos, tanto cerrados como abiertos:
Alentamos a los desarrolladores a utilizar exclusivamente el campo de herramientas para pasar herramientas, en lugar de inyectar manualmente descripciones de herramientas en su solicitud y escribir un analizador separado para llamadas de herramientas, como algunos han informado haber hecho en el pasado.
Es decir, no necesitamos proporcionar listas con un formato minucioso de ejemplos de uso de herramientas en el mensaje. El tools: this.availableToolsparámetro es suficiente.
Cargar las herramientas en el Agente es literalmente solo conectarse a los servidores MCP que queremos (en paralelo porque es muy fácil hacerlo en JS):
async loadTools(): Promise<void> {
await Promise.all(this.servers.map((s) => this.addMcpServer(s)));
}
Agregamos dos herramientas adicionales (fuera de MCP) que pueden ser utilizadas por el LLM para el flujo de control de nuestro Agente:
const taskCompletionTool: ChatCompletionInputTool = {
type: "function",
function: {
name: "task_complete",
description: "Call this tool when the task given by the user is complete",
parameters: {
type: "object",
properties: {},
},
},
};
const askQuestionTool: ChatCompletionInputTool = {
type: "function",
function: {
name: "ask_question",
description: "Ask a question to the user to get more info required to solve or clarify their problem.",
parameters: {
type: "object",
properties: {},
},
},
};
const exitLoopTools = [taskCompletionTool, askQuestionTool];
Al llamar a cualquiera de estas herramientas, el Agente romperá su bucle y devolverá el control al usuario para una nueva entrada.
El bucle while completo
Mira nuestro bucle while completo.🎉
La esencia del bucle while principal de nuestro Agente es que simplemente iteramos con el LLM alternando entre llamar a la herramienta y alimentarle los resultados de la herramienta, y lo hacemos hasta que el LLM comienza a responder con dos mensajes que no son de herramienta seguidos .
Este es el bucle while completo:
let numOfTurns = 0;
let nextTurnShouldCallTools = true;
while (true) {
try {
yield* this.processSingleTurnWithTools(this.messages, {
exitLoopTools,
exitIfFirstChunkNoTool: numOfTurns > 0 && nextTurnShouldCallTools,
abortSignal: opts.abortSignal,
});
} catch (err) {
if (err instanceof Error && err.message === "AbortError") {
return;
}
throw err;
}
numOfTurns++;
const currentLast = this.messages.at(-1)!;
if (
currentLast.role === "tool" &&
currentLast.name &&
exitLoopTools.map((t) => t.function.name).includes(currentLast.name)
) {
return;
}
if (currentLast.role !== "tool" && numOfTurns > MAX_NUM_TURNS) {
return;
}
if (currentLast.role !== "tool" && nextTurnShouldCallTools) {
return;
}
if (currentLast.role === "tool") {
nextTurnShouldCallTools = false;
} else {
nextTurnShouldCallTools = true;
}
}
Próximos pasos
Hay muchos próximos pasos interesantes una vez que tenga un cliente MCP en funcionamiento y una forma sencilla de crear agentes 🔥
- Experimente con otros modelos
- mistralai/Mistral-Small-3.1-24B-Instruct-2503 está optimizado para la llamada a funciones
- Gemma 3 27B, los modelos Gemma 3 QAT son una opción popular para llamar a funciones, aunque requeriría que implementemos el análisis de herramientas ya que no utiliza herramientas nativas
tools(¡una solicitud de incorporación de cambios sería bienvenida!).
- Experimente con todos los proveedores de inferencia :
- Cerebras, Cohere, Fal, Fireworks, Hiperbólico, Nebius, Novita, Replicar, SambaNova, Together, etc.
- ¡Cada uno de ellos tiene diferentes optimizaciones para la llamada de funciones (también dependiendo del modelo) por lo que el rendimiento puede variar!
- Enganche LLM locales usando llama.cpp o LM Studio
¡Aceptamos solicitudes de extracción y contribuciones! ¡De nuevo, todo aquí es de código abierto ! 💎❤️