Ai Mcp logo

Ai Mcp

OrganizationPopular
TanStack
ai-mcp

Host-side Model Context Protocol (MCP) client for TanStack AI: connect to external MCP servers, discover and run their tools inside any adapter's chat() loop, read resources and prompts, generate TypeScript types (typed tool names/pool keys) with the bundled CLI, and manage lifecycle with close()/await using.

Overview

PublisherTanStack
Repositoryai
Skill nameai-mcp
Stars
3.1K
Forks
330
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by TanStack on GitHub. Read the source before you install it.

Installation

Install the Ai Mcp AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/TanStack/ai.git /tmp/ai
mkdir -p .claude/skills
cp -r /tmp/ai/packages/ai-mcp/skills/ai-mcp .claude/skills/ai-mcp
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Ai Mcp in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Ai Mcp on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Ai Mcp is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

@tanstack/ai-mcp

This skill covers the @tanstack/ai-mcp package. Read ai-core/tool-calling/SKILL.md first — MCP tools flow into chat() the same way hand-written tools do.

When to use this package

Use @tanstack/ai-mcp when:

  • A third-party MCP server exposes tools you want an agent or chat loop to call.
  • You want to read MCP server resources (files, text, data) or prompts into a chat() message list.
  • You want generated TypeScript types for an external MCP server's tool signatures (via the bundled generate CLI).
  • You are running tool execution on the server side and want to connect to MCP servers with HTTP (Streamable HTTP or SSE) or stdio transports.

Do NOT use this package for browser/client-side code — MCP connections are server-side only.

Install

bash
pnpm add @tanstack/ai-mcp

The package has two subpath exports:

  • . — main client API (createMCPClient, createMCPClients, converters, types)
  • ./stdio — Node-only stdio transport factory (stdioTransport); import it separately so edge bundles stay clean

createMCPClient — single server

typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  prefix: 'weather', // optional: prefixes all tool names (e.g. 'weather_get_forecast')
  name: 'my-app', // optional: client identity sent to the server
})

createMCPClient connects immediately and returns an MCPClient. Throws MCPConnectionError if the connection fails.

Transports

Streamable HTTP (default for internet-facing servers)
typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://mcp.example.com/mcp',
    headers: { Authorization: 'Bearer sk-...' },
  },
})
SSE
typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: {
    type: 'sse',
    url: 'https://mcp.example.com/sse',
    headers: { Authorization: 'Bearer sk-...' },
  },
})
stdio (Node-only — import from /stdio subpath)
typescript
import { createMCPClient } from '@tanstack/ai-mcp'
import { stdioTransport } from '@tanstack/ai-mcp/stdio'

const client = await createMCPClient({
  transport: stdioTransport({
    command: 'npx',
    args: ['-y', 'my-mcp-server'],
    env: { API_KEY: process.env.API_KEY ?? '' },
  }),
})
Custom transport (escape hatch)

Pass any SDK Transport instance directly:

typescript
// InMemoryTransport (from @modelcontextprotocol/sdk) is re-exported for
// in-process testing; any SDK Transport instance works the same way.
import { createMCPClient, InMemoryTransport } from '@tanstack/ai-mcp'

const [clientTransport] = InMemoryTransport.createLinkedPair()
const client = await createMCPClient({ transport: clientTransport })

Authentication

Two levels:

  • Static tokens — pass headers on the http/sse config (sent with every request): headers: { Authorization: 'Bearer ...' }.
  • OAuth 2.1 (MCP authorization spec) — pass authProvider on the http/sse config. It accepts any OAuthClientProvider from @modelcontextprotocol/sdk/client/auth.js; the SDK transport attaches tokens, refreshes them, and retries on 401.
typescript
import { createMCPClient } from '@tanstack/ai-mcp'
// An OAuthClientProvider (from @modelcontextprotocol/sdk/client/auth.js)
// backed by tokens you persist server-side.
import { myOAuthProvider } from './oauth-provider'

const client = await createMCPClient({
  transport: {
    type: 'http',
    url: 'https://mcp.example.com/mcp',
    authProvider: myOAuthProvider,
  },
})

Caveat: interactive authorization-code flows need transport.finishAuth(code), and createMCPClient does not expose its internal transport. For redirect flows, construct the StreamableHTTPClientTransport yourself with the authProvider, keep a reference, call finishAuth(code) in the OAuth callback route, then pass the transport via the escape hatch above. For server-side providers backed by pre-provisioned/refreshable tokens, the config form is sufficient.

Three type-safety modes

Mode 1 — Auto-discovery (no types needed)

client.tools() lists every tool the server exposes. Args are typed unknown at compile time but the tool's JSON Schema is forwarded to the LLM.

typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

const tools = await client.tools()
// tools: McpServerTool[]  (args unknown)

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
  tools,
})

Use { lazy: true } to defer schema sending via the existing LazyToolManager:

typescript
import { createMCPClient } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

const tools = await client.tools({ lazy: true })

Mode 2 — Typed via toolDefinition instances

Pass bare toolDefinition() instances (no .server() call) to client.tools([...]). The MCP client binds a callTool proxy as the execute function while input/output validation and TypeScript types come from the definitions' Zod schemas. Only the named tools are returned (allowlist = the definitions' names). Throws MCPToolNotFoundError if the server does not expose a tool with that name.

typescript
import { toolDefinition } from '@tanstack/ai'
import { createMCPClient } from '@tanstack/ai-mcp'
import { z } from 'zod'

const getWeatherDef = toolDefinition({
  name: 'get_weather',
  description: 'Current weather for a city',
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
})

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// Returns MappedServerTools<typeof defs> — fully typed per definition.
const tools = await client.tools([getWeatherDef])

Mode 3 — Generated types (via generate CLI)

Run npx @tanstack/ai-mcp generate to introspect live servers and emit a ServerDescriptor interface per server. Pass the generated interface as the generic to createMCPClient<WeatherServer>(...) to narrow discovered tool names to the server's literals (args stay untyped — use Mode 2 for typed args).

See the "Codegen CLI" section below for details.

Lifecycle

The caller owns the lifecycle. chat() never closes the client.

Tools execute lazily while the response stream is consumed — close only after the stream is drained. In a streaming route handler, try/finally around the return (or await using at function scope) closes the client before the body streams; use a middleware terminal hook there instead (see Common Mistakes below).

typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import type { ModelMessage } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

// Option 1: middleware terminal hooks (streaming route handlers)
export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => client.close(),
        onAbort: () => client.close(),
        onError: () => client.close(),
      },
    ],
  })
  return toServerSentEventsResponse(stream)
}

// Option 2: explicit close after in-scope consumption
export async function runToCompletion(messages: Array<ModelMessage>) {
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  try {
    const stream = chat({
      adapter: openaiText('gpt-5.5'),
      messages,
      tools: await client.tools(),
    })
    for await (const chunk of stream) {
      // stream fully consumed inside this block
    }
  } finally {
    await client.close()
  }
}

// Option 3: await using (TypeScript 5.2+ with Symbol.asyncDispose) —
// same rule: consume the stream before the scope exits.
export async function runWithUsing(messages: Array<ModelMessage>) {
  await using client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
  })
  for await (const chunk of stream) {
    // ... consume the stream in this scope; close() runs at scope exit
  }
}

chat({ mcp }) — discovery + lifecycle in one prop

Rather than calling client.tools() and client.close() yourself, pass the mcp option to chat() and let it manage the full lifecycle.

typescript
// ChatMCPOptions shape:
// mcp: {
//   clients: Array<MCPClient | MCPClients>,
//   connection?: 'close' | 'keep-alive',  // default: 'close'
//   lazyTools?: boolean,
//   onDiscoveryError?: (error: unknown, source) => void,
// }

Behavior:

  • chat() calls .tools() on every entry in clients at run start and merges all results into the tool list.
  • lazyTools: true is forwarded to tools({ lazy: true }).
  • connection: 'close' (default) — each client is closed when the run ends (after the agent loop completes and the stream is drained). With 'keep-alive', chat() never closes the clients — the caller owns their lifecycle (keep connections warm across requests).
  • onDiscoveryError: throw (or re-throw) to abort the entire call; return normally to skip that source and continue. Omitting the handler re-throws (fail-fast).

When to use mcp vs. the tools spread:

ApproachUse when
chat({ mcp: { clients: [...] } })Convenience: discovery + lifecycle handled for you; untyped args are fine
tools: [...await client.tools([toolDefinition(...)])]Fully-typed args/results via Zod schemas (toolDefinition mode)

Server-side example:

