Skip to content

AI & MCP Support

@matthesketh/utopia-ai provides adapter-based AI integration for UtopiaJS applications. Same factory pattern as email: createAI(adapter) returns a unified interface for chat completions, streaming, embeddings, and agentic tool loops. Supports OpenAI, Anthropic, Google Gemini, and Ollama out of the box.

The package also includes a full MCP (Model Context Protocol) implementation -- server, client, and HTTP handler -- for tool interop between AI agents and external services.

Quick Start

bash
pnpm add @matthesketh/utopia-ai openai
ts
import { createAI } from '@matthesketh/utopia-ai';
import { openaiAdapter } from '@matthesketh/utopia-ai/openai';

const ai = createAI(openaiAdapter({
  apiKey: process.env.OPENAI_API_KEY!,
}));

const response = await ai.chat({
  messages: [{ role: 'user', content: 'Hello!' }],
});

console.log(response.content);

Adapters

Each adapter is imported from a separate entry point so unused providers are never bundled.

ProviderImport PathConfig TypeDefault ModelFeatures
OpenAI@matthesketh/utopia-ai/openaiOpenAIConfiggpt-4ochat, stream, embeddings
Anthropic@matthesketh/utopia-ai/anthropicAnthropicConfigclaude-sonnet-4-5-20250929chat, stream
Google Gemini@matthesketh/utopia-ai/googleGoogleConfiggemini-2.0-flashchat, stream, embeddings
Ollama@matthesketh/utopia-ai/ollamaOllamaConfigllama3.2chat, stream, embeddings

All adapters lazy-load their provider SDK on first use. The Ollama adapter requires no external SDK -- it uses native fetch against the local Ollama API.

Config Types

ts
interface OpenAIConfig {
  apiKey: string;
  baseURL?: string;
  organization?: string;
  defaultModel?: string;
}

interface AnthropicConfig {
  apiKey: string;
  baseURL?: string;
  defaultModel?: string;
}

interface GoogleConfig {
  apiKey: string;
  defaultModel?: string;
}

interface OllamaConfig {
  baseURL?: string;       // default: 'http://localhost:11434'
  defaultModel?: string;
}

Adapter Example

ts
import { createAI } from '@matthesketh/utopia-ai';
import { anthropicAdapter } from '@matthesketh/utopia-ai/anthropic';

const ai = createAI(anthropicAdapter({
  apiKey: process.env.ANTHROPIC_API_KEY!,
}));

Chat API

ai.chat(request)

Send a chat completion request. Returns a Promise<ChatResponse>.

ChatRequest:

FieldTypeDescription
messagesChatMessage[]Conversation history (required)
modelstringOverride the adapter's default model
temperaturenumberSampling temperature
maxTokensnumberMaximum tokens to generate
topPnumberNucleus sampling parameter
stopstring[]Stop sequences
toolsToolDefinition[]Available tools for function calling
toolChoice'auto' | 'none' | 'required' | { name: string }Tool selection mode
extraRecord<string, unknown>Adapter-specific options passed through untouched

ChatResponse:

FieldTypeDescription
contentstringThe generated text
toolCallsToolCall[]Tool calls requested by the model (if any)
finishReason'stop' | 'tool_calls' | 'length' | 'error'Why generation stopped
usageTokenUsageToken counts (prompt, completion, total)
rawunknownRaw provider response for advanced use cases

ChatMessage:

FieldTypeDescription
role'system' | 'user' | 'assistant' | 'tool'Message role
contentstring | MessageContent | MessageContent[]Text, images, tool calls, or tool results
namestringOptional sender name

Content types include TextContent, ImageContent, ToolCallContent, and ToolResultContent.

Streaming

ai.stream(request)

Returns an AsyncIterable<ChatChunk> for incremental consumption.

ts
for await (const chunk of ai.stream({ messages })) {
  process.stdout.write(chunk.delta);
}

ChatChunk:

