npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ollama-agent-kit

v0.2.1

Published

Minimal Node.js library for building autonomous tool-calling AI agents on local LLMs with Ollama. A unified registry merges local (Zod) tools and MCP tools: define a tool once — your agent uses it, your MCP server exposes it.

Readme

ollama-agent-kit

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.

npm version license: MIT node >= 18

→ npm · → Examples


Table of contents

Why an agent on a local model

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.

⚠️ runShellCommandTool executes arbitrary commands with your process's permissions. Only enable it for models and prompts you trust — see A note on trust.

How it works

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

Install

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).

Built-in tools

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 |

A note on trust

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.

Defining a tool

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.
  • Tools are validated up front (name present, handler is a function, no duplicates) so a malformed tool fails immediately instead of mid-conversation.
  • MCP tools arrive already carrying JSON Schema (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.

One registry, two directions

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.

Configuring the agent

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.

Connecting to MCP servers

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).

Serving your tools as an MCP server

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 server.

Exposing it over HTTP

An MCP server speaks over one of two transports:

  • stdio — the server is a local process the client spawns; it talks over stdin/stdout. No network, no auth. Ideal on a Raspberry Pi next to your hardware. Use serveMcpStdio(tools).
  • Streamable HTTP — the server exposes an HTTP endpoint (https://your-host/mcp) any client can reach. This is what "online" means. Use serveMcpHttp(tools, { port }).

serveMcpHttp starts a plain Node HTTP server (no Express dependency) in stateless mode — a fresh MCP server is created per request, which suits low-traffic home deployments. To actually expose it:

  1. Run it on a machine with a public address (VPS, Raspberry Pi, ...), or a tunnel like Cloudflare Tunnel / ngrok for quick tests.
  2. Add HTTPS with a reverse proxy (Caddy, Nginx) in front of the port.
  3. Protect it. An open endpoint lets anyone with the URL run your tools. The simplest guard is a shared bearer token — pass authToken and clients must send Authorization: Bearer <token>. For full OAuth, use the SDK's auth middleware.
await serveMcpHttp([bulb], {
    port: 3000,
    authToken: process.env.MCP_TOKEN,   // require Authorization: Bearer <token>
})

| Option | Default | Description | | ------ | ------- | ----------- | | port | 3000 | Listen port | | host | all interfaces | Bind address | | path | /mcp | Endpoint path | | authToken | – | If set, require Authorization: Bearer <token> | | enableJsonResponse | false | Return plain JSON instead of an SSE stream | | name / version | kit defaults | Advertised MCP server identity | | includeAll | false | Register every tool, ignoring exposeMcp |

API

import {
    createAgent, createOllamaClient, defaultSystemPrompt,
    createRegistry, defineTool, validateTools, toOllamaTool, toHandlerMap,
    webSearchTool, webFetchTool,
    readFileTool, writeFileTool, editFileTool, listDirectoryTool, runShellCommandTool,
    McpClientManager, createMcpTools, loadMcpConfigFile, FileOAuthProvider, DEFAULT_CALLBACK_PORT,
    createMcpServer, serveMcpStdio, serveMcpHttp,
    AgentError, MaxTurnsError, ToolNotFoundError, RegistryError,
} from 'ollama-agent-kit'

FAQ

Why run an agent on a local model instead of a cloud API? Because of what the agent is allowed to do. Filesystem and shell access on a cloud-connected agent means your files and command output transit someone else's servers. On a local model everything stays on your hardware: full privacy, zero inference cost after setup, and it keeps working with no internet connection.

Which Ollama models support tool calling? Any model tagged with tool support in the Ollama librarygemma4:latest (the default), llama3.1, mistral, and others. Note that Ollama rejects a chat request that carries tools if the model doesn't support them, so pick a tool-capable model when the agent has tools configured.

Does it work fully offline? Yes. The agent loop, local tools and stdio MCP servers need no network beyond your Ollama instance. Only webSearchTool / webFetchTool and cloud models require an OLLAMA_API_KEY.

Can the same tool be used by the agent and published over MCP? Yes — that's the point of the registry. One defineTool definition with exposeAgent: true and exposeMcp: true serves both directions; the Zod schema is converted to JSON Schema wherever it's needed.

How does this compare to LangChain or the Vercel AI SDK? Much smaller scope, on purpose: two runtime dependencies (ollama, zod), one loop, no prompt/chain abstractions, Ollama only. If you need multi-provider support, streaming UI helpers or RAG pipelines, use those frameworks. If you want a small, readable agent loop that gives a local model real capabilities — files, shell, MCP in both directions — this is it.

Is it written in TypeScript? No — plain ESM JavaScript, no bundled type definitions. Zod gives you runtime validation of tool arguments; editors still infer a fair amount from the JSDoc and Zod schemas.

Where does MCP fit if I'm new to it? MCP is a standard protocol for exposing tools to AI clients. This kit is both a client (your agent can call tools from any MCP server) and a server (your tools become callable by Claude Desktop or any other MCP client).

Tests

npm test        # node --test

License

MIT niceunderground