Skip to content

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.

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 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.

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

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

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:

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 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 for an interactive sanity check.
  3. Make sure your UI exposes the Connected Apps page by setting connectedApps: true in the 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 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 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); until Claude.ai honours it, the org allowlist is the only knob.