Token导航 LogoToken导航TokenDH.com
MCP Rick And Morty logo
运维云端stdio官方级别未说明来源级核验

MCP Rick And Morty

MCP Server

tsc

一个基于Node.js和gpt-5-nano的聊天机器人,通过调用Rick and Morty API并以自然语言回答用户问题,具有幽默风格和API查询功能。

工具数

3

提示词数

0

GitHub Stars

0

资源数

0
聊天机器人TypeScript自然语言处理API集成

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

作者 / 组织

IgnacioAlmada

提供方

IgnacioAlmada

最后核验

2026/5/17 20:22

运行时

Node.js

快速接入

先看主来源和安装命令,再打开仓库或文档;下面只保留这个条目的关键接入事实。

命令预览

npx tsc --init

详细介绍

RickGPT MCP — Chat con la API de Rick and Morty (Node.js + gpt-5-nano)

Objetivo: construir un MCP-like chat que consulta la Rick & Morty API y responde en lenguaje natural usando OpenAI gpt-5-nano. Trae “wow factor” de workshop: consultas reales a una API pública, razonamiento del modelo, function calling y respuestas con humor al estilo Rick.

Nota: Implementamos el flujo “MCP-like” con OpenAI tools/function calling (ideal para un demo que use gpt-5-nano). Si querés un MCP Server formal compatible con clientes MCP (p. ej., MCP Inspector), podés portar los mismos handlers a @modelcontextprotocol/sdk más adelante. Mantengo esta guía centrada en el demo con gpt-5-nano para que funcione de inmediato en el workshop.

🧭 Qué vas a lograr

  • Chatear en consola: “¿En qué episodios aparece Morty?” → el bot llama a la API, agrega contexto y responde.
  • Herramientas (tools) que el modelo puede invocar: get_character, get_episode, get_location.
  • Manejo de paginación, filtros (status/species), cache y resúmenes amigables.
  • Extensible: podés sumar más endpoints o llevarlo a una UI web luego.

✅ Requisitos

  • Node.js 18+ (recomendado 20+)
  • Cuenta y API key de OpenAI en OPENAI_API_KEY
  • Conexión a Internet (la API de Rick & Morty es pública)

🗂 Estructura del proyecto

rickgpt-mcp/
├─ src/
│  ├─ index.ts                 # loop de chat + orquestación de tools
│  ├─ tools/
│  │  ├─ rmApi.ts              # wrappers de la Rick & Morty API
│  │  ├─ schemas.ts            # JSON Schemas (zod opcional) p/ validación
│  │  └─ toolHandlers.ts       # ejecutores de tools consumidos por el modelo
│  └─ prompts/
│     └─ system.ts             # tono y estilo (Rick-sarcástico, etc.)
├─ .env.example
├─ package.json
├─ tsconfig.json
└─ README.md (este archivo)

Si preferís JavaScript, podés usar .js y quitar tipos; la lógica es la misma.


🚀 Instalación rápida

mkdir rickgpt-mcp && cd rickgpt-mcp
npm init -y
npm i openai zod quick-lru node-fetch
npm i -D typescript ts-node @types/node
npx tsc --init

package.json (scripts sugeridos)

{
  "name": "rickgpt-mcp",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "ts-node src/index.ts",
    "start": "node dist/index.js",
    "build": "tsc"
  },
  "dependencies": {
    "node-fetch": "^3.3.2",
    "openai": "^4.60.0",
    "quick-lru": "^7.0.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20.12.12",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.3"
  }
}

.env.example

OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5-nano

Copiá a .env y poné tu clave.


🧱 Paso 1 — Prompt del sistema (tono y rol)

Archivo: src/prompts/system.ts

export const SYSTEM_PROMPT = `
Sos "RickGPT", un asistente con humor ácido inspirado en Rick Sánchez.
Respondé con precisión basándote en la API de Rick and Morty cuando sea posible.
Si el usuario pide datos, usá las herramientas disponibles. 
Sé conciso y claro. Si hay mucha data, resumila en bullets.
Evitá inventar hechos del universo canon si la API no los respalda.

Reglas:
- Si el usuario pide listados largos, devolvé TOP 10 y ofrecé "ver más".
- Mostrá imágenes como URLs cuando existan (image).
- Dejá claro cuando inferís vs. cuando citás la API.
`;

