|
| 1 | +# MCP integration — wiring a Model Context Protocol server into a zaileys AI bot |
| 2 | + |
| 3 | +zaileys is the WhatsApp transport. When you build an **AI bot** on top of it (LLM replies via the |
| 4 | +Vercel AI SDK), you can give the model extra capabilities by connecting **MCP (Model Context |
| 5 | +Protocol)** servers — e.g. a scraper catalog like zpi (`mcp.zpi.web.id`). This recipe shows the |
| 6 | +pattern: connect an MCP server, expose its tools to the model, and keep the per-turn tool payload |
| 7 | +small with a lazy router. |
| 8 | + |
| 9 | +> Scope: this is **app-level** (AI SDK + MCP SDK), not a zaileys API. zaileys only handles |
| 10 | +> sending/receiving the WhatsApp messages; the MCP tools are wired into your LLM call. |
| 11 | +
|
| 12 | +## Concepts (quick) |
| 13 | + |
| 14 | +- **MCP** exposes **tools** (callable functions w/ JSON Schema), **resources**, and **prompts**. |
| 15 | +- **Transport:** `stdio` (local process), `SSE` (legacy remote), `Streamable HTTP` (modern remote, POST + optional SSE stream). Remote catalogs like zpi use Streamable HTTP. |
| 16 | +- **Handshake (Streamable HTTP):** POST `initialize` → server returns `protocolVersion`/`capabilities`/`serverInfo` + `mcp-session-id` header → client sends `notifications/initialized` → then `tools/list` / `tools/call`. `Accept` header must be `application/json, text/event-stream`. |
| 17 | +- **Auth:** header (`Authorization: Bearer <key>` or a custom header). Some clients are OAuth-only and need the `mcp-remote` bridge. |
| 18 | + |
| 19 | +## Install |
| 20 | + |
| 21 | +```bash |
| 22 | +npm i @modelcontextprotocol/sdk ai |
| 23 | +``` |
| 24 | + |
| 25 | +## Connect + expose tools to the model |
| 26 | + |
| 27 | +```ts |
| 28 | +import { Client } from '@modelcontextprotocol/sdk/client/index.js' |
| 29 | +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' |
| 30 | +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' |
| 31 | +import { dynamicTool, jsonSchema, type ToolSet } from 'ai' |
| 32 | + |
| 33 | +export async function loadMcpTools(url: string, headers: Record<string, string>): Promise<ToolSet> { |
| 34 | + const client = new Client({ name: 'my-bot', version: '1.0.0' }) |
| 35 | + const requestInit = { headers } |
| 36 | + try { |
| 37 | + await client.connect(new StreamableHTTPClientTransport(new URL(url), { requestInit })) |
| 38 | + } catch { |
| 39 | + await client.connect(new SSEClientTransport(new URL(url), { requestInit })) // fallback |
| 40 | + } |
| 41 | + |
| 42 | + const { tools } = await client.listTools() |
| 43 | + const out: ToolSet = {} |
| 44 | + for (const t of tools) { |
| 45 | + out[t.name] = dynamicTool({ |
| 46 | + description: t.description ?? t.name, |
| 47 | + inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]), |
| 48 | + execute: async (args) => { |
| 49 | + const res = await client.callTool({ name: t.name, arguments: args as Record<string, unknown> }) |
| 50 | + // IMPORTANT: real data is often in structuredContent, not content[].text |
| 51 | + const structured = (res as { structuredContent?: unknown }).structuredContent |
| 52 | + const text = (res.content as { type: string; text?: string }[]) |
| 53 | + .filter((c) => c.type === 'text').map((c) => c.text ?? '').join('\n').trim() |
| 54 | + return structured != null ? { summary: text, data: structured } : (text || '(empty)') |
| 55 | + }, |
| 56 | + }) |
| 57 | + } |
| 58 | + return out |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +Then pass them into your model call alongside zaileys: |
| 63 | + |
| 64 | +```ts |
| 65 | +import { streamText } from 'ai' |
| 66 | +import { Client as Zaileys } from 'zaileys' |
| 67 | + |
| 68 | +const mcpTools = await loadMcpTools('https://mcp.zpi.web.id/mcp', { |
| 69 | + Authorization: `Bearer ${process.env.ZPI_API_KEY}`, |
| 70 | +}) |
| 71 | + |
| 72 | +const wa = new Zaileys({ /* ...zaileys options... */ }) |
| 73 | +wa.on('messages', async (ctx) => { |
| 74 | + const res = streamText({ model, system, messages, tools: { ...mcpTools /*, ...yourTools */ } }) |
| 75 | + // stream res.text back via the zaileys send builder |
| 76 | + // await wa.send(ctx.roomId).text(finalText) |
| 77 | +}) |
| 78 | +``` |
| 79 | + |
| 80 | +## Gotchas (learned in production) |
| 81 | + |
| 82 | +1. **Read `structuredContent`.** Many servers put the actual payload in `result.structuredContent`; `content[].text` may be just a summary (e.g. `"Success: ..."`). Returning only the text starves the model of data. |
| 83 | +2. **Path params go in `params`.** For endpoints like `/:username`, pass `params: { username }` to the server's run/call tool — don't bake the value into the endpoint string. |
| 84 | +3. **Don't dump every tool as always-active.** A catalog can expose dozens of tools. Use a **lazy router**: keep tools defined but inactive, expose one `find_tools(query)` meta-tool that ranks them (semantic + keyword) and activates only the matches for the next step. Mark a few "primary" tools always-active. |
| 85 | +4. **find_tools ranking: merge semantic + keyword.** Pure embedding similarity misses exact-name hits (query `"instagram"` vs a generic tool description can score < 0.3). Union keyword matches so exact hits always surface. |
| 86 | +5. **Keep reflexive/expensive tools lazy.** A model will reach for `web_search` first if it's always on. Make it discoverable instead, so cheaper/primary tools win by default. |
| 87 | +6. **Accept header + session.** Streamable HTTP needs `Accept: application/json, text/event-stream` and you must carry the `mcp-session-id` from `initialize` on subsequent calls. |
| 88 | + |
| 89 | +## Example: zpi scraper catalog |
| 90 | + |
| 91 | +- Endpoint `https://mcp.zpi.web.id/mcp` (Streamable HTTP), auth `Authorization: Bearer <key>`. |
| 92 | +- Tools: `search_scrapers`, `list_categories`, `get_scraper_schema`, `run_scraper`, `get_account`, `get_usage`, `bulk_submit`, `bulk_status`. |
| 93 | +- Flow (2 hop): `search_scrapers` (natural-language query) → `run_scraper` (skip schema when params are obvious). |
| 94 | +- Public scraper page URL: `https://zpi.web.id/api/<category>/<slug>`. |
| 95 | + |
| 96 | +## Sources |
| 97 | + |
| 98 | +- MCP docs: <https://modelcontextprotocol.io> |
| 99 | +- MCP TypeScript SDK: <https://github.com/modelcontextprotocol/typescript-sdk> |
| 100 | +- Vercel AI SDK tools: <https://ai-sdk.dev/docs> |
0 commit comments