typescript
// Any framework route handler that receives a Request works (TanStack Start,
// Next.js, Hono, ...).
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

// Created once at module scope; connection: 'keep-alive' below keeps it warm
// across requests.
const mcpClient = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

export async function POST(request: Request) {
  const { messages } = await request.json()

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    mcp: {
      clients: [mcpClient],
      connection: 'keep-alive', // chat() won't close it — reuse across requests
      onDiscoveryError: (err, source) => {
        console.warn('MCP discovery failed for source, skipping:', err)
        // returning skips this source; throw to fail the whole call fast
      },
    },
  })

  return toServerSentEventsResponse(stream)
  // connection: 'keep-alive' — chat() never closes mcpClient; it stays warm for the next request.
}

You can also pass an MCPClients pool directly:

typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'

const pool = await createMCPClients({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
  linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
})

export async function POST(request: Request) {
  const { messages } = await request.json()

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    mcp: { clients: [pool], connection: 'keep-alive' },
  })

  return toServerSentEventsResponse(stream)
}

createMCPClients — multiple servers

Connect to many MCP servers in parallel. Each config key becomes the default prefix for that server's tools, preventing name collisions across servers.

typescript
import { createMCPClients } from '@tanstack/ai-mcp'

await using pool = await createMCPClients({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
  linear: { transport: { type: 'http', url: 'https://mcp.linear.app/mcp' } },
})

// Tool names auto-prefixed: 'github_search_repos', 'linear_create_issue', etc.
const tools = await pool.tools()

// Forward lazy flag to every server:
const lazyTools = await pool.tools({ lazy: true })

// Per-server typed access (keys are typed as string here; generated
// MCPServers types make them literal — see Codegen CLI below):
const githubTools = await pool.clients.github!.tools()

createMCPClients connects in parallel, closes already-connected clients if any connection fails (no leaks), and throws MCPConnectionError naming the failed server(s).

Override or disable prefixing:

typescript
import { createMCPClients } from '@tanstack/ai-mcp'

await using pool = await createMCPClients({
  // 'gh_search_repos'
  github: {
    transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
    prefix: 'gh',
  },
  // 'create_issue' (no prefix)
  linear: {
    transport: { type: 'http', url: 'https://mcp.linear.app/mcp' },
    prefix: '',
  },
})

Abort signal — cancelling in-flight MCP calls

TanStack AI stops waiting for MCP tool calls when the chat run's AbortController fires (e.g. client disconnect, server abort). The abortSignal is threaded through ToolExecutionContext into every tool call with no extra code. For a task-required tool, aborting stops the local task stream and sends a best-effort tasks/cancel for a remote task the MCP server has already created. Cancel is best-effort: a server that ignores tasks/cancel may keep running until TTL.

You can also read it in a hand-written server tool that wraps an MCP call:

typescript
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

const fetchData = toolDefinition({
  name: 'fetch_data',
  description: 'Fetch a record from a slow upstream API',
  inputSchema: z.object({ id: z.string() }),
})

const myTool = fetchData.server(async (args, ctx) => {
  // Forward to any async work that accepts an AbortSignal.
  const result = await fetch(`https://slow.api/data/${args.id}`, {
    signal: ctx?.abortSignal,
  })
  return result.json()
})

Resources

typescript
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// List all resources the server exposes.
const resources = await client.resources()

// Read a specific resource by URI.
const resource = await client.readResource(resources[0]!.uri)

// Convert one content block to a TanStack ContentPart.
const part = mcpResourceToContentPart(resource.contents[0]!)
// part: ContentPart  (type: 'text' always for v1)

Inject resources into a chat turn:

typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})
const resource = await client.readResource('file:///project/README.md')
const parts = resource.contents.map(mcpResourceToContentPart)

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [
    {
      role: 'user',
      content: [
        ...parts,
        { type: 'text', content: 'Summarize this document.' },
      ],
    },
  ],
})

Prompts

typescript
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpPromptToMessages } from '@tanstack/ai-mcp'

const client = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
})

// List prompts the server exposes.
const prompts = await client.prompts()

// Get a prompt (with optional arguments).
const prompt = await client.getPrompt('review_code', { language: 'TypeScript' })

// Convert to TanStack ModelMessage[] for use in chat().
const messages = mcpPromptToMessages(prompt)
// messages: ModelMessage[]  (role: 'user' | 'assistant')

