Skip to content
Open MCP Portal
v0.1.0Open source, MIT licensed

Open MCP Portal

One governed place for your company's MCP servers

Open MCP Portal (OMP) is where your teams register the MCP servers they build, publish them to a reviewed company catalog, and combine their tools into gateways. People and agents connect to a gateway with one URL, under your own sign-in, and see only what they may use.

Self-hosted: one Go service with its web UI built in, and PostgreSQL. Run it with Docker Compose or on Kubernetes.

Connect an MCP client to a gateway
# Add a gateway to Claude Code: no key, no header
claude mcp add --transport http support \
  https://mcp.example.com/mcp/support

# Run /mcp in Claude Code and sign in with your company account.
# The client sees only the tools this gateway gives you:
#   status__check_outage
#   tickets__search_tickets
#   tickets__create_ticket

Every call is checked again, carries a signed identity to the server, and leaves a record. The client's own credential is never passed on.

The problem

MCP servers multiply faster than anyone can keep track of them

MCP lets AI clients and agents use your company's systems through tools. That works until there are dozens of servers, hundreds of tools and everyone connecting to them in their own way.

  • Servers nobody can find

    Every team builds MCP servers for its own systems. Nobody has the full list, knows who owns each one, or can tell whether its tools changed since someone last looked.

  • Credentials in every config

    People paste URLs, keys and tokens into each client, one server at a time. The servers can't tell who is calling, and taking access back means finding every copy.

  • No one place to govern it

    There is nowhere to decide which people, teams or agents may use which tools, to review a server before the company relies on it, or to see afterwards what was called.

How it works

Clients in front, your MCP servers behind, rules in between

Teams register servers and the portal reads what they offer. Admins publish them to the catalog. Gateways pick the tools a group of people needs, and every call through a gateway takes the same path.

People and agents

  • Claude Code
  • VS Code
  • Cursor
  • Claude
  • Agents, CI and scripts

to /mcp/<gateway>

Open MCP Portal

  1. 1Sign in. An OAuth access token bound to this gateway, a personal API key or a service account key.
  2. 2Allow. Access to the gateway, the tool selected in it, the minimum role, the server enabled (and, from the catalog, published and approved), the rate limit.
  3. 3Vouch. A short-lived signed identity token, and the person's own token where a tool needs one.
  4. 4Forward. To the server over Streamable HTTP, through an address guard and your internal-network allowlist.
  5. 5Record. Who called what, the outcome and the latency. Arguments and results only where a team opted in.

Registry, catalog, gateways and records in PostgreSQL · web UI for teams and admins

Your teams' MCP servers

  • On your internal network
  • Public, over HTTPS
  • GitHub's hosted MCP server

Accounts people connect

  • GitHub
  • Google
  • Microsoft Entra ID
  • Atlassian
  • Slack
  • GitLab
Tool lists come from the stored definitions (for catalog servers, the approved ones) without contacting the servers; every call is checked again. A tool that isn't allowed looks exactly like one that doesn't exist.

Features

From a team's server to a governed tool call

OMP is one installation for one company. Teams own their servers and gateways; platform admins set the rules, review what is published and see everything; platform auditors see the same, read-only.

Registry and discovery

Register a server; the portal reads what it offers

A team adds its MCP server's URL. The portal discovers its tools, prompts, resources and resource templates, and keeps checking that the server is healthy.

  • Owned by teams, private to the team until it is published. Editors register and test servers; owners and admins publish them.
  • Change detection: each definition is fingerprinted. When an approved tool changes, it drops out of the catalog until someone approves the new version; a tool that disappears loses its approval.
  • Health checks every five minutes by default, with the error class and a “failing since” time.
  • A static header such as an API key can be stored per server, encrypted, and shown only as its last four characters.

Servers are remote, reached over Streamable HTTP. The portal doesn't host or run MCP servers.

Company catalog

Published after review, not before

The catalog lists the servers the whole company may build on, with only their approved capabilities. Any signed-in person can browse it.

  • A publishing policy: approval required (the default) or self-publish, with an override per team, so a trusted team can publish on its own.
  • Back to review, under approval required, when a published server changes its URL, starts asking people to sign in, or asks for wider access.
  • Descriptions are treated as untrusted text: a model will read them, so reviewers see them before approving. Tool annotations are shown as hints and never used for authorization.

Gateways

