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

mcphero

v1.1.6

Published

MCP Hero Toolkit

Readme

MCPHero

Write your logic once. Run it everywhere.

MCPHero is a TypeScript toolkit that lets you define application logic as portable Actions and instantly expose them through any combination of transports — MCP servers (stdio and HTTP), REST APIs with auto-generated OpenAPI docs, and fully-featured CLIs. No glue code required.

                     ┌─────────────────────┐
                     │       Action         │
                     │  name + zod schema   │
                     │  + async run()       │
                     └──────────┬──────────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                  │
     ┌────────▼────────┐ ┌─────▼──────┐ ┌────────▼────────┐
     │   MCP (stdio)   │ │  Fastify   │ │      CLI        │
     │   MCP (http)    │ │  REST API  │ │   commander +   │
     │                 │ │  + Swagger │ │   clack         │
     └─────────────────┘ └────────────┘ └─────────────────┘

Table of Contents


Why MCPHero

Building an MCP server usually means wiring up transports, registering tools, serializing responses, and handling errors — for every single tool. If you also want a CLI or REST API for the same logic, you're writing it all again.

MCPHero eliminates this duplication. You define an Action — a name, a Zod schema, and an async function — and MCPHero handles the rest:

  • MCP adapters register your actions as MCP tools with proper notifications, progress reporting, and error handling
  • Fastify adapter generates a REST API with full OpenAPI/Swagger documentation derived from your Zod schemas
  • CLI adapter builds a complete command-line interface with argument parsing, option flags, and beautiful terminal output via clack

Your action doesn't know or care which transport is running it.


Quick Start

npm install mcphero
# or
pnpm add mcphero

Create an action:

// actions/greet.ts
import z from 'zod'
import { createAction } from 'mcphero'

export const GreetAction = createAction({
  name: 'greet',
  description: 'Greet someone by name',
  input: z.object({
    name: z.string().describe('The name to greet'),
    enthusiastic: z.boolean().describe('Add excitement').default(false)
  }),
  run: async ({ name, enthusiastic }, { logger }) => {
    const greeting = enthusiastic ? `HELLO, ${name.toUpperCase()}!!!` : `Hello, ${name}.`
    logger.info(greeting)
    return { greeting }
  }
})

Serve it as an MCP server:

// server.ts
import { mcphero, stdio } from 'mcphero'
import { GreetAction } from './actions/greet.js'

await mcphero({ name: 'my-server', description: 'My MCP Server', version: '1.0.0' })
  .with(stdio())
  .mount(GreetAction)
  .start()

That's it. Your action is now an MCP tool called Greet that any MCP client (Claude Desktop, Cursor, Claude Code, etc.) can discover and invoke.


Core Concepts

Actions

An Action is the fundamental unit of logic in MCPHero. It is completely transport-agnostic.

import z from 'zod'
import { createAction } from 'mcphero'

export const SearchAction = createAction({
  name: 'search',
  description: 'Search documents by query',
  input: z.object({
    query: z.string().describe('Search query'),
    limit: z.number().int().min(1).max(100).describe('Max results').default(10),
    includeArchived: z.boolean().describe('Include archived documents').default(false)
  }),
  run: async ({ query, limit, includeArchived }, { logger }) => {
    logger.info(`Searching for: ${query}`)
    // ... your logic here
    const results = await performSearch(query, { limit, includeArchived })
    return { results, count: results.length }
  }
})

createAction accepts an object with:

| Field | Type | Description | |-------|------|-------------| | name | string | Unique identifier. Adapters transform this automatically (PascalCase for MCP tools, kebab-case for CLI commands, as-is for REST routes). | | description | string | Human-readable description. Shown in MCP tool listings, CLI help, and Swagger docs. | | input | z.ZodObject | A Zod object schema defining the input. .describe() on each field provides per-field documentation across all adapters. | | args | (keyof I)[] | (Optional) Fields to expose as positional CLI arguments instead of --options. Only relevant for the CLI adapter. | | run | (input, context) => Promise<O> | The implementation. Receives validated input and an ActionContext with a logger. Returns any object — adapters handle serialization. |