const stream = chat({
  adapter: openaiText('gpt-5.5'),
  messages: [...messages, { role: 'user', content: 'Review src/index.ts.' }],
})

MCP Apps

MCP Apps let an MCP tool surface a UI widget (static or interactive) on the client. Two variants exist. See docs/mcp/apps.md for the full guide.

Static widgets — UIResourcePart

When an MCP tool result carries a ui:// resource, TanStack AI emits a UIResourcePart on the assistant UIMessage, alongside the normal ToolCallPart / ToolResultPart. It is purely presentational — it never enters model input. The resource is read eagerly during the chat() run; if the read fails the tool result still flows to the model and the widget is simply absent (fail-soft). Static widgets require the MCP source to expose readResource — both createMCPClient and a createMCPClients pool do.

typescript
import type { UIResourcePart } from '@tanstack/ai'

// UIResourcePart shape (on the assistant UIMessage):
// {
//   type: 'ui-resource'
//   resource: { uri: string; mimeType: string; text?: string; blob?: string }
//   serverId?: string     // pool prefix / config key — routes interactive calls
//   toolCallId: string    // links to the originating tool call
//   toolName: string      // server-native MCP tool name whose UI this renders
//   meta?: Record<string, unknown>  // reserved — currently always undefined
// }

Interactive apps — createMcpAppCallHandler

For interactive apps (the widget iframe posts tool-call / prompt / link actions back), mount createMcpAppCallHandler from @tanstack/ai-mcp/apps at a POST route. Pass the MCP client(s) you already created — a single MCPClient, an MCPClients pool, or an array of either. The handler reads each client's transport descriptor via client.getInfo() / pool.getServers() (pure config, not a live socket) and reconnects per-call (stateless / serverless-safe). It matches the widget-supplied native (unprefixed) tool name against the server's unprefixed tool names, enforces a same-server allowlist, and returns { ok: true, result } or { ok: false, error }.

For a pool, the serverId on the UIResourcePart is the config key (the tool prefix); for a single client it is the client's prefix (or the sole default when serverId is absent and there is exactly one client).

typescript
import { createMCPClients } from '@tanstack/ai-mcp'
import {
  createMcpAppCallHandler,
  inMemoryMcpSessionStore,
} from '@tanstack/ai-mcp/apps'

// Reuse the same pool you pass to chat({ mcp: { clients: [mcp] } }).
const mcp = await createMCPClients({
  weather: {
    transport: { type: 'http', url: 'https://mcp-app.example.com/mcp' },
  },
})

// Minimal — reconnect-per-call via getServers() descriptor.
const handler = createMcpAppCallHandler({ clients: mcp })

// Options:
// clients   — MCPClient | MCPClients | Array<MCPClient | MCPClients> (required).
//             The handler reads transport descriptors via client.getInfo() /
//             pool.getServers() — the client does not need a live connection.
// store     — optional dynamic/stateful session store (e.g.
//             inMemoryMcpSessionStore()); used alongside clients.
// allowTool — optional authorizer receiving the WHOLE request:
//             (req: McpAppCallRequest) => boolean | Promise<boolean>.
//             The server-exposure check is ALWAYS enforced (the handler
//             rejects any tool the server does not expose). `allowTool`
//             is an ADDITIONAL restriction AND-ed on top: a request must
//             satisfy BOTH the server-exposure check and allowTool.
const handlerWithStore = createMcpAppCallHandler({
  clients: mcp,
  store: inMemoryMcpSessionStore(),
  allowTool: (req) => req.toolName === 'place_order',
})

The handler invokes the server (body: { threadId, serverId?, toolName, args?, messageId? }):

typescript
export async function POST(request: Request) {
  const body = await request.json()
  const result = await handler(body)
  // { ok: true; result: unknown } | { ok: false; error: string }
  return Response.json(result)
}

Client side — useMcpAppBridge + MCPAppResource

In React/Preact, create the bridge with the useMcpAppBridge hook (from @tanstack/ai-react / @tanstack/ai-preact) — it returns a stable bridge per threadId/callEndpoint and always calls your latest sendMessage/onLink, so the bridge isn't recreated on every render (no useMemo / exhaustive-deps by hand). It's a thin wrapper over the framework-agnostic createMcpAppBridge from @tanstack/ai-client (use that directly outside React/Preact). Render resources with MCPAppResource from @tanstack/ai-react/mcp-apps (also @tanstack/ai-preact/mcp-apps, which requires a preact/compat alias). MCPAppResource uses @mcp-ui/client's AppRenderer under the hood — React only. Solid, Vue, Svelte, and Angular renderers are deferred.

