# How to Build and Deploy a Custom MCP Server with OAuth Support in OpenWork

> Learn to build and deploy a custom MCP server with OAuth support in OpenWork. Extend the reference implementation and configure environment variables for seamless deployment via Helm or Docker.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-04

---

**You can build and deploy a custom MCP server with OAuth support by extending the reference implementation in `scripts/mock-oauth-mcp-server.mjs`, configuring the required environment variables (`DEN_MCP_RESOURCE_URL`, `OAUTH_ISSUER_URL`), and deploying via Helm or Docker.**

OpenWork's **MCP (Multi-Capability Protocol)** server acts as the gateway that enables AI agents to invoke external tools and services securely. When you build and deploy a custom MCP server with OAuth support in OpenWork, you create an authenticated bridge between your internal APIs and OpenWork-compatible agents like Claude Code or Cursor. This guide walks through the architecture, source files, and deployment steps using the actual OpenWork codebase.

## Architecture Overview

OpenWork's MCP implementation consists of several coordinated components that handle request dispatching, OAuth validation, and deployment configuration.

| Component | Role | Key Source File |
|-----------|------|-----------------|
| **MCP Core** | Handles `/mcp` routes, dispatches capability requests, and returns JSON-RPC style results. | [`packages/types/src/den/mcp-connection-action.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/mcp-connection-action.ts) |
| **OAuth Provider** | Issues JWT access tokens, validates PKCE code challenges, and supplies the token endpoint. | `scripts/mock-oauth-mcp-server.mjs` (reference implementation) |
| **Connection Registry** | Persists external MCP connection definitions including URL and OAuth configuration. | [`packages/types/src/agent-context-diagnostics.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/agent-context-diagnostics.ts) |
| **Deployment Configuration** | Provides environment variable wiring and SSRF guard configuration. | [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml) |

The **Den API** (OpenWork's backend service) orchestrates these components, but you can instantiate them locally for custom on-premise or cloud deployments.

## Prerequisites

- **Node.js** ≥ 18 and **pnpm** (OpenWork's package manager)
- OpenWork source code checked out from the `dev` branch
- An OAuth 2.0 provider that supports **Authorization Code with PKCE** (Auth0, Okta, Keycloak, or similar)
- Kubernetes cluster (for Helm deployment) or Docker (for local testing)

## Step-by-Step Build and Deploy Guide

### Clone and Install Dependencies

Start by cloning the repository and installing workspace dependencies:

```bash
git clone https://github.com/different-ai/openwork.git
cd openwork
git checkout dev
pnpm install

```

This installs all packages required to build the MCP core and supporting libraries.

### Create a Minimal MCP Server

The repository provides a complete reference implementation in `scripts/mock-oauth-mcp-server.mjs`. Copy this file to create your custom server:

```bash
cp scripts/mock-oauth-mcp-server.mjs scripts/custom-mcp-server.mjs

```

Modify the following sections in your new file:

- **Lines 24-33**: Update the provider description strings to reflect your OAuth provider details.
- **Lines 305-319**: Replace the mock HTML login UI with a redirect to your provider's `/authorize` endpoint.
- **Lines 447-460**: Customize the tool descriptions that your MCP server advertises to agents.
- **Lines 676-686**: Replace the mock token validation with real JWT verification using your provider's JWKS endpoint.

The `allowUnauthenticatedMcp` flag (around line 33) should remain `false` in production to enforce authentication.

### Configure OAuth Environment Variables

OpenWork expects specific environment variables to wire OAuth and MCP routing. Set these in your deployment environment:

| Variable | Description | Example |
|----------|-------------|---------|
| `DEN_MCP_RESOURCE_URL` | Base URL for the MCP endpoint where agents connect. | `https://mcp.company.com/mcp` |
| `DEN_MCP_ADDITIONAL_RESOURCES` | Comma-separated list of additional public MCP URLs for multi-tenant setups. | `https://api.company.com/mcp` |
| `DEN_ALLOW_PRIVATE_MCP_URLS` | Set to `1` to disable SSRF protection for private network deployments. | `1` (trusted environments only) |
| `OAUTH_ISSUER_URL` | URL of your OAuth provider's OpenID Connect discovery document. | `https://auth.company.com/.well-known/openid-configuration` |
| `OAUTH_CLIENT_ID` | Client identifier registered with your OAuth provider. | `openwork-mcp-client` |

### Build and Run Locally

Compile the workspace and start your custom server:

```bash
pnpm run build
node scripts/custom-mcp-server.mjs

```

The server starts on `0.0.0.0:8788` and logs the MCP URL. Test locally by visiting `http://127.0.0.1:8788/mcp/health`, which should return `{ "ok": true }`.

### Deploy with Helm (Kubernetes)

For production deployments, use the provided Helm chart at `packaging/helm/openwork-ee/`.

1. Create a custom [`values.yaml`](https://github.com/different-ai/openwork/blob/main/values.yaml) based on [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml).
2. Set the MCP-specific configuration under the `config` section.
3. Deploy to your cluster:

```bash
helm upgrade --install openwork-ee ./packaging/helm/openwork-ee \
  -f my-values.yaml \
  --namespace openwork

```

The chart automatically injects environment variables into the `den-api` deployment and configures the SSRF guard based on `DEN_ALLOW_PRIVATE_MCP_URLS`.

### Register the Server in OpenWork

Add your server to OpenWork's configuration using the connection schema from [`packages/types/src/den/mcp-connection-action.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/mcp-connection-action.ts):

```json
{
  "mcp": {
    "myCustomMcp": {
      "type": "remote",
      "enabled": true,
      "url": "https://mcp.company.com/mcp",
      "oauth": {
        "issuer": "https://auth.company.com",
        "clientId": "openwork-mcp-client",
        "scopes": ["openid", "profile", "email"]
      }
    }
  }
}

```

The OpenWork client automatically initiates the OAuth Authorization Code flow with PKCE, stores the resulting JWT, and begins invoking capabilities.

### Verify the Deployment

Test the complete flow using the OpenWork CLI or compatible agent:

```bash
opencode mcp list
opencode mcp exec myCustomMcp echo --args "Hello from custom MCP"

```

The first command triggers the OAuth consent flow. After authorization, the second command should return the echoed message, confirming that your server correctly handles authenticated requests.

## Code Examples

### Minimal Express MCP Server (TypeScript)

This example shows the essential wiring for OAuth validation and MCP request handling:

```typescript
import express from "express";
import { json } from "body-parser";
import { verifyJwt } from "./jwt-utils";
import { handleMcpRequest } from "./mcp-core";

const app = express();
app.use(json());

const OAUTH_ISSUER = process.env.OAUTH_ISSUER_URL!;
const CLIENT_ID = process.env.OAUTH_CLIENT_ID!;

// OAuth callback handler
app.get("/mcp/oauth/callback", async (req, res) => {
  const { code, state } = req.query;
  
  const tokenResp = await fetch(`${OAUTH_ISSUER}/oauth/token`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      client_id: CLIENT_ID,
      code,
      redirect_uri: `${process.env.DEN_MCP_RESOURCE_URL}/mcp/oauth/callback`,
      code_verifier: state
    }),
  });
  
  const { access_token } = await tokenResp.json();
  res.redirect(`${process.env.DEN_MCP_RESOURCE_URL}/mcp?access_token=${access_token}`);
});

// MCP request handler
app.post("/mcp", async (req, res) => {
  const auth = req.headers.authorization?.split(" ")[1];
  if (!auth) return res.status(401).json({ error: "Missing token" });

  const claims = await verifyJwt(auth, OAUTH_ISSUER);
  if (!claims) return res.status(401).json({ error: "Invalid token" });

  const result = await handleMcpRequest(req.body, claims);
  res.json(result);
});

app.listen(8788, () => console.log("Custom MCP server listening on :8788"));

```

The `handleMcpRequest` function corresponds to the core dispatcher logic found in [`packages/types/src/den/mcp-connection-action.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/mcp-connection-action.ts).

### Dockerfile

Create a production-ready container image:

```dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN pnpm install && pnpm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/packages/types/dist ./packages/types/dist
COPY scripts/custom-mcp-server.mjs .
ENV NODE_ENV=production
EXPOSE 8788
CMD ["node", "custom-mcp-server.mjs"]

```

Push this image to your registry and reference it in your Helm [`values.yaml`](https://github.com/different-ai/openwork/blob/main/values.yaml).

### Helm Configuration

Example excerpt for [`values.yaml`](https://github.com/different-ai/openwork/blob/main/values.yaml):

```yaml
config:
  public:
    mcpResourceUrl: "https://mcp.company.com/mcp"
    mcpAdditionalResources: ""
  env:
    OAUTH_ISSUER_URL: "https://auth.company.com"
    OAUTH_CLIENT_ID: "openwork-mcp-client"
    DEN_ALLOW_PRIVATE_MCP_URLS: "0"

```

## Key Source Files Reference

| File | Purpose |
|------|---------|
| [`packages/types/src/den/mcp-connection-action.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/mcp-connection-action.ts) | Defines the JSON schema for MCP connection actions and validates incoming requests. |
| [`packages/types/src/agent-context-diagnostics.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/agent-context-diagnostics.ts) | Contains the `cloudMcpPathSchema` used to validate MCP URL formats. |
| `scripts/mock-oauth-mcp-server.mjs` | Complete reference implementation including OAuth UI and MCP dispatcher. |
| [`packaging/helm/openwork-ee/values.yaml`](https://github.com/different-ai/openwork/blob/main/packaging/helm/openwork-ee/values.yaml) | Helm values showing environment variable configuration for MCP and OAuth. |
| [`packaging/docker/docker-compose.den-dev.yml`](https://github.com/different-ai/openwork/blob/main/packaging/docker/docker-compose.den-dev.yml) | Docker Compose setup for local development with MCP environment variables. |

## Summary

- **Clone** the OpenWork repository and install dependencies using **pnpm** to access the MCP core libraries.
- **Extend** `scripts/mock-oauth-mcp-server.mjs` to implement real OAuth PKCE flows instead of the mock implementation.
- **Configure** environment variables including `DEN_MCP_RESOURCE_URL` and `OAUTH_ISSUER_URL` to connect your server to OpenWork.
- **Build** the server using `pnpm run build` and test locally before containerizing.
- **Deploy** using the provided Helm chart or Docker Compose, ensuring SSRF protection is configured appropriately via `DEN_ALLOW_PRIVATE_MCP_URLS`.
- **Register** your server in OpenWork's configuration with the OAuth block to enable automatic token management.

## Frequently Asked Questions

### What is the MCP protocol in OpenWork?

**MCP (Multi-Capability Protocol)** is OpenWork's standardized protocol that allows AI agents to discover and execute external tools. According to the source code in [`packages/types/src/den/mcp-connection-action.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/mcp-connection-action.ts), MCP servers expose capabilities via JSON-RPC style endpoints and handle authentication through OAuth 2.0 PKCE, enabling secure integration between OpenWork agents and third-party services.

### How does OAuth PKCE work with OpenWork MCP servers?

OpenWork MCP servers use **OAuth 2.0 with PKCE (Proof Key for Code Exchange)** to secure the authentication flow between agents and the server. As implemented in `scripts/mock-oauth-mcp-server.mjs`, the server generates a code challenge during the authorization request and validates the code verifier when exchanging the authorization code for tokens, preventing authorization code interception attacks.

### Can I deploy an MCP server without Kubernetes?

Yes, you can deploy a custom MCP server using **Docker** or running the Node.js process directly on a virtual machine. The repository includes [`packaging/docker/docker-compose.den-dev.yml`](https://github.com/different-ai/openwork/blob/main/packaging/docker/docker-compose.den-dev.yml) which demonstrates the required environment variable configuration for non-Kubernetes deployments. Ensure you set `DEN_MCP_RESOURCE_URL` and OAuth variables correctly regardless of the deployment method.

### How do I troubleshoot OAuth authentication failures?

Check the following based on the source implementation: verify that `OAUTH_ISSUER_URL` points to a valid `.well-known/openid-configuration` endpoint, ensure `DEN_MCP_RESOURCE_URL` matches your deployed URL exactly (including the `/mcp` path), and confirm that your OAuth provider has registered the redirect URI `${DEN_MCP_RESOURCE_URL}/oauth/callback`. The mock server implementation in `scripts/mock-oauth-mcp-server.mjs` includes detailed logging around lines 676-686 that you can replicate in your custom server for debugging token validation failures.