Adapters

Adapters are the bridge between your actions and the outside world. Each adapter is a factory function that takes configuration and returns a generator compatible with the MCPHero builder.

import { stdio, http, fastify, cli } from 'mcphero'

// No config needed
stdio()

// Host and port required
http({ host: 'localhost', port: 8080 })

// Full Fastify options pass-through
fastify({ host: 'localhost', port: 8080, logger: true })

// No config needed
cli()

The adapter lifecycle:

  1. Factorystdio() / http({ ... }) / etc. captures configuration
  2. Generator — MCPHero calls the factory result with { name, description, version } to produce an Adapter
  3. Startadapter.start(actions) registers all actions and begins listening
  4. Stopadapter.stop() tears down gracefully

The MCPHero Builder

The builder provides a fluent, chainable API:

const app = mcphero({
  name: 'my-toolkit',
  description: 'A collection of useful tools',
  version: '2.1.0'
})

app
  .with(stdio())               // Add an adapter
  .with(http({ ... }))         // Add another — they run in parallel
  .mount(SearchAction)         // Mount an action
  .mount(GreetAction)          // Mount another

await app.start()              // Start all adapters concurrently

.with() and .mount() return the same instance, so you can chain freely. .start() launches all adapters in parallel via Promise.all.


Adapters in Depth

MCP Stdio

The standard MCP transport for local tool servers. Communicates over stdin/stdout using the MCP protocol.

import { mcphero, stdio } from 'mcphero'

