Deuz SDK
Modules

MCP

Connect to Model Context Protocol servers and expose their tools to the agentic loop as a canonical ToolSet.

The MCP module connects to a Model Context Protocol server, lists its tools, and maps them into a canonical ToolSet you can hand straight to generateText or streamChat. Tool execution proxies transparently to the server — when the model calls an MCP tool, the SDK invokes it over the live connection and feeds the result back into the loop.

Beyond tools, the client also exposes the server's resources and prompts, and can answer server-initiated elicitation requests (all per the 2025-11-25 MCP revision).

@modelcontextprotocol/sdk is a lazy optional peer (^1.29.0): it is imported with a dynamic import() only when you actually create a client, so the edge bundle never pulls it in unless you use MCP. Install it yourself:

npm i @modelcontextprotocol/sdk

If it is missing at runtime, createMcpClient / createStdioMcpClient reject with an InvalidRequestError telling you to install it.

Transports

Two transports live behind two different subpath exports, split by where they can run:

SubpathFunctionTransportRuntime
@deuz-sdk/core/mcpcreateMcpClienthttp (Streamable HTTP) or sse (legacy)Edge-safe (fetch-only, no Node builtins)
@deuz-sdk/core/mcp/stdiocreateStdioMcpClientstdio (spawns a child process)Node-only

Use http for remote MCP servers reachable over the network — it is the current Streamable HTTP transport. sse is the legacy fallback some servers (e.g. Firecrawl's /v2/sse) still expose. The stdio transport spawns a local command (e.g. npx -y firecrawl-mcp) and talks to it over stdin/stdout, so it only works in a Node process and is kept on its own subpath to keep the edge core free of Node builtins.

createMcpClient

import { createMcpClient } from '@deuz-sdk/core/mcp';

const client = await createMcpClient(options);

createMcpClient is async and returns a Promise<McpClient>. It loads the SDK, builds the transport, and connects before resolving.

Options

interface McpClientOptions {
  transport: McpHttpTransport;
  name?: string;
  version?: string;
  onElicitationRequest?: McpElicitationHandler;
}

interface McpHttpTransport {
  type: 'http' | 'sse';
  url: string;
  headers?: Record<string, string>;
}
OptionTypeDefaultNotes
transport.type'http' | 'sse'http = Streamable HTTP; sse = legacy SSE fallback.
transport.urlstringThe server endpoint.
transport.headersRecord<string, string>Extra request headers (e.g. Authorization). Sent on every request.
namestring'deuz'Client name advertised to the server.
versionstring'0.0.0'Client version advertised to the server.
onElicitationRequestMcpElicitationHandlerAnswer server-initiated user-input requests — see Elicitation. Providing it declares the elicitation capability (form + url).

createStdioMcpClient

import { createStdioMcpClient } from '@deuz-sdk/core/mcp/stdio';

const client = await createStdioMcpClient(options);

Options

interface McpStdioOptions {
  command: string;
  args?: string[];
  env?: Record<string, string>;
  name?: string;
  version?: string;
  onElicitationRequest?: McpElicitationHandler; // same semantics as the HTTP client
}
OptionTypeDefaultNotes
commandstringExecutable to spawn, e.g. 'npx'.
argsstring[][]Arguments, e.g. ['-y', 'firecrawl-mcp'].
envRecord<string, string>Environment for the child process (e.g. API keys the server needs).
namestring'deuz'Client name advertised to the server.
versionstring'0.0.0'Client version advertised to the server.

The McpClient

Both factories return the same transport-agnostic McpClient:

interface McpClient {
  /** MCP tools mapped to a ToolSet ready for generateText({ tools }). */
  listTools(namespace?: string): Promise<ToolSet>;
  /** Call a tool directly (rarely needed; the loop uses execute). */
  callTool(name: string, args: unknown): Promise<unknown>;
  /** All resources / prompts, auto-paginated (cursor handled for you). */
  listResources(): Promise<McpResource[]>;
  readResource(uri: string): Promise<McpResourceContent[]>;
  listPrompts(): Promise<McpPrompt[]>;
  getPrompt(name: string, args?: Record<string, string>): Promise<McpGetPromptResult>;
  close(): Promise<void>;
}
MethodReturnsNotes
listTools(namespace?)Promise<ToolSet>Lists the server's tools and maps each into a canonical Tool. The optional namespace prefixes every tool name with `${namespace}_` so you can merge multiple servers without collisions.
callTool(name, args)Promise<unknown>Invokes a tool directly. The loop never needs this — each mapped tool's execute calls it for you.
listResources()Promise<McpResource[]>All resources across pages — cursor pagination is handled internally (capped at 100 pages against endless cursors).
readResource(uri)Promise<McpResourceContent[]>One resource's contents: entries carry text or base64 blob plus mimeType.
listPrompts()Promise<McpPrompt[]>All prompt templates across pages (same pagination handling).
getPrompt(name, args?)Promise<McpGetPromptResult>Renders a prompt with string arguments. Messages come back in MCP's own shape ({ role, content } blocks), not the canonical Message — map them yourself if you feed them to a model.
close()Promise<void>Closes the connection / terminates the child process. Always call it when done.

The resource/prompt methods need SDK ^1.29.0 — on an older installed SDK they reject with an actionable upgrade error.

How tools map

listTools() returns a ToolSet — a Record<string, Tool> — built from the server's tool definitions:

  • The MCP inputSchema is a JSON Schema, so it goes straight onto Tool.parameters (no conversion). The server's outputSchema, when present, is carried onto Tool.outputSchema as metadata.
  • Each tool's execute proxies to the server via callTool.
  • Structured results win: when the server returns structuredContent, execute returns that object verbatim (per spec, the text blocks are just a redundant serialization). Otherwise the text content blocks are joined into a string. If the server marks the result as an error (isError), execute throws — which the tool loop catches and feeds back as an is_error tool result, so the model can self-heal rather than the call crashing.

Because the mapped tools have an execute, the agentic loop runs them automatically; you do not write the round-trip yourself.

Behavior note: before 1.3.0, execute always returned joined text. Servers that declare an outputSchema now yield the structured object instead — if your code string-matched those results, read the object.

Elicitation

Servers can pause mid-operation and ask the user for input (elicitation/create). Pass onElicitationRequest to handle it; the request is a two-mode union:

const mcp = await createMcpClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  onElicitationRequest: async (req) => {
    if (req.mode === 'form') {
      // req.requestedSchema: flat object, primitive props — render a form.
      const content = await showForm(req.message, req.requestedSchema);
      return content ? { action: 'accept', content } : { action: 'decline' };
    }
    // req.mode === 'url': show req.url to the user and ask for consent.
    // NEVER auto-open or prefetch it (spec requirement). accept = consent
    // only — the interaction completes out-of-band on the server's side.
    const consented = await confirmOpenUrl(req.message, req.url);
    return { action: consented ? 'accept' : 'decline' };
  },
});

