MCP服务器与OpenAI工具桥接器

apcore-mcp 可将任意基于 apcore 的项目零代码自动转换为 MCP Server 和 OpenAI 工具提供者,适用于将现有 apcore 模块接入 Claude、Cursor 等 AI 客户端的场景。

  • by aiperceivable
  • Rust
  • 混合部署
  • 1 星标
  • 更新于
在 GitHub 查看 aipartnerup/apcore-mcp-rust

使用说明

apcore-mcp-rust

Automatic MCP Server & OpenAI Tools Bridge for apcore (Rust edition).

apcore-mcp turns any apcore-based project into an MCP Server and OpenAI tool provider — with zero code changes to your existing project.

┌──────────────────┐
│  axum-apcore     │  ← your existing apcore project (unchanged)
│  project         │
└────────┬─────────┘
         │  extensions directory
         ▼
┌──────────────────┐
│  apcore-mcp-rust │  ← just install & point to extensions dir
└───┬──────────┬───┘
    │          │
    ▼          ▼
  MCP       OpenAI
 Server      Tools

MCP input schemas

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.

Design Philosophy

  • Zero intrusion — your apcore project needs no code changes, no imports, no dependencies on apcore-mcp
  • Zero configuration — point to an extensions directory, everything is auto-discovered
  • Pure adapter — apcore-mcp reads from the apcore Registry; it never modifies your modules
  • Works with any apcore project — if it uses the apcore Module Registry, apcore-mcp can serve it

Documentation

For full documentation, including Quick Start guides, visit: https://aiperceivable.github.io/apcore-mcp/

Installation

As a library

cargo add apcore-mcp

As a CLI tool

cargo install apcore-mcp

Requires Rust 1.86+ and apcore >= 0.32.3 + apcore-toolkit >= 0.14.1. The OpenAPI backend registers the module IDs emitted by OpenAPIScanner; toolkit 0.13 introduced the required Canonical ID normalization.

Complete stdio shutdown draining of core invocations after timeout/cancellation requires apcore >= 0.32.3. The 0.32.2 compatibility fallback preserves stdout isolation and bridge-owned callback/sweep cleanup, but cannot join late core module execution. See stdio stdout ownership for the process and platform boundaries.

Quick Start

Zero-code approach (CLI)

If you already have an apcore-based project with an extensions directory, just run:

apcore-mcp --extensions-dir /path/to/your/extensions

All modules are auto-discovered and exposed as MCP tools. No code needed.

Programmatic approach (Rust API)

The APCoreMCP builder is the recommended entry point — one object, all capabilities:

use std::sync::Arc;
use apcore::config::Config;
use apcore::executor::Executor;
use apcore::registry::registry::Registry;
use apcore_mcp::APCoreMCP;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a registry and register your modules.
    let registry = Registry::new();
    // registry.register("my.tool", Box::new(MyModule), descriptor)?;

    // 2. Wrap it in an Executor — APCoreMCP requires an executor backend.
    let executor = Arc::new(Executor::new(registry, Config::default()));

    // 3. Build and serve.
    let mcp = APCoreMCP::builder()
        .backend(executor)
        .name("my-server")
        .transport("streamable-http")
        .port(8000)
        .build()?;

    // serve() is synchronous and blocks. It spawns its own Tokio runtime;
    // do NOT call it from inside an active runtime (use async_serve() for
    // embedded use, see API Overview below).
    mcp.serve()?;

    Ok(())
}

Backend note (v0.15.0): The Rust SDK's BackendSource::Executor is the functional path. BackendSource::ExtensionsDir (string path) and BackendSource::Registry are reserved variants that currently return a BackendResolution error from build() — wrap your registry in an Executor first, as shown above.

Function-based API (still supported)
use apcore_mcp::{serve, to_openai_tools, ServeConfig, OpenAIToolsConfig};

// Pass an Arc<Executor> as the backend (same constraint as the builder API).
serve(executor.clone(), ServeConfig::default())?;

let tools = to_openai_tools(executor, OpenAIToolsConfig::default())?;

Integration with Existing Projects

Typical apcore project structure

your-project/
├── extensions/          ← modules live here
│   ├── image_resize/
│   ├── text_translate/
│   └── ...
├── src/main.rs          ← your existing code (untouched)
└── ...

Adding MCP support

No changes to your project. Just run apcore-mcp alongside it:

# Install (one time)
cargo install apcore-mcp

# Run
apcore-mcp --extensions-dir ./extensions

Your existing application continues to work exactly as before. apcore-mcp operates as a separate process that reads from the same extensions directory.

Adding OpenAI tools support

For OpenAI integration, a thin script is needed — but still no changes to your existing modules:

use apcore_mcp::{to_openai_tools, OpenAIToolsConfig};

let tools = to_openai_tools("./extensions", OpenAIToolsConfig {
    strict: true,
    ..Default::default()
})?;
// Use with the OpenAI API

MCP Client Configuration

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "apcore": {
      "command": "apcore-mcp",
      "args": ["--extensions-dir", "/path/to/your/extensions"]
    }
  }
}

Claude Code

Add to .mcp.json in your project root:

{
  "mcpServers": {
    "apcore": {
      "command": "apcore-mcp",
      "args": ["--extensions-dir", "./extensions"]
    }
  }
}

Cursor

Add to .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "apcore": {
      "command": "apcore-mcp",
      "args": ["--extensions-dir", "./extensions"]
    }
  }
}

Remote HTTP access

apcore-mcp --extensions-dir ./extensions \
    --transport streamable-http \
    --host 0.0.0.0 \
    --port 9000

Connect any MCP client to http://your-host:9000/mcp.

SSE transport (deprecated)

--transport sse mounts GET /sse and POST /messages/. Prefer streamable HTTP; SSE remains for older clients.

Each GET /sse opens an independent session. The stream's first frame is the MCP endpoint event, which carries the URL to post to:

event: endpoint
data: /messages/?sessionId=1f8c...  <- server-minted, per connection

POST /messages/ requires that sessionId query parameter. It names the stream that receives the response, so a client must read the endpoint event before posting; posting to the bare /messages/ URL returns 400, as does naming a session that is not open. A spec-compliant SSE client already does this, and the TypeScript SDK behaves the same way.

Two consequences worth knowing:

  • params._meta.sessionId is overwritten with the connection's own session id on every message. Any value a client sends there is ignored. This is what binds async tasks to the connection, so closing the stream cancels the tasks it launched — and what stops a client naming someone else's session.
  • Closing the stream deregisters the session immediately. A message posted afterwards is rejected rather than silently absorbed.
  • Messages on one session are handled concurrently (up to 16 in flight), so a cheap request is not stuck behind an expensive one. Responses are not guaranteed to arrive in the order posted — correlate them by JSON-RPC id.

CLI Reference

apcore-mcp --extensions-dir PATH [OPTIONS]
Option Default Description
--extensions-dir (required) Path to apcore extensions directory
--transport stdio Transport: stdio, streamable-http, or sse
--host 127.0.0.1 Host for HTTP-based transports
--port 8000 Port for HTTP-based transports (1-65535)
--name apcore-mcp MCP server name (max 255 chars)
--version package version MCP server version string
--log-level INFO Logging: 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 auth (HTTP only)
--jwt-key-file — Path to PEM key file for JWT verification (e.g. RS256 public key)
--jwt-algorithm HS256 JWT signing algorithm
--jwt-audience — Expected JWT audience claim
--jwt-issuer — Expected JWT issuer claim
--jwt-require-auth on Require valid token; use --no-jwt-require-auth for permissive mode
--exempt-paths — Comma-separated paths exempt from auth (e.g. /health,/metrics)
--approval off Approval handler: elicit, auto-approve, always-deny, or off
--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.

Exit codes: 0 normal, 1 invalid arguments, 2 startup failure.

Rust API Reference

APCoreMCP (recommended)

The unified entry point — configure once, use everywhere:

use apcore_mcp::APCoreMCP;

let mcp = APCoreMCP::builder()
    .backend(executor)                   // Arc<Executor> (the functional backend in v0.15.0)
    .name("apcore-mcp")                  // server name
    .version("1.0.0")                    // defaults to crate version
    .tags(vec!["public".into()])         // filter modules by tags
    .prefix("image")                     // filter modules by ID prefix
    .transport("streamable-http")        // "stdio" | "streamable-http" | "sse"
    .host("127.0.0.1")                  // host for HTTP transports
    .port(8000)                          // port for HTTP transports
    .validate_inputs(true)               // validate inputs against schemas
    .authenticator(auth)                 // Authenticator for JWT/token auth (HTTP only)
    .metrics_collector(collector)         // MetricsExporter for /metrics endpoint
    .output_formatter(formatter)         // custom result formatting
    .approval_handler(handler)           // approval handler for runtime approval
    .build()?;

// Launch as MCP server (synchronous, blocking; spawns its own Tokio runtime).
// Use `serve_with_options(ServeOptions { ... })` to pass on_startup/on_shutdown
// hooks, the explorer config, or `dynamic = true`.
mcp.serve()?;

// Export as OpenAI tools.
// The method takes (embed_annotations, strict) as positional booleans;
// use named-arg-style locals so the call reads consistently with the
// OpenAIToolsConfig form shown later in this guide.
let embed_annotations = false;
let strict = true;
let tools = mcp.to_openai_tools(embed_annotations, strict)?;

// Inspect
let tool_names = mcp.tools();     // list of module IDs
let registry = mcp.registry();    // underlying Registry
let executor = mcp.executor();    // underlying Executor

serve() (function-based)

use apcore_mcp::{serve, ServeConfig};
use std::sync::Arc;

// As noted in the builder caveat above, BackendSource::ExtensionsDir
// (string path) currently returns a BackendResolution error. Build an
// Executor first and pass it as the backend.
let registry: Arc<apcore::Registry> = /* load extensions, e.g. via apcore::load_extensions("./extensions")? */;
let executor: Arc<apcore::Executor> = Arc::new(apcore::Executor::new(registry, apcore::Config::default()));

serve(executor, ServeConfig {
    transport: "streamable-http".into(),
    host: "127.0.0.1".into(),
    port: 8000,
    name: "apcore-mcp".into(),
    explorer: true,
    allow_execute: true,
    ..Default::default()
})?;

async_serve()

Embed the MCP server into a larger application (e.g. co-host with other services):

use apcore_mcp::{AsyncServeOptions, ExplorerOptions};

let app = mcp.async_serve(AsyncServeOptions {
    // explorer is an ExplorerOptions struct, not a bool — set the
    // inner `explorer: true` flag to mount the Tool Explorer UI.
    explorer: ExplorerOptions {
        explorer: true,
        ..Default::default()
    },
    ..Default::default()
}).await?;
// Mount `app` (an axum::Router) into your own axum Router

Tool Explorer

When explorer.explorer = true is passed via ServeOptions, a browser-based Tool Explorer UI is mounted on HTTP transports. It provides an interactive page for browsing tool schemas and testing tool execution.

use apcore_mcp::{ExplorerOptions, ServeOptions};

mcp.serve_with_options(ServeOptions {
    explorer: ExplorerOptions {
        explorer: true,
        allow_execute: true,
        ..Default::default()
    },
    ..Default::default()
})?;
// Open http://127.0.0.1:8000/explorer/ in a browser

Endpoints:

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 allow_execute=true)
  • HTTP transports only (streamable-http, sse). Silently ignored for stdio.
  • Execution disabled by default — set allow_execute=true to enable Try-it.
  • Custom prefix — use explorer_prefix="/browse" to mount at a different path.

JWT Authentication

Optional Bearer token authentication for HTTP transports. Supports symmetric (HS256) and asymmetric (RS256) algorithms.

use apcore_mcp::JWTAuthenticator;

let auth = JWTAuthenticator::new("my-secret", None, None, None, None, None, None);

let mcp = APCoreMCP::builder()
    .backend(executor)  // Arc<Executor> — see Quick Start for setup
    .transport("streamable-http")
    .authenticator(auth)
    .build()?;

Permissive mode — allow unauthenticated access (identity is None when no token is provided):

let auth = JWTAuthenticator::new("my-secret", None, None, None, None, None, Some(false));

Path exemption — bypass auth for specific paths via CLI:

apcore-mcp --extensions-dir ./extensions --jwt-secret my-secret --exempt-paths /health,/metrics

Approval Mechanism

Optional runtime approval for tool execution. Bridges MCP elicitation to apcore's approval system.

use apcore_mcp::ElicitationApprovalHandler;

let handler = ElicitationApprovalHandler::new(None);

let mcp = APCoreMCP::builder()
    .backend(executor)  // Arc<Executor> — see Quick Start for setup
    .approval_handler(Arc::new(handler))
    .build()?;

Built-in handlers:

Handler Description
ElicitationApprovalHandler Prompts the MCP client for user confirmation via elicitation
AutoApproveHandler Auto-approves all requests (dev/testing only)
AlwaysDenyHandler Rejects all requests (enforcement)

CLI usage:

apcore-mcp --extensions-dir ./extensions --approval elicit

Output Formatting

By default, tool execution results are serialized as JSON. You can customize this by passing an output_format name or a custom output_formatter closure.

Built-in formats (requires apcore-toolkit 0.7.0+):

// Via CLI
// apcore-mcp --extensions-dir ./extensions --output-format csv

// Via API
let mcp = APCoreMCP::builder()
    .backend(executor)
    .output_format(OutputFormat::Csv)
    .build()?;

Supports json, csv, and jsonl. Non-tabular data gracefully falls back to JSON.

Custom formatter: Pass a closure that converts a serde_json::Value into a string.

Extension Helpers

Modules can report progress and request user input during execution via MCP protocol callbacks. Both helpers no-op gracefully when called outside an MCP context.

use apcore_mcp::{report_progress, elicit};

// Inside a module's execute():
report_progress(&context, progress_cb.as_ref(), 50.0, Some(100.0), Some("Halfway done")).await;

let result = elicit(&context, elicit_cb.as_ref(), "Confirm deletion?", Some(&schema)).await;
if let Some(r) = result {
    if r.action == ElicitAction::Accept {
        // proceed
    }
}

/metrics Prometheus Endpoint

When metrics_collector is provided, a /metrics HTTP endpoint is exposed that returns metrics in Prometheus text exposition format.

  • Available on HTTP-based transports only (streamable-http, sse). Not available with stdio transport.
  • Returns Prometheus text format with Content-Type text/plain; version=0.0.4; charset=utf-8.
  • Returns 404 when no metrics_collector is configured.

to_openai_tools()

use apcore_mcp::{to_openai_tools, OpenAIToolsConfig};
use std::sync::Arc;

// Same backend constraint as serve(): pass an Arc<Executor>, not a
// path string (BackendSource::ExtensionsDir currently errors out).
let registry: Arc<apcore::Registry> = /* load extensions, e.g. via apcore::load_extensions("./extensions")? */;
let executor: Arc<apcore::Executor> = Arc::new(apcore::Executor::new(registry, apcore::Config::default()));

let tools = to_openai_tools(executor, OpenAIToolsConfig {
    embed_annotations: false,   // append annotation hints to descriptions
    strict: true,               // OpenAI Structured Outputs strict mode
    tags: Some(vec!["image".into()]),  // filter by tags
    prefix: None,               // filter by module ID prefix
})?;

Strict mode (strict: true): sets additionalProperties: false, makes all properties required (optional ones become nullable), removes defaults.

Annotation embedding (embed_annotations: true): appends [Annotations: read_only, idempotent] to descriptions.

Filtering: tags or prefix to expose a subset of modules.

Features

  • Auto-discovery — all modules in the extensions directory are found and exposed automatically
  • Display overlay — metadata["display"]["mcp"] controls MCP tool names, descriptions, and guidance per module (§5.13)
  • Markdown tool descriptions (MCPServerFactory::with_rich_description(true), v0.15+) — render Tool.description as canonical apcore-toolkit Markdown so LLMs get more decision-relevant signal per token; backed by apcore_toolkit::format_module(ModuleStyle::Markdown).
  • Module preview meta-tool (__apcore_module_preview, v0.15+) — drives executor.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.
  • Three transports — stdio (default, for desktop clients), Streamable HTTP, and SSE
  • JWT authentication — optional Bearer token auth for HTTP transports with JWTAuthenticator, permissive mode, PEM key file support, and env var fallback
  • Approval mechanism — runtime approval via MCP elicitation, auto-approve, or always-deny handlers
  • AI guidance — error responses include retryable, ai_guidance, user_fixable, and suggestion fields for agent consumption
  • AI intent metadata — tool descriptions enriched with x-when-to-use, x-when-not-to-use, x-common-mistakes, x-workflow-hints from module metadata
  • Extension helpers — modules can call report_progress() and elicit() during execution for MCP progress reporting and user input
  • Annotation mapping — apcore annotations (readonly, destructive, idempotent) map to MCP ToolAnnotations
  • Schema conversion — JSON Schema $ref/$defs inlining, strict mode for OpenAI Structured Outputs
  • Error sanitization — ACL errors and internal errors are sanitized; stack traces are never leaked
  • Dynamic registration — modules registered/unregistered at runtime are reflected immediately
  • Dual output — same registry powers both MCP Server and OpenAI tool definitions
  • Tool Explorer — browser-based UI for browsing schemas and testing tools interactively
  • Config Bus integration — registers an mcp namespace with the apcore Config Bus; configure transport, host, port, and more via unified apcore.yaml or APCORE_MCP_* env vars
  • Error Formatter Registry — registers an MCP-specific error formatter for ecosystem-wide consistent error handling

Config Bus Integration

apcore-mcp registers an mcp namespace with the apcore Config Bus during APCoreMCPBuilder::build(). 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: false

Environment variable overrides use the APCORE_MCP_ prefix:

APCORE_MCP_TRANSPORT=streamable-http
APCORE_MCP_PORT=9000
APCORE_MCP_EXPLORER=true

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

use apcore_mcp::{MCP_NAMESPACE, MCP_ENV_PREFIX, mcp_defaults, register_mcp_namespace};

Security note — system.* management modules. Enabling apcore's sys_modules.enabled registers system.health.* / system.usage.* / system.manifest.* (served here as MCP resources) and, with sys_modules.events.enabled, system.control.* (served as tools) into the registry this server exposes — but enabling it does not by itself protect them. With no mcp.acl section configured, ACL::discover() finds no acl/ directory and returns nothing, which is indistinguishable from "no ACL was ever configured": every caller can read health/usage/manifest and, more importantly, invoke system.control.* (reload modules, change runtime config, toggle features). Always pair sys_modules.enabled: true with an mcp.acl section — see the copy-pasteable rule template in acl_builder's module documentation (aiperceivable/apcore-mcp#14, #15). serve() / async_serve() log a loud startup warning via Executor::governance_state() when this gap is detected, but the warning is advisory and does not block startup.

How It Works

Mapping: apcore to MCP

apcore MCP
module_id Tool name
description Tool description
input_schema inputSchema
annotations.readonly ToolAnnotations.readOnlyHint
annotations.destructive ToolAnnotations.destructiveHint
annotations.idempotent ToolAnnotations.idempotentHint
annotations.open_world ToolAnnotations.openWorldHint

Mapping: apcore to OpenAI Tools

apcore OpenAI
module_id (image.resize) name (image-resize)
description description
input_schema parameters

Module IDs with dots are normalized to dashes for OpenAI compatibility (bijective mapping).

Architecture

Your apcore project (unchanged)
    │
    │  extensions directory
    ▼
apcore-mcp-rust (separate process / library call)
    │
    ├── MCP Server path
    │     SchemaConverter + AnnotationMapper
    │       → MCPServerFactory → ExecutionRouter → TransportManager
    │
    └── OpenAI Tools path
          SchemaConverter + AnnotationMapper + IDNormalizer
            → OpenAIConverter → Vec<Value>

Development

Toolkit 0.14.1 is published, but the crates.io index did not return that version during local verification. Source builds currently use the sibling checkout ../apcore-toolkit-rust with the >=0.14.1 version requirement; clone that repository alongside this SDK. The local release commit matches the published 0.14.1 crate. Remove the temporary path dependency once Cargo can resolve the indexed release.

git clone https://github.com/aiperceivable/apcore-mcp-rust.git
cd apcore-mcp-rust
make setup                       # install toolchain + pre-commit hook
make check                       # run all checks
cargo test                       # comprehensive test suite spanning unit + integration tests across server, auth, adapters, converters, helpers, async-task, explorer layers (~821 tests)

Common Commands

Command Description
make check Run all checks (format, lint, chars, tests)
make test Run all tests
make lint Run Clippy with warnings-as-errors
make fmt Auto-format code
make clean Clean build artifacts

License

Apache-2.0

Structured MCP results (0.23.0, pending publication)

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.

The development implementation negotiates MCP 2025-06-18, preserves legacy 2024-11-05 and 2025-03-26, advertises object outputSchema and returns redacted structuredContent together with JSON text. Results are validated after redaction against the raw advertised schema with its declared draft. Format keywords remain annotations; local definitions are resolved, while external HTTP/file retrieval is disabled. Schema failures discard result metadata as well as the payload. An advertised object schema requires a primary JSON object text result. Additional text and trace diagnostics remain intact. Stdio/SSE negotiation is session-scoped; stateless HTTP uses each request's MCP-Protocol-Version header and defaults to legacy when absent. Present unsupported or malformed protocol headers return HTTP 400 on POST, GET and DELETE. The Rust implementation supports 2024-11-05, 2025-03-26 and 2025-06-18; other initialize versions receive one of these supported alternatives. Native Python and TypeScript MCP SDKs can negotiate additional protocol revisions. Legacy responses omit structured fields even when a custom handler provides them. Authentication, approvals and executor governance retain their existing paths. The prepared 0.23.0 SDK and its corrected apcore dependency must be published before consumers can use these additions.