nanoagent

MCP.Pizza Chef: hbbio

NanoAgent is a minimalist TypeScript client framework designed to run LLM-powered AI agents with no runtime dependencies except Bun. It focuses solely on the control loop of agent execution, delegating retrieval, vector search, and external calls to MCP-compatible tools. This results in a tiny, transparent, and auditable core that simplifies building AI agents while leveraging the Model Context Protocol for extensibility and interoperability.

Use This MCP client To

Run LLM-powered AI agents with minimal setup and dependencies Implement custom control loops for AI agents in TypeScript Integrate with MCP-compatible tools for retrieval and external API calls Build transparent and auditable AI agent runtimes Use local or cloud LLM models interchangeably Develop lightweight AI assistants for web or server environments

README

NanoAgent Logo

NanoAgent

NanoAgent is a micro‑framework (≈ 1 kLOC) for running LLM‑powered agents in pure TypeScript with zero runtime dependencies outside of bun. You only need your favorite chat models: OpenAI, or a local engine like Ollama.

Why another agent runtime?
Model Context Protocol (MCP) is bringing the opportunity to de-clutter agent frameworks. Most features should be tools, retrieval sources, etc. in a standard JSON envelope, then hand that context to any model. NanoAgent focuses on one job: the control loop and leaves RAG, vector search, databases and cloud calls to MCP‑compatible tools. The result is a tiny, transparent core you can audit in an afternoon.

Note that this projects implements a few extensions over the current specifications of MCP and/or tool calling.

Highlights

  • Pure functions, repeatable runs – every step returns a new AgentState; nothing mutates in place.
  • Deterministic steppingstepAgent drives exactly one model call → tool call → state update.
  • Built‑in stuck detection – empty answers, duplicate assistant messages or missing tool output trigger a recovery hook.
  • Halting reasonsawait_user, tool_error, done, stopped.
  • Multi‑stage workflows – chain Sequence objects for wizard‑style flows.
  • JSON‑Schema tools – validate inputs at the boundary, patch memory with pure lambdas.
  • No hidden packages – just TypeScript.
  • First‑class Bun support – fast test runner, edge‑ready.

Quick tour

import {
  type AgentContext,
  type AgentState,
  type ChatMemory,
  ChatModel,
  Llama32,
  SystemMessage,
  ToolRegistry,
  UserMessage,
  content,
  lastMessageIncludes,
  loopAgent,
  tool,
} from "@hbbio/nanoagent";

// 1) a trivial tool
const echo = tool(
  "echo",
  "Echo user input back in uppercase",
  {
    type: "object",
    properties: { txt: { type: "string" } },
    required: ["txt"],
  },
  async ({ txt }: { txt: string }) => content(txt.toUpperCase()),
);

// 2) agent context
const ctx: AgentContext<ChatMemory> = {
  registry: new ToolRegistry({ echo }),
  isFinal: lastMessageIncludes("HELLO"),
};

// 3) initial state
const init: AgentState<ChatMemory> = {
  model: new ChatModel(Llama32),
  messages: [
    SystemMessage(
      "You must call the `echo` tool once. Reply very concisely and NEVER ASK any further question to the user!",
    ),
    UserMessage(
      "Call the tool with the parameter `hello` and tell me what is the response",
    ),
  ],
};

// 4) run and display the whole conversation
const done = await loopAgent(ctx, init);
console.log(done.messages);

Run it with Bun:

bun run examples/echo.ts

Concepts in 60 seconds

Concept What it holds
AgentState Immutable snapshot: model driver, messages, memory, halt
AgentContext Pure hooks: goal test, tool registry, controller, etc.
stepAgent One transition – may call the model and at most one tool
loopAgent While‑loop around stepAgent until a halt condition
Sequence Wrapper that chains multi‑stage flows

Memory is plain JSON. Tools may patch it by returning { memPatch(state)‐>newState }.

Multi‑stage workflows

const seq1 = new Sequence(ctxStage1, state1, { maxSteps: 8 });
const { final, history } = await runWorkflow(seq1);