Curated tools at one URL

A gateway is a selection of tools, prompts and resources from one or more servers, served at https://mcp.example.com/mcp/<gateway>. Build as many as you need: one for support, one for an on-call team, one for a release pipeline.

  • Team gateways are curated by a team's owners and admins from their own servers and the catalog, for the team's members.
  • Platform gateways are curated by platform admins and granted to teams, people or SSO groups from your identity provider.
  • A minimum team role per tool on team gateways: members might read, while only editors may call the tool that writes.
  • No clashes: tools are exposed as server__tool, and resources under omp://server/, so servers can use the same names.
  • Checked on every call: removing someone from a team or a grant, turning a gateway off or unpublishing a server takes effect at the next request.
  • Limits per gateway: a rate limit per minute, and a switch to record call contents.

Client sign-in

Sign-in for any MCP client

The portal is an OAuth 2.1 authorization server as the MCP authorization spec describes, in front of your company's single sign-on. Interactive clients open the browser; headless ones use keys.

  • OAuth sign-in with PKCE, Client ID Metadata Documents and dynamic registration. Each connection is bound to one gateway; self-registered apps are marked unverified.
  • Consent per person, app and gateway. Apps your admins trust (Claude, Claude Code and VS Code out of the box) skip the consent page. People see and disconnect their apps under Connected apps.
  • Personal API keys for scripts and clients that can't open a browser, optionally limited to some gateways, with an expiry of up to a year.
  • Service accounts for agents and CI that must not depend on one person: owned by a team, with a member or editor role and up to two active keys, so they rotate without downtime.

Per-person credentials

Tools that act as the person calling them

Some tools must act as you: on GitHub's hosted MCP server, or on Google, Microsoft or Atlassian behind your own server. The portal signs each person in once and keeps their token, so neither the client nor the model ever sees it.

  • OAuth-protected MCP servers: Detect sign-in reads the server's metadata; the portal registers itself as a client, or uses one you enter or a platform provider. Each person's token goes only to that server.
  • Delegated credentials for your own servers: admins configure auth providers (presets for GitHub, Google, Microsoft Entra ID, Atlassian, Slack and GitLab), and a server asks for a provider and scopes, per tool if needed.
  • A link, not an error: the first call answers with a one-time link (JSON-RPC -32042) to a page that names the tool, the gateway and every scope. It works only for that person, once, for 10 minutes.
  • Kept fresh, easy to revoke: tokens are encrypted at rest and refreshed by the portal. People disconnect under My connections; admins can revoke anyone's.

Signed identity

Your servers know who is calling

Every forwarded call carries a short-lived token signed by the portal: who is calling, through which gateway, for which tool. Servers verify it against the portal's published keys.

  • A JWT per call, ES256, valid for five minutes, with the audience set to the server's own URL, so it can't be replayed elsewhere.
  • Keys at /.well-known/jwks.json, rotated every 30 days, with the next key published a day before it signs.
  • People and service accounts are told apart, with email and teams for people.
  • Off for outside servers: a per-server switch sends no identity to servers run outside the company.

The details for server authors are below.

Governance

Audit, limits and records you can answer for

Every administrative change is audited in the same transaction that makes it. Every call is recorded with its metadata. Call contents are kept only where a team chose to keep them.

  • An audit log for the platform and each team: servers, reviews, gateways, grants, keys, consents and connections. Never a secret or a token.
  • Call records with the caller, the client, the gateway, server and tool, the outcome, latency and sizes; teams see calls through their gateways and to their servers.
  • Rate limits per person, credential, service account and gateway, with ceilings and team overrides, counted in PostgreSQL and shared by every replica.
  • Opt-in payload capture per gateway or server: arguments and results, redacted by built-in rules and your own patterns, cut to 16 KiB and encrypted. Only the owning team's admins and platform staff can open them, and every view is audited.
  • Retention with team overrides: call records 90 days, captured contents 14 days (at most 90), the audit log 400 days by default.
  • Usage dashboards for teams and platform admins: calls, errors by outcome, p50 and p95 latency by gateway, server, tool, team or kind of caller, over up to 90 days.
  • Prometheus metrics on a separate listener, with no user, key or team IDs in labels.
  • Key rotation for the encryption key and the API key pepper, while serving.

Connect your AI client

Add the URL; sign in in the browser

