An open-source MCP gateway proxies the call, not the approval
An MCP gateway puts many MCP servers behind one endpoint and one credential store. It applies the access rules configured in it, and injects the credential each call carries. Kortix is the open-source AI Operating System, and the rule that governs a call is a line in kortix.yaml, in the project repo you own.
One endpoint, and the two decisions it leaves open
A team with six MCP servers usually buys a gateway for arithmetic. Six client configurations. Six sign-ins. Six places a token is stored. A gateway collapses that into one endpoint, with routing and credentials handled in the same place. The arithmetic holds.
Kortix is the open-source AI Operating System. Your agents, skills, company memory, and connectors live in one git repo you own. Kortix itself is open source, and the manifest in your project repo is versioned. One entry can point at somebody else's MCP server: provider: mcp, with a url and a transport of http by default or sse.
What a gateway centralizes is the connection. It also applies the access rules configured in it, and those rules live in its own config: an Admin UI and an API for ContextForge, a profile for Docker, a database for MCPJungle, Git-backed catalogs and vMCP profiles for Obot. None of that config holds an approval of one exact call, with its arguments, on a named account. The policy that lets a call run is a line in kortix.yaml, and the entry it belongs to is what an MCP connector is.
Four gateways, described by their own projects
- IBM ContextForge calls itself "an open source registry and proxy that federates tools, agents, and APIs into one clean endpoint for your AI clients. It provides centralized governance, discovery, and observability" (ContextForge). It runs as a compliant MCP server and installs from PyPI or Docker. It ships an Admin UI, OpenTelemetry tracing into Phoenix, Jaeger, Zipkin or another OTLP backend, 40+ plugins, and JWT secrets that have to be set before it starts.
- Docker's gateway is "Docker's open source solution for orchestrating Model Context Protocol (MCP) servers", which "acts as a centralized proxy between clients and servers, managing configuration, credentials, and access control" (Docker docs). Each MCP server runs in an isolated container with restricted privileges, network access and resource use. The gateway injects the credentials a server needs, applies security restrictions, and keeps logging and call tracing. A profile decides which servers are available. Docker states in the same place that "MCP Gateway as part of Docker AI Governance is an invite-only feature."
- MCPJungle is "a self-hosted MCP gateway for developers and teams who want to manage multiple MCP servers without scattered client configurations, duplicated setup, or inconsistent access control" (MCPJungle). A server is registered once from the command line. Every client then connects to a single endpoint at
/mcp. Optional tool groups expose only the tools a client should see, and the project documents access-control and observability hooks for shared deployments. It runs on Docker Compose and keeps its state in SQLite by default, with PostgreSQL supported. - Obot is "an open-source platform for organizations to manage, secure, and govern their AI ecosystems" (Obot). Its MCP Gateway exposes virtual MCP servers that combine one or more catalog servers behind a single endpoint. Each vMCP component is pinned "to a snapshot of its catalog entry, so catalog changes take effect only through an explicit upgrade". Access is controlled by user or identity-provider group through vMCP profiles. MCP tunnels connect outbound, so no inbound port has to be opened, and the MCP and Skills registries are "built on curated Git-backed catalogs".
Four descriptions, one shape. All four move a call from a client to a server, keep the server's credentials off the client, and record something about the call. ContextForge and Docker add tracing. MCPJungle adds tool groups. Obot adds catalog pinning and an audit log of MCP and LLM activity.
Routing, credential injection, isolation and logging are infrastructure work. A gateway does all four in one service, and that is the reason to buy one. How a gateway differs from the server it fronts settles which of the two words applies to a product.
The account behind the credential is a second decision
A gateway holds credentials and injects them into the right call. Docker's gateway injects the credentials a server needs. Portkey's gateway "acts as a proxy between MCP Clients and MCP servers, handling authentication, access control, and logging", with credential injection and permission checks per server (Portkey). Holding a credential is not the same as knowing whose it is.
Kortix makes the account an object on the connector. A connector holds one or more accounts, and an account is the authorized identity a call runs as, carrying a label and an owner type. When a call names no account, or names me or project, Kortix uses one only when the choice is certain: exactly one is reachable, or exactly one is pinned as the default. Otherwise the call is denied with account_required and the list of available_accounts. Kortix never substitutes another account (Kortix docs).
Each account carries an owner type. A shared account is owner_type: project. It covers everyone in the project and every agent whose connectors grant lists the connector. A private account is owner_type: member. It runs only in its owner's private sessions. An unattended run, a trigger or code holding a service-account key kortix_sa_…, never uses a private account.
A concrete case shows why the two facts differ. Acme connects its issue tracker and two people authorize an account each. The gateway in front of it holds both credentials and forwards either one. Kortix has two reachable accounts for one connector, so a call that names neither stops with account_required. The fix is to pin a shared account as the default, in the app or with kortix connectors accounts <slug> --default, or to name the account in the call. Either way a person wrote down whose identity the work runs as.
That pair of rules is where a proxy runs out of road. A gateway can prove a credential was attached to a request. Whether the credential belongs to a person, to a project, or to a job that starts at 3am is a property of the account, and the account is a live connection with a label, an owner type and a default pin, kept in the product's own state. Kortix shows the set: the Accounts tab lists every account a connector can run as, to everyone with access to the project. Whose permissions an MCP connector uses takes the choice apart.
The approval question is narrower. It asks whether this exact call was approved, with these arguments, on this account. Kortix answers it per request: Allow runs the call (always_run), Ask holds it (require_approval), Block refuses it (block). "Approval holds the call; the agent's turn pauses and resumes." The scope is tight. An approval covers one exact request, the same connector, action, arguments and account. A changed argument needs a new approval. An identical repeat within 15 minutes runs once, and a held call expires in 24 hours or the project's approvals.ttl.
Where each product keeps the rule
Here the four descriptions part company, and the split decides a review. ContextForge takes its rules through an Admin UI and an API. Docker writes the server list in a profile you choose in Docker Desktop or through its CLI plugin. MCPJungle keeps registered servers and their access rules in its own database. Obot keeps catalogs in Git-backed repositories and pins each vMCP to a snapshot of its catalog entry, so a catalog change arrives through an explicit upgrade, while its per-tool rules are configuration in Obot.
Kortix keeps the connector entry and the rules on its actions in kortix.yaml, inside the project's git repo. The policy that lets a call run is part of that entry. The account is not part of it: the account is a live connection on the connector, with a label, an owner type and a default pin, managed in the app or through the CLI.
The difference shows up on the day someone asks who changed a rule. A rule in a database changes for anyone who can reach the database. A rule in a profile changes for anyone who edits the profile. A rule in a repo arrives as a diff a teammate can read, and session work reaches the default branch through a change request a person approves unless an admin granted the merge capability. A dashboard edit or a direct push can still land without that read, and the diff is what makes the change visible either way.
What each product governs, where its rule lives, who can read a change
Kortix is open source and the recommendation. Each rival row restates that product's own description of what it governs.
kortix.yaml in the project's git repoWhat a file looks like when it holds the decision
connectors:
- slug: linear
provider: mcp
url: https://mcp.linear.app/mcp
transport: http
policies:
- match: "*.delete*"
action: block
- match: "linear.create_issue"
action: require_approval
conditions:
- arg: team.key
negate: true
match: "ENG"Kortix reads that file in a fixed order, and the first match wins. Project-wide policies: come first, with fully qualified patterns such as gmail.send_email. A connector rule cannot override them. Then the connector's own policies:, with patterns relative to the connector, such as send_*. Then sensitive: true, which sends every remaining action to approval, reads included. Then policy.default_mode, where allow_all runs everything still unmatched and risk runs reads while holding writes and destructive actions. A narrow rule near the top cannot be loosened by a broad one further down.
A rule can test the arguments of a call as well. arg is a dot path into the arguments, team.key in the example, and negate: true inverts the condition. This rule holds issue creation for every team except ENG. A rule holds up to 10 conditions and all of them must match. A missing argument never matches the pattern. A malformed condition fails closed, so a typo in a rule cannot open an action.
The order matters at the bottom of the file too. policy.default_mode falls back to allow_all, so a project with no rules runs every action its connector can reach. "Approval gates you set. Off until you set them."
Two facts decide the rest. Connector credentials are brokered server-side and never enter the machine, which is why no secret appears anywhere in the file. And "Every action is recorded. Reading, exporting and streaming the audit log depend on the plan."
The rules answer whether a call runs. Reach is decided again one level up. agents.<name>.connectors in kortix.yaml lists the slugs an agent may use, or all. An omitted value means none. What an agent can see reads that grant against the connector's tool list and the account it resolves to.
When a gateway is the right call
Some teams should buy the gateway and stop there. A platform team fronting thirty third-party MCP servers for a dozen client applications needs a proxy that holds one address per environment. It needs OAuth brokered per person, so each call carries the caller's own token. It needs each server process isolated with its own limits, and one trace per call. Building that yourself is a quarter of work. ContextForge, Docker, MCPJungle and Obot each document a version of it, and a buyer with that shape of estate is not overbuying.
Two things are worth adding on top, whichever product fronts the servers. First, write the rule about each call where the rest of the configuration is reviewed, so the change arrives as a diff a teammate can read. Second, pin the account the unattended work runs as, because a scheduled job and a person at a keyboard should not resolve to the same identity by accident.
A short list of questions separates the products on that ground. Where does the rule live, and can you read its history? Who can change it, and does the change wait for anyone? Can a rule test an argument, so a write to one channel and a write to another get different answers? What happens when a call arrives with no account named? Each answer points at a file, a table or a screen, and the first one you cannot answer is the gap.
Kortix takes the file route for teams whose remote servers number in the single digits or the low tens. Each MCP server becomes one entry in kortix.yaml, next to the rule that governs its actions, and the account it runs as is a connection you pin on that connector. The entry and its rules reach main as a diff a teammate can read. The trade is plain. A gateway collects more per-call infrastructure into one running service. A manifest gives a project one file a reviewer can finish in a minute. A team that needs both can point provider: mcp entries at a gateway's own MCP endpoint, because the transport is the same http or sse a server speaks.
Questions about gateways, accounts and rules
Short answers on the words, the account and the rule.
Is an MCP gateway the same as an MCP server?
No. A gateway is a proxy in front of servers, and each of the four says so in its own description: ContextForge federates tools into one endpoint, Docker proxies clients to servers, MCPJungle registers servers once and serves /mcp, Obot combines catalog servers into a vMCP. The server publishes tools. The gateway carries the call to it.
Do I need a gateway for one MCP server?
No. One server needs one entry, one rule and one account. MCPJungle describes itself for teams managing multiple servers, which is the case a gateway is built for. In Kortix a single remote server is a provider: mcp entry with a url and a transport.
Where should MCP tool permissions live?
Where the rest of the project's configuration is reviewed. Kortix writes them in kortix.yaml: project-wide policies: first, the connector's own policies: second, then sensitive: true, then policy.default_mode. All four are in the repo, so a rule change is a diff.
Can a gateway enforce per-tool approval?
Each gateway applies the access rules configured in it. Docker's gateway applies security restrictions per server and per profile. Portkey's gateway does permission checks, guardrails and request logging per server. Ask where the rule is written and who reviews a change to it, because that part is left to configuration.
Does Kortix replace an MCP gateway?
It answers the same need for a smaller estate. A provider: mcp connector is Kortix calling someone else's MCP server, and its rules sit in the repo beside it, with its account a connection on the connector. A team fronting dozens of servers for many client applications still wants a proxy, and can point its Kortix entries at one.
Give the rule a diff a teammate can read
Connector entries and the rules on their actions, in one repo you own.