Automatic MCP Server & OpenAI Tools Bridge for apcore.
Converts apcore module registries into Model Context Protocol (MCP) tool definitions and OpenAI-compatible function calling formats — zero boilerplate required.
MCP discovery preserves the module's runtime input semantics: optional properties,
explicit nullable values, defaults, local references and free-form objects remain
unchanged. Arguments pass through without null-placeholder deletion. Explicit
additional-property restrictions still apply. Factory strict options are retained
for compatibility and do not project OpenAI strict schemas onto MCP. OpenAI tool
exports and standalone SchemaConverter APIs retain their existing behavior.
- MCP Server — Expose apcore modules as MCP tools over stdio, Streamable HTTP, or SSE
- OpenAI Tools — Convert modules to OpenAI function calling format with strict mode support
- Markdown tool descriptions (
richDescription: true, v0.15+) — renderTool.descriptionand OpenAIfunction.descriptionas canonical apcore-toolkit Markdown so LLMs get more decision-relevant signal per token. Requiresapcore-toolkit(declared asoptionalDependencies) andawait MCPServerFactory.prepare()at startup. - Module preview meta-tool (
__apcore_module_preview, v0.15+) — drivesexecutor.validate()to predict state changes WITHOUT executing the module (apcore PROTOCOL_SPEC §5.6). Returns{valid, requires_approval, predicted_changes, checks}so AI orchestrators can ask "what would change?" before invoking. - Schema Conversion — Inline
$defs/$reffrom Pydantic-generated JSON Schema - Annotation Mapping — Map module annotations to MCP hints and OpenAI description suffixes
- Approval Mechanism — Built-in elicitation-based approval flow for sensitive tool executions
- Error Mapping — Sanitize internal errors for safe client-facing responses
- Dynamic Registration — Listen for registry changes and update tools at runtime
- Tool Explorer — Browser-based UI for browsing schemas and testing tools interactively
- CLI — Launch an MCP server from the command line
- Config Bus integration — Registers an
mcpnamespace with the apcore Config Bus; configure via unifiedapcore.yamlorAPCORE_MCP_*env vars - Error Formatter Registry — Registers an MCP-specific error formatter for ecosystem-wide consistent error handling
For full documentation, including Quick Start guides for both Python and TypeScript, visit: https://aiperceivable.github.io/apcore-mcp/
- Node.js >= 20.0.0
apcore-js >= 0.32.0apcore-toolkit >= 0.14.0— a runtime dependency: Markdown-rendered tool descriptions, the output formatters, and the OpenAPI backend. 0.13.0 is the floor because the OpenAPI backend registers the module IDs itsOpenAPIScanneremits, and only 0.13.0+ emits them in apcore's Canonical ID alphabet.
npm install apcore-mcpapcore-js is included as a direct dependency — no separate install needed.
import { serve, toOpenaiTools } from "apcore-mcp";
// Launch MCP server over stdio
await serve(executor);
// Launch over Streamable HTTP
await serve(executor, {
transport: "streamable-http",
host: "127.0.0.1",
port: 8000,
});
// Export OpenAI tool definitions
const tools = toOpenaiTools(registry, {
embedAnnotations: true,
strict: true,
});# stdio (default)
npx apcore-mcp --extensions-dir ./extensions
# Streamable HTTP
npx apcore-mcp --extensions-dir ./extensions --transport streamable-http --port 8000
# SSE
npx apcore-mcp --extensions-dir ./extensions --transport sse --port 8000| Argument | Default | Description |
|---|---|---|
--extensions-dir |
(required) | Path to apcore extensions directory |
--transport |
stdio |
stdio, streamable-http, or sse |
--host |
127.0.0.1 |
Host for HTTP transports |
--port |
8000 |
Port for HTTP transports (1-65535) |
--name |
apcore-mcp |
MCP server name |
--version |
package version | MCP server version |
--log-level |
INFO |
DEBUG, INFO, WARNING, ERROR |
--explorer |
off | Enable the browser-based Tool Explorer UI (HTTP only) |
--explorer-prefix |
/explorer |
URL prefix for the explorer UI |
--allow-execute |
off | Allow tool execution from the explorer UI |
--jwt-secret |
— | JWT secret key for Bearer token authentication |
--jwt-key-file |
— | Path to PEM key file for JWT verification (RS256/ES256) |
--jwt-algorithm |
HS256 |
JWT algorithm |
--jwt-audience |
— | Expected JWT audience claim |
--jwt-issuer |
— | Expected JWT issuer claim |
--jwt-require-auth |
true |
Require auth (use --jwt-permissive to override and allow unauthenticated requests) |
--jwt-permissive |
false |
Permissive mode: allow unauthenticated requests (overrides --jwt-require-auth) |
--exempt-paths |
/health,/metrics,/usage |
Comma-separated paths exempt from auth |
--output-format |
json |
Built-in output format: json, csv, or jsonl |
JWT key resolution priority: --jwt-key-file > --jwt-secret > APCORE_JWT_SECRET environment variable.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"apcore": {
"command": "npx",
"args": ["apcore-mcp", "--extensions-dir", "/path/to/your/extensions"]
}
}
}Add to .mcp.json in your project root:
{
"mcpServers": {
"apcore": {
"command": "npx",
"args": ["apcore-mcp", "--extensions-dir", "./extensions"]
}
}
}Add to .cursor/mcp.json in your project root:
{
"mcpServers": {
"apcore": {
"command": "npx",
"args": ["apcore-mcp", "--extensions-dir", "./extensions"]
}
}
}npx apcore-mcp --extensions-dir ./extensions \
--transport streamable-http \
--host 0.0.0.0 \
--port 9000Connect any MCP client to http://your-host:9000/mcp.
The APCoreMCP class is the recommended OOP entry point. It bundles a unified configuration object, lazy backend resolution (path / Registry / Executor), and exposes serve / asyncServe / toOpenaiTools as instance methods so you configure once and use everywhere.
import { APCoreMCP } from "apcore-mcp";
// 1. Point at an extensions directory (lazy discovery on first use)
const mcp = new APCoreMCP("./extensions", {
name: "my-server",
tags: ["public"],
observability: true,
});
// 2. Launch as MCP server (blocks until shutdown)
await mcp.serve({ transport: "streamable-http", port: 8000, explorer: true });
// 3. Or export OpenAI tool definitions
const tools = mcp.toOpenaiTools({ strict: true });
// 4. Or embed into an existing HTTP server
const app = await mcp.asyncServe({ explorer: true });
// app.handler is a Node.js request handler; call app.close() on shutdown
// 5. Or pass an existing Registry / Executor
import { Registry } from "apcore-js";
const registry = new Registry({ extensionsDir: "./extensions" });
await registry.discover();
const mcp2 = new APCoreMCP(registry, { name: "my-server", tags: ["public"] });Constructor
new APCoreMCP(
extensionsDirOrBackend: string | Registry | Executor,
options?: APCoreMCPOptions,
);The first argument is either a path to an apcore extensions directory (discovery is deferred to first use) or an existing Registry / Executor instance.
APCoreMCPOptions fields
name— MCP server name. Default:"apcore-mcp"version— MCP server version. Default: package versiontags— Filter modules by tag listprefix— Filter modules by ID prefixlogLevel— Minimum log level (DEBUG|INFO|WARNING|ERROR|CRITICAL)validateInputs— Validate inputs against schemas. Default:falsemetricsCollector—MetricsExporterortrueto auto-instantiateobservability— Enable the full metrics + usage observability stackasync—boolean | { enabled?, maxConcurrent?, maxTasks? }for the Async Task Bridge (F-043)authenticator— OptionalAuthenticator(HTTP transports only)requireAuth— Iftrue(default), reject unauthenticated requests with 401exemptPaths— Paths exempt from authenticationapprovalHandler— Optional approval handler passed to the ExecutoroutputFormatter— Custom function to format tool execution resultsmiddleware— Array of apcoreMiddlewareinstalled viaexecutor.use()acl— Optional apcoreACLinstance installed viaexecutor.setAcl()
Properties
.registry— The underlying apcoreRegistry(resolved on first access).executor— The underlying apcoreExecutor(populated afterserve()/asyncServe()).tools— List of discovered module IDs that will be exposed as tools (honourstags/prefix)
Methods
.serve(options?)— Launch an MCP server. AcceptsAPCoreMCPServeOptions:transport,host,port,onStartup,onShutdown,explorer,explorerPrefix,allowExecute,explorerTitle,explorerProjectName,explorerProjectUrl. Constructor-level options (auth, observability, middleware, acl, async, etc.) are applied automatically..asyncServe(options?)— Build an embeddable Node.js HTTP request handler. AcceptsAPCoreMCPAsyncServeOptions:explorer,explorerPrefix,allowExecute,explorerTitle,explorerProjectName,explorerProjectUrl,endpoint. Returns{ handler, close }..toOpenaiTools(options?)— Export modules as OpenAI-compatible tool definitions. AcceptsToOpenaiToolsOptions:embedAnnotations,strict.tags/prefixare inherited from the constructor.
Launch an MCP Server that exposes all apcore modules as tools.
function serve(
registryOrExecutor: Registry | Executor,
options?: {
// Transport
transport?: "stdio" | "streamable-http" | "sse";
host?: string;
port?: number;
// Identity
name?: string;
version?: string;
// Lifecycle
onStartup?: () => void | Promise<void>;
onShutdown?: () => void | Promise<void>;
// Module filtering / discovery
tags?: string[] | null;
prefix?: string | null;
dynamic?: boolean;
validateInputs?: boolean;
logLevel?: "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL";
// Async Task Bridge (F-043)
async?: boolean | { enabled?: boolean; maxConcurrent?: number; maxTasks?: number };
// Executor wiring
middleware?: unknown[];
acl?: unknown;
approvalHandler?: unknown;
strategy?: string;
// Observability (F-044)
metricsCollector?: MetricsExporter | boolean;
observability?: ObservabilityFlag;
trace?: boolean;
// Output handling
outputFormatter?: (result: Record<string, unknown>) => string;
redactOutput?: boolean;
// Auth (HTTP transports only)
authenticator?: Authenticator;
requireAuth?: boolean;
exemptPaths?: string[];
// Tool Explorer UI
explorer?: boolean;
explorerPrefix?: string;
allowExecute?: boolean;
explorerTitle?: string;
explorerProjectName?: string;
explorerProjectUrl?: string;
// Adapter overrides (advanced — Extension Bridge)
schemaConverter?: SchemaConverter;
annotationMapper?: AnnotationMapper;
errorMapper?: ErrorMapper;
}
): Promise<void>;Options reference:
Transport
transport—"stdio"(default),"streamable-http", or"sse"host— Host address for HTTP-based transports. Default:"127.0.0.1"port— Port for HTTP-based transports. Default:8000
Identity
name— MCP server name. Default:"apcore-mcp"version— MCP server version. Default: package version
Lifecycle
onStartup— Async callback invoked before the server startsonShutdown— Async callback invoked after the server stops (or on error)
Module filtering / discovery
tags— Filter modules by tag list. Default:null(no filtering)prefix— Filter modules by ID prefix. Default:null(no filtering)dynamic— Enable dynamic tool registration viaRegistryListener. Default:falsevalidateInputs— Validate inputs against schemas before dispatch. Default:falselogLevel— Minimum log level. Suppresses console methods below this level
Async Task Bridge (F-043)
async— Enable the AsyncTaskBridge and__apcore_task_*meta-tools. Passfalseto disable, or{ maxConcurrent, maxTasks }for fine-grained tuning. Default:true
Executor wiring
middleware— Array of apcoreMiddlewareinstances installed viaexecutor.use(). Appended to any middleware declared under Config Bus keymcp.middlewareacl— Optional apcoreACLinstance installed viaexecutor.setAcl(). Caller-supplied ACL takes precedence overmcp.aclConfig Bus entry. Ifsys_modules.enabledis turned on with noacl/directory configured,ACL.discover()returnsnull— identical to a project that never configured an ACL — and the entiresystem.*management surface (includingsystem.control.*) is reachable with no authorization. Seesrc/acl-builder.tsfor a copy-pasteable rule template that gates it correctly.approvalHandler— Optional approval handler passed to the Executor (e.g.ElicitationApprovalHandler)strategy— Execution strategy name passed to the Executor (e.g."standard","internal")
Observability (F-044)
metricsCollector—MetricsExporterinstance, ortrueto auto-instantiate apcore-js'sMetricsCollectorand installMetricsMiddlewareobservability— Enable the full observability stack (metrics + usage middleware) and expose/metrics+/usageendpointstrace— Whentrue, enables pipeline trace viacallWithTrace(). Adds_meta.traceto non-streaming tool responses. Default:false
Output handling
outputFormatter— Custom function to format tool execution results. When undefined, results are serialized withJSON.stringify(result)redactOutput— Whentrue(default), redact sensitive fields from tool output via apcore'sredactSensitive()before formatting
Auth (HTTP transports only)
authenticator—Authenticatorinstance for request authenticationrequireAuth— Iftrue(default), unauthenticated requests are rejected with 401. Set tofalsefor permissive modeexemptPaths— Paths exempt from authentication. Default:["/health", "/metrics"]
Tool Explorer UI
explorer— Enable the browser-based Tool Explorer UI (HTTP only). Default:falseexplorerPrefix— URL prefix for the explorer. Default:"/explorer"allowExecute— Allow tool execution from the explorer UI. Default:falseexplorerTitle— Custom title for the Tool Explorer UI pageexplorerProjectName— Project name shown in the explorer UI footerexplorerProjectUrl— Project URL shown in the explorer UI footer
Adapter overrides (advanced — Extension Bridge, F-042)
schemaConverter— Override the defaultSchemaConverter(custom JSON Schema strictness/dialect)annotationMapper— Override the defaultAnnotationMapper(custom annotation wire format)errorMapper— Override the defaultErrorMapperconsumed byExecutionRouter
Embed the MCP server into a larger Node.js HTTP application. Returns an HTTP request handler and a close function for lifecycle management.
import { asyncServe } from "apcore-mcp";
const { handler, close } = await asyncServe(executor, {
name: "apcore-mcp",
explorer: true,
allowExecute: true,
});
// Mount in a custom HTTP server
const server = http.createServer(handler);
server.listen(8000);
// Clean up when done
await close();Accepts the same options as serve() except transport, host, port, onStartup, and onShutdown.
By default, tool execution results are serialized as JSON (JSON.stringify). You can customize this by passing an outputFormat name or a custom outputFormatter function.
Built-in formats (requires apcore-toolkit 0.7.0+):
// Via CLI
// npx apcore-mcp --extensions-dir ./extensions --output-format csv
// Via API
const mcp = new APCoreMCP("./extensions", { outputFormat: "csv" });Supports json, csv, and jsonl. Non-tabular data gracefully falls back to JSON.
The common structured-output validation subset is Draft-07, Draft 2019-09 and Draft 2020-12 (default). Mixed-draft embedded schemas and other declared drafts are rejected rather than reinterpreted; repeating the same draft is allowed. Numeric JSON text is promoted only when parsing and serialization preserve its decimal value. Rounded/underflowed values return a payload-free error when an output schema is advertised, otherwise the original text remains unchanged. Runtime integer ranges differ; use JSON strings for portable arbitrary-size IDs.
This capability is prepared in the 0.23.0 development source. Publication awaits the corrected apcore dependency; published 0.22.3 does not include it.
Clients negotiating MCP 2025-06-18 or a later supported version receive
outputSchema for modules with an object output schema, and successful JSON
object results include structuredContent. The schema preserves the descriptor's
constraints and references. Async task submission wrappers omit the final module
output schema because their response is a task envelope.
Structured content is derived from the governed router's already-redacted JSON
text, so content and structuredContent describe the same value. The bridge
validates that value against the advertised raw JSON schema after redaction.
Validation defaults to Draft 2020-12 and respects explicitly declared Draft-07
or Draft 2019-09 semantics. Local references and anchors are resolved without fetching external
schemas; unresolved references or unsupported dialects fail closed. Formats are
annotations, and validation does not coerce values, insert defaults, or remove
additional properties. In-document embedded resources respect their identifier
scopes. Every reference-bearing schema resource is checked, including unused
definitions and inactive branches; opaque defaults, examples and unknown
vocabulary data are preserved. A mismatch,
or an output that cannot represent the advertised object, returns a payload-free
isError: true result. Arrays, scalars, and custom text without an advertised
object schema remain text-only. Custom CSV/JSONL output cannot satisfy an
advertised object schema; use JSON output for those structured tool calls.
MCP 2025-03-26 and 2024-11-05 clients retain text-only results and omit these
fields. Streamable HTTP selects the format from each request's
MCP-Protocol-Version header, defaulting to 2025-03-26 when absent; request
body metadata cannot enable structured results.
registerHandlers supports caller-supplied MCP SDK Server instances. Register
handlers before client initialization so the bridge can observe the SDK's
negotiated protocol. A server already connected but awaiting initialization is
supported. An initialization completed before handler registration cannot be
recovered through the upstream SDK's public API; such session transports retain
legacy output until initialization is observed. HTTP requests still select the
format from their protocol-version header.
When explorer: true is passed to serve(), a browser-based Tool Explorer UI is mounted on HTTP transports. It provides an interactive page for browsing tool schemas and testing tool execution.
await serve(registry, {
transport: "streamable-http",
explorer: true,
allowExecute: true,
});
// Open http://127.0.0.1:8000/explorer/ in a browserEndpoints:
| Endpoint | Description |
|---|---|
GET /explorer/ |
Interactive HTML page (self-contained, no external dependencies) |
GET /explorer/tools |
JSON array of all tools with name, description, annotations |
GET /explorer/tools/<name> |
Full tool detail with inputSchema |
POST /explorer/tools/<name>/call |
Execute a tool (requires allowExecute: true) |
- HTTP transports only (
streamable-http,sse). Silently ignored forstdio. - Execution disabled by default — set
allowExecute: trueto enable Try-it. - Custom prefix — use
explorerPrefix: "/browse"to mount at a different path. - Authorization UI — Swagger-UI-style Authorization input field. Paste a Bearer token to authenticate tool execution requests. Generated cURL commands automatically include the Authorization header.
apcore-mcp supports JWT Bearer token authentication for HTTP-based transports.
import { serve, JWTAuthenticator } from "apcore-mcp";
const authenticator = new JWTAuthenticator({
key: "your-secret-key",
algorithms: ["HS256"],
audience: "my-app",
issuer: "auth-service",
// Map custom claims to Identity fields
claimMapping: {
id: "sub",
type: "type",
roles: "roles",
attrs: ["email", "org"], // Extra claims → Identity.attrs
},
// Claims that must be present in the token (default: ["sub"])
requireClaims: ["sub", "email"],
// Set to false for permissive mode (allow unauthenticated requests)
requireAuth: true,
});
await serve(executor, {
transport: "streamable-http",
authenticator,
// Custom exempt paths (default: ["/health", "/metrics"])
exemptPaths: ["/health", "/metrics", "/status"],
});| Flag | Default | Description |
|---|---|---|
--jwt-secret |
— | JWT secret key for Bearer token authentication |
--jwt-key-file |
— | Path to PEM key file for JWT verification |
--jwt-algorithm |
HS256 |
JWT algorithm |
--jwt-audience |
— | Expected audience claim |
--jwt-issuer |
— | Expected issuer claim |
--jwt-require-auth |
true |
Require auth. Use --jwt-permissive to allow unauthenticated requests |
--jwt-permissive |
false |
Overrides --jwt-require-auth and allows unauthenticated requests |
--exempt-paths |
/health,/metrics,/usage |
Comma-separated paths exempt from auth |
JWT key resolution priority: --jwt-key-file > --jwt-secret > APCORE_JWT_SECRET environment variable.
# Authenticated request
curl -X POST http://localhost:8000/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-jwt-token>" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
# Health check (always exempt)
curl http://localhost:8000/healthWhen auth is enabled, each request runs inside an AsyncLocalStorage
context whose value is the verified Identity. Modules and middleware
read it with getCurrentIdentity() — no need to thread it through every
function.
import { getCurrentIdentity, identityStorage } from "apcore-mcp";
import type { Identity } from "apcore-mcp";
// Inside a module / middleware called from a tool invocation:
const id: Identity | null = getCurrentIdentity();
if (id) {
// id.id (user id), id.type ("user" | "service" | ...), id.roles, id.attrs
}
// Lower-level access (e.g. for running custom code inside a known identity):
identityStorage.run(myIdentity, async () => {
/* code that calls getCurrentIdentity() */
});Python parity: apcore_mcp.get_current_identity() / auth_identity_var.
Rust parity: apcore_mcp::AUTH_IDENTITY task-local.
If you embed the bridge via asyncServe(...) instead of serve(...),
gate the explorer routes behind an auth hook with
buildExplorerAuthHook(authenticator):
import { buildExplorerAuthHook } from "apcore-mcp";
import { JWTAuthenticator } from "apcore-mcp";
const auth = new JWTAuthenticator({ key: process.env.JWT_SECRET! });
const guard = buildExplorerAuthHook(auth);
// `guard` is a `(req, next) => Promise<Response>` hook accepted by
// mcp-embedded-ui's Hono router. Returns a 401 response when the
// Authorization header is missing or invalid; otherwise runs `next()`
// inside `identityStorage.run(identity, ...)` so the explorer's
// downstream handlers see the authenticated identity.For callers who don't want the serve(... observability: true) shorthand,
installObservability wires the metrics + usage middleware onto an
existing Executor directly. It is what serve() calls internally.
import { installObservability } from "apcore-mcp";
import type { ObservabilityFlag } from "apcore-mcp";
const stack = await installObservability(executor, /* metricsCollector */ null, true satisfies ObservabilityFlag);
// stack.metricsCollector / stack.usageCollector are then exposed on
// the `/metrics` and `/usage` endpoints by your own HTTP wiring.Export apcore modules as OpenAI-compatible tool definitions.
function toOpenaiTools(
registryOrExecutor: Registry | Executor,
options?: {
embedAnnotations?: boolean;
strict?: boolean;
tags?: string[];
prefix?: string;
}
): OpenAIToolDef[];Options:
embedAnnotations— Append annotation metadata to tool descriptions (default:false)strict— Enable OpenAI strict mode: addsadditionalProperties: false, makes all properties required, wraps optional properties with nullable (default:false)tags— Filter modules by tagsprefix— Filter modules by ID prefix
Report execution progress to the MCP client. No-ops silently when called outside an MCP context (no callback injected).
import { reportProgress } from "apcore-mcp";
// Inside a module's execute() method:
await reportProgress(context, 5, 10, "Processing item 5 of 10");Parameters:
context— Object with adatadict (apcore Context or BridgeContext)progress— Current progress valuetotal— Optional total for percentage calculationmessage— Optional human-readable progress message
Ask the MCP client for user input via the elicitation protocol. Returns null when called outside an MCP context.
import { elicit } from "apcore-mcp";
import type { ElicitResult } from "apcore-mcp";
// Inside a module's execute() method:
const result: ElicitResult | null = await elicit(
context,
"Are you sure you want to proceed?",
{
type: "object",
properties: {
confirmed: { type: "boolean", description: "Confirm action" },
},
required: ["confirmed"],
},
);
if (result?.action === "accept") {
// User confirmed
}Parameters:
context— Object with adatadict (apcore Context or BridgeContext)message— Message to display to the userrequestedSchema— Optional JSON Schema describing the expected input
Returns: ElicitResult with action ("accept", "decline", or "cancel") and optional content, or null if not in an MCP context.
apcore-mcp registers an mcp namespace with the apcore Config Bus when serve() or asyncServe() is called. MCP settings can live alongside other apcore configuration in a single apcore.yaml:
apcore:
version: "1.0.0"
mcp:
transport: streamable-http
host: 0.0.0.0
port: 9000
explorer: true
require_auth: falseEnvironment variable overrides use the APCORE_MCP_ prefix:
APCORE_MCP_TRANSPORT=streamable-http
APCORE_MCP_PORT=9000
APCORE_MCP_EXPLORER=trueDefaults: transport=stdio, host=127.0.0.1, port=8000, explorer=false, require_auth=true.
The namespace, prefix, and defaults are also available as importable constants:
import { MCP_NAMESPACE, MCP_ENV_PREFIX, MCP_DEFAULTS, registerMcpNamespace } from "apcore-mcp";# Install dependencies
npm install
# Type check
npm run typecheck
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build
npm run build
# Watch mode
npm run devApache-2.0