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

@lordcraymen/rivetbench

v1.1.0

Published

Lightweight TypeScript framework for dual-exposed endpoints (REST + MCP).

Downloads

69

Readme

RivetBench

RivetBench is a lightweight TypeScript framework for building triple-exposed endpoints that work over REST, MCP (Model Context Protocol), and a runtime-generated CLI — with OpenAPI 3 documentation generated automatically.

Write an endpoint once — expose it everywhere.


Features

  • Unified endpoint definitions with Zod schemas for validation and typing
  • Three transports from one definition: REST routes, MCP tools, and CLI commands
  • RPC over REST: POST-only routes dispatched by endpoint name (no resource modeling)
  • Runtime-generated CLI with named parameters, JSON input, and raw/JSON output modes
  • Automatic OpenAPI 3 spec generation and built-in Swagger UI
  • HTTP Transport: delegate a CLI or MCP adapter to a remote RivetBench server with optional auto-spawn
  • Dynamic tool notifications: signal clients when the tool list changes at runtime
  • Tool enrichers: transform the tool list per-request based on session, transport, or app logic
  • REST ETag support: conditional requests (If-None-Match) on the tool listing endpoint
  • Dependency injection: typed custom context injected into every handler via the registry
  • Production-ready error handling with specific error classes and consistent responses
  • Structured logging with Pino (MCP stdio-compatible)

Install

npm install @lordcraymen/rivetbench

Requires Node.js >= 20.


Quick Start

Define an endpoint

import { z } from 'zod';
import { makeEndpoint } from '@lordcraymen/rivetbench';

export const echo = makeEndpoint({
  name: 'echo',
  summary: 'Echo a message',
  input:  z.object({ message: z.string() }),
  output: z.object({ echoed:  z.string() }),
  handler: async ({ input }) => ({ echoed: input.message }),
});

Expose via REST + MCP + CLI

import {
  InMemoryEndpointRegistry,
  createRestServer,
  createCli,
  loadConfig,
  createLogger,
  createPinoLoggerPort,
  createTransportPort,
} from '@lordcraymen/rivetbench';
import { echo } from './endpoints/echo.js';

const registry = new InMemoryEndpointRegistry();
registry.register(echo);
const config = loadConfig();
const logger = createLogger(config);
const loggerPort = createPinoLoggerPort(logger);
const transport = createTransportPort(registry, loggerPort);

// REST + MCP server — Swagger UI at /docs, MCP at /mcp
const rest = await createRestServer({ registry, config, logger, loggerPort, transport });
await rest.start();
// → REST:    POST http://localhost:3000/rpc/echo
// → MCP:    POST http://localhost:3000/mcp  (Streamable HTTP)
// → Swagger: http://localhost:3000/docs

// CLI (same endpoints, same validation)
const cli = createCli({ registry, config, transport });
await cli.run(process.argv.slice(2));

Key: createRestServer() mounts MCP automatically at /mcp. No separate MCP setup required.


Core Concepts

Endpoint definition

import { makeEndpoint } from '@lordcraymen/rivetbench';

const greet = makeEndpoint({
  name: 'greet',            // unique name — REST route, MCP tool, CLI command
  summary: 'Greet a user',  // shown in OpenAPI, MCP tool list, and CLI help
  description: 'Optional longer description',
  input:  z.object({ name: z.string() }),
  output: z.object({ greeting: z.string() }),
  handler: async ({ input, config, ctx }) => ({ greeting: `Hello, ${input.name}!` }),
});

Registry

import { InMemoryEndpointRegistry } from '@lordcraymen/rivetbench';

const registry = new InMemoryEndpointRegistry();
registry.register(greet);
registry.get('greet');
registry.list();
registry.listEnriched({ transportType: 'rest' });

registry.setContextFactory(() => ({ db: dbPool }));
registry.setToolEnricher((tools, ctx) => tools);
registry.signalToolsChanged();
registry.onToolsChanged(() => {});
// registry.etag    — current ETag string
// registry.version — monotonic version counter

Dependency Injection

interface AppCtx { db: DatabasePool; }

