hono-apcore
Provides an adapter for Hono applications, exposing existing routes or explicitly defined tools as MCP tools and OpenAI-compatible function definitions, with route replay through the Hono app so middleware, validators, and error handlers still run.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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 changesRoute replay — a scanned route becomes a module that calls back through
app.request(), so middleware, validators, and error handlers all still runOne port —
mountMcp()serves the MCP endpoint, the Tool Explorer, and/healthfrom the same Hono appAnnotation 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 apcoreContextwith W3C trace propagation, so ACL rules govern your routes tooRuntime-agnostic core —
apcore-mcp,apcore-cli, andapcore-a2aare optional peers loaded lazily, so importinghono-apcorenever dragsnode:httpinto an edge buildCLI —
hono-apcore scan | serve | exportworks against a plain Hono appYAML bindings — register modules declaratively, without touching source
Related MCP server: hono-mcp-server
Installation
npm install hono-apcore honoOptional 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 schemasRequirements: Node.js >= 20, Hono >= 4 (tested with Hono 4.13). SDK minimums: apcore-js 0.32.3, apcore-toolkit 0.14.1; optional peers apcore-mcp 0.23.3, apcore-cli 0.12.7, and apcore-a2a 0.8.5. See SDK upgrade notes for compatibility changes and new options.
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/todosMCP at
http://localhost:3000/mcpTool 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 |
| Used verbatim. Otherwise |
| TypeBox, Zod, or plain JSON Schema |
| Partial SDK annotations, including |
| SDK metadata (including |
| 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 |
|
|
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 |
|
|
|
|
|
|
|
| — |
|
|
|
|
|
|
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({
config?: Config, // shared SDK configuration, including extension roots
executorOptions?: { strategy?, approvalHandler?, policy?, toggleState?, eventEmitter? },
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 |
| Discover, register tools and bindings, scan routes, start standalone surfaces. Idempotent |
| Await an in-flight |
| Register tool definitions at run time |
| Register the methods of a plain service object |
| Scan and register an app's routes |
| The merged route-scan options this instance would use |
| Load a YAML bindings file |
| Mount |
| OpenAI-compatible function definitions |
| Shut down the MCP and A2A surfaces |
The executor also exposes callWithTrace(id, inputs?, context?, { strategy? })
and addStepMiddleware(middleware). Strategies can be SDK instances or registered
names. config is passed to both SDK objects; an explicitly configured
extensions.root or extensions.roots is discovered during init().
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 explicitlyKey MCP options:
Field | Type | Description |
|
| Standalone transport. Setting it makes |
|
| Bind address for HTTP transports |
|
| Server identity |
| Tool Explorer web UI | |
| JWT or custom auth | |
| Expose only matching modules | |
|
| Enforce input schemas on every call |
| Metrics + usage middleware and their endpoints | |
| Result serialisation | |
| Approval gate for destructive tools | |
| Extra apcore middleware / ACL for the MCP executor |
Schema adapters
Schemas are auto-detected and converted through a priority chain:
Adapter | Priority | Input |
| 100 |
|
| 50 | Zod 3 ( |
| 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:
spec_version: "1.0"
bindings:
- module_id: email.send
target: EmailService.send
description: Send an email
input_schema:
type: object
properties:
to: { type: string }
output_schema:
type: object
properties:
sent: { type: boolean }
tags: [email, mutate]
annotations:
readonly: falseimport { 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. The toolkit parser rejects
unknown document/entry keys and one-sided inline schema pairs. Supply both
schemas or neither; move custom entry data into metadata. YAML annotations
use snake_case (requires_approval); TypeScript tool annotations use camelCase
(requiresApproval).
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.jsonThe 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.tsThe optional ap.cli.createProgram() builds application module commands such
as demo ping, sharing the application's executor, ACL, middleware and approval
handler. It also adapts CLI --trace / --strategy calls to the core SDK.
Configuration (APCORE_*)
The canonical settings every apcore integration implements, read from the
environment and overridable via settings:
Variable | Type | Default | Purpose |
| bool |
| Master switch — |
| bool |
| Verbose logging / introspection |
| list |
| Enabled scanner identifiers |
| list |
| Route patterns to include (empty = all) |
| list |
| Route patterns to exclude |
| str |
| Prefix prepended to generated module IDs |
| bool |
| Require auth for MCP/A2A endpoints |
| str |
|
|
| str |
| MCP transport: |
| str |
| Bind address when the transport is not stdio |
| int |
| 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 |
Full app: hand-written tools and route scanning, JWT, ACL, system modules, Docker | |
Routes governed by apcore ACL — |
pnpm install && pnpm build
cd examples/demo && pnpm install && pnpm devDetailed documentation
Feature overview — architecture and dependency graph
Tool definition —
defineTool,defineToolset, module IDsRoute scanner — how routes become modules, and what replay costs
MCP integration — embedded vs standalone, the Node bridge
Schema extraction — the adapter chain and custom adapters
Context and ACL — identity, tracing, and governing routes
Scripts
Command | Description |
| Compile TypeScript |
| Watch-mode compilation |
| Run the test suite (vitest) |
| Tests with coverage (90% thresholds) |
| Type-check without emitting |
| Lint source and tests |
License
Apache-2.0
This server cannot be deployed
Maintenance
Related MCP Connectors
Convert, lint and generate LLM tool schemas for OpenAI, Anthropic, Gemini and MCP
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
Build, validate, deploy — HTTP APIs, cron jobs, webhooks and MCP tools — from your AI client.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceExposes 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.56 npm86MIT
- AlicenseNot gradedqualityCmaintenanceEasily expose your Hono API endpoints as MCP tools with minimal configuration, supporting type-safe input handling and tool registration.56 npm2MIT
- AlicenseNot gradedqualityCmaintenanceExposes FastAPI routes as MCP tools with auto-discovery, Pydantic schema extraction, and built-in observability.Apache 2.0
- AlicenseNot gradedqualityBmaintenanceAutomatically turns existing Elysia routes into MCP tools, reusing schemas and running full request lifecycle.42 npmMIT