FieldTypeDescription
deltastringIncremental text delta
toolCallDeltaPartial<ToolCall> & { index?: number }Incremental tool call data
finishReasonChatResponse['finishReason']Set on the final chunk
usageTokenUsageSet on the final chunk

If the adapter does not implement stream(), the AI instance falls back to a single-chunk wrapper around chat().

streamSSE(res, stream, options?)

Stream ChatChunks as Server-Sent Events over an HTTP response. Use this in API routes to stream AI responses to the browser.

ts
// Server — API route
import { createAI, streamSSE } from '@matthesketh/utopia-ai';
import { openaiAdapter } from '@matthesketh/utopia-ai/openai';

const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY! }));

export async function POST(req: any, res: any) {
  const { messages } = JSON.parse(await readBody(req));
  const stream = ai.stream({ messages });
  await streamSSE(res, stream);
}

Sets Content-Type: text/event-stream and writes data: <JSON>\n\n for each chunk, ending with data: [DONE]\n\n.

parseSSEStream(response)

Browser-side parser. Takes a fetch Response and yields ChatChunk objects.

ts
// Browser
const res = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages }),
});

for await (const chunk of parseSSEStream(res)) {
  output.textContent += chunk.delta;
}

collectStream(stream)

Collect an AsyncIterable<ChatChunk> into a single string.

ts
const text = await collectStream(ai.stream({ messages }));

Tool Calling

ai.run(options)

Run an agentic tool-calling loop. Sends messages to the model, executes any tool calls via the provided handlers, appends results, and repeats until the model stops calling tools or the round limit is reached.

RunOptions:

FieldTypeDescription
messagesChatMessage[]Initial conversation (required)
toolsToolHandler[]Tool definitions + handler functions (required)
modelstringOverride default model
temperaturenumberSampling temperature
maxTokensnumberMax tokens per round
maxRoundsnumberMaximum tool-calling rounds (default: 10)
onToolCall(call: ToolCall, result: unknown) => voidCallback after each tool execution
extraRecord<string, unknown>Adapter-specific options

ToolHandler:

ts
interface ToolHandler {
  definition: ToolDefinition;
  handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
}

Example:

ts
const response = await ai.run({
  messages: [{ role: 'user', content: 'What is the weather in NYC?' }],
  tools: [{
    definition: {
      name: 'get_weather',
      description: 'Get the current weather for a city',
      parameters: {
        type: 'object',
        properties: {
          city: { type: 'string', description: 'City name' },
        },
        required: ['city'],
      },
    },
    handler: async ({ city }) => {
      // Call your weather API
      return { temperature: 72, condition: 'sunny' };
    },
  }],
  onToolCall: (call, result) => {
    console.log(`Tool ${call.name} returned:`, result);
  },
});

console.log(response.content); // "The weather in NYC is 72F and sunny."

When maxRounds is reached, a final request is made with toolChoice: 'none' to force a text response.

MCP Server

createMCPServer(config)

Create an MCP server that exposes tools, resources, and prompts via the JSON-RPC 2.0-based Model Context Protocol (version 2024-11-05).

MCPServerConfig:

FieldTypeDescription
namestringServer name (required)
versionstringServer version (default: '1.0.0')
toolsMCPToolHandler[]Tools to expose
resourcesMCPResourceHandler[]Resources to expose
promptsMCPPromptHandler[]Prompts to expose

Example:

ts
import { createMCPServer } from '@matthesketh/utopia-ai/mcp';

const mcp = createMCPServer({
  name: 'my-app',
  version: '1.0.0',
  tools: [{
    definition: {
      name: 'get_user',
      description: 'Look up a user by ID',
      inputSchema: {
        type: 'object',
        properties: { id: { type: 'string', description: 'User ID' } },
        required: ['id'],
      },
    },
    handler: async ({ id }) => ({
      content: [{ type: 'text', text: JSON.stringify({ id, name: 'Alice' }) }],
    }),
  }],
  resources: [{
    definition: {
      uri: 'config://app',
      name: 'App Config',
      description: 'Application configuration',
      mimeType: 'application/json',
    },
    handler: async (uri) => ({
      uri,
      text: JSON.stringify({ version: '1.0.0' }),
      mimeType: 'application/json',
    }),
  }],
});

