hono-apcore

hono-apcore

Enables turning a Hono app into MCP tools and OpenAI-compatible function definitions, either by declaring tools explicitly or by scanning existing routes.

Category
Visit Server

README

hono-apcore

Hono adapter for the apcore AI-Perceivable module ecosystem. Turn a Hono app into MCP tools and OpenAI-compatible function definitions — either by declaring tools explicitly, or by scanning the routes you already have.

Features

  • Two ways in — declare tools with defineTool() / defineToolset(), or scan your existing routes with zero code changes
  • Route replay — a scanned route becomes a module that calls back through app.request(), so middleware, validators, and error handlers all still run
  • One port — mountMcp() serves the MCP endpoint, the Tool Explorer, and /health from the same Hono app
  • Annotation inference — GET → readonly + cacheable, PUT → idempotent, DELETE → destructive (RFC 9110 safe-method semantics)
  • Multi-schema — TypeBox, Zod 3, Zod 4, and plain JSON Schema, auto-detected through a priority chain
  • Context, ACL, and identity — the apcore() middleware builds a per-request apcore Context with W3C trace propagation, so ACL rules govern your routes too
  • Runtime-agnostic core — apcore-mcp, apcore-cli, and apcore-a2a are optional peers loaded lazily, so importing hono-apcore never drags node:http into an edge build
  • CLI — hono-apcore scan | serve | export works against a plain Hono app
  • YAML bindings — register modules declaratively, without touching source

Installation

npm install hono-apcore hono

Optional peers, installed only for the surfaces you use:

npm install apcore-mcp @modelcontextprotocol/sdk   # MCP server + Tool Explorer
npm install @hono/node-server                      # mountMcp() on the Node runtime
npm install apcore-cli                             # CLI surface
npm install apcore-a2a                             # A2A agent surface
npm install @sinclair/typebox                      # TypeBox schemas (recommended)
npm install zod                                    # Zod schemas

Requirements: Node.js >= 18, Hono >= 4 (tested with Hono 4.13).

Quick start

1. Declare some tools

// todo.tools.ts
import { Type } from '@sinclair/typebox';
import { defineToolset } from 'hono-apcore';

export const todoTools = defineToolset({
  namespace: 'todo',
  description: 'Todo list management',
  tags: ['todo'],
  tools: {
    list: {
      description: 'List all todos, optionally filtered by status',
      inputSchema: Type.Object({ done: Type.Optional(Type.Boolean()) }),
      annotations: { readonly: true, idempotent: true },
      handler: (inputs) => ({ todos: store.list(inputs.done as boolean | undefined) }),
    },
    add: {
      description: 'Add a new todo item',
      inputSchema: Type.Object({ title: Type.String() }),
      annotations: { readonly: false },
      handler: (inputs) => ({ todo: store.add(String(inputs.title)) }),
    },
  },
});

2. Wire it into the app

// app.ts
import { Hono } from 'hono';
import { apcore, createApcore } from 'hono-apcore';
import { todoTools } from './todo.tools.js';

export const ap = createApcore({
  tools: todoTools,
  mcp: { name: 'my-app', explorer: true, allowExecute: true },
});

export const app = new Hono();
app.use('*', apcore(ap));
app.get('/todos', (c) => c.json(store.list()));

3. Boot

// main.ts
import { serve } from '@hono/node-server';
import { app, ap } from './app.js';

await ap.init(app);              // register tools + scan routes
await ap.mountMcp(app);          // mount /mcp, /explorer, /health

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

Your app now answers:

  • REST at http://localhost:3000/todos
  • MCP at http://localhost:3000/mcp
  • Tool Explorer at http://localhost:3000/explorer/

Two ways to expose a capability

defineTool() — explicit tools

The Hono counterpart to NestJS's @ApTool decorator. Hono has no classes or DI container to decorate, so a tool is a plain object that carries its own metadata and handler.

import { defineTool } from 'hono-apcore';

const sendEmail = defineTool({
  namespace: 'email',
  name: 'send',                 // -> module id "email.send"
  description: 'Send an email',
  inputSchema: Type.Object({ to: Type.String(), body: Type.String() }),
  outputSchema: Type.Object({ messageId: Type.String() }),
  annotations: { readonly: false, destructive: false, requiresApproval: true },
  tags: ['email'],
  params: { to: 'Recipient address' },   // merged into the schema descriptions
  handler: async (inputs, context) => mailer.send(inputs, context),
});
Field Notes
id Used verbatim. Otherwise "<namespace>.<name>", with name snake-cased
inputSchema / outputSchema TypeBox, Zod, or plain JSON Schema
annotations readonly, destructive, idempotent, requiresApproval, openWorld, streaming, cacheable, …
params Per-parameter prose merged into the input schema. JavaScript cannot read a function's leading comments at run time the way Python reads a docstring, so this is explicit
handler (inputs, context) => result. A non-object result is wrapped as { result }

Route scanning — zero-intrusion tools

Point the scanner at an app and every route becomes a module that replays it in-process through app.request():

const ap = createApcore({
  routes: {
    excludePaths: ['/health', '/mcp*', '/explorer*'],
    modulePrefix: 'api',
  },
});

await ap.init(app);   // -> api.todos.list, api.todos.get, api.todos.create, …

Module IDs come from the path and the HTTP verb:

Route Module ID Inferred annotations
GET /todos todos.list readonly, cacheable
GET /todos/:id todos.get readonly, cacheable
POST /todos todos.create —
PUT /todos/:id todos.update idempotent
DELETE /todos/:id todos.delete destructive

The generated input schema carries one required string property per path parameter, plus a free-form query object (GET/DELETE) or body object (POST/PUT/PATCH). Override any of it per route:

routes: {
  overrides: {
    'GET /todos': {
      id: 'todo.all',
      description: 'Every todo, newest first',
      inputSchema: Type.Object({ done: Type.Optional(Type.Boolean()) }),
      annotations: { readonly: true, idempotent: true },
    },
    'DELETE /admin/wipe': { skip: true },
  },
}

Because execution goes back through app.request(), an AI call runs the same code path as an HTTP call — auth middleware, validators, error handlers and all. Identity and W3C trace headers from the apcore Context are forwarded onto the replayed request.

API reference

createApcore(options)

Returns a HonoApcore — the Registry, the Executor, and every surface hang off it.

createApcore({
  extensionsDir?: string | null,   // scanned by Registry.discover()
  acl?: ACL,                       // enforced by the Executor on every call
  middleware?: Middleware[],       // apcore middleware installed on the Executor
  bindings?: string,               // YAML bindings file loaded during init()
  tools?: ApToolDefinition[],      // registered during init()
  routes?: RouteScanOptions,       // route-scanner configuration
  settings?: Partial<ApcoreSettings>,  // overrides for the APCORE_* settings
  mcp?: ApcoreMcpOptions,          // presence enables the MCP surface
  cli?: ApcoreCliOptions,          // presence enables the CLI surface
  a2a?: ApcoreA2aOptions,          // presence enables the A2A surface
})
Method Description
init(app?, routeOptions?) Discover, register tools and bindings, scan routes, start standalone surfaces. Idempotent
ready() Await an in-flight init()
registerTool(tool) / registerTools(tools) Register tool definitions at run time
registerMethod(opts) / registerObject(opts) Register the methods of a plain service object
scanRoutes(app, opts?) Scan and register an app's routes
routeOptions The merged route-scan options this instance would use
loadBindings(path?, resolver?) Load a YAML bindings file
mountMcp(app, opts?) Mount /mcp, the Explorer, and /health into the app
toOpenaiTools(opts?) OpenAI-compatible function definitions
close() Shut down the MCP and A2A surfaces

apcore(instance | options, middlewareOptions?)

Hono middleware that puts the instance and a per-request apcore Context on the Hono context.

app.use('*', apcore(ap));

app.get('/orders', async (c) =>
  c.json(await getApcore(c).executor.call('orders.list', {}, getApcoreContext(c))),
);

The variable map is augmented, so c.get('apcore') and c.get('apcoreContext') are typed too. Pass { skipContext: true } on routes that never call modules, or { contextFactory } to plug in real authentication.

HonoContextFactory

Builds the apcore Context from a Hono context, a Request, or bare Headers.

Identity resolution, in order: x-user-id → Authorization: Bearer … (identity id "bearer") → a bare x-roles header (a demo shortcut) → anonymous. A traceparent header supplies the trace id; x-correlation-id (or x-request-id) lands in context.data.

new HonoContextFactory({
  resolveIdentity: (headers) => identityFromSession(headers),  // wins over the above
  data: (headers) => ({ tenant: headers.get('x-tenant') }),
});

MCP

ApcoreMcpService runs the MCP server two ways.

Embedded — one process, one port:

await ap.mountMcp(app, { endpoint: '/mcp', explorer: true, allowExecute: true });

This needs the raw Node request and response objects that @hono/node-server exposes on c.env, so it is Node-only; a mounted handler on another runtime answers 501 with that explanation. endpoint must be the path as the HTTP server sees it — include the prefix if the app sits under a basePath.

Standalone — a separate port, or stdio for a CLI-launched server:

createApcore({ mcp: { transport: 'streamable-http', host: '0.0.0.0', port: 8000 } });
// init() starts it, because `transport` was set explicitly

Key MCP options:

Field Type Description
transport 'stdio' | 'streamable-http' | 'sse' Standalone transport. Setting it makes init() start the server
host / port string / number Bind address for HTTP transports
name / version string Server identity
explorer / explorerPrefix / allowExecute Tool Explorer web UI
authenticator / requireAuth / exemptPaths JWT or custom auth
tags / prefix Expose only matching modules
validateInputs boolean Enforce input schemas on every call
observability Metrics + usage middleware and their endpoints
outputFormat / outputFormatter / redactOutput / trace Result serialisation
approvalHandler / approvalStore / approvalNotify Approval gate for destructive tools
mcpMiddleware / mcpAcl Extra apcore middleware / ACL for the MCP executor

Schema adapters

Schemas are auto-detected and converted through a priority chain:

Adapter Priority Input
TypeBoxAdapter 100 @sinclair/typebox schemas
ZodAdapter 50 Zod 3 (_def.typeName) and Zod 4 (_zod.def.type)
JsonSchemaAdapter 30 Plain JSON Schema objects

Detection is structural — neither TypeBox nor Zod is imported at run time — so whichever the host app installs (or neither) is fine. Register your own with SchemaExtractor.registerAdapter().

YAML bindings

Register modules without touching source:

bindings:
  - module_id: email.send
    target: EmailService.send
    description: Send an email
    input_schema:
      type: object
      properties:
        to: { type: string }
    tags: [email, mutate]
    annotations:
      readonly: false
import { resolverFromObjects } from 'hono-apcore';

await ap.loadBindings('./bindings.yaml', resolverFromObjects({ EmailService: mailer }));

Going the other way, writeBindingsFile() serialises scanned modules back out — which is what hono-apcore scan --format yaml does.

CLI

hono-apcore scan   ./src/app.ts            # print the modules a scan would produce
hono-apcore scan   ./src/app.ts --format yaml --out bindings.yaml
hono-apcore serve  ./src/app.ts --transport http --port 8000 --explorer
hono-apcore export ./src/app.ts --out tools.json

The entry is path[:export]; the export defaults to default, then app. If the module exports a HonoApcore under any name, its configuration — route filters, module prefix, MCP options — is honoured, so scan reports exactly the modules the app itself registers; CLI flags override it. An entry with no instance still works, so serve runs against an app that has never heard of apcore. TypeScript entries need a loader:

npx tsx node_modules/.bin/hono-apcore scan ./src/app.ts

Configuration (APCORE_*)

The canonical settings every apcore integration implements, read from the environment and overridable via settings:

Variable Type Default Purpose
APCORE_ENABLED bool true Master switch — false makes init() a no-op
APCORE_DEBUG bool false Verbose logging / introspection
APCORE_SCANNERS list ["auto"] Enabled scanner identifiers
APCORE_INCLUDE_PATHS list [] Route patterns to include (empty = all)
APCORE_EXCLUDE_PATHS list [] Route patterns to exclude
APCORE_MODULE_PREFIX str "" Prefix prepended to generated module IDs
APCORE_AUTH_ENABLED bool false Require auth for MCP/A2A endpoints
APCORE_AUTH_STRATEGY str "bearer" bearer / session / custom
APCORE_TRANSPORT str "stdio" MCP transport: stdio / http / sse
APCORE_HOST str "0.0.0.0" Bind address when the transport is not stdio
APCORE_PORT int 8808 Bind port when the transport is not stdio

Optional peers are not re-exported

Unlike the NestJS adapter, hono-apcore does not re-export the apcore-mcp / apcore-cli / apcore-a2a surfaces. Doing so would make them load eagerly, and apcore-mcp pulls in node:http — which breaks a Workers, Deno, or Bun build of an app that never uses the MCP surface. Import those symbols from their own packages:

import { JWTAuthenticator, getCurrentIdentity } from 'apcore-mcp';
import { createCli } from 'apcore-cli';
import { A2AClient } from 'apcore-a2a';

apcore-js and apcore-toolkit are hard dependencies, so their common symbols (ACL, Config, registerSysModules, TraceContext, BaseScanner, formatModules, …) re-export from hono-apcore directly.

Examples

Example Shows
examples/demo Full app: hand-written tools and route scanning, JWT, ACL, system modules, Docker
examples/acl_demo Routes governed by apcore ACL — orders.delete for admins only
pnpm install && pnpm build
cd examples/demo && pnpm install && pnpm dev

Detailed documentation

Scripts

Command Description
pnpm build Compile TypeScript
pnpm dev Watch-mode compilation
pnpm test Run the test suite (vitest)
pnpm test:coverage Tests with coverage (90% thresholds)
pnpm typecheck Type-check without emitting
pnpm lint Lint source and tests

License

Apache-2.0

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured