🇺🇸 EN
AI
·8 min read·↗·🇺🇸 EN

Cómo crear un servidor MCP en TypeScript: conecta tus propias herramientas con agentes de IA

Portada principal del artículo: Cómo crear un servidor MCP en TypeScript: conecta tus propias herramientas con agentes de IA

Cuando desarrollo proyectos o escribo artículos, quiero que mis agentes de IA me ayuden a encontrar fotos reales en lugar de intentar generarlas. Los fotógrafos en Unsplash comparten un trabajo excelente de forma gratuita y merecen reconocimiento por sus imágenes.

Por esta razón, investigué el Model Context Protocol (MCP) y creé unsplashx.

Con unsplashx, mi asistente de IA puede buscar fotos en Unsplash, descargarlas con las dimensiones correctas y agregarlas a mis archivos con los créditos automáticos para el fotógrafo.

En este artículo, comparto lo que aprendí mientras construía unsplashx para que puedas crear tus propias herramientas personalizadas para agentes de IA usando TypeScript.


¿Qué es MCP y por qué es útil?

El Model Context Protocol funciona como un estándar abierto para herramientas de IA. Antes de MCP, conectar una API a un LLM significaba escribir adaptadores personalizados de llamadas a funciones (function calling) para OpenAI, reescribirlos para Anthropic y crear extensiones independientes para cada editor o IDE.

MCP estandariza esta integración mediante JSON-RPC 2.0:

ComponenteRolEjemplos
Host ApplicationLa aplicación que ejecuta la interfaz de usuario y el modelo de IACursor, Antigravity IDE, Claude Desktop
MCP ClientEl cliente del protocolo que gestiona la comunicación y los permisos de seguridadCapa interna del protocolo integrada en el editor
MCP ServerEl servicio independiente que expone herramientas, datos y recursosunsplashx, GitHub MCP, PostgreSQL MCP

Cuando ejecutas un servidor MCP de forma local, el host inicia tu script como un proceso secundario (child process) y se comunica con él a través de la entrada y salida estándar (stdio).

Ahora que entendemos este flujo de trabajo, pasemos a la configuración inicial del proyecto.


Paso 1: Configuración del proyecto y dependencias

Comencemos creando un nuevo directorio e inicializando un proyecto de Node.js:

mkdir unsplashx
cd unsplashx
npm init -y

Solo necesitamos el paquete oficial @modelcontextprotocol/sdk:

npm install @modelcontextprotocol/sdk

En el archivo package.json, agregamos "type": "module" para habilitar ESM nativo y definimos el binario ejecutable:

{
  "name": "unsplashx",
  "version": "1.1.0",
  "description": "Unsplash MCP Server",
  "main": "index.js",
  "type": "module",
  "bin": {
    "unsplashx": "./index.js"
  },
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.4"
  }
}

Ahora inicialicemos la instancia del servidor en index.js.


Paso 2: Crear la instancia del servidor

En index.js, importamos los módulos del SDK y leemos nuestra clave de API de Unsplash. Soportamos tanto una variable de entorno como un argumento en la línea de comandos:

#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs/promises";
import path from "path";
 
const UNSPLASH_ACCESS_KEY = process.env.UNSPLASH_ACCESS_KEY || process.argv[2];
const API_BASE = "https://api.unsplash.com";
 
if (!UNSPLASH_ACCESS_KEY) {
  console.error("Error: UNSPLASH_ACCESS_KEY is required.");
  process.exit(1);
}
 
const server = new Server(
  { name: "unsplash-mcp", version: "1.1.0" },
  { capabilities: { tools: {} } }
);

También creamos una pequeña función auxiliar para llamar a la API de Unsplash con nuestro encabezado de autorización:

async function fetchUnsplash(endpoint, params = {}) {
  const url = new URL(`${API_BASE}${endpoint}`);
  Object.entries(params).forEach(([key, value]) => {
    if (value !== undefined) url.searchParams.append(key, value);
  });
 
  const response = await fetch(url, {
    headers: {
      Authorization: `Client-ID ${UNSPLASH_ACCESS_KEY}`,
      "Accept-Version": "v1",
    },
  });
 
  if (!response.ok) {
    throw new Error(`Unsplash API error: ${response.statusText}`);
  }
 
  return response.json();
}

Con nuestro servidor base listo, veamos cómo definimos el esquema de las herramientas.


Paso 3: Definir el esquema de las herramientas

Un servidor MCP comunica a los clientes de IA qué herramientas están disponibles escuchando el evento ListToolsRequestSchema.