Every gateway has one MCP endpoint, https://mcp.example.com/mcp/<gateway>. My gateways lists the ones you can use, with these snippets filled in. There is no key to copy: the first time the client connects, it opens your browser and you sign in with your company account.

Claude Code
claude mcp add --transport http support \
  https://mcp.example.com/mcp/support

# then run /mcp in Claude Code
# and choose "support" to sign in
VS Code: .vscode/mcp.json
{
  "servers": {
    "support": {
      "type": "http",
      "url": "https://mcp.example.com/mcp/support"
    }
  }
}
Cursor: ~/.cursor/mcp.json
{
  "mcpServers": {
    "support": {
      "url": "https://mcp.example.com/mcp/support"
    }
  }
}
Scripts and agents: a personal or service account key
curl -X POST https://mcp.example.com/mcp/support \
  -H "Authorization: Bearer omp_..." \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Personal keys start with omp_, service account keys with omp_sa_. Both are shown once. Connecting clients

What a connected app can do

  • It acts as you, on that gateway only: its token is refused by every other gateway.
  • It sees the tools you may use there now. Lose access and the next request is refused.
  • Access tokens last an hour; the app refreshes them, and signs in again after 30 days unused. A refresh token reused after 30 seconds ends the connection.
  • Claude (claude.ai and Claude Desktop) connects as a custom connector; for claude.ai the portal must be reachable from the internet.

For MCP server authors

Build the tools; leave sign-in to the portal

Your server receives calls from the portal, never the client's own key or token. Verify the identity token and you know who is calling, from which team and through which gateway, without running sign-in or storing anyone's tokens yourself.

Verify the identity token (TypeScript, jose)
import { createRemoteJWKSet, jwtVerify } from "jose";

const PORTAL = "https://mcp.example.com";
const MY_URL = "https://status.example.com/mcp"; // as registered

// Caches keys and fetches the set again for an unknown kid.
const jwks = createRemoteJWKSet(
  new URL(`${PORTAL}/.well-known/jwks.json`),
);

