# The MCP Connector JSON Config Schema: What Saves and What Fails at Execution

> How the mcp connector json config schema is validated, how secrets become secret_paths, and three mistakes that pass the save and fail at execution.

**TL;DR** Elaichi checks a custom connector's JSON config against the catalog's integration config schema at save. Shape errors come back as a 422 with the issues listed. What it does not check is where the config points. So a connector can save cleanly and still fail when someone runs it. Secrets never read back: the configuration read returns public values plus secret_paths, with none of the values.

## The situation

GitHub is not in Elaichi's connector catalog. Neither is every internal service a company runs. When the catalog does not have the app, an organization authors the connector itself. The mcp connector json config schema is what that authored connector is checked against at save.

The first decision is which of two shapes to use. One is a JSON `config`, checked against the catalog's integration config schema. The other is a `remote_mcp` definition, pointing at a server that already speaks MCP. A create request sends exactly one of the two. GitHub itself is a case for the second shape, because it runs its own MCP server that a team can bring in as a remote MCP connector. GitHub documents a separate policy for MCP servers in Copilot, which it says is disabled by default (checked October 2026, [GitHub's own docs](https://docs.github.com/en/copilot/concepts/context/mcp)).

This post is about the first shape. The mcp connector json config schema is what a `POST /connector` body with a `config` object is checked against. Knowing where that check stops is most of the skill. MCP means Model Context Protocol, the way AI clients find and run tools; the protocol's own documentation covers both local and remote servers (checked October 2026, [modelcontextprotocol.io](https://modelcontextprotocol.io/docs/2026-07-28/develop/connect-local-servers)). A connector turns one app's HTTP API into those tools.

Two things gate authoring at all. The first is the `connector:create` permission, which among the built-in roles only Org Owner and Org Admin hold. The second is the `custom_connectors` plan feature, which Gold includes. `connector:create` is the one permission Elaichi flags as high trust, and the role editor shows a "High trust" tag on it. The reason sits in the next section.

## What the mcp connector json config schema actually validates

It validates shape, not destination. A config is checked against the catalog's integration config schema. Problems come back as HTTP 422, with the issues listed one per problem rather than as one generic failure.

The config carries top-level keys the connector's methods share. Pull review treats nine of them as a unit: `base_url`, `headers`, `query`, `query_array_format`, `authorization`, `credentials`, `pagination`, `rate_limit` and `error_expression`. Each is taken whole from upstream or kept as is when a fork pulls. That is a useful hint about how Elaichi thinks of them. They are the connector's envelope, and the methods are what fills it.

Beyond the config itself, a few identifiers are checked at save. The slug is 2 to 64 characters of lowercase letters, digits and hyphens. It must start and end with a letter or digit, and `new` is reserved. The label is 1 to 120 characters, the category up to 80. One rule bites more often than it should: two methods of one connector cannot answer to the same tool name. A config edit, a documentation update or an upstream pull that would cause a clash is refused.

What the check does not do is look at where the connector points. Elaichi checks a custom connector's config for shape, not for destination. The `connector:create` permission text warns that a connector "can be pointed at any destination". That matters for governance. Restrictions bind a connector's identity, its slug and the forks that declare descent from it, never the host it calls. A block on one connector does not stop a new connector aimed at the same API.

The audit row records the hosts, though. Creating, forking and editing a custom connector writes `connector.created`, `connector.forked` or `connector.updated`. The row carries up to 8 hosts the config calls, plus the total count. An edit that changes them also records the previous hosts. If you want to know whether somebody pointed a connector somewhere new, that is where to look.

## How secrets become secret_paths

Secrets do not live in Elaichi's main data, and they do not read back. A separate credential service holds every connected account's secrets in encrypted form. Elaichi's own row carries only ownership and lifecycle. Stored connector secrets are encrypted at rest with AES-256-GCM. Each ciphertext records the id of the key that wrote it, so the key can be rotated without re-encrypting old rows.

Read-back is where this shows. Reading a connected account's configuration returns its public values plus `secret_paths`. That is the list of dot-paths the credential service encrypted away. `secret_paths` carries none of the actual values. It is also the only thing that decides whether a configuration variable counts as a secret on Elaichi's side. So a review of a connection's config tells you which fields are secret, and nothing about what they contain.

Writing to one is refused. A write to a configuration variable named in `secret_paths` answers 409 `secret_variable`. The router has its own pre-check, and the credential service refuses independently. Both are translated into the identical 409 response, so a caller cannot tell which side caught it.

For an OAuth connector, the organization's own OAuth app is a separate sub-resource under `/connector/:slug/oauth-app`. It is gated on `connector:manage` rather than `connection:manage`. Its write body accepts only `client_id`, an optional `client_secret`, `scope`, `optional_scope` and `user_scope`. Everything endpoint-shaped is deliberately unrepresentable in it: an authorization URL, a token URL, any other OAuth endpoint field. Changes are audited as `connector.oauth_app.updated` on save and `connector.oauth_app.cleared` on removal.

## Three mistakes that pass the save and fail at execution

All three save cleanly. Each surfaces the first time a person or an AI client runs something.

**The connector saves but nobody can see it.** A new custom connector is private until shared. Only its owner and the members a grant reaches can list or open it. Everyone else gets the same 404 as for a connector that does not exist, including Org Owners and holders of `connector:view` or `connector:manage`. There are [three grant levels](/blog/resource-acl-primitive-view-use-edit/). `view` lets a member see and fork it. `use` lets them create connections from it. `edit` lets them change its config and documentation, pull from upstream and manage its shares. The author sees it working, the team sees nothing.

**The connector is shared but a connection is blocked.** A custom connector can stop being shared with the owner of a connection built on it. That connection is then blocked with `connector_not_shared`. Every tool call through it is refused, for that connection's grantees too, until the connector is shared with its owner again. A toolbox entry pinned to that connection goes unmet for everyone who opens the toolbox, its owner included. This failure looks like a broken connector and is really a share.

**The tool tier is not what the name suggests.** Elaichi tiers a connected tool from the catalog by its HTTP verb and unified method name. An HTTP DELETE is destructive whatever the method is called. A `list` or `get` method is read only when its verb is GET or HEAD, and otherwise write. A `delete` method is destructive, and anything else, a custom method included, is write. So a method you named `get_report` that sends a POST is tiered as a write. The gate re-derives the tier at call time. A custom tool edited from GET to DELETE is dispatched as a delete on the next call, not on the definition the reviewer approved.

One more case to know. A connection that has not finished sign-in cannot be pinned into a toolbox entry. That write is refused with 409 `connection_pending`.

## The trade-off: expressiveness for reviewability

A config-authored connector is less expressive than a server you write yourself. You describe HTTP calls, headers, pagination and error handling in a schema somebody else designed. The shapes it does not cover are shapes you do not get. That is the cost. It is real for anything with unusual auth, a non-HTTP transport, or behavior that does not decompose into methods.

What you buy is a reviewable artifact. The config is JSON, checked against a schema at save, with problems reported as a list. Its hosts are on the audit row. Its [forks declare descent](/blog/declared-lineage-not-dial-host/), so a block on the original also blocks every fork. Lineage is walked toward the root for up to 8 hops. A chain that is longer, loops or is broken is refused outright rather than checked partially. A fork with a recorded upstream can pull changes through a review step. That step sorts each difference into `new`, `updated`, `conflict`, `removed` or `config`, and starts conflicts and upstream removals unselected.

The other shape is the right answer often enough to say so plainly. If the thing you want already speaks MCP over Streamable HTTP at a public HTTPS address, add it as a remote MCP connector. OpenAI documents the same transport expectation: its Responses API mcp tool works with remote MCP servers that support Streamable HTTP or HTTP/SSE (checked October 2026, [OpenAI's own guide](https://developers.openai.com/api/docs/guides/tools-connectors-mcp)). Public reachability is not an Elaichi quirk either. Anthropic says Claude reaches a custom connector's remote MCP server from Anthropic's cloud, so the server has to be reachable from Anthropic's IP ranges over the public internet (checked October 2026, [Anthropic's own article](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp)).

Elaichi then sits in front of that server. Restrictions and OAuth scopes are checked before a call leaves. Every call that runs is written to the audit log. Only a tool on that connection's own active list can run. A remote MCP connector cannot be forked. On any other connector the `config.remote_mcp` key is reserved, so a plain connector cannot pose as a remote one.

## Deleting, transferring and the ownerless case

Only a custom connector's owner can delete it. Deletion is refused with 409 while any connection in the organization still uses it, naming how many remain. Deleting removes every share on it and its owner record. Forks keep their place in the lineage, so a restriction naming the deleted connector still binds its forks.

Transfer is the owner's too, outside offboarding. `POST /connector/:slug/transfer` hands it to another active member and leaves existing grants untouched. The previous owner is left with `use`, unless they already reach it at `use` or `edit`, so their own connections keep working. At offboarding the previous owner keeps nothing. Transfer is also the only offboarding action for a connector. A removal is refused with 409 `member_owns_connectors` while the departing member still owns any custom connector.

A connector can end up with no owner, created before ownership existed, or left behind when its owner was removed without offboarding. `edit` grantees can still edit it. Nobody can delete or transfer it until someone with `connector:manage` who can see it assigns an owner. One correction to the permission's own description, which says `connector:manage` can edit or delete other members' custom connectors: the code does not let it. Editing needs an `edit` grant or ownership, and deleting needs ownership.

## Where to go next

Choosing between authoring a config and bringing in a server that already exists is its own decision. [Running your own MCP servers](/blog/self-hosted-mcp-servers-vs-control-plane/) puts real numbers on the second option. For the governance layer on top of whatever you author, [MCP governance](/blog/mcp-governance/) covers roles and restrictions. The post on [why blocks match tool names but allows don't](/blog/block-matches-name-allow-matches-operation/) explains why renaming a method cannot slip it past a block. To see which apps are already in the catalog, start at [/connectors/](/connectors/).

## FAQ

### What happens if a custom MCP connector's JSON config is invalid?

Elaichi validates a custom connector's JSON config against the catalog's integration config schema when it is created. An invalid shape returns HTTP 422 with the specific issues listed, not a single generic error. Separate rules apply at save. The slug must be 2 to 64 characters of lowercase letters, digits and hyphens, starting and ending with a letter or digit. Two methods of one connector cannot answer to the same tool name.

### Does reading a connection's configuration show its API key or client secret?

No. Reading a connected account's configuration in Elaichi returns its public values plus secret_paths. That is the list of dot-paths the separate credential service encrypted away. secret_paths carries none of the actual values. Writing to a configuration variable named in secret_paths is refused with a 409 secret_variable response.

### Who can create a custom connector in Elaichi?

Creating a custom connector needs the connector:create permission, which among Elaichi's built-in roles only Org Owner and Org Admin hold. It also needs the custom_connectors plan feature, which the Gold plan includes. connector:create is the only permission Elaichi flags as high trust. A connector can be pointed at any destination, and restrictions bind a connector's identity rather than the host it calls.

### Why does a custom connector work for its author but not for the team?

A new custom connector in Elaichi is private until it is shared. Only its owner and the members a grant reaches can list or open it. Everyone else gets the same 404 as for a connector that does not exist, Org Owners and holders of connector:view or connector:manage included. Grants come at three levels. view lets a member see and fork it. use lets them create connections from it. edit lets them change its config and documentation.

### Should I write a JSON config or add a remote MCP server?

If the thing already speaks MCP over Streamable HTTP at a public HTTPS address, add it to Elaichi as a remote MCP connector. Reimplementing it as a JSON config is the longer road. A create request sends exactly one of config or remote_mcp. A JSON config trades expressiveness for reviewability. It is checked against a schema, its hosts are recorded on the audit row, and its forks declare descent, so a block on the original also blocks them.

## Read next

- [When to fork public MCP connector definitions, and when not to](/blog/forking-public-connector-three-cases/) — Three cases to fork public MCP connector definitions in Elaichi, one case to file the change upstream, and how pull review sorts the differences.
- [MCP connector lineage identity and fork blocks](/blog/declared-lineage-not-dial-host/) — MCP connector lineage identity is a connector's own slug plus the forks that declare descent from it, walked toward the root, block-only, and failing closed.
- [Resource ACL view use edit: how Elaichi shares access](/blog/resource-acl-primitive-view-use-edit/) — How the resource ACL view use edit grant works in Elaichi, and why no org-level permission widens what a member can list.