En unsplashx, definí cuatro herramientas:

  1. search_photos: busca fotos por palabra clave, orientación y color.
  2. download_to_project: guarda un archivo de imagen en una carpeta local.
  3. insert_to_page: agrega una imagen con créditos del fotógrafo a un archivo Markdown o HTML.
  4. get_photo: obtiene metadatos técnicos de una foto específica.

Así es como definimos el esquema de la herramienta search_photos, explicando cada parámetro al modelo de IA:

const searchPhotosTool = {
  name: "search_photos",
  description: "Search for high-quality photos on Unsplash.",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Search keyword (e.g. 'abstract dark network')" },
      count: { type: "number", description: "Number of results (max 30)", default: 5 },
      orientation: {
        type: "string",
        enum: ["landscape", "portrait", "squarish"],
        description: "Filter by orientation",
      },
    },
    required: ["query"],
  },
};

A continuación, definimos el esquema de la herramienta download_to_project. Esta herramienta permite al agente guardar archivos localmente:

const downloadToProjectTool = {
  name: "download_to_project",
  description: "Download a photo locally to your project directory. Automatically tracks the download.",
  inputSchema: {
    type: "object",
    properties: {
      photo_id: { type: "string", description: "The Unsplash photo ID" },
      destination_dir: { type: "string", description: "Absolute path to destination directory" },
      file_name: { type: "string", description: "Desired file name (e.g., 'cover.jpeg')" },
      width: { type: "number", description: "Optional width in pixels" },
      height: { type: "number", description: "Optional height in pixels" },
      quality: { type: "number", description: "Optional quality (1-100)", default: 80 },
    },
    required: ["photo_id", "destination_dir", "file_name"],
  },
};

Ahora registramos estas herramientas en el manejador de nuestro servidor:

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [searchPhotosTool, downloadToProjectTool],
  };
});

Una vez que el cliente de IA sabe qué herramientas existen, implementemos el código que se ejecuta cuando se invoca una herramienta.


Paso 4: Ejecutar herramientas y dar crédito a los fotógrafos

Cuando el asistente de IA decide ejecutar una herramienta, el servidor recibe una solicitud CallToolRequestSchema.

Veamos cómo manejamos cada herramienta.

1. Búsqueda de fotos (search_photos)

Al buscar fotos, llamamos al endpoint /search/photos y devolvemos una lista limpia con los enlaces de atribución para cada fotógrafo:

async function handleSearchPhotos(args) {
  const results = await fetchUnsplash("/search/photos", {
    query: args.query,
    per_page: args.count || 5,
    orientation: args.orientation,
  });
 
  const photos = results.results.map((p) => ({
    id: p.id,
    description: p.description || p.alt_description,
    preview_url: p.urls.small,
    photographer: p.user.name,
    attribution: `Photo by [${p.user.name}](${p.user.links.html}?utm_source=unsplashx&utm_medium=referral) on [Unsplash](https://unsplash.com/?utm_source=unsplashx&utm_medium=referral)`,
  }));
 
  return {
    content: [{ type: "text", text: JSON.stringify(photos, null, 2) }],
  };
}

Observa cómo construimos el campo attribution. Esto le proporciona al asistente de IA el enlace exacto en Markdown para dar crédito al fotógrafo.

2. Descargas de imágenes (download_to_project)

Al descargar una imagen, existe una regla importante impuesta por Unsplash: debemos llamar al endpoint /photos/:id/download. Esto permite a Unsplash contabilizar la descarga y reconocer al fotógrafo.

Así es como descargamos la imagen y la guardamos en el disco local:

async function handleDownloadToProject(args) {
  const photo = await fetchUnsplash(`/photos/${args.photo_id}`);
 
  // Requerido por Unsplash: registrar la descarga de la foto
  await fetchUnsplash(`/photos/${args.photo_id}/download`);
 
  const url = new URL(photo.urls.regular);
  if (args.width) url.searchParams.set("w", args.width);
  if (args.height) url.searchParams.set("h", args.height);
  if (args.quality) url.searchParams.set("q", args.quality);
 
  const response = await fetch(url.toString());
  const buffer = Buffer.from(await response.arrayBuffer());
 
  const fullPath = path.join(args.destination_dir, args.file_name);
  await fs.mkdir(args.destination_dir, { recursive: true });
  await fs.writeFile(fullPath, buffer);
 
  const attribution = `Photo by [${photo.user.name}](${photo.user.links.html}?utm_source=unsplashx&utm_medium=referral) on [Unsplash](https://unsplash.com/?utm_source=unsplashx&utm_medium=referral)`;
 
  return {
    content: [
      {
        type: "text",
        text: `Successfully downloaded photo to ${fullPath}.\n\nREQUIRED ATTRIBUTION:\n${attribution}`,
      },
    ],
  };
}

