> OctoPerf load testing - Deploy the OctoPerf MCP server alongside a self-hosted Enterprise instance.

Source: https://api.octoperf.com/doc/on-premise-infra/mcp-server/

# MCP Server

This page describes how to run the OctoPerf MCP server alongside an OctoPerf Enterprise installation. For an overview of what the MCP server is and how AI agents connect to it, see the [MCP Server overview](https://api.octoperf.com/doc/mcp/).

## Architecture

The MCP server is a separate, stateless Spring Boot service shipped as the `octoperf/mcp-server` Docker image. It does not own any data: every tool call resolves to a REST call against the regular OctoPerf backend, carrying the same OAuth JWT the agent received during the OAuth flow.

```
+-----------------+        +-------------------+        +---------------------+
|   AI agent      | --1--> |   octoperf-mcp    | --3--> | OctoPerf backend    |
| (Claude.ai,     | <-2--- |  (JWT validation, | <-4--- | (enterprise-edition |
|  Claude Code...)|        |   stateless)      |        |   + traefik)        |
+-----------------+        +-------------------+        +---------------------+
                                    ^
                                    | 5. JWT signed / discovered
                                    v
                           +-------------------+
                           | OctoPerf IdP      |
                           | (bundled in       |
                           |  enterprise-      |
                           |  edition)         |
                           +-------------------+
```

1. The agent hits `/mcp` with a JWT in `Authorization: Bearer ...`,
2. The MCP server validates signature / issuer / audience / expiry against the IdP (OctoPerf backend), then forwards the call,
3. OctoPerf backend resolves the `sub` claim to the same user the SPA login lands on,
4. Response goes back through the MCP server (no body mutation),
5. JWTs are signed by the IdP, which is bundled in the OctoPerf backend (no separate process); see [IdP configuration](https://api.octoperf.com/doc/on-premise-infra/configuration/#mcp-server-oauth-identity-provider) for how to enable and persist signing keys.

## Prerequisites

Before deploying the MCP server, make sure the **bundled OAuth Identity Provider** on the OctoPerf backend advertises itself at its public origin. The IdP issuer and the audiences it stamps on JWTs are derived from the backend's `server.scheme` / `server.hostname` / `server.public.port` — the public host clients reach through the reverse proxy, not the container bind address. The full reference (including persisting the JWK signing key across restarts and graceful key rotation) lives in [Advanced Configuration → MCP Server / OAuth Identity Provider](https://api.octoperf.com/doc/on-premise-infra/configuration/#mcp-server-oauth-identity-provider).

## Configuration

The MCP server reads its settings from `application.yml`, mounted into the container at `/home/octoperf/config/application.yml`. The pre-built [Enterprise zips](https://api.octoperf.com/doc/on-premise-infra/download/) ship with a working `config/mcp/application.yml` you can edit in place.

The MCP server derives the JWT issuer (the backend URL it discovers JWKS from), the accepted audiences, and its own server-to-server REST calls from a single **public origin** — the same one the backend advertises. Point both services at the same origin and front them with one reverse proxy (`/` → backend, `/mcp` → MCP):

```yaml
server:
  # Public origin shared with the OctoPerf backend — the host clients
  # reach through the reverse proxy, NOT the container bind address.
  # Must match the backend's server.scheme / hostname / public.port.
  scheme: https
  hostname: YOUR-OCTOPERF-HOST
  public:
    port: 443
  port: 8091   # internal container listen port (proxied by the /mcp location)
```

| Property                  | Required | Default        | Description                                                                                                                                       |
|---------------------------|----------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| `server.hostname`         | ✅        | local IP       | Public host clients reach through the proxy. Must equal the backend's `server.hostname`.                                                          |
| `server.scheme`           |          | `http`         | Public scheme (`http` / `https`). Must equal the backend's `server.scheme`.                                                                       |
| `server.public.port`      |          | `server.port`  | Public port advertised to clients. Omitted from URLs when it is the scheme default (`80` / `443`). Must equal the backend's `server.public.port`. |
| `server.port`             |          | `8091`         | Internal container HTTP listen port (the port the `/mcp` reverse-proxy location forwards to).                                                     |
| `server.mcp-context-path` |          | `/mcp`         | Path the MCP API is exposed at on the public origin.                                                                                              |

!!! warning
    The issuer and accepted audiences are derived from `server.scheme` / `server.hostname` / `server.public.port` and validated against every inbound JWT. They must resolve to the **same public origin the backend mints tokens with**, or every call returns `401`.

!!! warning
    The public origin must be **reachable from outside**: the same host is embedded in presigned URLs handed to Claude.ai's sandbox / the user's browser, and the MCP server routes its own server-to-server calls back through it. Pointing it at an internal Docker name (`http://enterprise-edition:8090`, `http://traefik`) breaks both.

The JVM also reads two environment variables: `JAVA_OPTS` (default `-Xmx512m`) and `OCTOPERF_MCP_FILE_SOURCE_MAX_BYTES` (default `1073741824` — 1 GiB hard cap on any file resolved by `import_*_virtual_user` tools).

## Docker compose

The standard [Enterprise zips](https://api.octoperf.com/doc/on-premise-infra/download/) (HTTP, SSL, offline, k8s) bundle the MCP server out of the box from version 16.2.0 onwards. A minimal `mcp` service entry looks like this:

```yaml
services:
  mcp:
    image: docker.io/octoperf/mcp-server:16.2.0
    container_name: octoperf-mcp
    restart: unless-stopped
    depends_on:
      - enterprise-edition
    volumes:
      - ./config/mcp:/home/octoperf/config
```

And on the Traefik load balancer, a router and a service in `traefik/dynamic.yml` expose the MCP API alongside the backend:

```yaml
http:
  routers:
    # MCP API: Traefik streams the server-sent-event responses of the
    # Streamable HTTP transport as they come, no extra setting is needed.
    # No compression middleware here, as gzip would buffer the events.
    mcp-server:
      rule: "Path(`/mcp`) || PathPrefix(`/mcp/`)"
      entryPoints: [web]
      service: mcp-server

  services:
    mcp-server:
      loadBalancer:
        servers:
          - url: "http://mcp:8091"
```

Traefik adds the `X-Forwarded-*` headers automatically.

!!! warning
    Always write the rule as `` Path(`/mcp`) || PathPrefix(`/mcp/`) ``. Traefik compares path prefixes character by character: a plain `` PathPrefix(`/mcp`) `` also captures backend routes that start with the same letters, such as `/mcp-entity-schemas.json`. The same applies to the `/ui`, `/doc` and `/utilities` routes: a plain `` PathPrefix(`/doc`) `` captures the `/docker/...` routes used by load generators, which then fail to run tests.

    On Kubernetes, enable `providers.kubernetesIngress.strictPrefixMatching: true` in the values of the Traefik Helm chart, so that the `Prefix` path of an ingress matches whole path segments only.

## Public download routes

The MCP server exposes a small, **unauthenticated** route family under `/mcp/public/**` so any agent host can bootstrap a project before it has IdP credentials. These are the only paths on the server that don't require a JWT — see [MCP Server overview → Public HTTP downloads](https://api.octoperf.com/doc/mcp/#public-http-downloads) for the full list.

Unknown paths under `/mcp/public/**` return `404`; every other path is gated by the OAuth filter chain. If your reverse-proxy enforces additional authentication, make sure to whitelist `/mcp/public/**` or your users won't be able to bootstrap.

## Verifying the deployment

After starting the MCP server:

1. Hit `https://YOUR-MCP-HOST/mcp/public/AGENTS.md` - should return the markdown file. If this fails, your reverse-proxy is probably stripping `/mcp/public/`.
2. Connect with the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) for an interactive sanity check.
3. Make sure your UI exposes the **Connected Apps** page by setting `connectedApps: true` in the [UI configuration](https://api.octoperf.com/doc/on-premise-infra/configuration/#ui-configuration); your users won't be able to review or revoke connectors otherwise.

## Telling your Claude.ai users about the sandbox domain allowlist

Your users connecting from **Claude.ai web** (or Desktop) hit the same sandbox egress constraint as SaaS users: file upload / download tools return presigned URLs on `https://YOUR-MCP-HOST` that Claude's sandbox can only fetch once a workspace admin has added the host to **Claude.ai → Organization Settings → Capabilities → Domain allowlist**. There is no operator-side way to pre-authorize the host on your users' behalf — see [MCP Server overview → Claude.ai web file uploads](https://api.octoperf.com/doc/mcp/#claudeai-custom-connector) for the full step-by-step and screenshot.

Practical recommendation:

- **Surface the requirement in your own onboarding** - list `YOUR-MCP-HOST` (or `*.YOUR-DOMAIN`) and the exact settings path alongside the connector URL in the doc you give your users, and flag that it needs the Claude.ai workspace admin.
- **Steer file-heavy users to [Claude Code](https://api.octoperf.com/doc/mcp/#claude-code)** if the allowlist is out of reach (personal Claude.ai accounts, locked-down orgs). Claude Code fetches presigned URLs through its local shell, no sandbox allowlist applies, uploads / downloads work out of the box. Cursor, Continue.dev, and the MCP Inspector behave the same way.

The upstream MCP spec is tracking a server-declared trusted-hosts mechanism that would remove this manual step ([modelcontextprotocol#1541](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1541)); until Claude.ai honours it, the org allowlist is the only knob.

## Related topics

- **[MCP Server overview](https://api.octoperf.com/doc/mcp/)**: what the server is and why use it
- **[Connect a client](https://api.octoperf.com/doc/mcp/#connect-a-client)**: point an agent at your self-hosted MCP server
- **[IdP configuration](https://api.octoperf.com/doc/on-premise-infra/configuration/#mcp-server-oauth-identity-provider)**: enable the bundled OAuth IdP, persist the JWK signing key, rotate keys gracefully
- **[UI configuration](https://api.octoperf.com/doc/on-premise-infra/configuration/#ui-configuration)**: expose the Connected Apps page to your users
- **[OAuth Clients (admin)](https://api.octoperf.com/doc/administration/oauth-clients/)**: audit registered OAuth clients on your instance
