hono-apcore
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hono-apcorescan my Hono app routes and expose them as MCP tools"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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: openapi-mcp-bridge
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 >= 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/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 |
|
|
| 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({
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 |
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:
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: 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.
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.tsConfiguration (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
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.
Give any MCP-compatible AI assistant a builder for live, hosted web tools and workflows.
Point Gecko at an OpenAPI spec; get first-call-correct, auth-hidden agent tools.
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.25 npm86MIT
- AlicenseBqualityCmaintenanceTransforms OpenAPI definitions into MCP tools for seamless LLM-API integration.89 npm1MIT
- AlicenseNot gradedqualityCmaintenanceEasily expose your Hono API endpoints as MCP tools with minimal configuration, supporting type-safe input handling and tool registration.25 npm2MIT
- AlicenseNot gradedqualityDmaintenanceExposes OpenAPI endpoints as MCP tools, enabling LLMs to discover and interact with REST APIs through the MCP protocol.4 npmMIT