# MCP Admin & Consumer Servers (/docs/ai/mcp-server)



Ductor includes two inbound &#x2A;*Model Context Protocol (MCP)** servers. External
AI clients can discover and call typed tools without receiving broader access
to the REST API. This is one way an agent becomes a <Term name="Worker" /> that
operates the clearing layer: the same tools a person reaches through the REST
API, offered to an agent through a governed, typed surface — shipping today, and
the same auth chain either way.

| Control plane | Default route | Identity boundary              | Purpose                                                                     |
| ------------- | ------------- | ------------------------------ | --------------------------------------------------------------------------- |
| Consumer      | `/mcp`        | Authenticated, active tenant   | Operate the products and workflows that tenant policy exposes.              |
| Admin         | `/mcp/admin`  | Authenticated `platform-admin` | Provision and govern tenants, members, roles, invites, and tenant API keys. |

The routes have different registries, server identities, and authorization
chains. Admin tools never appear on the consumer route, and the admin route
does not inherit a consumer tenant.

Ductor implements MCP &#x2A;*`2026-07-28`**, sometimes described as the stateless MCP
core. Each request contains everything the protocol needs. Ductor does not run
an initialization exchange, issue `Mcp-Session-Id`, or require a client to stay
connected to one replica.

<Callout type="info" title="Protocol statelessness does not remove durable application state">
  Tool authorization leases, manifests, chat turns, approvals, jobs, and audit
  records can still be durable. They are explicit Ductor resources, not hidden
  MCP transport sessions. This makes retries and recovery observable while the
  HTTP transport remains stateless.
</Callout>

## Enable the control planes [#enable-the-control-planes]

Both planes are off by default and mount on the existing API server. Enable the
consumer plane first; configuration validation rejects an admin plane enabled
on its own.

```yaml title="ductor.yaml"
mcp:
  enabled: true
  path: /mcp
  admin_enabled: true
  admin_path: /mcp/admin
  expose_unwired_tools: false

cors:
  allowed_origins: "https://console.example.com"
```

* `mcp.enabled` mounts the endpoint when set to `true`.
* `mcp.path` sets the mount path. The default is `/mcp`.
* `mcp.admin_enabled` mounts the platform-admin endpoint when set to `true`.
* `mcp.admin_path` sets its distinct mount path. The default is `/mcp/admin`.
* `mcp.expose_unwired_tools` controls whether discovery includes tools without
  a live backend. Keep it `false` in production.
* `cors.allowed_origins` controls browser origins accepted by the MCP endpoint.
  Enterprise mode requires an explicit list and rejects `*`.

The equivalent environment overrides are `DUCTOR_MCP_ENABLED`,
`DUCTOR_MCP_PATH`, `DUCTOR_MCP_ADMIN_ENABLED`, and
`DUCTOR_MCP_ADMIN_PATH`. Enterprise mode also requires an audit sink before it
will enable admin MCP.

## Consumer plane [#consumer-plane]

The gateway derives the tenant from the authenticated principal, confirms that
the tenant is active, authorizes MCP access, and applies the tenant's tool
exposure manifest. A client cannot select another tenant in tool arguments or
replace its identity with an `X-Tenant-ID` header.

The consumer catalog is assembled from the services wired into the running
Ductor process. It can cover workflow and routing operations, connectors,
workspaces, environments, governance, usage, reliability, cases, and work
assignments. Connector actions and triggers appear only when their catalog and
execution boundaries are available.

Call `consumer.readiness` to see live counts by tool family and mutation class.
This reports whether a family is wired; `tools/list` remains authoritative for
the tools this particular identity may discover. Write and destructive tools
still require their manifest grants, matching schema hashes, and configured
safeguards.

## Admin plane [#admin-plane]

Before MCP dispatch, `/mcp/admin` requires the dedicated `platform-admin` role and a
successful authorization decision for the fixed `platform_admin` resource.
Every tenant-scoped tool then requires a valid, explicit `tenant_id`.