The server handles the following JSON-RPC methods: initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get, and ping.

Resource URIs support template matching (e.g. users://{id} matches users://123).

MCP Client

createMCPClient(config)

HTTP client that connects to an MCP server.

MCPClientConfig:

FieldTypeDescription
urlstringURL of the MCP server (required)
headersRecord<string, string>Optional headers for authentication

MCPClient methods:

MethodDescription
initialize()Handshake with the server, get capabilities
listTools()List available tools
callTool(name, args?)Call a tool by name
listResources()List available resources
readResource(uri)Read a resource by URI
listPrompts()List available prompts
getPrompt(name, args?)Get a prompt with arguments
toToolHandlers()Convert server tools to ToolHandler[] for ai.run()

Bridge Pattern: MCP + AI Tool Loop

The toToolHandlers() method bridges MCP tools directly into the ai.run() tool-calling loop. This is the core interop pattern:

ts
import { createAI } from '@matthesketh/utopia-ai';
import { openaiAdapter } from '@matthesketh/utopia-ai/openai';
import { createMCPClient } from '@matthesketh/utopia-ai/mcp';

// Connect to an MCP server
const client = createMCPClient({ url: 'http://localhost:3001/mcp' });
await client.initialize();

// Bridge MCP tools into AI
const ai = createAI(openaiAdapter({ apiKey: process.env.OPENAI_API_KEY! }));
const toolHandlers = await client.toToolHandlers();

const response = await ai.run({
  messages: [{ role: 'user', content: 'Look up user 123' }],
  tools: toolHandlers,
});

MCP HTTP Handler

createMCPHandler(server)

Create a Node.js HTTP handler for an MCP server. Supports three transports:

MethodPathDescription
POST/JSON-RPC request/response
GET/sseServer-Sent Events (Streamable HTTP)
POST/sseJSON-RPC over SSE

CORS headers are set automatically. Use as a standalone server or as Express middleware.

ts
import http from 'node:http';
import { createMCPServer, createMCPHandler } from '@matthesketh/utopia-ai/mcp';

const mcp = createMCPServer({
  name: 'my-app',
  tools: [/* ... */],
});

const handler = createMCPHandler(mcp);

// Standalone
http.createServer(handler).listen(3001);

// Or as Express middleware
app.use('/mcp', handler);

Middleware & Hooks

createAI accepts an options object with hooks and retry configuration.

ts
const ai = createAI(adapter, {
  hooks: {
    onBeforeChat: (request) => {
      // Modify request before sending
      return { ...request, temperature: 0.7 };
    },
    onAfterChat: (response, request) => {
      // Log or modify response
      console.log(`Used ${response.usage?.totalTokens} tokens`);
      return response;
    },
    onError: (error, context) => {
      console.error(`Error in ${context.method}:`, error);
    },
  },
  retry: {
    maxRetries: 3,
    baseDelay: 1000,
  },
});

AIHooks:

HookSignatureDescription
onBeforeChat(request: ChatRequest) => ChatRequest | Promise<ChatRequest>Modify request before sending. Runs for chat(), stream(), and each round in run().
onAfterChat(response: ChatResponse, request: ChatRequest) => ChatResponse | Promise<ChatResponse>Modify response after receiving. Runs for chat() and each round in run().
onError(error: Error, context: { method: string; request?: ChatRequest }) => voidCalled on any adapter error.

Retry

Retry configuration for chat() and run() (streaming is not retried).

RetryConfig:

FieldTypeDefaultDescription
maxRetriesnumber0Maximum retry attempts
baseDelaynumber1000Base delay in ms (doubles each attempt)
shouldRetry(error: Error) => booleanBuilt-inCustom retry predicate

Default retry predicate retries on network errors, timeouts, and HTTP status codes 429, 500, 502, 503.

Architecture

  Application Code
        |
   createAI(adapter, options?)
        |
   ┌────┴────┐
   │   AI    │── hooks (onBeforeChat, onAfterChat, onError)
   │ instance│── retry (exponential backoff)
   └────┬────┘

   ┌────┴──────────────────────────────────┐
   │            AIAdapter                   │
   ├──── openaiAdapter(config)              │
   ├──── anthropicAdapter(config)           │
   ├──── googleAdapter(config)              │
   └──── ollamaAdapter(config)              │
              │                             │
         Provider SDK / HTTP API            │

   ┌────────────────────────────────────────┘

   │  MCP Interop

   │  createMCPServer(config)          createMCPClient(config)
   │       │                                │
   │  handleRequest(jsonRpc)           rpc(method, params)
   │       │                                │
   │  createMCPHandler(server)         toToolHandlers()
   │       │                                │
   │  HTTP POST / SSE                  ai.run({ tools })
   │       │                                │
   └───────┴──── JSON-RPC 2.0 ─────────────┘

Type Reference

All types are exported from the main @matthesketh/utopia-ai entry point. MCP types are exported from @matthesketh/utopia-ai/mcp.

Core types (@matthesketh/utopia-ai):

TypeDescription
AIAI instance interface (chat, stream, embeddings, run)
AIAdapterAdapter interface (implement to add a provider)
CreateAIOptionsOptions for createAI (hooks, retry)
ChatRequestChat completion request
ChatResponseChat completion response
ChatChunkStreaming chunk
ChatMessageConversation message
MessageRole'system' | 'user' | 'assistant' | 'tool'
MessageContentUnion of content types
TextContent{ type: 'text', text: string }
ImageContent{ type: 'image', source: string, mediaType?: string }
ToolCallContent{ type: 'tool_call', id, name, arguments }
ToolResultContent{ type: 'tool_result', id, content, isError? }
ToolDefinitionTool name, description, and JSON Schema parameters
ToolCallA tool call from the model (id, name, arguments)
ToolHandlerTool definition + handler function for ai.run()
RunOptionsOptions for ai.run()
TokenUsage{ promptTokens, completionTokens, totalTokens }
JsonSchemaJSON Schema type for tool parameters
EmbeddingRequestEmbedding generation request
EmbeddingResponseEmbedding generation response
OpenAIConfigOpenAI adapter configuration
AnthropicConfigAnthropic adapter configuration
GoogleConfigGoogle Gemini adapter configuration
OllamaConfigOllama adapter configuration
AIHooksMiddleware hooks
RetryConfigRetry configuration

MCP types (@matthesketh/utopia-ai/mcp):

TypeDescription
MCPServerServer instance (handleRequest, info)
MCPClientClient instance (all RPC methods + toToolHandlers)
MCPServerConfigServer configuration (name, version, tools, resources, prompts)
MCPClientConfigClient configuration (url, headers)
MCPServerInfoServer info (name, version, protocolVersion)
MCPToolDefinitionTool schema (name, description, inputSchema)
MCPToolHandlerTool definition + handler
MCPToolResultTool execution result (content array, isError)
MCPContentContent block (text, image, or resource)
MCPResourceDefinitionResource schema (uri, name, description, mimeType)
MCPResourceHandlerResource definition + handler
MCPResourceContentResource content (uri, text/blob, mimeType)
MCPPromptDefinitionPrompt schema (name, description, arguments)
MCPPromptArgumentPrompt argument (name, description, required)
MCPPromptHandlerPrompt definition + handler
MCPPromptResultPrompt result (description, messages)
JsonRpcRequestJSON-RPC 2.0 request
JsonRpcResponseJSON-RPC 2.0 response
JsonRpcErrorJSON-RPC 2.0 error
JsonRpcNotificationJSON-RPC 2.0 notification

Released under the MIT Licence.