Skip to main content
Glama

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 portmountMcp() serves the MCP endpoint, the Tool Explorer, and /health from the same Hono app

  • Annotation inferenceGET → 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 coreapcore-mcp, apcore-cli, and apcore-a2a are optional peers loaded lazily, so importing hono-apcore never drags node:http into an edge build

  • CLIhono-apcore scan | serve | export works against a plain Hono app

  • YAML bindings — register modules declaratively, without touching source

Related MCP server: Graft

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-idAuthorization: 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

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Exposes Hono API endpoints as Model Context Protocol tools, allowing LLMs to interact with your API routes through a dedicated MCP endpoint. It provides helpers to describe routes and includes a codemode for dynamic API interaction via search and execute tools.
    32
    86
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables building agent-ready APIs that expose tools as both HTTP and MCP endpoints from a single server definition, with automatic OpenAPI, discovery docs, and interactive API reference.
    5
    Apache 2.0
  • A
    license
    B
    quality
    C
    maintenance
    Transforms OpenAPI definitions into MCP tools for seamless LLM-API integration.
    8
    39
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Easily expose your Hono API endpoints as MCP tools with minimal configuration, supporting type-safe input handling and tool registration.
    32
    2
    MIT

View all related MCP servers

Related MCP Connectors

  • Point Gecko at an OpenAPI spec; get first-call-correct, auth-hidden agent tools.

  • Free public MCP for AI agents — 193 tools, 44 workflows. No API key.

  • MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aiperceivable/hono-apcore'

If you have feedback or need assistance with the MCP directory API, please join our Discord server