The tenant `admin` role is not sufficient for this endpoint. Tenant API keys cannot
mint `platform-admin`; use a separately provisioned platform machine identity. This
keeps a compromised tenant credential from crossing into product-lifecycle
administration even when that credential legitimately administers its own tenant.

The admin registry contains 19 tools:

| Domain    | Read operations                               | Mutations                                                                 |
| --------- | --------------------------------------------- | ------------------------------------------------------------------------- |
| Readiness | `admin.readiness`                             | —                                                                         |
| Tenants   | `admin.tenant.list`, `admin.tenant.get`       | `admin.tenant.create`, `admin.tenant.update`, `admin.tenant.delete`       |
| Members   | `admin.member.list`, `get`, `roles`, `scopes` | `assign_role`, `invite`, `revoke_invite`, `disable`, `reenable`, `remove` |
| API keys  | `admin.api_key.list`                          | `admin.api_key.create`, `admin.api_key.revoke`                            |

Member operations in the table retain the `admin.member.` prefix. Call
`admin.readiness` to inspect the live tenant, member, API-key, and audit
dependencies. If a service is absent, its tool family is not advertised.

Tenant deletion is retained soft deletion: the record remains with status
`deleted`. MCP does not provide a hard-delete shortcut around retention or
audit policy.

### Destructive confirmation [#destructive-confirmation]

Destructive calls require a target-specific `confirmation` string. A generic
approval cannot be replayed against another object.

| Target  | Required confirmation     |
| ------- | ------------------------- |
| Tenant  | `<tenant_id>`             |
| Member  | `<tenant_id>/<member_id>` |
| Invite  | `<tenant_id>/<invite_id>` |
| API key | `<tenant_id>/<key_id>`    |

Role replacement uses the member form because it can remove existing
authority.

### Create a tenant API key [#create-a-tenant-api-key]

`admin.api_key.create` uses a stateless, two-round approval:

1. The first call validates the tenant, name, roles, scopes, environment
   restrictions, and optional expiry.
2. Ductor returns `input_required`, an `elicitation/create` request, and opaque
   state bound to those arguments.
3. The client collects typed approval and retries the same call with unchanged
   arguments, matching `requestState`, and `inputResponses`.
4. Ductor persists the hashed credential and returns plaintext exactly once.

The result is marked `io.ductor/sensitive: true` and
`io.ductor/retention: none`. Deliver it directly to an approved secret store;
do not put it into model context, chat history, logs, traces, certification
transcripts, or retry payloads. List operations return only redacted records
and prefixes.

### Admin audit trail [#admin-audit-trail]

Every successful admin mutation emits an `mcp_admin` audit event when an audit
sink is configured. The event records the authenticated subject, action,
tenant, target identifier, decision, timestamp, and trace ID. Arguments are not
copied into the event, so one-time credentials and invite content do not enter
the admin audit record.

Enterprise mode refuses to enable admin MCP without the sink. Strict auditing
also rejects a mutation before storage changes if the required sink is
unavailable.

## Supported methods [#supported-methods]

The server exposes a tools-only MCP surface:

```mermaid
sequenceDiagram
  participant C as MCP client
  participant M as Ductor MCP server
  C->>M: server/discover
  M-->>C: version, capabilities, cache hints
  C->>M: tools/list
  M-->>C: authorized tool catalog
  C->>M: tools/call
  M-->>C: complete or input_required
```

| Method            | Purpose                                                                            | Identity context                       |
| ----------------- | ---------------------------------------------------------------------------------- | -------------------------------------- |
| `server/discover` | Returns supported versions, capabilities, instructions, identity, and cache hints. | Authenticated tenant or platform admin |
| `tools/list`      | Returns the deterministic catalog visible to this caller.                          | Authenticated tenant or platform admin |
| `tools/call`      | Re-authorizes and invokes one named tool.                                          | Authenticated tenant or platform admin |

`initialize` and `ping` are not part of this version. An unknown method returns
HTTP 404 with JSON-RPC code `-32601`. Ductor accepts valid notifications with
HTTP 202 and no response body.

