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

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
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
Deployment Configuration Provides environment variable wiring and SSRF guard configuration. 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:

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:

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:

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 based on packaging/helm/openwork-ee/values.yaml.
  2. Set the MCP-specific configuration under the config section.
  3. Deploy to your cluster:
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:

{
  "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:

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:

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.

Dockerfile

Create a production-ready container image:

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.

Helm Configuration

Example excerpt for values.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 Defines the JSON schema for MCP connection actions and validates incoming requests.
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 Helm values showing environment variable configuration for MCP and OAuth.
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, 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →