---
title: Factry Historian MCP Server (HTTP mode)
slug: how-tos/meTS-factry-h
createdAt: 2026-04-20T13:43:49.615Z
---

This guide walks you through running the Factry Historian MCP Server in HTTP mode. Use this when you have a **custom LLM application** or a web-based AI platform that connects to MCP servers over HTTP rather than launching them as a local process.

If you are using Claude Desktop or a similar desktop AI client, the [standard Docker guide](docId\:Hf-aRFFozSKIYDJUzOULH) is simpler and recommended instead.

## When to use HTTP mode

| Scenario                                              | Recommended transport                                      |
| ----------------------------------------------------- | ---------------------------------------------------------- |
| Claude Desktop, Cursor, VS Code Copilot               | stdio (see [standard guide](docId\:Hf-aRFFozSKIYDJUzOULH)) |
| Custom LLM application or internal AI platform        | **HTTP**                                                   |
| Web-based AI tools that connect to remote MCP servers | **HTTP**                                                   |
| Multiple users or clients sharing one MCP server      | **HTTP**                                                   |

## Prerequisites

Before you start, make sure you have:

**A running Factry Historian instance**

- The **API URL** of your Historian instance (e.g. `https://historian.example.com/api`)
- An **API token** with access to Factry Historian. [Obtaining an API token](docId:-VR6PRLDWhuLaWeKLZWoL)
- Your **Organization UUID,** visible in the Historian web interface under organization settings. [Organizations](docId\:ODmaDpIfQLvqBpGFarnXi)

**Docker** installed on the server that will run the MCP server. Install from the official Docker website:

- **Mac / Windows:** [Docker Desktop](https://www.docker.com/products/docker-desktop/)
- **Linux:** [Docker Engine install guide](https://docs.docker.com/engine/install/)

**A domain name and TLS certificate** if you intend to expose the server to an external LLM platform (required for production, see [Exposing over HTTPS](docId\:meTSsZcQeFUdbdqUjhS6V)).

## Basic setup (no authentication)

This is the quickest way to get the server running. It is suitable for testing on a private network, or when your LLM app and the MCP server are on the same machine. Do not expose this setup to the internet without adding authentication.

Start the container:

```bash
docker run -d \
  --name historian-mcp \
  -p 8080:8080 \
  -e FACTRY_HISTORIAN_API_URL=https://historian.example.com/api \
  -e FACTRY_HISTORIAN_API_TOKEN=your-historian-token \
  -e FACTRY_HISTORIAN_ORGANIZATION_UUID=your-org-uuid \
  -e TRANSPORT_MODE=http \
  -e HTTP_HOST=0.0.0.0 \
  -e HTTP_PORT=8080 \
  factry/mcp-server:latest
```

The server is now reachable at `http://your-server:8080`.

### Verify it is running

```bash
curl http://localhost:8080/health
```

You should receive a `200 OK` response with a JSON body like `{"status":"healthy","time":"..."}`.

### Test with MCP Inspector

The [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a tool for testing MCP servers. Install and launch it with:

```bash
npx @modelcontextprotocol/inspector
```

Then open the Inspector UI in your browser and:

1. Select **StreamableHttp** as the transport type
2. Enter URL: `http://localhost:8080`
3. Click **Connect**
4. Open the **Tools** tab and try calling `search-assets`

## Exposing over HTTPS

Most LLM platforms require the MCP server to be reachable over **HTTPS on port 443**. Even if your platform accepts a custom port, using HTTPS is strongly recommended to protect data in transit.

Run the MCP server on localhost and put a reverse proxy in front of it. The proxy handles TLS and forwards traffic to the container.

### Bind the container port to localhost only

When a reverse proxy handles all external traffic, there is no reason for the container's HTTP port to be reachable from outside the host. Use `127.0.0.1:8080:8080` instead of `8080:8080` in your `docker run` command to bind the port to localhost only:

```bash
docker run -d \
  --name historian-mcp \
  -p 127.0.0.1:8080:8080 \
  ...
  factry/mcp-server:latest
```

This ensures the unauthenticated HTTP port is never directly reachable from the network, only the reverse proxy can reach it.

### With Caddy (recommended, automatic TLS)

Install Caddy and create a `Caddyfile`:

```caddyfile
mcp.your-domain.com {
    reverse_proxy localhost:8080
}
```

Caddy automatically obtains and renews a Let's Encrypt certificate. Start it with:

```bash
caddy run
```

### With nginx

```nginx
server {
    listen 443 ssl;
    server_name mcp.your-domain.com;

    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/privkey.pem;

    location / {
        proxy_pass         http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   Connection '';
        proxy_buffering    off;
    }
}
```

The `proxy_buffering off` line is important, it ensures Server-Sent Events (SSE) are streamed in real time rather than buffered.

After setting up the reverse proxy, your MCP server is reachable at `https://mcp.your-domain.com`.

:::hint{type="info"}
Keep `HTTP_HOST=0.0.0.0` in the container if the reverse proxy is on the same host. If the proxy is on a different machine, make sure the port is reachable from that machine and not exposed to the wider internet directly.
:::

## Authentication

When the MCP server is accessible over a network, you should enable authentication so that only authorized users and applications can use it.

The server supports **JWT authentication**, which is compatible with any standard OAuth 2.0 / OpenID Connect provider, including Azure AD, Okta, Auth0, and Keycloak.

How it works:

1. Your LLM app obtains a JWT access token from your identity provider (it likely already does this as part of user login)
2. Your app passes the token in every MCP request as `Authorization: Bearer <token>`
3. The MCP server validates the token's signature and expiration, and (optionally) checks for required scopes

### Option A: JWKS validation (recommended for Azure AD and other OAuth providers)

Most identity providers sign tokens with a private key and publish the corresponding public keys at a JWKS endpoint. This is the most secure option and handles key rotation automatically.

Add these variables to your `docker run` command:

```bash
-e AUTH_PROVIDER=jwt \
-e JWKS_URL=https://your-identity-provider.com/.well-known/jwks.json
```

**Common JWKS URLs:**

| Provider                  | JWKS URL                                                                    |
| ------------------------- | --------------------------------------------------------------------------- |
| Azure AD (Entra ID)       | `https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys`         |
| Azure AD FS (on-premises) | `https://your-adfs.example.com/adfs/discovery/keys`                         |
| Okta                      | `https://your-domain.okta.com/oauth2/default/v1/keys`                       |
| Auth0                     | `https://your-domain.auth0.com/.well-known/jwks.json`                       |
| Keycloak                  | `https://your-keycloak.com/realms/your-realm/protocol/openid-connect/certs` |

Replace `{tenant-id}` with your Azure AD Directory (tenant) ID, visible in the Azure portal under **Azure Active Directory > Overview**.

Full example for Azure AD:

```bash
docker run -d \
  --name historian-mcp \
  -p 8080:8080 \
  -e FACTRY_HISTORIAN_API_URL=https://historian.example.com/api \
  -e FACTRY_HISTORIAN_API_TOKEN=your-historian-token \
  -e FACTRY_HISTORIAN_ORGANIZATION_UUID=your-org-uuid \
  -e TRANSPORT_MODE=http \
  -e HTTP_HOST=0.0.0.0 \
  -e HTTP_PORT=8080 \
  -e AUTH_PROVIDER=jwt \
  -e JWKS_URL=https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys \
  -e REQUIRED_SCOPES=api://your-app-id/historian.read \
  factry/mcp-server:latest
```

`REQUIRED_SCOPES` is optional but recommended, it ensures only tokens issued for the MCP server are accepted. Define a scope in your identity provider's app registration (e.g. `historian.read`) and pass its full URI here.

### Audience validation (strongly recommended)

Without audience validation, any token your identity provider issues for *any* application in the same tenant is accepted as long as the signature is valid. Set `EXPECTED_AUDIENCE` to the identifiers your tokens will carry:

```bash
-e EXPECTED_AUDIENCE=api://your-app-id,your-app-id
```

- Accepts a comma-separated list of audience strings
- Azure AD v1 tokens use `aud = api://{app-id}`, v2 tokens often use `aud = {app-id}` (bare GUID), listing both covers both cases
- If a token's `aud` claim doesn't match any of the configured values, the request is rejected with 401

### Option B: Shared secret (HMAC)

If your LLM app generates its own JWT tokens rather than using an OAuth provider, you can validate them with a shared secret.

```bash
-e AUTH_PROVIDER=jwt \
-e JWT_SECRET=your-secret-key-here
```

Use a long, randomly generated string. Tokens must use the HS256 algorithm and include `exp` and `sub` claims.

:::hint{type="warning"}
The shared secret must be kept confidential. Anyone who has it can generate valid tokens. Prefer JWKS validation when using an external identity provider.
:::

### OAuth 2.0 discovery (optional)

Some MCP clients (browser-based assistants, IDE integrations) don't let you paste a bearer token, they expect to run the OAuth flow themselves. For that to work, the server needs to tell them where to send the user for authentication. This is done via the [RFC 9728 Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728) endpoint at `/.well-known/oauth-protected-resource`.

To enable discovery, set two additional environment variables:

```bash
-e MCP_PUBLIC_URL=https://mcp.your-domain.com \
-e OAUTH_AUTHORIZATION_SERVER=https://login.microsoftonline.com/{tenant-id}/v2.0
```

- `MCP_PUBLIC_URL` the public URL clients reach the server on (behind your reverse proxy). When this is set, the server publishes metadata at `/.well-known/oauth-protected-resource` and sends a `WWW-Authenticate: Bearer resource_metadata=...` header on 401 responses. Leave unset to disable discovery (bearer-token-only mode).
- `OAUTH_AUTHORIZATION_SERVER` the authorization server's issuer URL. For Azure AD this is `https://login.microsoftonline.com/{tenant-id}/v2.0`. Without this, the metadata document is published but doesn't point clients anywhere.

If `REQUIRED_SCOPES` is set, those scopes are also advertised in the metadata as `scopes_supported`.

### Dynamic Client Registration shim (for turnkey MCP clients)

Some MCP clients require [RFC 7591 Dynamic Client Registration,](https://datatracker.ietf.org/doc/html/rfc7591) they refuse to proceed unless they can POST to a `registration_endpoint` and receive a client ID. Most enterprise identity providers (Azure AD / Entra ID, Okta without admin consent, on-prem ADFS) don't offer DCR. The server can bridge this gap by accepting registration requests itself and handing out a **pre-registered client ID** you configure in your IdP.

The shim is provider-agnostic, it works with any OIDC provider. Activate it by setting `OAUTH_REGISTERED_CLIENT_ID`:

```bash
-e OAUTH_REGISTERED_CLIENT_ID=your-preconfigured-client-id
```

When the shim is active the server also:

- Serves RFC 8414 AS metadata at `/.well-known/oauth-authorization-server` advertising itself as the authorization server
- Updates the Protected Resource Metadata so `authorization_servers` points at this server (clients discover our AS metadata rather than the upstream's)
- Serves `/authorize` (302 redirect to the upstream authorize endpoint, query params forwarded) and `/token` (reverse proxy to the upstream token endpoint)

**How it finds upstream endpoints:** on startup the server does OIDC discovery at `{OAUTH_AUTHORIZATION_SERVER}/.well-known/openid-configuration` to find `authorization_endpoint` and `token_endpoint`. If your provider doesn't support OIDC discovery, set both explicit overrides:

```bash
-e OAUTH_AUTHORIZE_URL=https://your-idp.example.com/oauth2/authorize \
-e OAUTH_TOKEN_URL=https://your-idp.example.com/oauth2/token
```

**No tokens are issued or signed by this server.** The shim only proxies endpoints and fakes registration. All JWTs continue to come from your upstream IdP and are validated against `JWKS_URL` as before.

**What to do in your IdP:** register one OAuth application manually with the client ID you'll pass as `OAUTH_REGISTERED_CLIENT_ID`. Register as many redirect URIs as you need (one per MCP client you plan to support). The shim hands out the same client ID to every MCP client that registers, each client uses its own redirect URI, which the IdP validates against the list you registered.

### Testing authentication with MCP Inspector

**Bearer token (simplest):**

1. Obtain a valid token from your identity provider
2. Launch the Inspector: `npx @modelcontextprotocol/inspector`
3. Select **StreamableHttp**, enter your URL, and expand **Custom Headers**
4. Add header name `Authorization` with value `Bearer your-token-here`
5. Click **Connect**

**Full OAuth flow (requires discovery enabled):**

1. Launch the Inspector and select **StreamableHttp**
2. Enter your server URL and open the **Auth** tab
3. Inspector fetches metadata from `/.well-known/oauth-protected-resource`, redirects you to the authorization server (e.g. Microsoft login), and exchanges the resulting code for a token
4. For this to work, register Inspector's redirect URI (shown in its UI) under **Authentication** in your Azure AD app registration

If you get `HTTP 401 trying to load well-known OAuth metadata` in Inspector, `MCP_PUBLIC_URL` isn't set. Verify the metadata endpoint is reachable without authentication:

```bash
curl https://mcp.your-domain.com/.well-known/oauth-protected-resource
```

## Connecting your LLM application

Your LLM app connects to the MCP server using a **Streamable HTTP** MCP transport. The exact API depends on which MCP SDK your app uses, but the connection details are always:

- **URL:** `https://mcp.your-domain.com` (or `http://localhost:8080` for local testing)
- **Transport:** Streamable HTTP (also called StreamableHTTP or HTTP+SSE depending on the SDK)
- **Header:** `Authorization: Bearer <token>` (when authentication is enabled)

The server exposes a single endpoint at `/` which handles both `POST` (for sending requests) and `GET` (for receiving server-sent events).

## Configuration reference

| Variable                             | Required    | Default     | Description                                                                                                                                                                                   |
| ------------------------------------ | ----------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FACTRY_HISTORIAN_API_URL`           | Yes         | None        | URL to your Factry Historian API                                                                                                                                                              |
| `FACTRY_HISTORIAN_API_TOKEN`         | Yes         | None        | Token for authenticating with Historian                                                                                                                                                       |
| `FACTRY_HISTORIAN_ORGANIZATION_UUID` | Yes         | None        | UUID of the organization in Historian                                                                                                                                                         |
| `TRANSPORT_MODE`                     | Yes         | `stdio`     | Set to `http` to enable HTTP mode                                                                                                                                                             |
| `HTTP_HOST`                          | No          | `localhost` | Interface to bind to. Default `localhost` binds to loopback only (`127.0.0.1`). Set to `0.0.0.0` to bind all interfaces                                                                       |
| `HTTP_PORT`                          | No          | `8080`      | Port to listen on                                                                                                                                                                             |
| `AUTH_PROVIDER`                      | No          | `none`      | Authentication method: `none` or `jwt`                                                                                                                                                        |
| `JWKS_URL`                           | Conditional | None        | JWKS endpoint for public key validation (RS256/ES256). Required when `AUTH_PROVIDER=jwt` and no `JWT_SECRET` is set                                                                           |
| `JWT_SECRET`                         | Conditional | None        | Shared secret for HMAC validation (HS256). Required when `AUTH_PROVIDER=jwt` and no `JWKS_URL` is set                                                                                         |
| `REQUIRED_SCOPES`                    | No          | None        | Comma-separated list of scopes that must be present in the token                                                                                                                              |
| `EXPECTED_AUDIENCE`                  | No          | None        | Comma-separated list of accepted `aud` claim values. Strongly recommended to prevent cross-resource token reuse                                                                               |
| `MCP_PUBLIC_URL`                     | No          | None        | Public URL of this MCP server. When set, enables RFC 9728 OAuth discovery via `/.well-known/oauth-protected-resource`                                                                         |
| `OAUTH_AUTHORIZATION_SERVER`         | No          | None        | OAuth authorization server issuer URL (e.g. `https://login.microsoftonline.com/{tenant}/v2.0`). Advertised in the discovery metadata and used for OIDC discovery when the DCR shim is enabled |
| `OAUTH_REGISTERED_CLIENT_ID`         | No          | None        | Pre-registered client ID to hand out on `/register`. Setting this activates the DCR shim (AS metadata, `/register`, `/authorize`, `/token`)                                                   |
| `OAUTH_AUTHORIZE_URL`                | No          | None        | Explicit upstream authorize endpoint. Overrides OIDC discovery if set. Useful when your IdP doesn't publish a `.well-known/openid-configuration` document                                     |
| `OAUTH_TOKEN_URL`                    | No          | None        | Explicit upstream token endpoint. Same semantics as `OAUTH_AUTHORIZE_URL`                                                                                                                     |
| `GRAFANA_URL`                        | No          | None        | URL to your Grafana instance (enables Grafana tools)                                                                                                                                          |
| `GRAFANA_API_KEY`                    | No          | None        | Grafana service account token                                                                                                                                                                 |
| `LOG_LEVEL`                          | No          | `info`      | Log verbosity: `debug`, `info`, `warn`, `error`                                                                                                                                               |
| `STATISTICS_DISABLED`                | No          | `false`     | Set to `true` to disable anonymous usage statistic                                                                                                                                            |

## Troubleshooting

**The server is unreachable from my LLM app**

- Check that the container is running: `docker ps`
- Verify the reverse proxy is running and listening on port 443
- Confirm DNS for your domain resolves to the correct server
- Test from the server itself: `curl http://localhost:8080/health`

**Requests are rejected with 401 Unauthorized**

- Confirm `AUTH_PROVIDER=jwt` is set and the token is included in the `Authorization` header
- Check the token has not expired
- Verify `JWKS_URL` is reachable from inside the container: `docker exec historian-mcp wget -qO- $JWKS_URL`
- Check the token contains the required scopes if `REQUIRED_SCOPES` is set

**Server-Sent Events are not streaming (responses are delayed)**

- Ensure `proxy_buffering off` is set in nginx, or that your reverse proxy does not buffer responses
- Some load balancers have their own buffering settings, consult your proxy's documentation

**The container exits immediately**

Run it without `-d` to see the error output:

```bash
docker run --rm \
  -e FACTRY_HISTORIAN_API_URL=https://historian.example.com/api \
  -e FACTRY_HISTORIAN_API_TOKEN=your-token \
  -e FACTRY_HISTORIAN_ORGANIZATION_UUID=your-uuid \
  -e TRANSPORT_MODE=http \
  factry/mcp-server:latest
```

Common causes: missing or misspelled environment variables, invalid Historian API token, or an invalid `AUTH_PROVIDER` / `TRANSPORT_MODE` value.