The current implementation does not advertise prompts, resources,
subscriptions, tasks, or server-initiated event streams.

## Make a request [#make-a-request]

Every request must include the protocol version and client capabilities in both
the HTTP and JSON-RPC contract. Empty client capabilities are valid; omitting
the field is not.

```bash
curl https://api.example.com/mcp \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  --data '{
    "jsonrpc": "2.0",
    "id": "discover-1",
    "method": "server/discover",
    "params": {
      "_meta": {
        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
        "io.modelcontextprotocol/clientCapabilities": {},
        "io.modelcontextprotocol/clientInfo": {
          "name": "my-client",
          "version": "1.0.0"
        }
      }
    }
  }'
```

Use `https://api.example.com/mcp/admin` with a dedicated platform-admin
credential for the admin plane. Do not reuse that credential in consumer
agents.

For `tools/call`, also send `Mcp-Name` with the tool name. Ductor verifies the
version, method, and name headers against the body before it looks up or runs a
tool. Trace context can be sent as `traceparent`, `tracestate`, and `baggage`
inside `_meta`.

Every successful response includes:

* `resultType`, either `complete` or `input_required`;
* `_meta["io.modelcontextprotocol/serverInfo"]`; and
* method-specific content.

Discovery results include public cache hints. Tool catalogs are private and
currently use a zero TTL because visibility can change with tenant policy.

## Tool parameter headers [#tool-parameter-headers]

A tool schema can mark a string, number, integer, or boolean input with
`x-mcp-header`:

```json
{
  "type": "object",
  "properties": {
    "region": {
      "type": "string",
      "x-mcp-header": "Region"
    }
  }
}
```

The client sends the same value in `arguments.region` and
`Mcp-Param-Region`. Ductor rejects missing required, malformed, or conflicting
recognized parameter headers before execution. Values that are unsafe as plain
HTTP field values use MCP's Base64 sentinel encoding.

Ductor does not advertise a tool whose input schema has a non-object root or an
invalid `x-mcp-header` annotation. This prevents a client from discovering a
tool it cannot call correctly.

## Request more input without a session [#request-more-input-without-a-session]

A tool can pause for typed client work by returning `input_required`:

```json
{
  "resultType": "input_required",
  "inputRequests": {
    "approval": {
      "method": "elicitation/create",
      "params": {
        "message": "Approve this operation?"
      }
    }
  },
  "requestState": "opaque-tool-state"
}
```

The client completes the requested operation, then retries the original
`tools/call` with `inputResponses` and the unchanged `requestState`. Ductor
passes both values to the tool handler; it does not keep a transport session
between calls.

The request's client capabilities must cover every input request. Ductor
supports `elicitation/create`, `sampling/createMessage`, and `roots/list` for
this flow. Missing capabilities return HTTP 400 with code `-32021`. Unsupported
input-request methods fail closed.

## Tool catalog and governance [#tool-catalog-and-governance]

The catalog includes read, trigger, governance, and authoring tools. Ductor also
projects connector actions and workflow triggers into the same catalog, so they
can be called without a second execution path.

### Descriptor-generated API reads [#descriptor-generated-api-reads]

Ductor generates ordinary read tools from the same canonical operation
descriptors that produce the REST and OpenAPI surface. Mutations remain bespoke
and operations marked for internal use are excluded.

A generated tool is eligible only when the operation is all of the following:

* available in the running API inventory;
* lifecycle `supported` and audience `tenant`;
* externally callable, not `internal_only`;
* covered by complete authentication and authorization metadata; and
* classified with the `read` or `preview` authorization action.

Tool names use `api_` plus the normalized operation id—for example,
`AIInferenceService_GetUsageStats` becomes
`api_aiinferenceservice_getusagestats`. Input JSON Schema is derived from the
protobuf request: path fields become required, timestamps retain their format,
enums retain their closed values, and unknown fields are rejected.

Tenant and environment identity are never accepted from model arguments.
`tenantId` and `environmentId` fields are removed from the advertised schema and
injected from the authenticated MCP context. Execution forwards the caller's
credential to Ductor's fixed loopback REST listener, refuses redirects, applies
a bounded timeout, and caps response bodies at 10 MiB.

<Callout type="warn" title="Automatic projection never turns a mutation into a tool">
  Write, delete, cancel, execute, publish, promote, and other mutating actions
  stay `bespoke`. They appear only when an explicit MCP handler supplies the
  required approval, confirmation, idempotency, and audit behavior. Adding an
  OpenAPI mutation does not make it agent-callable automatically.
</Callout>

When tool exposure is enabled, `tools/list` creates an explicit authorization
lease and manifest. The list response includes the identifiers and schema hashes
the client must echo on `tools/call`. Ductor checks the lease, manifest, tool
version, schema hash, tenant, and arguments again before execution.

This application-level lease is intentionally visible in the request. It is not
an MCP session and does not require connection affinity.

## Metering and errors [#metering-and-errors]

Every `tools/call` records a usage event under `mcp.tool_calls` with one of these
outcomes:

| Outcome             | Meaning                                                      |
| ------------------- | ------------------------------------------------------------ |
| `success`           | The tool completed normally.                                 |
| `input_required`    | The tool returned a typed continuation request.              |
| `application_error` | The tool returned `isError: true` for the client to surface. |
| `error`             | Request or handler execution failed.                         |
| `denied`            | Tool exposure policy rejected the call before execution.     |

Protocol failures use stable error codes:

| Failure                               |                     HTTP | JSON-RPC code |
| ------------------------------------- | -----------------------: | ------------: |
| Header and body do not match          |                      400 |      `-32020` |
| Required client capability is missing |                      400 |      `-32021` |
| Protocol version is unsupported       |                      400 |      `-32022` |
| Method is unknown                     |                      404 |      `-32601` |
| Parameters or tool name are invalid   | 400 or JSON-RPC response |      `-32602` |

## Production checklist [#production-checklist]

* Keep both planes disabled until gateway authentication and authorization are
  configured.
* Roll out `/mcp` first, then expose `/mcp/admin` only through operator ingress.
* Use a dedicated platform-admin machine identity for admin automation.
* Set explicit browser origins. Do not use `*` in production.
* Keep `expose_unwired_tools` off.
* Call both readiness tools and investigate unavailable families.
* In a sandbox tenant, test wrong and correct destructive confirmations.
* Create and revoke a short-lived key; confirm plaintext is delivered only to
  the intended secret store and never appears in audit output.
* Confirm clients send `2026-07-28` metadata and all required routing headers.
* Treat tool-lease fields and multi-round state as opaque values.
* Monitor `mcp.tool_calls` outcomes and JSON-RPC error codes during rollout.
* Deploy clients and servers together. Ductor does not include an old-version
  compatibility path.

## What an agent can—and cannot—administer [#what-an-agent-canand-cannotadminister]

Together, the two planes let an agent bootstrap a tenant, govern membership and
authority, issue and revoke scoped credentials, discover the tenant's allowed
product tools, operate those tools, and disable access with auditable outcomes.

They do not expose the host that runs Ductor. Process and deployment control,
static environment changes, migrations, raw SQL, backup and restore, raw secret
reads, arbitrary HTTP/filesystem/shell execution, authorization-policy
replacement, and retention-bypassing hard deletion remain in the deployment
control plane or a documented break-glass procedure. This boundary keeps a
compromised agent inside Ductor's product authorization model.

## Related documentation [#related-documentation]

<Cards>
  <Card title="Agent tool security" href="/docs/ai/agent-tool-security">
    See how Ductor builds manifests and authorizes every visible tool.
  </Card>

  <Card title="External MCP providers" href="/docs/connectors/external-mcp-providers">
    Use Ductor as a stateless MCP client for governed connector actions.
  </Card>

  <Card title="Authentication" href="/docs/auth/authentication">
    Configure the identity boundary shared by MCP and REST.
  </Card>
</Cards>