const getUser = makeEndpoint<typeof Input, typeof Output, AppCtx>({
  name: 'get-user',
  summary: 'Get a user by id',
  input:  z.object({ id: z.string() }),
  output: z.object({ name: z.string() }),
  handler: async ({ input, ctx }) => ctx.db.findUser(input.id),
  //                          ^^^ fully typed as AppCtx
});

registry.setContextFactory(() => ({ db: dbPool }));

Dynamic Tool Notifications

Signal connected clients when endpoint availability changes:

registry.signalToolsChanged();

| Transport | Mechanism | |-----------|-----------| | MCP | notifications/tools/list_changed sent to all active sessions | | REST | ETag on GET /tools; clients use If-None-Match | | CLI | Each invocation always reads the current list (stateless) |

Tool Enrichers

registry.setToolEnricher((tools, context) => {
  if (context.transportType === 'rest') {
    return tools.filter(t => !t.name.startsWith('internal-'));
  }
  return tools;
});

HTTP Transport

createHttpTransport returns a TransportPort that delegates invocations to a running RivetBench REST server over HTTP. No external dependencies — uses node:net and the global fetch API.

import { createHttpTransport } from '@lordcraymen/rivetbench/http-transport';

Remote-only (server already running):

const transport = createHttpTransport({ url: 'http://localhost:3000' });

Auto-spawn (start the server process if the port is not open on first use):

import { spawn } from 'node:child_process';

const transport = createHttpTransport({
  url: 'http://localhost:3000',
  spawn: () => spawn('node', ['dist/server.js']),
  spawnTimeoutMs: 15_000, // default
  pollIntervalMs: 200,    // default
});

Use with CLI (stateful server with runtime-generated CLI):

import { createCli, loadConfig, InMemoryEndpointRegistry } from '@lordcraymen/rivetbench';
import { createHttpTransport } from '@lordcraymen/rivetbench/http-transport';
import { spawn } from 'node:child_process';

const transport = createHttpTransport({
  url: 'http://localhost:3000',
  spawn: () => spawn('node', ['dist/server.js']),
});

const cli = createCli({ registry, config, transport });
await cli.run(process.argv.slice(2));

Symbol.dispose is implemented — using using or an explicit .dispose() kills the spawned child process on cleanup.


CLI

# List registered endpoints
rivetbench list

# Call with named parameters
rivetbench call echo -message "Hello World"

# Automatic type parsing for numbers and booleans
rivetbench call myfunc -count 42 -enabled true

# JSON input for complex objects
rivetbench call complexFunc --params-json '{"config": {"timeout": 30}}'

# Raw output — extracts single-property values for scripting
rivetbench call uppercase -text "world" --raw
# Output: WORLD

CLI flags use -- (double dash); endpoint parameters use - (single dash) to avoid collisions.


MCP Server

createRestServer() serves MCP natively at /mcp via Streamable HTTP — no extra setup needed. Every registered endpoint is automatically exposed as an MCP tool.

VS Code integration

Point VS Code directly at the running server — no stdio bridge required:

// .vscode/mcp.json
{
  "servers": {
    "my-app": {
      "type": "http",
      "url": "http://localhost:3000/mcp"
    }
  }
}

Start the server (npm run dev:rest or your composition), then reload the MCP server list in VS Code.

Standalone MCP HTTP server (no Fastify)

If you don't need REST/Swagger and want a minimal MCP-only HTTP server:

import http from 'node:http';
import { createMcpHandler } from '@lordcraymen/rivetbench/mcp';

const handler = createMcpHandler({ transport, registry, logger: loggerPort, application: config.application });
const server = http.createServer((req, res) => handler.handleRequest(req, res));
server.listen(3001);
// → MCP available at http://localhost:3001/mcp

Stdio bridge (for environments that require stdio)

Some MCP clients only support stdio transport. Use createHttpTransport with auto-spawn + an stdio↔HTTP bridge like mcp-proxy:

import { createHttpTransport } from '@lordcraymen/rivetbench/http-transport';
import { spawn } from 'node:child_process';

// Auto-spawn the server if not already running
const transport = createHttpTransport({
  url: 'http://localhost:3000',
  spawn: () => spawn('node', ['dist/server.js']),
});
// Then bridge stdio ↔ HTTP MCP at http://localhost:3000/mcp

