Loading repository data…
Loading repository data…
niceunderground / repository
Minimal Node.js agent loop for Ollama: local LLMs that read/write/edit files, run shell commands, and call MCP tools. MCP client and server, two dependencies, fully offline.
Give a local LLM hands. A minimal Node.js agent loop for Ollama that lets the model read, write and edit files, run shell commands, and call tools from any MCP server — entirely on your machine.
An agent is only useful when it can act: modify files, run commands, touch the system it lives on. Nobody in their right mind hands that power to a cloud API. With a model running on your own hardware the equation changes: prompts, file contents and command output never leave your machine, inference costs nothing after setup, and everything keeps working offline.
ollama-agent-kit is built for exactly that scenario. Two runtime dependencies (ollama, zod), no chains, no prompt abstractions, no framework tax — small enough to run comfortably next to the model on a Raspberry Pi or a home server, readable enough to audit in an afternoon.
import { createAgent, readFileTool, writeFileTool, editFileTool, runShellCommandTool } from 'ollama-agent-kit'
const agent = createAgent({
model: 'gemma4:latest',
tools: [readFileTool, writeFileTool, editFileTool, runShellCommandTool],
})
await agent.run('Read package.json, bump the patch version, and run the tests')
That's a fully autonomous loop on a local model: it reads the file, edits it, runs the command, checks the output — no API key, no network, no data leaving the LAN.
⚠️
runShellCommandToolexecutes arbitrary commands with your process's permissions. Only enable it for models and prompts you trust — see A note on trust.
The core is intentionally small: a chat loop that sends the conversation to Ollama, executes any requested tool calls in parallel, feeds the results back, and stops when the model produces a final answer (or maxTurns is reached).
createAgent(config).run(prompt)
│
├── resolves tools: your local registry + MCP servers
│
└── loop (up to maxTurns):
├── ollama.chat() with the merged tool list
├── no tool calls? → return the final answer
└── run all tool calls in parallel → feed results back → next turn
npm install ollama-agent-kit
# MCP support is optional — install the SDK only if you connect to MCP servers:
npm install @modelcontextprotocol/sdk
Requires Node.js 18+ and a reachable Ollama instance with a tool-calling model. webSearchTool / webFetchTool need an OLLAMA_API_KEY (Ollama Cloud).
A handful of tool factories ship with the kit. Pass them bare in tools (they reuse the agent's client) or call them with options. They split into two groups: tools that act on your machine and tools that fetch information.
Act on your machine
| Tool | Purpose | Notes |
|---|---|---|
writeFileTool | Create/overwrite a file | Writes to the local filesystem |
editFileTool | Replace an exact string occurrence in a file (all occurrences with replaceAll) | Modifies files in place |
runShellCommandTool | Execute a shell command, returns stdout/stderr | Runs arbitrary commands with your process's permissions — only enable it for models and prompts you trust. |
Fetch information
| Tool | Purpose | Notes |
|---|---|---|
readFileTool | Read a file's full text content | Local filesystem |
listDirectoryTool | List a directory's entries | Local filesystem |
webSearchTool | Web search via the Ollama web API | Needs OLLAMA_API_KEY |
webFetchTool | Fetch the content of a URL | Needs OLLAMA_API_KEY |
The "act" tools give an autonomous loop real power over your machine: it can overwrite files and run shell commands without asking. That's the point — but scope what you pass in. On untrusted input, prefer the read-only tools, run the agent in a sandbox/container, or drop runShellCommandTool. The advantage of a local model is that the blast radius is a machine you own, not an account on someone else's cloud.
One object, a Zod schema, a handler. Anything on your system with an API — a light, a script, a device — becomes something the model can operate.
import { defineTool } from 'ollama-agent-kit'
import { z } from 'zod'
export const bulb = defineTool({
name: 'bulb',
description: 'Control a smart light: turn it on/off and set brightness.',
parameters: z.object({
room: z.string().describe('Room name, e.g. "studio"'),
on: z.boolean().optional(),
brightness: z.number().min(0).max(100).optional(),
}),
exposeAgent: true, // available to the agent loop
exposeMcp: true, // and publishable by your own MCP server
handler: async ({ room, on, brightness }) => setLight(room, { on, brightness }),
})
parameters (Zod) is converted to JSON Schema automatically for the Ollama API.rawParameters), so both kinds live in one registry.See examples/home-lights.js for the full home-automation demo — an agent controlling real lights from a local model, the kind of thing that runs happily on a Raspberry Pi.
The registry is what makes the tool definition above reusable: define a tool once — your agent uses it, and your MCP server exposes it. exposeAgent / exposeMcp decide where it shows up; the same definition serves both the agent loop and your own MCP server, and external MCP tools land in the same registry next to your local ones.
createAgent injects the Ollama client, model and tools; .run() executes a prompt.
const agent = createAgent({
host: 'http://localhost:11434', // optional (this is the default) — or a pre-built `client`
apiKey: process.env.OLLAMA_API_KEY, // one key for cloud models + web tools
model: 'gemma4:latest',
tools: [bulb, webSearchTool], // bare tool factories reuse the agent's client/apiKey
onTurn: ({ turn }) => console.log(`turn ${turn}`),
onToolCall: ({ name, result }) => console.log(`→ ${name}`, result),
})
await agent.run('Turn on the studio light and tell me the weather in Naples')
createAgent(config) options| Option | Default | Description |
|---|---|---|
host | http://localhost:11434 | Ollama host (ignored if client is given) |
apiKey | – | Ollama API key (ignored if client is given) |
fetch | – | Custom fetch, injected instead of patching globalThis |
client | – | A pre-built Ollama client (overrides host/apiKey/fetch) |
model | gemma4:latest | Any Ollama model with tool-calling support |
think | unset | Ollama thinking effort ('low'|'medium'|'high'). Only sent when set, so non-thinking models work out of the box |
temperature | 0.8 | Sampling temperature |
systemPrompt | built-in | System prompt for the agent |
maxTurns | 10 | Safety cap on loop iterations (throws MaxTurnsError if exceeded) |
tools | [] | Local tools available to the agent. An entry can also be a factory (ctx) => Tool called with { client, apiKey, host } — so webSearchTool / webFetchTool can be passed bare and reuse the agent's client |
mcp | null | McpClientManager | async () => tools | tools[] | falsy |
onTurn | no-op | ({ turn, message, messages }) => void after each model turn |
onToolCall | no-op | ({ name, arguments, result, error, turn }) => void per tool call |
onFinal | no-op | ({ content, turns, messages }) => void on the final answer |
run(prompt, { model, tools }) accepts per-run overrides and returns the model's final answer.
External MCP servers (stdio or HTTP) are connected by McpClientManager. Their tools are loaded at run time, prefixed with the server name (filesystem__read_file), and merged into the same registry — local names win on collision.
import { createAgent, McpClientManager } from 'ollama-agent-kit'
const mcp = new McpClientManager({
servers: {
filesystem: {
enabled: true,
type: 'stdio',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
},
},
})
const agent = createAgent({ mcp })
const answer = await agent.run('List the files in the current directory')
await mcp.close()
HTTP servers behind OAuth are supported via FileOAuthProvider (tokens persisted per-server under .mcp-auth/). Log in once with examples/mcp-auth.js. You can also load a { servers: {...} } JSON file with loadMcpConfigFile(path).
The reciprocal of createAgent: hand the same tool definitions to an MCP server and every tool flagged exposeMcp: true becomes callable by any MCP client (Claude Desktop, another agent, ...). The Zod schema is converted to JSON Schema for you.
import { createMcpServer, serveMcpStdio, serveMcpHttp } from 'ollama-agent-kit'
// Just build the configured server (register onto any transport yourself):
const server = await createMcpServer([bulb, add]) // only exposeMcp tools are registered
// Or serve it directly, one line:
await serveMcpStdio([bulb, add]) // local process, spawned over stdio
await serveMcpHttp([bulb, add], { port: 3000 }) // online at http://localhost:3000/mcp
See examples/serve-mcp.js for the full, runnable