await mcphero({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
  .with(stdio())
  .mount(MyAction)
  .start()

How actions become MCP tools:

| Action property | MCP tool property | |-----------------|-------------------| | name: 'searchDocs' | Tool name: SearchDocs (PascalCase) | | description | Tool description | | input (Zod schema) | inputSchema (JSON Schema via Zod) | | Return value | TextContent JSON response | | Thrown errors | Structured error response with name, message, and stack |

MCP logging notifications are wired automatically — logger.info("message") in your action sends a notifications/message to the MCP client. Progress reporting works via logger.progress() when the client provides a progress token.

Claude Desktop / Claude Code configuration:

{
  "mcpServers": {
    "my-tools": {
      "command": "node",
      "args": ["path/to/server.js"]
    }
  }
}

MCP Streamable HTTP

A session-based MCP transport over HTTP with Server-Sent Events (SSE) for streaming. Supports multiple concurrent sessions, resumability via Last-Event-ID, and session termination.

import { mcphero, http } from 'mcphero'

await mcphero({ name: 'my-tools', description: 'My Tools', version: '1.0.0' })
  .with(http({
    host: 'localhost',
    port: 8080,
    allowedHosts: ['localhost']
  }))
  .mount(MyAction)
  .start()

Endpoints exposed:

| Method | Path | Purpose | |--------|------|---------| | POST | /mcp | Initialize session or invoke tools | | GET | /mcp | Establish SSE stream for a session | | DELETE | /mcp | Terminate a session |

The adapter manages session lifecycle automatically — each new initialize request creates a new StreamableHTTPServerTransport with a UUID session ID. Sessions are cleaned up when the transport closes.

CORS is configured out of the box, exposing MCP-specific headers (Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-Id).

HttpAdapterOptions:

| Option | Type | Description | |--------|------|-------------| | host | string | Bind address | | port | number | Listen port | | allowedHosts | string[] | Hosts allowed to connect (passed to Express MCP app) |

Fastify REST API

Turns your actions into a REST API with auto-generated OpenAPI documentation and an interactive Swagger UI powered by Scalar.

import { mcphero, fastify } from 'mcphero'

await mcphero({ name: 'my-api', description: 'My REST API', version: '1.0.0' })
  .with(fastify({
    host: 'localhost',
    port: 8080,
    logger: true
  }))
  .mount(SearchAction)
  .mount(GreetAction)
  .start()

Route mapping:

Each action becomes a POST /{action.name} route. The Zod schema is converted to JSON Schema for request body validation and OpenAPI documentation.

| Action | Route | Body | |--------|-------|------| | name: 'search' | POST /search | { "query": "...", "limit": 10 } | | name: 'greet' | POST /greet | { "name": "World" } |

Visit http://localhost:8080/ for the interactive Scalar API reference with try-it-out functionality.

FastifyAdapterOptions:

Extends Fastify's native FastifyHttpOptions, so any Fastify config is supported:

fastify({
  host: 'localhost',
  port: 8080,
  logger: {
    level: 'debug',
    transport: { target: 'pino-pretty' }
  },
  connectionTimeout: 30000
})

CLI

Transforms your actions into a complete command-line application with help text, option parsing, and styled terminal output via clack.

import { mcphero, cli } from 'mcphero'

await mcphero({ name: 'mytool', description: 'My CLI Tool', version: '1.0.0' })
  .with(cli())
  .mount(GreetAction)
  .mount(SearchAction)
  .start()

How Zod types map to CLI options:

| Zod Type | CLI Representation | |----------|-------------------| | z.string() | --option-name <string> | | z.number() | --option-name <number> | | z.boolean() | --option-name / --no-option-name | | z.record() | --option-name <json> | | .default(value) | Sets the default in help text | | .describe('...') | Sets the option description |

Field names are automatically converted to kebab-case for flags. Action names become kebab-case subcommands.

Example output:

$ mytool greet --name "World" --enthusiastic
◇ mytool - greet
ℹ HELLO, WORLD!!!

Logging and Progress

Every action receives a context.logger with eight severity levels plus a progress reporter:

run: async (input, { logger }) => {
  logger.debug('Starting operation...')
  logger.info('Processing item')
  logger.warning('Rate limit approaching')
  logger.error('Failed to connect')

  // Progress reporting (renders as MCP progress notifications,
  // Fastify trace logs, or console output depending on adapter)
  for (let i = 1; i <= total; i++) {
    logger.progress({
      progress: i,
      total: total,
      message: `Processing item ${i} of ${total}`
    })
    await processItem(i)
  }

  return { processed: total }
}

How logging is handled per adapter:

| Level | MCP (stdio/http) | Fastify | CLI | |-------|-------------------|---------|-----| | debug | notifications/message | logger.debug() | log.message() | | info | notifications/message | logger.info() | log.info() | | warning | notifications/message | logger.warn() | log.warn() | | error | notifications/message | logger.error() | log.error() | | critical | notifications/message | logger.fatal() | log.error() | | progress | notifications/progress | logger.trace() | console.info() |


Advanced Patterns

Multiple Adapters Simultaneously

Run an MCP server and a REST API from the same set of actions:

await mcphero({ name: 'my-platform', description: 'Multi-transport', version: '1.0.0' })
  .with(stdio())
  .with(fastify({ host: 'localhost', port: 8080, logger: true }))
  .mount(SearchAction)
  .mount(GreetAction)
  .mount(AnalyzeAction)
  .start()

All adapters start concurrently. The MCP stdio server communicates over stdin/stdout while Fastify listens on port 8080 — each serving the exact same actions.

CLI Arguments vs Options

By default, all Zod fields become CLI --options. Use the args property to promote fields to positional arguments:

export const DeployAction = createAction({
  name: 'deploy',
  description: 'Deploy to an environment',
  input: z.object({
    environment: z.string().describe('Target environment'),
    tag: z.string().describe('Release tag'),
    dryRun: z.boolean().describe('Simulate without deploying').default(false)
  }),
  args: ['environment', 'tag'],
  run: async ({ environment, tag, dryRun }, { logger }) => {
    logger.info(`Deploying ${tag} to ${environment}${dryRun ? ' (dry run)' : ''}`)
    // ...
    return { deployed: !dryRun }
  }
})
$ mytool deploy production v2.1.0 --dry-run
◇ mytool - deploy
ℹ Deploying v2.1.0 to production (dry run)

Fields listed in args become positional arguments in the CLI. All other fields remain as --options. The args property has no effect on MCP or REST adapters — they always receive all fields as a single input object.

Complex Input Types

Zod's full expressiveness is available for input schemas:

export const ImportAction = createAction({
  name: 'import',
  description: 'Import data from a source',
  input: z.object({
    source: z.string().describe('Data source URL'),
    format: z.string().describe('File format').default('json'),
    batchSize: z.number().int().min(1).max(10000).describe('Records per batch').default(500),
    tags: z.record(z.string()).describe('Key-value metadata tags').optional(),
    overwrite: z.boolean().describe('Overwrite existing records').default(false)
  }),
  run: async (input, { logger }) => {
    logger.info(`Importing from ${input.source} in ${input.format} format`)
    logger.info(`Batch size: ${input.batchSize}, overwrite: ${input.overwrite}`)
    if (input.tags) {
      logger.debug(`Tags: ${JSON.stringify(input.tags)}`)
    }
    // ...
    return { imported: 1500 }
  }
})

In MCP, this becomes a tool with a full JSON Schema. In the CLI, tags becomes --tags <json> accepting a JSON string. In Fastify, it's a documented POST body.


MCP Client Configuration

Claude Desktop

Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "my-tools": {
      "command": "node",
      "args": ["/absolute/path/to/server.js"]
    }
  }
}