export async function identity(headers: Headers) {
  const token =
    headers.get("mcp-gateway-identity") ??
    headers.get("authorization")?.replace(/^Bearer /, "") ?? "";
  const { payload } = await jwtVerify(token, jwks, {
    algorithms: ["ES256"],
    issuer: PORTAL,
    audience: MY_URL,
    clockTolerance: 30,
  });
  return payload; // principal_type, sub, email, teams, tool
}
Identity token claims, besides iss, aud, exp and jti
principal_typeuser, or service_account
subThe person's or service account's stable ID
email, nameThe person's email (never for service accounts) and name
teamsSlugs of the caller's teams
gatewayThe gateway's slug
tool, prompt, resourceWhat was called, exactly one of them
clientThe MCP client, with client_verified for OAuth apps
providerSet when a provider token comes with the call
  • Where the token arrives: in Authorization: Bearer, or in MCP-Gateway-Identity when your server uses the Authorization header for something else. Check the signature (ES256, keys from the JWKS), the issuer, the audience (your server's URL as registered) and the expiry.
  • Provider tokens: when a tool needs the person's account on another service, the call also brings MCP-Gateway-Provider (the provider, such as github) and MCP-Gateway-Provider-Token (their access token, refreshed by the portal). Accept it only when the identity token's provider claim matches, and never log or store it.
  • OAuth servers work as they are: if your server is itself an OAuth resource server, the portal is an ordinary MCP client of it. Each person's token for your server arrives as the bearer, and the portal handles a 401 with one refresh and a 403 insufficient_scope by asking the person for more access.
  • Parameter headers: when a tool's input schema marks an argument with x-mcp-header, forwarded tool calls also carry its value in an Mcp-Param-<Name> header, built from the approved definition (SEP-2243).
  • Calls only: lists are served from the approved definitions, so your server sees tool calls, prompt gets and resource reads, plus discovery and health checks.

The repository includes a worked example: a Gmail MCP server that used to run its own Google OAuth and now relies on the portal's identity token and delegated Google token. It stores no tokens, sessions or client secret. examples/mail-mcp

The web UI

One portal for people, teams and administrators

Everyone signed in sees the catalog and their gateways. Teams manage their servers, gateways, service accounts and usage. Platform admins review, grant and configure; auditors see the same pages, read-only.

Every gateway has one URL. Interactive clients sign in in the browser; scripts and clients that can't sign in use an API key.
The catalog lists only published servers, and only the capabilities whose current definition was approved.
Detect sign-in reads an OAuth-protected server's metadata, such as GitHub's hosted MCP server, and shows what is still needed.
A gateway exposes only what was selected for it, named server__tool so tools from different servers never clash.
Consent is remembered per person, app and gateway. Apps your platform admins trust skip this page; people still sign in.
A tool that needs a person's own account answers with a one-time link to this page. The token stays in the portal; the AI client never sees it.
Usage is rolled up hourly and kept for ranges of up to 90 days. There is no grouping by person.
Call metadata is always kept. Arguments and results only where a team turned recording on: redacted, and every view audited.

Self-host

Run it on your own infrastructure

Open MCP Portal is one stateless binary with the web UI built in, in a distroless container image. Every replica shares PostgreSQL, so you scale it by adding replicas; background loops such as health checks and retention take a database lock, so only one replica runs each.

Servers on your internal network need an allowlist entry: platform admins add networks and host names under Admin › Internal networks, and the deployment can add its own. Loopback and cloud metadata addresses stay blocked whatever the list says.

Try Open MCP Portal on your machine with Docker Compose
git clone https://github.com/ncecere/open-mcp-portal.git
cd open-mcp-portal
make demo    # portal + PostgreSQL + a fake MCP server
             # on http://localhost:8090, with development sign-in
  1. Open http://localhost:8090 and sign in as admin. Create a team under Admin › Teams.
  2. In the team, add the fake MCP server as http://127.0.0.1:8091/mcp. The portal reads its tools, prompts and resources.
  3. Create a gateway from some of its tools and an API key under Account › API keys, then connect a client to http://localhost:8090/mcp/<gateway>.

The demo uses development sign-in and public example keys, which the portal accepts only on a loopback address. Never expose it.

Kubernetes: the runtime secret the Kustomize base expects
kubectl create namespace open-mcp-portal
kubectl -n open-mcp-portal create secret generic open-mcp-portal-runtime \
  --from-literal=DATABASE_URL='postgres://omp:<password>@db.example.com:5432/omp?sslmode=require' \
  --from-literal=ENCRYPTION_KEY="$(openssl rand -base64 32)" \
  --from-literal=API_KEY_PEPPER="$(openssl rand -base64 32)" \
  --from-literal=OIDC_CLIENT_SECRET='<from your IdP>'

What you need

  • PostgreSQL: the only dependency. No cache, queue, worker or object storage
  • An OpenID Connect identity provider for sign-in, optionally with a groups claim for SSO group grants
  • TLS in front of the portal, on the host in APP_URL
  • Two 32-byte secrets: ENCRYPTION_KEY and API_KEY_PEPPER, kept with your backups

On Kubernetes

A Kustomize base and an example overlay: two replicas with a migrate init container, a disruption budget, default-deny NetworkPolicies and a pod that meets the restricted Pod Security Standard, with Ingress or Gateway API routes. /healthz and /readyz for probes; graceful shutdown on SIGTERM.

There is no published image or Helm chart yet: build the image with make docker and pin it by digest in your overlay.

Open source and project status

Open source

MIT licensed, built in the open

Open MCP Portal is released under the MIT licence. Run it for as many people as you like, and change the code if you need to.

It is written in Go with PostgreSQL, and a React web UI built into the binary. The management API is described in OpenAPI, which generates the Go and TypeScript types. Integration tests run against real PostgreSQL, an authorization matrix covers every operation for every role, and component tests cover the UI.

Project status

v0.1.0: the first release, and honest about it

Everything on this page is built and tested against real PostgreSQL. Real clients work: Claude Code and pi have signed in to gateways over OAuth and called tools, and GitHub's hosted MCP server has run end to end with each person's own GitHub sign-in.

It is pre-1.0 software and hasn't run in production. Not tried yet: delegated credentials with a real Google or Microsoft provider (only a fake one so far), the CI workflow on GitHub's runners, and a smoke test on a real Kubernetes cluster. Pilot it before you depend on it.

Not built yet: streaming responses through to the client (calls are buffered, up to 1 MiB), resource subscriptions, and forwarding of sampling, elicitation and roots. Later: servers hosted by the portal itself, and personal gateways.

Read the status and handoff notes and the releases.