Skip to main content
Glama
i2mint

enlace_connector

by i2mint

enlace_connector

Deploy Python functions as authenticated MCP connectors on an enlace platform — so Claude.ai (and any MCP host) can call them.

A Claude.ai "custom connector" is a remote MCP server. enlace_connector wraps your tool functions with py2mcp, plugs auth into the platform's enlace_auth OAuth server, and follows enlace's app conventions so deployment is one declaration.

from enlace_connector import ConnectorSpec, make_connector_app, scaffold_app

spec = ConnectorSpec(
    name="trufflepig",
    tools=["truffle.mcp:search_trufflepig", "truffle.mcp:search_wallow"],
    auth="enlace",                 # validate tokens issued by the platform's enlace_auth
    extras=["truffle"],            # what the connector's own venv must install
)

# Develop locally (stdio, no auth) — Claude Desktop / Claude Code:
make_stdio_server(spec).run()

# The hosted ASGI app (Streamable HTTP + OAuth) a server runs:
app = make_connector_app(spec, issuer="https://apps.thorwhalen.com")

# Or scaffold an enlace mode=process app dir (own venv for heavy deps):
scaffold_app(spec, "tw_platform/apps/trufflepig_mcp", port=8030)

Why a separate venv (mode="process")

A connector with heavy dependencies (ML models, large libraries) runs as an enlace mode="process" app: enlace spawns it as a supervised subprocess in its own venv and reverse-proxies the route to it — keeping those deps out of the shared platform backend. The connector validates bearer tokens itself, so enlace treats it as access="public" (no session gate).

Related MCP server: Remote MCP Server on Cloudflare

Auth is pluggable

The connector is an OAuth 2.1 resource server — it validates the bearer JWTs an authorization server issued. Who that AS is is just config:

auth=

Authorization server

Use

"enlace"

the platform's enlace_auth OAuth server

self-contained platform auth

idp_resource(...)

a managed IdP (Auth0, WorkOS, …)

offload OAuth to a vendor

None / "none"

—

local stdio / unauthenticated internal pilot

The resource-server validation is identical regardless — picking an AS doesn't change the connector, only where the token comes from.

Usage logging is a switch, not code

A deployed connector otherwise leaves only the web server's access log. With py2mcp's usage logger (its ADR-0001) the connector can keep one record per tool call — time, caller from the OAuth token, tool, arguments, outcome, result size, latency — and one flagged record per handshake, so "enabled" and "used" stop looking the same. It is off by default. The host switches it on with environment variables (the unit file's Environment= lines), never by editing connector code:

Variable

Meaning

CONNECTOR_USAGE_LOG_DIR

absolute directory for the day files, one per connector; unset = off. Use the connector's own data root, off the deploy tree (a relative path is refused)

CONNECTOR_USAGE_LOG_RETENTION_DAYS

days to keep (default 90; 0 keeps today only; none keeps all)

CONNECTOR_USAGE_LOG_REDACT

comma-separated argument names to mask (error text is dropped too, since it echoes arguments)

CONNECTOR_USAGE_LOG_ARGS

0 to drop the arguments entirely

make_connector_app(spec) reads them itself; a connector that builds FastMCP by hand passes middleware=usage_middleware(spec) (an empty list when off). The spec's version and usage_outcome (a "module:function" ref to the connector's own (tool, result) -> label | mapping classifier, e.g. one that knows the search tool's "no match" shape) travel into the records. The rendered systemd unit ships the CONNECTOR_USAGE_LOG_DIR line commented out. The arguments are the users' own questions: tell them before you turn it on, and read the log with py2mcp usage <dir>.

Deploy the whole bundle

generate_deploy_bundle(spec, dest) writes everything a mode="process" connector needs — the app dir (app.toml + server.py), a systemd unit (runs it in its own venv), a provisioning script (build that venv + install extras/git_installs

  • create data dirs + post_install), the two platform.toml fragments (oauth-server.toml — session longevity; allowlist.toml — from allowed_users), and a runbook:

spec = ConnectorSpec(
    name="acme", tools=["acme.mcp:search"], route="/api/acme_mcp", port=8031,
    extras=["ir", "sentence-transformers"], git_installs=["git+ssh://git@github.com/acme/acme"],
    data=[("~/.local/share/ir/corpora/acme", "{base}/xdg-data/ir/corpora/acme")],
    env={"XDG_DATA_HOME": "{base}/xdg-data", "HF_HOME": "{base}/hf-cache"},
    allowed_users=["a@acme.com", "b@acme.com"],
)
generate_deploy_bundle(spec, "apps/acme_mcp")   # → app dir + deploy/{unit,provision,toml fragments,runbook}

Platform specifics (paths, origin) are parameters with tw_platform defaults; the package stays connector-type-agnostic (it knows nothing of ir).

Verify before you call it deployed

A connector can be perfectly healthy and completely dead to its users. If the platform's authorization server does not advertise the refresh_token grant, every client session dies at the first access-token expiry (typically one hour) and only a human re-running the interactive browser authorization can bring it back — while the process stays up, the port answers, and nothing errors anywhere. It has happened; the first signal was a person saying the connector had been down all day.

So the deployment isn't good until the authorization server is checked:

python -m enlace_connector.preflight https://apps.thorwhalen.com   # exit 1 if it would strand
from enlace_connector import verify_deployment, format_report

results = verify_deployment(spec, issuer="https://apps.thorwhalen.com")
print(format_report(results))
assert all(results)          # or pass strict=True to raise

It fetches {issuer}/.well-known/oauth-authorization-server and asserts what the deployed server advertises — config claiming refresh is enabled is not evidence, since the running build may predate the feature. Checks are plain callables of a PreflightContext (checks= to extend or replace them) and the HTTP fetcher is a parameter (fetch=), so this is testable offline and CI-friendly.

Configure it from the start too: deploy/oauth-server.toml in the bundle carries the [auth.oauth_server] refresh_token_ttl_seconds key (30 days by default; 0 disables the grant), so a new platform doesn't inherit a stranding default.

Cost / LLM note

A connector over ir's core search is offline and free — no LLM, no tokens. An agentic connector (query reformulation, LLM-selection, synopsis) needs an LLM; that LLM is an injected callable and can ride the client's Claude subscription via MCP sampling (where the client supports it) instead of a metered key — see ir's ir_10 note and issue #1 here.

Status

The connector factory, auth, scaffolding, and full deploy-bundle generation are stable and tested; auth="enlace" is live in production. Building the connector app directly (to attach a server icon, custom tools, etc.) is also supported — see the server_py= override on generate_deploy_bundle.

pip install -e ".[dev]" && pytest -q

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables deploying a Model Context Protocol (MCP) server on Cloudflare Workers with built-in OAuth authentication. It allows local clients like Claude Desktop to securely connect to and use remote tools through an HTTP/SSE transport.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables deploying and connecting to MCP servers on Cloudflare Workers with OAuth login, allowing remote access to tools via MCP clients like Claude Desktop.
    284 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables deploying a remote Model Context Protocol (MCP) server on Cloudflare Workers with OAuth authentication, allowing tools to be exposed and called from MCP clients like Claude Desktop.
    -