See example/webmcp-bridge for a complete stdio bridge implementation.


Sub-path Exports

| Import path | Contents | |-------------|----------| | @lordcraymen/rivetbench | Full bundle — all adapters | | @lordcraymen/rivetbench/core | makeEndpoint, InMemoryEndpointRegistry, error classes (no transport deps) | | @lordcraymen/rivetbench/fastify | createRestServer, rivetBenchPlugin | | @lordcraymen/rivetbench/rest | createRestHandler (framework-agnostic) | | @lordcraymen/rivetbench/mcp | createMcpHandler, mcpOpenApiPaths | | @lordcraymen/rivetbench/cli | createCli | | @lordcraymen/rivetbench/openapi | buildOpenApiDocument | | @lordcraymen/rivetbench/pino | createLogger, createPinoLoggerPort | | @lordcraymen/rivetbench/http-transport | createHttpTransport |


Configuration

loadConfig(overrides?) reads environment variables and deep-merges optional programmatic overrides:

const config = loadConfig({ rest: { port: 4000 } });

| Env var | Default | Description | |---------|---------|-------------| | RIVETBENCH_REST_HOST | 0.0.0.0 | REST listen host | | RIVETBENCH_REST_PORT | 3000 | REST listen port | | RIVETBENCH_MCP_TRANSPORT | stdio | stdio or tcp | | RIVETBENCH_MCP_PORT | 3001 | MCP TCP port | | RIVETBENCH_APP_NAME | rivetbench | Application name | | RIVETBENCH_APP_VERSION | 1.0.0 | Application version | | RIVETBENCH_LOG_LEVEL | info | Pino log level | | RIVETBENCH_LOG_PRETTY | false | Pretty-print logs | | NODE_ENV | development | Sets environment field |


Benchmark

In-process vs HTTP server adapters (20 concurrent requests per iteration, unique payloads, response-validated):

| Transport | ops/sec | |-----------|--------:| | In-process | ~3,595 | | Koa | ~101 | | Fastify | ~88 | | Hono | ~79 | | Node http | ~70 | | Express | ~62 |

Run: npm run bench


Transports at a Glance

| Capability | REST | MCP | CLI | |---|---|---|---| | Input validation (Zod) | ✅ | ✅ | ✅ | | Output validation (Zod) | ✅ | ✅ | ✅ | | Request ID tracing | ✅ | ✅ | ✅ | | Error handling | Structured JSON | Structured JSON | Stderr + exit code | | Tool discovery | GET /tools, OpenAPI | tools/list | rivetbench list | | Change notification | ETag / If-None-Match | notifications/tools/list_changed | N/A (stateless) | | Tool enricher | ✅ | ✅ | ✅ |


Architecture

┌──────────────────────────────────────────────────────────────┐
│                         Adapters                             │
│  REST (Fastify)  │  MCP Handler  │  CLI  │  HTTP Transport  │
└──────────┬───────────────┬────────────────────────┬──────────┘
           │               │                        │
┌──────────▼───────────────▼────────────────────────▼──────────┐
│                     Application Layer                         │
│         invokeEndpoint · listEndpoints · createTransportPort  │
└──────────────────────────┬───────────────────────────────────┘
                           │
┌──────────────────────────▼───────────────────────────────────┐
│                       Domain Layer                            │
│     makeEndpoint · InMemoryEndpointRegistry · Errors         │
└──────────────────────────────────────────────────────────────┘
         ↑                                       ↑
   TransportPort                            LoggerPort
  (ports/transport)                       (ports/logger)

Hexagonal architecture (ADR-0001). Domain has zero framework dependencies. Adapters depend on domain types — never the reverse. See docs/ARCHITECTURE.md.


RPC-over-REST semantics

  • POST-only: every call is POST /rpc/:name
  • Stateless: all data comes from the request body; no server session
  • Opaque handler: only the Zod input/output are published; implementation never leaks
  • OpenAPI: each endpoint becomes POST /rpc/{name} with request body = input schema
  • MCP parity: same definition becomes an MCP tool with matching JSON Schema

License

MIT