Claude Code

Add to .mcp.json in your project root:

{
  "mcpServers": {
    "my-tools": {
      "command": "pnpm",
      "args": ["tsx", "server.ts"]
    }
  }
}

MCP Inspector

For debugging with the MCP Inspector:

{
  "mcpServers": {
    "my-tools": {
      "command": "pnpm",
      "args": ["tsx", "server.ts"]
    }
  }
}
npx @modelcontextprotocol/inspector --config .mcp.json --server my-tools

HTTP Transport

For the streamable HTTP adapter, point your client at the /mcp endpoint:

http://localhost:8080/mcp

API Reference

mcphero(options)

Creates a new MCPHero builder instance.

function mcphero(options: MCPHeroOptions): MCPHero

interface MCPHeroOptions {
  name: string          // Server/app name
  description: string   // Human-readable description
  version: string       // Semantic version
}

interface MCPHero {
  with(generator: AdapterGenerator): MCPHero   // Add an adapter
  mount(action: Action): MCPHero               // Mount an action
  start(): Promise<MCPHero>                    // Start all adapters
}

createAction(action)

Type-safe action factory. Returns the action object as-is with full type inference.

function createAction<I extends object, O extends object>(
  action: Action<I, O>
): Action<I, O>

interface Action<I, O> {
  name: string
  description: string
  input: z.ZodType<I> & { shape: Record<string, z.ZodTypeAny> }
  args?: (keyof I)[]
  run: (input: I, context: ActionContext) => Promise<O>
}

interface ActionContext {
  logger: Logger
}

Logger

interface Logger {
  debug: (data: unknown) => void
  info: (data: unknown) => void
  notice: (data: unknown) => void
  warning: (data: unknown) => void
  error: (data: unknown) => void
  critical: (data: unknown) => void
  alert: (data: unknown) => void
  emergency: (data: unknown) => void
  progress: (options: ProgressOptions) => void
}

interface ProgressOptions {
  progress: number
  total?: number
  message?: string
}

Adapter Factories

// MCP over stdin/stdout
function stdio(): AdapterFactory

// MCP Streamable HTTP
function http(options: HttpAdapterOptions): AdapterFactory
interface HttpAdapterOptions {
  host: string
  port: number
  // ...plus CreateMcpExpressAppOptions (e.g., allowedHosts)
}

// Fastify REST API with Swagger
function fastify(options: FastifyAdapterOptions): AdapterFactory
interface FastifyAdapterOptions {
  host?: string
  port?: number
  // ...plus all FastifyHttpOptions (logger, connectionTimeout, etc.)
}

// CLI via commander + clack
function cli(): AdapterFactory

Development

# Install dependencies
pnpm install

# Build
pnpm build

# Build in watch mode
pnpm watch

# Type-check
pnpm typecheck

# Lint
pnpm lint

# Lint + typecheck
pnpm check

Requires Node.js >= 18.


License

MIT