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
devbranch - 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
/authorizeendpoint. - 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/.
- Create a custom
values.yamlbased onpackaging/helm/openwork-ee/values.yaml. - Set the MCP-specific configuration under the
configsection. - 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.mjsto implement real OAuth PKCE flows instead of the mock implementation. - Configure environment variables including
DEN_MCP_RESOURCE_URLandOAUTH_ISSUER_URLto connect your server to OpenWork. - Build the server using
pnpm run buildand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →