Skip to content

Repository files navigation

apcore-mcp logo

apcore-mcp

Automatic MCP Server & OpenAI Tools Bridge for apcore.

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

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

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 xxx-apcore project — if it uses the apcore Module Registry, apcore-mcp can serve it

Documentation

For full documentation, including Quick Start guides for both Python and TypeScript, visit: https://aiperceivable.github.io/apcore-mcp/

Installation

Install apcore-mcp alongside your existing apcore project:

pip install apcore-mcp

That's it. Your existing project requires no changes.

Markdown tool descriptions (rich_description=True) and the built-in csv / jsonl output formats are powered by apcore-toolkit, which ships as an optional extra — install it when you want either:

pip install 'apcore-mcp[markdown]'

Without the extra, both features degrade gracefully: descriptions fall back to the plain one-liner and output falls back to JSON, each with a WARN log.

Quick Start

Try it now

The repo includes 5 example modules (class-based + binding.yaml) you can run immediately:

pip install -e .
PYTHONPATH=./examples/binding_demo python examples/run.py
# Open http://127.0.0.1:8000/explorer/

See examples/README.md for all run modes and module details.

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 (Python API)

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

fromapcore_mcpimportAPCoreMCPmcp=APCoreMCP("./extensions")
# Launch as MCP Servermcp.serve()
# Or with HTTP + Explorer UImcp.serve(transport="streamable-http", port=8000, explorer=True)
# Or export as OpenAI toolstools=mcp.to_openai_tools()

You can also pass an existing Registry or Executor:

fromapcoreimportRegistryfromapcore_mcpimportAPCoreMCPregistry=Registry(extensions_dir="./extensions")
registry.discover()
mcp=APCoreMCP(registry, name="my-server", tags=["public"])
Function-based API (still supported)
fromapcoreimportRegistryfromapcore_mcpimportserve, to_openai_toolsregistry=Registry(extensions_dir="./extensions")
registry.discover()
serve(registry)
tools=to_openai_tools(registry)

Integration with Existing Projects

Typical apcore project structure

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

Adding MCP support

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

# Install (one time)
pip 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:

fromapcoreimportRegistryfromapcore_mcpimportto_openai_toolsregistry=Registry(extensions_dir="./extensions")
registry.discover()
tools=to_openai_tools(registry)
# Use with openai.chat.completions.create(tools=tools)

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.

CLI Reference

apcore-mcp --extensions-dir PATH [OPTIONS]
OptionDefaultDescription
--extensions-dir(required)Path to apcore extensions directory
--transportstdioTransport: stdio, streamable-http, or sse
--host127.0.0.1Host for HTTP-based transports
--port8000Port for HTTP-based transports (1-65535)
--nameapcore-mcpMCP server name (max 255 chars)
--versionpackage versionMCP server version string
--log-levelINFOLogging: DEBUG, INFO, WARNING, ERROR
--exploreroffEnable the browser-based Tool Explorer UI (HTTP only)
--explorer-prefix/explorerURL prefix for the explorer UI
--allow-executeoffAllow tool execution from the explorer UI
--jwt-secretJWT secret key for Bearer token auth (HTTP only)
--jwt-key-filePath to PEM key file for JWT verification (e.g. RS256 public key)
--jwt-algorithmHS256JWT signing algorithm
--jwt-audienceExpected JWT audience claim
--jwt-issuerExpected JWT issuer claim
--jwt-require-authonRequire valid token; use --no-jwt-require-auth for permissive mode
--exempt-pathsComma-separated paths exempt from auth (e.g. /health,/metrics)
--approvaloffApproval handler: elicit, auto-approve, always-deny, or off
--output-formatjsonBuilt-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.

Python API Reference

APCoreMCP (recommended)

The unified entry point — configure once, use everywhere:

fromapcore_mcpimportAPCoreMCPmcp=APCoreMCP(
"./extensions", # path, Registry, or Executorname="apcore-mcp", # server nameversion=None, # defaults to package versiontags=None, # filter modules by tagsprefix=None, # filter modules by ID prefixlog_level=None, # logging level ("DEBUG", "INFO", etc.)validate_inputs=False, # validate inputs against schemasmetrics_collector=None, # MetricsExporter | bool — `True` auto-instantiates the collectorobservability=False, # enable MetricsMiddleware + UsageMiddleware + /metrics + /api/usageauthenticator=None, # Authenticator for JWT/token auth (HTTP only)require_auth=True, # False = permissive mode (no 401)exempt_paths=None, # exact paths that bypass authapproval_handler=None, # approval handler for runtime approvaloutput_formatter=None, # default: None (raw JSON); pass to_markdown to opt into apcore-toolkit Markdownmiddleware=None, # list[Middleware] — user middleware applied after built-insacl=None, # apcore.ACL — module access controlasync_tasks=True, # enable F-043 Async Task Bridgeasync_max_concurrent=10, # max concurrent async tasksasync_max_tasks=1000, # max queued async tasks
)
# Note: redact_output, strategy, and trace are configurable on the# function-based serve() / async_serve(); they are not exposed on# APCoreMCP.serve() (see the `serve()` reference below).# Launch as MCP server (blocking)mcp.serve(transport="streamable-http", port=8000, explorer=True)
# Export as OpenAI toolstools=mcp.to_openai_tools(strict=True)
# Embed into ASGI appasyncwithmcp.async_serve(explorer=True) asapp:
...
# Inspectmcp.tools# list of module IDsmcp.registry# underlying Registrymcp.executor# underlying Executor

serve() (function-based)

fromapcore_mcpimportserveserve(
registry_or_executor, # Registry or Executortransport="stdio", # "stdio" | "streamable-http" | "sse"host="127.0.0.1", # host for HTTP transportsport=8000, # port for HTTP transportsname="apcore-mcp", # server nameversion=None, # defaults to package versionon_startup=None, # callback before transport startson_shutdown=None, # callback after transport completestags=None, # filter modules by tagsprefix=None, # filter modules by ID prefixlog_level=None, # logging level ("DEBUG", "INFO", etc.)dynamic=False, # rebuild tools on registry eventsvalidate_inputs=False, # validate inputs against schemasmetrics_collector=None, # MetricsExporter | bool — `True` auto-instantiates apcore.observability.MetricsCollectorexplorer=False, # enable browser-based Tool Explorer UIexplorer_prefix="/explorer", # URL prefix for the explorerallow_execute=False, # allow tool execution from the explorerexplorer_title="MCP Tool Explorer",
explorer_project_name=None,
explorer_project_url=None,
authenticator=None, # Authenticator for JWT/token auth (HTTP only)require_auth=True, # False = permissive mode (no 401)exempt_paths=None, # exact paths that bypass authapproval_handler=None, # approval handler for runtime approvaloutput_formatter=None, # default None (raw JSON); pass apcore_toolkit.to_markdown to opt instrategy=None, # pipeline strategy preset: "standard" | "internal" | "testing" | "performance" | "minimal"redact_output=True, # mask x-sensitive / _secret_* fields in outputstrace=False, # enable per-call apcore pipeline trace metadatamiddleware=None, # list[Middleware] — applied after built-insacl=None, # apcore.ACL — module access controlobservability=False, # enable MetricsMiddleware + UsageMiddleware + /metrics + /api/usageasync_tasks=True, # enable F-043 Async Task Bridgeasync_max_concurrent=10, # max concurrent async tasksasync_max_tasks=1000, # max queued async tasks# Note: schema_converter / annotation_mapper / error_mapper hooks are reserved for v0.16+ (EB-2)
)

Accepts either a Registry or Executor. When a Registry is passed, an Executor is created automatically.

async_serve()

Embed the MCP server into a larger ASGI application (e.g. co-host with A2A, Django ASGI):

fromapcore_mcpimportasync_serveasyncwithasync_serve(registry, explorer=True) asmcp_app:
combined=Starlette(routes=[
Mount("/mcp", app=mcp_app),
Mount("/a2a", app=a2a_app),
])
config=uvicorn.Config(combined, host="0.0.0.0", port=8000)
awaituvicorn.Server(config).serve()

Accepts the same parameters as serve() (except transport, host, port, on_startup, on_shutdown). Returns a Starlette app via async context manager.

Tool Explorer

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.

serve(registry, transport="streamable-http", explorer=True, allow_execute=True)
# Open http://127.0.0.1:8000/explorer/ in a browser

Endpoints:

EndpointDescription
GET /explorer/Interactive HTML page (self-contained, no external dependencies)
GET /explorer/toolsJSON array of all tools with name, description, annotations
GET /explorer/tools/<name>Full tool detail with inputSchema
POST /explorer/tools/<name>/callExecute 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.

fromapcore_mcp.authimportJWTAuthenticatorauth=JWTAuthenticator(key="my-secret")
serve(
registry,
transport="streamable-http",
authenticator=auth,
explorer=True,
allow_execute=True,
)

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

serve(registry, transport="streamable-http", authenticator=auth, require_auth=False)

Path exemption — bypass auth for specific paths:

serve(registry, transport="streamable-http", authenticator=auth, exempt_paths={"/health", "/metrics"})

See examples/README.md for a runnable JWT demo with a pre-generated test token.

Approval Mechanism

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

fromapcore_mcp.adapters.approvalimportElicitationApprovalHandlerhandler=ElicitationApprovalHandler()
serve(
registry,
transport="streamable-http",
approval_handler=handler,
explorer=True,
)

Built-in handlers:

HandlerDescription
ElicitationApprovalHandlerPrompts the MCP client for user confirmation via elicitation
AutoApproveHandlerAuto-approves all requests (dev/testing only)
AlwaysDenyHandlerRejects all requests (enforcement)

CLI usage:

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

Output Formatting

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

Built-in formats (requires the [markdown] extra — pip install 'apcore-mcp[markdown]'):

# Via CLI# apcore-mcp --extensions-dir ./extensions --output-format csv# Via APImcp=APCoreMCP("./extensions", output_format="csv")

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

Custom formatter: Pass a callable that converts a dict or list result into a string.

defmy_formatter(data: dict) ->str:
return"\n".join(f"{k}: {v}"fork, vindata.items())
mcp=APCoreMCP("./extensions", output_formatter=my_formatter)

The output_formatter parameter is also available on the function-based serve() API and on ExecutionRouter directly.

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.

fromapcore_mcpimportreport_progress, elicit# Inside a module's execute():awaitreport_progress(context, progress=50, total=100, message="Halfway done")
result=awaitelicit(context, "Confirm deletion?", {"type": "object", "properties": {"confirm": {"type": "boolean"}}})
ifresultandresult["action"] =="accept":
# proceed
...

/metrics Prometheus Endpoint

When metrics_collector is provided to serve(), 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.
fromapcore.observabilityimportMetricsCollectorfromapcore_mcpimportservecollector=MetricsCollector()
serve(registry, transport="streamable-http", metrics_collector=collector)
# GET http://127.0.0.1:8000/metrics -> Prometheus text format

to_openai_tools()

fromapcore_mcpimportto_openai_toolstools=to_openai_tools(
registry_or_executor, # Registry or Executorembed_annotations=False, # append annotation hints to descriptionsstrict=False, # OpenAI Structured Outputs strict modetags=None, # filter by tags, e.g. ["image"]prefix=None, # filter by module ID prefix, e.g. "image"
)

Returns a list of dicts directly usable with the OpenAI API:

importopenaiclient=openai.OpenAI()
response=client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Resize the image to 512x512"}],
tools=tools,
)

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=["image"] or prefix="text" to expose a subset of modules.

Using with an Executor

If you need custom middleware, ACL, or execution configuration:

fromapcoreimportRegistry, Executorregistry=Registry(extensions_dir="./extensions")
registry.discover()
executor=Executor(registry)
serve(executor)
tools=to_openai_tools(executor)

Features

  • Auto-discovery — all modules in the extensions directory are found and exposed automatically
  • Display overlaymetadata["display"]["mcp"] controls MCP tool names, descriptions, and guidance per module (§5.13); set via binding_path in fastapi-apcore
  • Markdown tool descriptions (rich_description=True, v0.15+) — render Tool.description / OpenAI function.description as canonical apcore-toolkit Markdown (parameters, returns, behavior table, tags, examples) so LLMs get more decision-relevant signal per token. Requires the [markdown] extra.
  • Module preview meta-tool (__apcore_module_preview, v0.15+) — lets AI orchestrators run executor.validate() to predict state changes WITHOUT executing the module (apcore PROTOCOL_SPEC §5.6). Returns {valid, requires_approval, predicted_changes, checks}.
  • 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, with Swagger-UI-style auth input
  • 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 at import time. This means MCP settings can live alongside other apcore configuration in a single apcore.yaml:

apcore:
version: "1.0.0"mcp:
transport: streamable-httphost: 0.0.0.0port: 9000explorer: truerequire_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:

fromapcore_mcpimportMCP_NAMESPACE, MCP_ENV_PREFIX, MCP_DEFAULTS

How It Works

Mapping: apcore to MCP

apcoreMCP
metadata["display"]["mcp"]["alias"] or module_idTool name
metadata["display"]["mcp"]["description"] + guidance suffix or descriptionTool description
input_schemainputSchema
annotations.readonlyToolAnnotations.readOnlyHint
annotations.destructiveToolAnnotations.destructiveHint
annotations.idempotentToolAnnotations.idempotentHint
annotations.open_worldToolAnnotations.openWorldHint

Mapping: apcore to OpenAI Tools

apcoreOpenAI
module_id (image.resize)name (image-resize)
descriptiondescription
input_schemaparameters

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

Architecture

Your apcore project (unchanged)
│
│ extensions directory
▼
apcore-mcp (separate process / library call)
│
├── MCP Server path
│ SchemaConverter + AnnotationMapper
│ → MCPServerFactory → ExecutionRouter → TransportManager
│
└── OpenAI Tools path
SchemaConverter + AnnotationMapper + IDNormalizer
→ OpenAIConverter → list[dict]

Development

git clone https://github.com/aiperceivable/apcore-mcp-python.git
cd apcore-mcp
pip install -e ".[dev]"
pytest # ~689 tests
pytest --cov # with coverage report

License

Apache-2.0

Releases

Packages

Contributors

Languages