Skip to content

In-process client — overview

createInProcessClient builds an ordinary Client with no baseUrl, no apiKey and no style. Instead you pass the model itself:

you supply receives returns streams?
generate(request) (every port) messages, tools, model, body (the assembled request for this turn) one assistant message: { content } to finish, or { toolCalls } to ask for tools no: stream / runStream is refused with an error naming why
generateStream(request, emit) (every port since 0.25.0: Go GenerateStream(ctx, …), Python generate_stream, Elixir :generate_stream, Clojure :generate-stream, …) the same request, plus an emit function (Go also gets the run’s ctx) the same one message yes: every emit({type:"text", delta}) becomes a stream delta as it happens (the deltas must add up to content); thought, tool_call and tool_result events pass through to the stream consumer and are never executed

Return toolCalls and toolnexus executes them against the toolkit, appends the results and calls you again. Return content and the run is over. The contract is the same one documented in Run a model in-process, which also covers retries (in-process failures are final by default) and the per-port request shapes.

A “model” that asks for the add tool on its first turn and streams the answer on the second. This is the whole integration; swap the body of the generate function for your real model.

main.mjs
import { createInProcessClient, createToolkit, defineTool } from "toolnexus"
const add = defineTool({
name: "add", description: "Add two numbers.",
inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
run: ({ a, b }) => `${a + b}`,
})
// Your "model": first turn asks for a tool, second turn streams the answer.
const client = createInProcessClient({
model: "my-model",
generateStream: async (req, emit) => {
const last = req.messages.at(-1)
if (last.role !== "tool") return { toolCalls: [{ name: "add", arguments: { a: 2, b: 3 } }] }
const words = ["The ", "sum ", "is ", String(last.content), "."]
for (const w of words) emit({ type: "text", delta: w }) // deltas must add up to content
return { content: words.join("") }
},
})
const toolkit = await createToolkit({ builtins: false, extraTools: [add] })
for await (const ev of client.stream("What is 2 + 3?", { toolkit })) {
if (ev.type === "text") process.stdout.write(ev.delta)
else if (ev.type === "tool_call") console.log(`[calling ${ev.name}]`)
}
console.log() // The sum is 5.

There are two very different things you can put behind generate, and they split the tool loop differently:

who runs the tools what generate returns recipe
A plain model (weights in-process, a CLI asked for one reply) toolnexus: the model asks, the client executes toolCalls or content One-shot CLI, Run a model in-process
An agent (Claude Code, Copilot, opencode… over ACP) the agent: it has its own loop, and reaches your tools over MCP only content: the agent’s final text ACP agent

An agent already has a tool loop of its own. You do not want two loops fighting over the same call, so in the agent case you give the agent your toolkit as an MCP server, run the toolnexus client with an empty toolkit, and let generate return the agent’s answer.

Give your toolkit to an external agent: asMcpServer

Section titled “Give your toolkit to an external agent: asMcpServer”

Any toolkit, whatever it aggregates (MCP servers, skills, native functions, HTTP tools, builtins, A2A agents), can be handed to an agent you launch as one MCP server with toolkit.asMcpServer() (SPEC §8C): Go AsMCPServer(nil), Python as_mcp_server(), Java asMcpServer(), C# AsMcpServer(), Elixir Toolnexus.Toolkit.as_mcp_server/2, Clojure toolnexus.serve/as-mcp-server, Rust as_mcp_server (crate feature serve). It returns {url, token, close}:

  • it listens on 127.0.0.1:<ephemeral> only, and url is http://127.0.0.1:<port>/mcp;
  • every request must carry Authorization: Bearer <token> (a fresh 64-hex token per call), so other local processes cannot call your tools; a missing or wrong token is 401;
  • POST /mcp is the stateless streamable-HTTP endpoint; any other method is 405 (there is no server stream to hold open), any other path 404;
  • the optional config is the usual {name, version, tools}, and tools narrows what is exposed.

You build whatever mcpServers entry your agent accepts; for an ACP agent that is the entry below.

serve.mjs
import { createToolkit, defineTool } from "toolnexus"
const add = defineTool({
name: "add", description: "Add two numbers.",
inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
run: ({ a, b }) => `${a + b}`,
})
// Any toolkit (MCP servers, skills, native, HTTP, builtins) can be served to a local agent.
const toolkit = await createToolkit({ builtins: false, extraTools: [add] })
const server = await toolkit.asMcpServer() // { tools: [...] } narrows what is exposed
console.log("MCP endpoint:", server.url) // http://127.0.0.1:<port>/mcp, POST only
// Every request needs the bearer token; this is the entry an ACP agent takes in session/new.
const entry = { type: "http", name: "toolnexus", url: server.url,
headers: [{ name: "Authorization", value: `Bearer ${server.token}` }] }
const res = await fetch(entry.url, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", Authorization: entry.headers[0].value },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
})
console.log("tools/list:", res.status)
await server.close()

To serve a toolkit to the network rather than to one local agent, use toolkit.serve(addr, { mcp }) (owns a port, no token) or mount toolkit.mcpHandler(cfg) in your own HTTP server, behind your own auth: see serve MCP and mcpHandler.

  • ACP agent recipe: drive any ACP agent with loadACP, your toolkit attached through asMcpServer, streaming its text.
  • One-shot CLI recipe: one CLI process per turn (claude -p, codex exec), toolnexus runs the tools.
  • Extend it: add your own agent, isolation, model selection, permissions.