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
访问服务器

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

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选