🔌 Paso 2 — Wrappers de la API (fetch + utilidades)

Archivo: src/tools/rmApi.ts

import fetch from "node-fetch";

const BASE = "https://rickandmortyapi.com/api";

export type CharacterFilter = {
  name?: string;
  status?: "Alive" | "Dead" | "unknown";
  species?: string;
};

export async function searchCharacters(filter: CharacterFilter, page = 1) {
  const params = new URLSearchParams();
  if (filter.name) params.set("name", filter.name);
  if (filter.status) params.set("status", filter.status);
  if (filter.species) params.set("species", filter.species);
  params.set("page", String(page));
  const url = `${BASE}/character/?${params.toString()}`;

  const res = await fetch(url);
  if (!res.ok) {
    // La API responde 404 cuando no hay resultados
    if (res.status === 404) return { info: { count: 0, pages: 0 }, results: [] };
    throw new Error(`Error ${res.status} consultando: ${url}`);
  }
  return res.json() as Promise;
}

export async function getEpisodeByName(name: string) {
  const url = `${BASE}/episode/?name=${encodeURIComponent(name)}`;
  const res = await fetch(url);
  if (!res.ok) {
    if (res.status === 404) return { info: { count: 0, pages: 0 }, results: [] };
    throw new Error(`Error ${res.status} consultando: ${url}`);
  }
  return res.json();
}

export async function getLocationByName(name: string) {
  const url = `${BASE}/location/?name=${encodeURIComponent(name)}`;
  const res = await fetch(url);
  if (!res.ok) {
    if (res.status === 404) return { info: { count: 0, pages: 0 }, results: [] };
    throw new Error(`Error ${res.status} consultando: ${url}`);
  }
  return res.json();
}

export async function getCharacterById(id: number) {
  const url = `${BASE}/character/${id}`;
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Error ${res.status} consultando: ${url}`);
  return res.json();
}

🧪 Paso 3 — Schemas (validación de parámetros de tools)

Archivo: src/tools/schemas.ts

import { z } from "zod";

export const GetCharacterSchema = z.object({
  name: z.string().optional(),
  status: z.enum(["Alive", "Dead", "unknown"]).optional(),
  species: z.string().optional(),
  page: z.number().int().positive().optional()
});

export const GetEpisodeSchema = z.object({
  name: z.string().min(1, "name es requerido")
});

export const GetLocationSchema = z.object({
  name: z.string().min(1, "name es requerido")
});

export type GetCharacterInput = z.infer;
export type GetEpisodeInput = z.infer;
export type GetLocationInput = z.infer;

🧰 Paso 4 — Handlers de tools (con cache y paginación)

Archivo: src/tools/toolHandlers.ts

import QuickLRU from "quick-lru";
import {
  searchCharacters,
  getEpisodeByName,
  getLocationByName
} from "./rmApi.js";
import {
  GetCharacterSchema,
  GetEpisodeSchema,
  GetLocationSchema
} from "./schemas.js";

const cache = new QuickLRU({ maxSize: 200 });

function key(obj: any) { return JSON.stringify(obj); }

export async function handleGetCharacter(input: unknown) {
  const args = GetCharacterSchema.parse(input);
  const k = key({ tool: "get_character", ...args });
  if (cache.has(k)) return cache.get(k);

  const page = args.page ?? 1;
  const json = await searchCharacters(
    { name: args.name, status: args.status, species: args.species },
    page
  );

  // Normalizamos para el LLM (resumen friendly)
  const results = (json.results ?? []).map((c: any) => ({
    id: c.id,
    name: c.name,
    status: c.status,
    species: c.species,
    type: c.type,
    gender: c.gender,
    origin: c.origin?.name,
    location: c.location?.name,
    image: c.image
  }));

  const payload = { total: json.info?.count ?? 0, pages: json.info?.pages ?? 0, page, results };
  cache.set(k, payload);
  return payload;
}

export async function handleGetEpisode(input: unknown) {
  const args = GetEpisodeSchema.parse(input);
  const k = key({ tool: "get_episode", ...args });
  if (cache.has(k)) return cache.get(k);

  const json = await getEpisodeByName(args.name);
  const results = (json.results ?? []).map((e: any) => ({
    id: e.id,
    name: e.name,
    episode: e.episode, // Ej: "S02E05"
    air_date: e.air_date,
    characters_count: e.characters?.length ?? 0
  }));

  const payload = { total: json.info?.count ?? 0, results };
  cache.set(k, payload);
  return payload;
}

export async function handleGetLocation(input: unknown) {
  const args = GetLocationSchema.parse(input);
  const k = key({ tool: "get_location", ...args });
  if (cache.has(k)) return cache.get(k);

  const json = await getLocationByName(args.name);
  const results = (json.results ?? []).map((l: any) => ({
    id: l.id,
    name: l.name,
    type: l.type,
    dimension: l.dimension,
    residents_count: l.residents?.length ?? 0
  }));

  const payload = { total: json.info?.count ?? 0, results };
  cache.set(k, payload);
  return payload;
}

🧠 Paso 5 — Orquestación con gpt-5-nano (tools/function calling)

Archivo: src/index.ts

import "dotenv/config";
import OpenAI from "openai";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { SYSTEM_PROMPT } from "./prompts/system.js";
import { handleGetCharacter, handleGetEpisode, handleGetLocation } from "./tools/toolHandlers.js";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const MODEL = process.env.OPENAI_MODEL || "gpt-5-nano";

// Definición de tools para el modelo (JSON Schema)
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "get_character",
      description: "Busca personajes por nombre/status/species y pagina resultados",
      parameters: {
        type: "object",
        properties: {
          name: { type: "string", description: "Nombre a buscar, p.ej. 'Rick'." },
          status: { type: "string", enum: ["Alive", "Dead", "unknown"] },
          species: { type: "string", description: "Especie, p.ej. "Human"." },
          page: { type: "number", description: "Página de resultados (1..N)" }
        },
        additionalProperties: false
      }
    }
  },
  {
    type: "function",
    function: {
      name: "get_episode",
      description: "Obtiene episodios por nombre parcial (devuelve coincidencias)",
      parameters: {
        type: "object",
        properties: { name: { type: "string" } },
        required: ["name"],
        additionalProperties: false
      }
    }
  },
  {
    type: "function",
    function: {
      name: "get_location",
      description: "Busca ubicaciones/planetas por nombre parcial",
      parameters: {
        type: "object",
        properties: { name: { type: "string" } },
        required: ["name"],
        additionalProperties: false
      }
    }
  }
];

type Msg = OpenAI.Chat.Completions.ChatCompletionMessageParam[];
const messages: Msg = [{ role: "system", content: SYSTEM_PROMPT }];

async function callLLM() {
  const completion = await client.chat.completions.create({
    model: MODEL,
    temperature: 0.2,
    messages,
    tools,
    tool_choice: "auto"
  });

  const msg = completion.choices[0].message;

  // Si el modelo quiere usar herramientas:
  if (msg.tool_calls && msg.tool_calls.length > 0) {
    for (const tc of msg.tool_calls) {
      const name = tc.function?.name!;
      const args = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};

      let result: any;
      try {
        if (name === "get_character") result = await handleGetCharacter(args);
        else if (name === "get_episode") result = await handleGetEpisode(args);
        else if (name === "get_location") result = await handleGetLocation(args);
        else result = { error: `Tool desconocida: ${name}` };
      } catch (err: any) {
        result = { error: String(err?.message || err) };
      }

      // Devolvemos el resultado como mensaje "tool"
      messages.push({
        role: "tool",
        name,
        tool_call_id: tc.id,
        content: JSON.stringify(result)
      });
    }

    // Hacemos una segunda llamada para que el modelo produzca la respuesta final
    const completion2 = await client.chat.completions.create({
      model: MODEL,
      temperature: 0.2,
      messages,
      tools
    });
    const finalMsg = completion2.choices[0].message;
    if (finalMsg.content) console.log("\nAssistant:", finalMsg.content);
    messages.push({ role: "assistant", content: finalMsg.content || "" });
  } else {
    // Respuesta directa sin tools
    if (msg.content) console.log("\nAssistant:", msg.content);
    messages.push({ role: "assistant", content: msg.content || "" });
  }
}

async function main() {
  console.log("RickGPT listo. Escribí tu pregunta (Ctrl+C para salir).");
  const rl = readline.createInterface({ input, output });

  while (true) {
    const user = await rl.question("\nTú: ");
    if (!user.trim()) continue;
    messages.push({ role: "user", content: user });
    await callLLM();
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

▶️ Paso 6 — Ejecutar

Configurar .env (copiá desde .env.example):

OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5-nano

Correr en modo dev:

npm run dev

Probar preguntas:

  • ¿Cuántos personajes vivos humanos hay?
  • Mostrame 10 personajes con "Rick" en el nombre.
  • ¿Qué episodios tienen a "Evil Morty"?
  • ¿Qué ubicaciones contienen la palabra "Citadel"?

🧩 Detalles importantes de diseño

  • Paginación segura: la API devuelve 20 resultados por página. Permitimos page como arg; si el modelo necesita “ver más”, lo pedirá.
  • Rate limits: mínimas en la API pública; en workshop basta. Si querés harden, agregá backoff.
  • Cache: quick-lru evita repetir llamadas idénticas (mejora la latencia en vivo).
  • Salida amigable: normalizamos la data para que el modelo escriba resúmenes claros (evita pegar JSON crudo gigantesco).
  • Listas largas: el prompt instruye a devolver TOP 10 y ofrecer “¿querés ver más?”.

🧪 Pruebas de ejemplo (qué deberías ver)

Entrada: “¿En qué episodios aparece ‘Pickle Rick’?” Esperado: el modelo invoca get_episode con name: "rick", filtra/ordena y devuelve título + código SxxExx, con breve explicación.

Entrada: “Mostrame humanos vivos (página 2)” Esperado: invoca get_character con { species: "Human", status: "Alive", page: 2 }, devuelve TOP 10 y URLs de imágenes.


🎛 Extensiones opcionales (para más impacto)

  • Modo visual (rápido): imprimir las URL de image y abrir en el navegador con open (Mac/Linux) o start (Windows).
  • Filtro por género: agregar gender a get_character (enum: Female|Male|Genderless|unknown).
  • Tool character_episodes: dado un characterId, traer episodios usando los URLs que devuelva el personaje.
  • Embellecedores: usar Markdown (tablas) cuando pida “resumen con planetas y orígenes”.

🧯 Troubleshooting

  • El modelo no llama herramientas: asegurate de estar usando tool_choice: "auto" y que el prompt del sistema sugiera usar tools para datos.
  • 404 en la API: no hay coincidencias. El handler ya lo trata devolviendo total: 0.
  • Respuestas muy largas: ajustá temperature a 0.2 y reforzá en el prompt “sé conciso”.
  • Demasiadas llamadas: añadí lógica de debounce o fusioná queries (p. ej., si el usuario pide 3 cosas, el modelo podría secuenciar tools; está bien para el demo).

🧭 Roadmap a “MCP Server” formal (si lo querés mostrar)

  • Instalar @modelcontextprotocol/sdk y exponer tools como capabilities del servidor MCP (transport stdio).
  • Mapear inputSchema de cada herramienta a los zod ya definidos.
  • Reutilizar los mismos handlers (handleGetCharacter, etc.) en el servidor MCP.
  • Conectar con un cliente MCP (p. ej., MCP Inspector) y demostrar las invocaciones.
  • El LLM puede seguir siendo OpenAI gpt-5-nano a través del cliente que haga el tool routing.
  • Para el workshop, el flujo con gpt-5-nano + function calling es suficiente y 100% en vivo.

🧾 Licencia y créditos

  • API pública: https://rickandmortyapi.com
  • Este demo es educativo para workshop interno. Ajustá el tono del sistema si no querés sarcasmo “Rick-like”.

目录标签

目录标签

聊天机器人TypeScript自然语言处理API集成本地部署幽默风格RickandMorty

接入字段

传输方式(transport,传输协议)

stdio

鉴权方式(authType,认证方式)

api-key

运行时(runtime,运行环境)

Node.js

来源包(packageName,安装包名)

tsc

工具数量(toolCount,工具数)

3

资源数量(resourceCount,资源数)

0

提示词数量(promptCount,提示词数)

0

权限和风险

stdioapi-key部署方式未说明

接入前请确认传输方式、认证方式和部署位置,并根据实际工具能力限制访问范围。

安装前确认

不要直接授予不必要的文件、网络或账号权限;先核对安装命令和配置内容。

来源信息

继续浏览同类 MCP