Return { action: 'accept', content? }, { action: 'decline' }, or { action: 'cancel' }. Form-mode content must match requestedSchema. URL mode exists for sensitive flows (OAuth, payment, API keys) that must not pass through the client — treat the URL as untrusted, display the full host, and let the user decide.

HTTP server in a tool loop

Connect to a remote MCP server, list its tools, and let the model use them. Remember to close().

mcp-http.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { createMcpClient } from '@deuz-sdk/core/mcp';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const mcp = await createMcpClient({
  transport: {
    type: 'http',
    url: 'https://mcp.example.com/mcp',
    headers: { Authorization: `Bearer ${process.env.MCP_TOKEN!}` },
  },
});

try {
  const tools = await mcp.listTools();

  const { text, steps } = await generateText({
    model: anthropic('claude-opus-4-8'),
    messages: [{ role: 'user', content: 'Scrape example.com and summarize it.' }],
    maxSteps: 5,
    tools,
  });

  console.log(text);
  console.log(steps?.length);
} finally {
  await mcp.close();
}

stdio local server (Node)

Spawn a local MCP server as a child process. This only works in a Node runtime, so import from @deuz-sdk/core/mcp/stdio.

mcp-stdio.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { createStdioMcpClient } from '@deuz-sdk/core/mcp/stdio';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const mcp = await createStdioMcpClient({
  command: 'npx',
  args: ['-y', 'firecrawl-mcp'],
  env: { FIRECRAWL_API_KEY: process.env.FIRECRAWL_API_KEY! },
});

try {
  const tools = await mcp.listTools();

  const { text } = await generateText({
    model: anthropic('claude-opus-4-8'),
    messages: [{ role: 'user', content: 'Find the pricing page on example.com.' }],
    maxSteps: 5,
    tools,
  });

  console.log(text);
} finally {
  await mcp.close();
}

Combining MCP tools with local tools

A ToolSet is just a record, so spread MCP tools alongside your own. Use a namespace per server to avoid name collisions when you connect to more than one.

mcp-combined.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { createMcpClient } from '@deuz-sdk/core/mcp';
import { z } from 'zod';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const search = await createMcpClient({
  transport: { type: 'http', url: 'https://search.example.com/mcp' },
});
const docs = await createMcpClient({
  transport: { type: 'sse', url: 'https://docs.example.com/v2/sse' },
});

try {
  const tools = {
    // Namespaced MCP tools: e.g. `search_query`, `docs_fetch`.
    ...(await search.listTools('search')),
    ...(await docs.listTools('docs')),
    // A local tool, defined inline with a Zod schema.
    now: {
      description: 'Return the current ISO timestamp.',
      parameters: z.object({}),
      execute: async () => ({ now: new Date().toISOString() }),
    },
  };

  const { text } = await generateText({
    model: anthropic('claude-opus-4-8'),
    messages: [{ role: 'user', content: 'What time is it, and what does example.com sell?' }],
    maxSteps: 6,
    tools,
  });

  console.log(text);
} finally {
  await Promise.all([search.close(), docs.close()]);
}

Calling a tool directly

You rarely need this — the loop runs execute for you — but callTool is there for ad-hoc invocation outside a model call:

const result = await mcp.callTool('scrape', { url: 'https://example.com' });
console.log(result); // joined text content, or the raw blocks if there is no text

If the server returns an error result, callTool throws, mirroring the execute behavior.

Notes

  • Always close() the client when you are finished — for stdio this terminates the spawned child process.
  • Namespacing only renames keys. It prefixes tool names so multiple servers can coexist; it does not change the underlying tool the model calls.
  • Edge vs Node: createMcpClient (/mcp) is fetch-only and runs anywhere; createStdioMcpClient (/mcp/stdio) requires Node. Importing /mcp/stdio in an edge runtime will fail.

See also

  • generateText — the buffered agentic loop these tools plug into.
  • streamChat — the streaming counterpart; pass the same tools.
  • Tool loop — how tool calls, parallel execution, and self-healing work.

On this page