Skip to main content
Glama
README.md
# multi-mcp

Servidor MCP multi-propósito para [opencode](https://opencode.ai). Provee utilidades de texto/hash/tiempo y un **scraper web profesional** (`scrape-dom`).

Incluye un **CLI instalador** (`multi-mcp-setup`) que configura el MCP server y el plugin de slash commands en opencode desde cero, sin corromper la config existente ni chocar con otros servers/plugins.

## Requisitos

- Node.js ≥ 22
- npm

## Quick Start

### 1. Instala el paquete

```bash
npm install -g multi-mcp
# o para desarrollo local:
# npm link   # dentro del repo
```

### 2. Configura opencode

```bash
multi-mcp-setup install
```

El comando:
- Copia el plugin y sus slash commands a `~/.config/opencode/plugins/`
- Registra el MCP server `multi-mcp` en `~/.config/opencode/opencode.json`
- Preserva comentarios, orden y el resto de claves de tu config (edición quirúrgica JSONC)
- Es **idempotente**: re-ejecutarlo no duplica nada
- Detecta **conflictos**: si `mcp.multi-mcp` apunta a otro server, aborta sin pisarlo

### 3. Reinicia opencode

Tras instalar, reinicia opencode. Tendrás las tools `multi-mcp_*` y los slash commands `/multi-mcp-scrape`, `/multi-mcp-scrape-dom`, `/multi-mcp-dom`, `/multi-mcp-list-tools`.

### Verificar y desinstalar

```bash
multi-mcp-setup status              # estado actual
multi-mcp-setup install --dry-run   # previsualizar cambios sin escribir
multi-mcp-setup uninstall           # quita solo lo nuestro
```

> **Windows**: usa `npm install -g multi-mcp` o `npx multi-mcp-setup install` en vez de ejecutar `bin/cli.js` directamente. npm genera los wrappers `.cmd`/`.ps1` automáticamente.

## Configuración manual (sin CLI)

Alternativa al Quick Start: registra el servidor en `~/.config/opencode/opencode.json` como MCP **local** apuntando al bundle compilado:

```jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    // ...otros servers...
    "multi-mcp": {
      "type": "local",
      "command": ["node", "/root/.config/opencode/mcp/multi-mcp/build/server.mjs"],
      "enabled": true
    }
  }
}
```

Ajusta la ruta a donde tengas este repo. Tras editar el código del server:

```bash
npm run typecheck && npm run build
```

y **reinicia opencode** para que cargue las tools nuevas.

## Tools

| Tool | Descripción |
|------|-------------|
| `echo` | Devuelve el texto de entrada |
| `timestamp` | Fecha/hora actual (zona IANA opcional) |
| `math` | Evalúa expresiones aritméticas |
| `hash` | Hash sha256/sha1/md5 |
| `base64` | Codifica/decodifica base64 |
| `uuid` | Genera UUIDs |
| `list-tools` | Lista todas las tools con descripciones |
| `scrape` | Fetch de URL con bypass Cloudflare, retries y metadata. Devuelve HTML crudo |
| `dom` | Parsea HTML dado con jsdom y ejecuta operaciones (query, queryAll, attr, text, tables, forms, links...) |
| `scrape-dom` | **Scraper profesional**: fetch + parse jsdom + extract DSL + operations + paginación |

---

## `scrape-dom` — scraper profesional

Combina fetch (con bypass Cloudflare) + parseo jsdom + extracción estructurada, en una sola llamada.

### Parámetros comunes

| Parámetro | Tipo | Default | Descripción |
|-----------|------|---------|-------------|
| `url` | string (URL) | — | Página a scrapear |
| `method` | GET/POST/HEAD | `GET` | Método HTTP |
| `timeout` | int (1000–120000) | `30000` | Timeout en ms |
| `retries` | int (0–10) | `3` | Reintentos |
| `headers` | object | — | Headers HTTP custom |
| `operations` | array | — | Pipeline de operaciones DOM de bajo nivel |
| `extract` | object | — | DSL de extracción estructurada |
| `pagination` | object | — | Seguir enlaces "next" y acumular resultados |

### Modo 1: `extract` (DSL estructurado)

Ideal para scrapear listas/items repetidos o campos concretos de una página.

```js
scrape-dom({
  url: "https://example.com/list",
  extract: {
    itemSelector: ".card",                    // cada item repetido (omitir ⇒ campos leídos de la página completa)
    include: ".card:not(.sold-out)",          // filtro: solo items que matchean
    exclude: ".ad",                           // filtro: descarta items que matchean
    fields: [
      { key: "title", selector: "h2", transform: ["trim", "collapse"] },
      { key: "price", selector: ".price", type: "int" },
      { key: "link",  selector: "a", attr: "href", resolveUrl: true },
      { key: "desc",  selector: "p", optional: true },
      { key: "id",    selector: ".id", default: "unknown" }
    ]
  }
})
```

**Campo del DSL:**

| Campo | Descripción |
|-------|-------------|
| `key` | Clave de salida |
| `selector` | Selector CSS del campo (dentro del item, o de la página si no hay `itemSelector`) |
| `attr` | Atributo a leer en vez del texto (`href`, `src`...) |
| `type` | Coercionar: `string`, `int`, `float`, `number`, `bool` |
| `transform` | Array en orden: `trim`, `collapse`, `lower`, `upper`, `remove-whitespace`, `decode-entities`, `strip-tags` |
| `resolveUrl` | Resolver URLs relativas contra la base de la página (con `attr`) |
| `default` | Valor si el campo falta |
| `optional` | Si `false`, un campo faltante invalida el item |

**Resultado** (modo itemSelector): `{ items, count, skippedInvalid?, pagesFetched }`.
**Resultado** (sin itemSelector): `{ items, count, singleItem, pagesFetched }`.

### Modo 2: `operations` (pipeline de bajo nivel)

Operaciones planas sobre todo el documento, estilo `dom` pero tras scrapear la URL.

```js
scrape-dom({
  url: "https://example.com",
  operations: [
    { type: "title" },
    { type: "queryAll", selector: "li a" },
    { type: "tables" }
  ]
})
```

Tipos de operación: `title`, `meta`, `query`, `queryAll`, `attr`, `text`, `html`, `links`, `images`, `tables`, `forms`, `scripts`, `styles`, `custom` (con `scriptSelector` + `all`).

### Paginación multi-página

```js
scrape-dom({
  url: "https://example.com/news?page=1",
  extract: { itemSelector: "article", fields: [{ key: "title", selector: "h2" }] },
  pagination: {
    nextSelector: "a.next",   // selector del enlace "next"
    attr: "href",             // atributo con la URL (default href)
    maxPages: 5               // máx páginas (1-20, default 3)
  }
})
```

Acumula los items de todas las páginas en `items` y lista las URLs visitadas en `pagesFetched`. Evita loops detectando URLs repetidas.

---

## `scrape` / `dom` (componentes)

- `scrape({ url, method?, timeout?, retries?, metadata?, headers? })` → HTML crudo, con `metadata: true` devuelve status/headers/size/tiempo.
- `dom({ html, operations? })` → parsea HTML dado con jsdom y ejecuta las mismas operaciones que el modo 2 de `scrape-dom`.

## Build

```bash
npm run build           # tsup → build/server.mjs (server MCP)
npm run build:plugin    # tsup → install/plugin/multi-mcp.mjs + command/*.md
npm run build:cli       # tsup → bin/cli.js (instalador con shebang)
npm run build:all       # los tres
npm run dev             # npx tsx src/server.ts (sin build, desde fuente)
npm run typecheck       # tsc --noEmit
npm run start           # node build/server.mjs
```

> **Bun:** compilar a binario con `bun build --compile` **no funciona** — css-tree (dep de
> jsdom) carga `../data/patch.json` con un `require` dinámico que el bun compile no
> empaqueta (`Cannot find module '../data/patch.json'`). Usar `npm run build` (tsup).

TDQS

B3.4/5.0

Scored across 10 tools

Disambiguation3/5

The utility tools are clearly distinct, but scrape, scrape-dom, and dom overlap in fetching URLs and working with HTML/DOMs, so an agent could select the wrong one. Descriptions clarify raw HTML vs structured extraction vs in-memory DOM operations, but the boundaries are not immediately obvious from names alone.

Naming Consistency3/5

All names are lowercase, but the set mixes single-word nouns (timestamp, math, uuid), single-word verbs (echo, scrape), and hyphenated verb-noun names (list-tools, scrape-dom). The naming is readable and predictable in tone, but not structurally consistent.

Tool Count4/5

Ten tools is within a reasonable scope for a multi-purpose server, and each tool has a defined function. The utility side adds some trivial tools, but the overall count is not excessive or too thin.

Completeness4/5

The utility side covers common helper operations, and the scraping side provides raw fetch, structured extraction, and DOM manipulation with pagination support. There are minor gaps like limited HTTP request customization, but no critical dead ends for the apparent purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues