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

nanoapps

v0.1.0

Published

``` ███╗ ██╗ █████╗ ███╗ ██╗ ██████╗ █████╗ ██████╗ ██████╗ ███████╗ ████╗ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝ ██╔██╗ ██║███████║██╔██╗ ██║██║ ██║███████║██████╔╝██████╔╝███████╗ ██║╚██╗██║██╔══██║██║╚██╗██║██║ ██║

Downloads

224

Readme

 ███╗   ██╗ █████╗ ███╗   ██╗ ██████╗  █████╗ ██████╗ ██████╗ ███████╗
 ████╗  ██║██╔══██╗████╗  ██║██╔═══██╗██╔══██╗██╔══██╗██╔══██╗██╔════╝
 ██╔██╗ ██║███████║██╔██╗ ██║██║   ██║███████║██████╔╝██████╔╝███████╗
 ██║╚██╗██║██╔══██║██║╚██╗██║██║   ██║██╔══██║██╔═══╝ ██╔═══╝ ╚════██║
 ██║ ╚████║██║  ██║██║ ╚████║╚██████╔╝██║  ██║██║     ██║     ███████║
 ╚═╝  ╚═══╝╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝     ╚══════╝

Build MCP Apps servers with self-contained tools and interactive HTML UIs that render inline in MCP-compatible clients. Define your tools with defineApp(), build them with the CLI, and serve them on a single /mcp endpoint supporting both stateful and stateless connections.

Install

bun add nanoapps zod

zod is a required peer dependency (used for inputSchema definitions). If your apps include a UI, you'll also need the MCP Apps client SDK for host communication (theme, messaging, etc.):

bun add @modelcontextprotocol/ext-apps

If you're using the Hono adapter (nanoapps/hono), install Hono as well:

bun add hono

Define an app

Each app is a tool.ts file that default-exports a defineApp() call. There are three app types:

Single tool with UI

The simplest app — one tool backed by one UI.

// src/apps/greet/tool.ts
import { defineApp } from "nanoapps";
import { z } from "zod";

export default defineApp({
  name: "greet",
  title: "Greeter",
  description: "Greets someone by name.",
  inputSchema: { name: z.string() },
  handler: async (args) => `Hello, ${args.name}!`,
});

Add a ui/ folder alongside tool.ts with index.html, app.ts, and styles.css for an interactive UI. UI code uses @modelcontextprotocol/ext-apps to communicate with the MCP host — see src/apps/hello/ui/ for a working example.

Tool-only (no UI)

Omit the ui/ directory — the tool registers without a resource:

// src/apps/guid/tool.ts
import { defineApp } from "nanoapps";

export default defineApp({
  name: "guid",
  title: "GUID Generator",
  description: "Generate a random GUID/UUID v4.",
  handler: async () => crypto.randomUUID(),
});

Multi-tool

Multiple tools sharing a single UI resource. Pass a tools array instead of description/handler. Each tool name is auto-prefixed with the app name:

// src/apps/kanban/tool.ts
import { defineApp } from "nanoapps";
import { z } from "zod";

export default defineApp({
  name: "kanban",
  title: "Kanban Board",
  tools: [
    {
      name: "list_tasks",
      description: "List all tasks on the board",
      handler: async () => JSON.stringify({ board: {} }),
    },
    {
      name: "add_task",
      description: "Add a new task",
      inputSchema: { title: z.string() },
      handler: async (args) => JSON.stringify({ added: args.title }),
    },
  ],
});
// Registers: kanban_list_tasks, kanban_add_task

Multi-tool apps work with or without a UI. See src/apps/kanban/ for a full working example.

Authentication

nanoapps supports passing auth context from your HTTP middleware into tool handlers. Provide a resolveAuth callback when creating the handler, and every tool receives the resolved identity via the context parameter.

Configure auth resolution

import { Hono } from "hono";
import { collectApps } from "nanoapps";
import { mcpHandler } from "nanoapps/hono";
import { verifyJwt } from "./auth";

const app = new Hono();

app.all("/mcp", mcpHandler({
  name: "my-server",
  version: "1.0.0",
  registrations: await collectApps("./dist"),
  resolveAuth: async (request) => {
    const user = await verifyJwt(request.headers.get("authorization"));
    if (!user) return undefined;
    return {
      token: "",
      clientId: user.id,
      scopes: user.scopes,
      extra: { email: user.email, role: user.role },
    };
  },
}));

The resolveAuth callback receives the raw Request and returns an AuthInfo object (or undefined). It is called once per request — for both stateless and stateful session requests. Only read headers; do not consume the request body.

Access auth in tool handlers

Every handler receives a second context argument with the resolved auth:

import { defineApp } from "nanoapps";

export default defineApp({
  name: "profile",
  title: "User Profile",
  description: "Returns the current user's profile.",
  handler: async (args, context) => {
    const userId = context.authInfo?.clientId;
    const email = context.authInfo?.extra?.email;
    if (!userId) return "Not authenticated";
    return JSON.stringify({ userId, email });
  },
});

The context object contains:

| Field | Type | Description | |-------|------|-------------| | authInfo | AuthInfo \| undefined | Auth info returned by resolveAuth | | sessionId | string \| undefined | MCP session ID (stateful connections only) |

When no resolveAuth is configured, context.authInfo is undefined. The context parameter is always provided — handlers that don't need auth can simply ignore it.

Session Handling

By default, stateful sessions are stored in memory with a 30-minute idle TTL. You can replace the built-in store by passing a sessionStore to createMcpHandler (or mcpHandler).

Custom session store

Implement the SessionStore interface — three methods:

import type { SessionStore, SessionData } from "nanoapps";

class RedisSessionStore implements SessionStore {
  get(id: string): SessionData | undefined { /* look up session, reset idle timeout */ }
  set(id: string, session: SessionData): void { /* store session */ }
  delete(id: string): void {
    const session = this.sessions.get(id);
    if (session) {
      this.sessions.delete(id);
      session.transport.close(); // required — releases resources
    }
  }
}

Then pass it when creating the handler:

const handler = createMcpHandler({
  name: "my-server",
  version: "1.0.0",
  registrations: await collectApps("./dist"),
  sessionStore: new RedisSessionStore(),
});

When sessionStore is provided, sessionTtlMs is ignored — your store owns the lifecycle.

Configuring the default store

Without a custom store, use sessionTtlMs to change the idle timeout:

createMcpHandler({
  // ...
  sessionTtlMs: 10 * 60 * 1000, // 10 minutes
});

Or use MemorySessionStore directly for the same default behavior:

import { MemorySessionStore } from "nanoapps";

createMcpHandler({
  // ...
  sessionStore: new MemorySessionStore(10 * 60 * 1000),
});

Build

Build tool.ts files and UIs into self-contained dist directories:

bunx nanoapps build                       # defaults: src/apps → dist
bunx nanoapps build src/apps dist         # explicit paths
bunx nanoapps build --watch               # watch mode — rebuild on file changes
bunx nanoapps build --standalone          # bundle all deps into tool.js

By default, built tool.js files keep npm packages as external imports (resolved from your node_modules). Use --standalone to bundle everything into each tool.js so the output has no runtime dependencies.

Or use the programmatic API:

import { buildApps, watchApps } from "nanoapps/build";

// One-shot build
await buildApps({ appsDir: "src/apps", outDir: "dist" });

// Standalone — bundle all deps into tool.js
await buildApps({ appsDir: "src/apps", outDir: "dist", standalone: true });

// Watch mode — returns a handle to stop watching
const handle = await watchApps({ appsDir: "src/apps", outDir: "dist" });
// handle.close() to stop

Serve

Run without a server file

Serve your built apps directly from the CLI — no server code needed:

bunx nanoapps run                         # serves from ./dist on port 3000
bunx nanoapps run ./my-apps               # custom apps directory
bunx nanoapps run --port 8080             # custom port

This starts a server with /mcp and /health endpoints. The apps directory can also be set via NANOAPPS_DIR or PORT environment variables.

Mount with Hono

import { Hono } from "hono";
import { collectApps } from "nanoapps";
import { mcpHandler } from "nanoapps/hono";

const app = new Hono();

app.all("/mcp", mcpHandler({
  name: "my-server",
  version: "1.0.0",
  registrations: await collectApps("./dist"),
}));

export default { port: 3000, fetch: app.fetch };

Mount with any framework

The core createMcpHandler returns a (request: Request) => Promise<Response> function that works with any framework supporting the Web Standard Request/Response API.

import { createMcpHandler, collectApps } from "nanoapps";

const handler = createMcpHandler({
  name: "my-server",
  version: "1.0.0",
  registrations: await collectApps("./dist"),
});

Bun.serve({ port: 3000, fetch: handler });

Auto-discover apps with collectApps

Load all apps from a directory (pre-built or source):

import { collectApps } from "nanoapps";

// Load pre-built apps from dist/ (tool.js + app.html co-located)
const registrations = await collectApps("./dist");

// Load source tools from src/apps/ with built HTML from dist/
const registrations = await collectApps({
  appsDir: "./src/apps",
  distDir: "./dist",
});

Contributing

Quick start

mise install              # install Bun 1.3.10
bun install               # install dependencies
bun run build:apps        # build app UIs into single HTML files
bun run dev               # start dev server with watch mode

The server starts at http://localhost:3000. Connect any MCP client to http://localhost:3000/mcp.

Scripts

| Command | Description | |---------|-------------| | bun run dev | Build watcher + dev server | | bun run build:apps | Build all apps (tools + UIs) into dist/ | | bun run start | Production server | | bun test | Run tests | | bun run check | Lint with Biome | | bun run fix | Lint + autofix with Biome | | bun run inspector | Launch MCP protocol inspector |

Docker

docker build -t nanoapps .
docker run -p 3000:3000 nanoapps

Testing

bun test

See docs/testing.md for test structure and writing tests, and docs/curl-testing.md for manual testing with curl.