Each stage may produce a fresh context and state; user input handling can be preserved across stages.

MCP integration (client & server)

NanoAgent ships a tiny MCP server helper (serveMCP) and an MCP client (MCPClient). Your tools can therefore live outside the agent process—behind an HTTP endpoint—yet feel local.

Why MCP?

  • RAG anywhere – retrieval can run on an edge function, a GPU pod, or a browser worker.
  • Horizontal scaling – tools are stateless HTTP handlers; use normal infra.
  • Polyglot – heavy lifting in Go, Python or Rust without bloating the TS runtime.

Running a server

import { ToolRegistry, serveMCP, tool, content } from "@hbbio/nanoagent";

const tools = {
  echo: tool(
    "echo",
    "Echo input back",
    {
      type: "object",
      properties: { text: { type: "string" } },
      required: ["text"],
    },
    async ({ text }) => content(`Echo: ${text}`),
  ),
};

serveMCP(new ToolRegistry(tools), 3123); // → http://localhost:3123/v1/…

Calling remote tools from an agent

import { MCPClient, ToolRegistry } from "@hbbio/nanoagent";

const mcp = new MCPClient("http://localhost:3123");
const echoT = await mcp.registeredTool("echo");

const ctx = {
  registry: new ToolRegistry({ echoT }),
  /* … other AgentContext props … */
};

MCPClient provides the following features:

  • listTools(): Discover server capabilities (with a default 5‑minute cache)
  • tool(name): Fetch a single tool
  • callTool(name, input, memory?): Plain HTTP tool call
  • registeredTool(name): Wrap a remote tool so agents can call it seamlessly

Installation

bun add nanoagent   # or:  npm i nanoagent  pnpm add nanoagent  yarn add nanoagent

The package is published as ES 2020 modules with type‑definitions included.

Using OpenAI or Ollama

OpenAI

export CHATGPT_KEY=...

And then create instances with:

import { ChatModel, ChatGPT4o } from "@hbbio/nanoagent";
const model = new ChatModel(ChatGPT4o);

or one of the predefined model names. Call any present or future model using chatgpt("name").

Ollama

By default Ollama host is http://localhost:11434, but you can optionally define another host:

export OLLAMA_HOST=...

Then run any model, such as:

import { ChatModel, MistralSmall } from "@hbbio/nanoagent";
const model = new ChatModel(MistralSmall);

Debugging

Pass { debug: true } to stepAgent, loopAgent or Sequence. You will see:

STEP id=- msgs=3 last=assistant halted=-
💬 { role: "assistant", … }
💾 memory keys []

Provide your own logger via options.logger.

Contributing and License

Contributions are welcome: Make sure that all tests pass and that coverage includes your new code and feel free to submit PRs.

Please follow the coding guidelines and keep the project free of extra dependencies.

Written by Henri Binsztok and released under the MIT license.

nanoagent FAQ

How does NanoAgent handle dependencies?
NanoAgent is built in pure TypeScript with zero runtime dependencies outside of Bun, ensuring a lightweight and minimal setup.
Can NanoAgent work with different LLM providers?
Yes, NanoAgent supports any chat model including OpenAI, Ollama, and others compatible with MCP.
What is the main focus of NanoAgent compared to other agent frameworks?
NanoAgent focuses exclusively on the control loop of AI agents, leaving retrieval, vector search, and external calls to MCP-compatible tools.
Is NanoAgent easy to audit and understand?
Yes, its tiny core (~1k lines of code) is designed to be transparent and auditable within an afternoon.
Does NanoAgent implement any extensions to MCP?
Yes, it includes a few extensions over current MCP specifications and tool calling to enhance functionality.
What environments does NanoAgent support?
NanoAgent runs in environments supporting Bun and TypeScript, suitable for both local and server-side applications.
How does NanoAgent integrate with external data sources?
It relies on MCP-compatible tools to provide retrieval, vector search, and cloud API calls, keeping the core focused on control logic.
Can I use NanoAgent with local LLM engines?
Yes, NanoAgent supports local engines like Ollama as well as cloud-based models.