- MCP Server Tools
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type Generation
- MCP Client Input
- MCP Server Tools: Advanced
- MCP Events
- MCP AppsUpdated
- Serve Tools over HTTP
- Serve Resources and Prompts
- Ask for Input
- Draft Text from a Tool
- MCP Server TasksUpdated
- MCP Server Auth
- MCP Server Sessions
- MCP Server on stdio
Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clientsUpdated
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- EventsUpdated
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files APIUpdated
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-ShakingUpdated
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Effect Tools
Your tools need your database and your other Effect services. A core server tool is an async function, so it cannot read those services, and its errors are not typed.
AI.serverTool makes a tool from a function that returns an Effect. The services of that Effect become requirements of the AI.chat stream. You provide them with a Layer.
This page uses the server route from Effect. The client does not change.
1. Define the service and its Layer#
import { Context, Data, Effect, Layer } from 'effect'
export class OrderNotFound extends Data.TaggedError('OrderNotFound')<{
readonly id: string
}> {}
export class Db extends Context.Service<
Db,
{
readonly findOrder: (
id: string,
) => Effect.Effect<{ status: string }, OrderNotFound>
}
>()('Db') {}
// A fixed order. Read from your real database here.
export const DbLive = Layer.succeed(Db, {
findOrder: (id) =>
id === 'A-100'
? Effect.succeed({ status: 'shipped' })
: Effect.fail(new OrderNotFound({ id })),
})2. Write the tool as an Effect#
import { toolDefinition } from '@tanstack/ai'
import * as AI from '@tanstack/ai-effect'
import { Effect, Schema } from 'effect'
import { Db } from './db'
const getOrderDef = toolDefinition({
name: 'get_order',
description: 'Get the status of an order',
inputSchema: AI.toolSchema(Schema.Struct({ id: Schema.String })),
outputSchema: AI.toolSchema(Schema.Struct({ status: Schema.String })),
})
export const getOrder = AI.serverTool(getOrderDef, ({ id }) =>
Effect.gen(function* () {
const db = yield* Db
return yield* db.findOrder(id)
}),
)AI.toolSchema turns an Effect Schema into a tool schema. It closes every object (additionalProperties: false), so OpenAI can check the tool arguments in strict mode.
AI.serverTool takes a tool definition and a function. The function gets the parsed input and returns an Effect of the output. The type of getOrder keeps Db as a requirement.
3. Pass the tool and provide the Layer#
import { chatParamsFromRequest, toServerSentEventsResponse } from '@tanstack/ai'
import * as AI from '@tanstack/ai-effect'
import { openaiText } from '@tanstack/ai-openai'
import { Stream } from 'effect'
import { DbLive } from './db'
import { getOrder } from './tools'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = AI.chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
tools: [getOrder],
}).pipe(Stream.provide(DbLive))
return toServerSentEventsResponse(Stream.toAsyncIterable(stream))
}The stream needs Db until you provide DbLive. If you remove Stream.provide(DbLive), Stream.toAsyncIterable does not type-check.
Ask "Where is order A-100?". The model calls get_order, your Effect reads Db, and the reply says the order shipped.
When the tool fails#
If the tool Effect fails, the model gets a tool error:
- The error message goes back to the model as the tool result. The model can tell the user or try again.
- The run continues. The stream does not fail.
- A typed failure (OrderNotFound) and a defect (Effect.die) both work this way.
Ask for order B-200. findOrder fails with OrderNotFound. The model gets the tool error and answers the user.
To send the model a normal result instead, catch the error in the tool. For example, Effect.catchTag('OrderNotFound', () => Effect.succeed({ status: 'unknown' })).
When the run stops#
The tool Effect runs with the services of the AI.chat stream. It stops when the run stops:
- The browser closes the request.
- Your code stops the stream early, for example with Stream.take.
- Your code interrupts the fiber that runs the stream.
In each case, the running tool fiber is interrupted. Its finalizers run, so Effect.acquireRelease and Effect.onInterrupt clean up as usual.
Mix with other tools#
The tools array can also hold core tools, for example toolDefinition(...).server(...) or client tools. Only the AI.serverTool values add services to the stream.
The full app is in the Effect Chat tutorial. For images, embeddings, and other activities, see Effect activities.