Ahora conectamos estos manejadores al evento CallToolRequestSchema:

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;
 
  try {
    if (name === "search_photos") return await handleSearchPhotos(args);
    if (name === "download_to_project") return await handleDownloadToProject(args);
    throw new Error(`Unknown tool: ${name}`);
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error.message}` }],
      isError: true,
    };
  }
});

Ahora conectemos la capa de transporte para que el servidor pueda ejecutarse.


Paso 5: Conectar el transporte Stdio

Los servidores MCP locales se comunican con los editores de IA a través de la entrada y salida estándar (stdio).

Iniciamos el servidor al final de index.js:

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Unsplash MCP Server running on stdio");
}
 
main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Nota importante: Nunca uses console.log() en un servidor MCP que funcione sobre stdio. La salida estándar está reservada para los mensajes JSON-RPC. Utiliza siempre console.error() para registrar tus mensajes y logs.

Ahora conectemos unsplashx a tu editor.


Paso 6: Conectar unsplashx a tu editor

Puedes ejecutar unsplashx directamente con npx en cualquier cliente MCP.

Claude Desktop

Edita el archivo ~/Library/Application Support/Claude/claude_desktop_config.json (en macOS) o %APPDATA%\Claude\claude_desktop_config.json (en Windows):

{
  "mcpServers": {
    "unsplashx": {
      "command": "npx",
      "args": ["-y", "unsplashx"],
      "env": {
        "UNSPLASH_ACCESS_KEY": "YOUR_UNSPLASH_KEY"
      }
    }
  }
}

Cursor y Antigravity IDE

En el espacio de trabajo de tu proyecto o en el archivo global .agents/mcp_config.json:

{
  "mcpServers": {
    "unsplashx": {
      "command": "npx",
      "args": ["-y", "unsplashx"],
      "env": {
        "UNSPLASH_ACCESS_KEY": "YOUR_UNSPLASH_KEY"
      }
    }
  }
}

Una vez configurado, puedes pedirle a tu asistente de forma natural:

"Encuentra una foto abstracta y oscura de arquitectura en Unsplash, descárgala en public/images/posts/my-post/cover.jpeg a 1200x630 y muéstrame el crédito del fotógrafo."

El asistente buscará la foto, seleccionará la opción más adecuada, descargará la imagen y mostrará la atribución correspondiente.

Resultado de UnSplashX mostrando la búsqueda y descarga automática de imágenes
Resultado de UnSplashX mostrando la búsqueda y descarga automática de imágenes

Resumen

Construir unsplashx me demostró lo sencillo que resulta crear herramientas personalizadas para asistentes de IA usando Model Context Protocol:

  1. Escribes tus herramientas en TypeScript y las expones con el SDK estándar de MCP.
  2. Los agentes de IA pueden buscar y descargar fotos reales sin salir del editor.
  3. Los fotógrafos reciben su debido crédito y el registro de descargas por su trabajo.

Si quieres usar el paquete o revisar el código completo, visita el repositorio en GitHub (danywalls/unsplashx) o instálalo directamente desde npm.

Parte de la serie AI & Engineering

Bridging the gap between AI demos and real-world production applications.

Ver serie completa

Preguntas Frecuentes

¿Qué es unsplashx?

unsplashx es un servidor de código abierto para Model Context Protocol (MCP) que permite a asistentes de IA como Cursor, Claude y Antigravity buscar, descargar e insertar fotos reales de Unsplash con atribución automática al fotógrafo.

¿Por qué usar un servidor MCP de Unsplash en lugar de imágenes generadas por IA?

Permite a los desarrolladores acceder a fotografías de alta calidad tomadas por personas reales, respeta las directrices de seguimiento de descargas de Unsplash y garantiza enlaces de atribución correctos para los creadores.

¿Qué herramientas expone unsplashx a los agentes de IA?

Proporciona search_photos para filtrar por orientación o color, download_to_project para guardar archivos localmente, insert_to_page para actualizar archivos Markdown o HTML, y get_photo para obtener metadatos de las imágenes.

¿Cómo se comunican los editores de IA con el servidor MCP unsplashx?

Los clientes de IA inician el servidor como un proceso secundario local e intercambian mensajes JSON-RPC 2.0 a través de la entrada y salida estándar (stdio).

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.