The bridge exposes { callTool, sendPrompt, openLink } and routes the iframe's actions: tool → POST to callEndpoint; promptchat.sendMessage; linkonLink(url) if provided, otherwise the link is dropped (with a console warning) and openLink returns { isError: true } — it does NOT hang. toolName is read from part.toolName; it is not a prop. Omit bridge for display-only (inert) rendering.

tsx
import { useChat, useMcpAppBridge } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { MCPAppResource } from '@tanstack/ai-react/mcp-apps'

function ChatPage() {
  const threadId = 'weather-chat'
  const { messages, sendMessage } = useChat({
    connection: fetchServerSentEvents('/api/chat'),
  })

  const bridge = useMcpAppBridge({
    threadId,
    callEndpoint: '/api/mcp-app/call',
    chat: { sendMessage: async (content) => void sendMessage({ content }) },
    // Opt in to link navigation — absent means links are dropped.
    onLink: (url) => window.open(url, '_blank', 'noopener'),
  })

  return (
    <div>
      {messages.map((msg) =>
        msg.parts.map((part, i) => {
          if (part.type === 'text') return <p key={i}>{part.content}</p>
          if (part.type === 'ui-resource') {
            return (
              <MCPAppResource
                key={i}
                part={part}
                bridge={bridge}
                sandbox={{ url: new URL('https://sandbox.example.com') }}
                // toolInput is optional; toolName comes from part.toolName.
              />
            )
          }
          return null
        }),
      )}
    </div>
  )
}

Codegen CLI

Generate TypeScript types (typed tool names and pool keys) by introspecting live MCP servers.

1. Create mcp.config.ts at your project root:

typescript
import { defineConfig } from '@tanstack/ai-mcp'

export default defineConfig({
  servers: {
    github: {
      transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
      // prefix must match the runtime createMCPClient({ prefix }) value
    },
  },
  outFile: './src/mcp-types.generated.ts',
})

2. Run the generator:

bash
npx @tanstack/ai-mcp generate

This connects to each server, lists its tools/resources/prompts, converts JSON Schemas to TypeScript, and writes one interface <Name>Server extends ServerDescriptor per server plus a combined interface MCPServers for pool typing.

3. Use the generated types:

typescript
// Single server — narrows tools() return to descriptor-keyed tool names.
import type { GithubServer } from './src/mcp-types.generated'
import { createMCPClient, createMCPClients } from '@tanstack/ai-mcp'

const client = await createMCPClient<GithubServer>({
  transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
})
const tools = await client.tools() // typed to GithubServer's tool names

// Multiple servers via the generated MCPServers map.
import type { MCPServers } from './src/mcp-types.generated'

const pool = await createMCPClients<MCPServers>({
  github: { transport: { type: 'http', url: 'https://mcp.github.com/mcp' } },
})
// pool.clients.github is MCPClient<GithubServer>
// missing/extra keys are a compile error

Codegen deps (json-schema-to-typescript, jiti) are bundled into the CLI bin and do NOT appear in the library's runtime dependency graph.

Error classes

  • MCPConnectionError — thrown when a server connection fails or when calling methods after close().
  • MCPToolNotFoundError — thrown from client.tools([defs]) when a definition's name is not exposed by the server.
  • MCPTaskRequiredToolError — thrown when a task-required tool is bound via tools([defs]) or called via callTool() and the server does not declare the tasks capability for tools/call. Auto-discovery skips those tools instead of throwing.
  • DuplicateToolNameError — thrown by a single pool's own tools() when two tools within that pool share the same name (same server or pool clients with no prefix). Exported from @tanstack/ai-mcp.
  • MCPDuplicateToolNameError — thrown by chat() when tools from separate mcp.clients entries collide after merging. Exported from @tanstack/ai (not @tanstack/ai-mcp), so users can instanceof it at the chat() call site.
typescript
import {
  MCPConnectionError,
  MCPToolNotFoundError,
  MCPTaskRequiredToolError,
  DuplicateToolNameError,
} from '@tanstack/ai-mcp'

import { MCPDuplicateToolNameError } from '@tanstack/ai'

Complete server-route example

typescript
// src/routes/api.chat.ts — mount POST in your framework's route handler
// (TanStack Start server route, Next.js route handler, Hono, ...).
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()

  const pool = await createMCPClients({
    github: {
      transport: { type: 'http', url: 'https://mcp.github.com/mcp' },
    },
    linear: {
      transport: {
        type: 'http',
        url: 'https://mcp.linear.app/mcp',
        headers: {
          Authorization: `Bearer ${process.env.LINEAR_KEY ?? ''}`,
        },
      },
    },
  })

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await pool.tools(),
    // Close after the run ends — tools execute while the response streams,
    // so `await using` / try-finally would close the pool too early here.
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => pool.close(),
        onAbort: () => pool.close(),
        onError: () => pool.close(),
      },
    ],
  })

  return toServerSentEventsResponse(stream)
}

Common Mistakes

a. HIGH: closing the client before the stream finishes

chat() executes tools lazily as the model calls them during streaming. If you close the MCP client before the response stream is fully consumed, in-flight tool calls will fail.

Wrong:

typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })
  const tools = await client.tools()
  const stream = chat({ adapter: openaiText('gpt-5.5'), messages, tools })
  await client.close() // closes before the stream runs tools
  return toServerSentEventsResponse(stream)
}

This includes try/finally around the return, and await using at function scope — both close before the returned Response body streams.

Correct — close in middleware terminal hooks (exactly one of onFinish/onAbort/onError fires per run), or consume the stream in scope before closing:

typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'

export async function POST(request: Request) {
  const { messages } = await request.json()
  const client = await createMCPClient({
    transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  })

  const stream = chat({
    adapter: openaiText('gpt-5.5'),
    messages,
    tools: await client.tools(),
    middleware: [
      {
        name: 'mcp-close',
        onFinish: () => client.close(),
        onAbort: () => client.close(),
        onError: () => client.close(),
      },
    ],
  })
  return toServerSentEventsResponse(stream)
}

b. HIGH: importing stdioTransport from the main entry point

stdioTransport is only available from @tanstack/ai-mcp/stdio. Importing it from @tanstack/ai-mcp will fail with a module-not-found error and would bundle Node.js child-process code into edge bundles.

Wrong:

typescript
import { stdioTransport } from '@tanstack/ai-mcp' // does not exist here

Correct:

typescript
import { stdioTransport } from '@tanstack/ai-mcp/stdio'

c. MEDIUM: using client.tools([defs]) without matching names

The name field on each toolDefinition must exactly match the tool name the MCP server exposes. Mismatches throw MCPToolNotFoundError at call time, not at type-check time (unless generated types are in use).

d. MEDIUM: not setting a prefix when multiple servers share tool names

Two different errors can arise depending on where the collision is detected:

  • Within a single createMCPClients pool — calling pool.tools() throws DuplicateToolNameError (from @tanstack/ai-mcp) when two servers in that pool expose the same name with no prefix to separate them.
  • Across separate mcp.clients entries in chat()chat() throws MCPDuplicateToolNameError (from @tanstack/ai) after merging discovered tools from all mcp.clients entries.

In both cases, the fix is the same: use createMCPClients (which auto-prefixes by config key) or set an explicit prefix on each createMCPClient call.

Cross-References

  • See also: ai-core/tool-calling/SKILL.md — MCP tools are ServerTools; all tool patterns (approval, lazy, client-side) apply.
  • See also: ai-core/chat-experience/SKILL.md — wiring tools into chat().

Frequently asked questions

What does the Ai Mcp AI skill do?

Host-side Model Context Protocol (MCP) client for TanStack AI: connect to external MCP servers, discover and run their tools inside any adapter's chat() loop, read resources and prompts, generate TypeScript types (typed tool names/pool keys) with the bundled CLI, and manage lifecycle with close()/await using.

Why use Ai Mcp on TypingMind?

Because you install it once and use it with any model. Ai Mcp is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Ai Mcp in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/TanStack/ai/tree/main/packages/ai-mcp/skills/ai-mcp. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Ai Mcp?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Ai Mcp?

As many as you like. As long as a model supports skills, you can use Ai Mcp with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Ai Mcp AI skill free?

Yes. It is published on GitHub by TanStack under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