Why local Claude Code setups go wrong at team scale
Claude Code with Jira, GitHub and Slack is a short setup on one laptop. It does not survive twenty. Claude Code keeps local-scope and user-scope MCP servers in ~/.claude.json (Claude Code MCP docs, checked October 2026). Each person edits that file by hand. They paste a personal access token for Jira, a GitHub PAT and a Slack token. From then on nobody outside that laptop knows what those tokens can reach, or who is using them.
The tokens are the first problem, and it is bigger than "secrets in a file." A token sits on one laptop, in one file, under one person's control. Nobody else can list the tokens an engineer holds, narrow what they reach, or take them back in one action. The token outlives the project it was made for. When someone leaves, the only person who knows where their tokens live is the person leaving.
The second problem is quieter: the agent's calls never appear anywhere you can read. An issue closed, a Slack post, a repository change all happened. But there is no single log recording which tool ran, on whose behalf, across all three apps. Reconstructing "what did the agents do last week" means querying three systems and correlating by timestamp.
The third problem is the one managers find last. Nothing distinguishes a read from a delete. An agent that can read an issue with that token can usually also close it, reassign it or delete a comment. MCP tool exposure from a vendor's own server is typically all-or-nothing per connection. The token carries the whole person's access, and nobody narrowed it at the tool level.
This post describes one alternative shape. One MCP address for the organization, each engineer signing in as themselves via OAuth rather than pasting a PAT, and destructive tools blocked per role. MCP is the Model Context Protocol, the open standard an AI client uses to discover and call tools over a defined transport. An endpoint, in this context, is the single HTTP address a client talks to instead of spawning a local process per server.
Everything from here forward describes Elaichi's specific implementation of that shape. It is one way to do this, not the only way. The quantitative claims (rate limits, retention windows, propagation timing) are vendor facts you should verify against your own account, not protocol guarantees. The protocol-level claims (MCP's transport model, OAuth's authorization-code-with-PKCE flow, dynamic client registration) are independently verifiable against the specs linked throughout.
If you want the general case across coding agents rather than this one team setup, MCP for coding agents across an engineering org covers it.
How do you set up Claude Code with Jira, GitHub and Slack?
You set up Claude Code with Jira, GitHub and Slack by pointing the client at one address, https://api.elaichi.ai/mcp, and letting each engineer sign in through it. The same address serves every organization. Which organization and which person a client reaches comes from the OAuth grant behind its token, not from the URL.
The order that works:
- Connect the apps once, in Elaichi. Jira and Slack are both connectors in Elaichi's catalog; the Jira connector page lists its tools. Each engineer adds their own connection with their own sign-in, so the credential belongs to them, not the team.
- Add GitHub as a remote MCP connector. A team on Gold can bring GitHub's remote MCP server in as its own remote MCP connector, added by an Org Owner or Org Admin.
- Have each engineer connect Claude Code. Elaichi keeps no list of approved MCP clients. Any client that supports OAuth dynamic client registration (RFC 7591), PKCE with S256 (RFC 7636) and a browser sign-in can connect. Test Claude Code against those three requirements with one engineer before you roll it out to the team.
- Write the restriction rules before the first wide rollout. A restriction targets a role or one member. Write the block rules on the roles your engineers hold, then widen by exception, not the reverse.
There is no per-user, per-team or per-repo MCP address to generate. Each person connects their client once and approves their own grant. Grants are what a person lists and disconnects under Settings, Connected apps. This replaces the per-laptop config file: see MCP without JSON config files for the longer version.
The alternative, without a gateway: you can get partway here natively. GitHub fine-grained PATs scoped to specific repos and permissions, and Slack bot tokens with minimal OAuth scopes, reduce blast radius per app. What you don't get without a gateway is one place to revoke everything for a departing engineer. Nor one audit format across all three tools, nor a way to block a specific tool call without also blocking the whole connector. If your team is small, and your Jira, GitHub and Slack admins are comfortable auditing three separate systems, scoped native tokens may be sufficient. See "What this does not solve" below for where that threshold sits.
One address, four clients, one set of rules
Claude, ChatGPT, Cursor and Microsoft Copilot can all sign in to the same Elaichi address. That matters for a plain reason: your team does not use one client. Somebody runs Claude Code in a terminal, somebody else lives in Cursor, and the product manager asks the same Jira questions from ChatGPT.
With one MCP endpoint, the access rules live in one place. You write them in Elaichi, on the roles your engineers hold, and they apply to every client that signs in to that address. Registration is open to any client that supports dynamic client registration, with no Elaichi credential needed. Elaichi's authorization server is written with Claude and Cursor in mind. It recognizes the registered redirect addresses of Claude Desktop, claude.ai connectors and Cursor.
Claude Code is a loopback client. It binds a new local port each run, a pattern RFC 8252 for native apps explicitly allows, but a loopback-only client cannot be cryptographically verified. Elaichi's consent screen and audit entries display the name the client self-reported during registration, flagged "Name provided by the client itself." Codex CLI, Gemini CLI, Zed, Goose and Cline are in the same position: all register loopback redirects, none can be verified by name alone. Do not read a name on a consent screen as proof of identity. This is a general property of OAuth dynamic client registration with loopback redirects, not an Elaichi-specific gap.
Setup steps for Claude Code specifically live in Connect Elaichi to Claude Code.
Each engineer acts with their own account, not a service account
Every call made with a grant's tokens acts as the person who approved that grant, in the organization they picked. The token carries that user and organization. Permissions are looked up on each request rather than cached into the token itself.
An issue updated from one engineer's Claude Code session hits Jira as that engineer, with that engineer's Jira project permissions. Not as a bot account someone set up years ago that nobody can now describe. The apps on the far side keep enforcing what they already enforce: a developer querying Jira sees only the tickets their own Jira account can see.
The mechanism is the connection. Each person adds their own Jira connection and their own Slack connection, with their own sign-in. Connector credentials are not stored in Elaichi's main database. A separate credential service holds every connected account's secrets encrypted at rest, and Elaichi fetches them into memory only for the single call that needs them. The AI client itself never holds the Jira or Slack token; it holds only an Elaichi-issued token.
Every call through a connection fetches the credentials bound to that one connection row, regardless of who is calling. So a connection someone shares runs on its owner's sign-in to the third-party app. That is exactly the failure mode a per-engineer setup exists to avoid. Keep connections unshared, and let each engineer reach their own tools through their own toolbox.
There is a real trade-off. Per-engineer connections mean each engineer does a sign-in per app: three sign-ins here, not one. A shared connection would be a single setup for the whole team. You are trading setup time for identity. One extra sign-in per person, against every future action being attributable and individually revocable. For a team touching production issue trackers and repositories, that trade usually pays; for a hackathon team or a three-person side project, it may not. AI agents with employee permissions, no shared bot works through this trade-off in more detail.
Two kinds of connector, one set of rules
Elaichi's catalog holds two kinds of connector, and an engineering team's setup usually touches both. Connectors Elaichi authors and runs itself make up most of the catalog, Jira and Slack among them. These are translated into MCP tools by Elaichi, not by the vendor. Native MCP connectors are vendors' own remote MCP servers, built and run by the vendor, that Elaichi staff publish once for every organization. Linear and GitLab are in the catalog this way.
A team can also bring its own remote MCP server in under the same rules. For GitHub that means adding GitHub's remote MCP server as the organization's own remote MCP connector on Gold. This requires the connector:create permission, which among system roles only Org Owner and Org Admin hold.
To a restriction rule, a remote MCP tool is the resource mcp with the upstream tool name as its method. Restrictions, OAuth scopes and the audit log govern it as they govern any connector's tools. Every gate runs before the call leaves Elaichi. The tool that runs is re-read from that connection's own active list on every call, not cached from an earlier check.
One honest limit: calls a client makes directly to GitHub's MCP server, outside Elaichi, do not pass through Elaichi and are not governed by it. Adding the remote connector does not stop an engineer from configuring GitHub's server separately in their own local config file. That is a device-management problem, not something the endpoint architecture itself solves. Vendor MCP servers: why run them through Elaichi takes the argument further.
Blocking the destructive tools for most roles
In Elaichi, restriction targets are role or user only; the organization default is the absence of a rule, which means allow everything. An engineering-wide rule is therefore written on the roles your engineers hold. A member holds exactly one role; to combine two personas, an admin authors a custom role.
The mechanic that catches people out: a rule layer with only block rules has no implicit allowlist, so everything it does not name passes through. Blocking some of an app's write tools leaves every write tool it does not name reachable. Illustrative example, not literal syntax:
role: engineer
block:
- resource: jira, method: delete_a_jira_issue_by_id
- resource: jira, method: delete_a_jira_issue_comment_by_id
# everything else on jira, create_a_jira_issue_comment and update_a_jira_issue_by_id included, still passes
If you want engineers held to reads only on one app, you have two options. Write block rules naming every non-read tool on it, or write an allow rule naming only the read tools. Once a role carries any allow rule, every tool none of its allow rules names is blocked by default on that app. Mixing the two styles without understanding this flips the default, which is the single most common misconfiguration.
The second mechanic is subtler, and is good news for durability. An allow rule matches a tool only by the operation Elaichi stored at the time the rule was saved, never by the tool's name. A block rule matches by name or by operation. So renaming a tool on the vendor side cannot silently slip it past a block rule. It could still fall outside an allow rule's coverage, if the operation classification also changed. Why blocks match tool names but allows don't explains the asymmetry in full.
What a refused call looks like from Claude Code: the tool is left out of the tool list Elaichi advertises for search. A call by exact name returns an in-band error instead. It states that the tool is restricted for the caller's role or account, and points out that an admin can lift it through an access request. The refusal does not name the rule, who wrote it, or what else it covers. An admin resolves the request in the console, under Governance, Access requests. Approving records an access grant for that one person, lifting that one connector or tool out of their role's rules without editing the role itself.
Timing matters for a rollout plan. A role, team-membership, restriction or grant change takes effect about two minutes. In Elaichi, removing or suspending a member revokes every live grant in the same transaction as the membership change. Revoking a share and disconnecting an account are effective on the next call as well, because the connection is re-read on every request rather than cached.
For the mechanics of choosing between the two rule shapes, MCP tool allowlist vs blocklist: which fits? is the better read.
How Claude Code finds the right tool
In Elaichi, connected tools are never listed one by one, however few there are. When a grant reaches any connected tool, the client gets search_tools, execute_tool and read_tool instead, plus run_code where Code Mode is available. Claude Code searches for the Jira or Slack tool it needs, then runs it. This is a deliberate departure from MCP's default pattern of listing every tool upfront. It keeps the tool list usable once an org has connected a dozen apps with hundreds of combined tools.
Search is lexical, not semantic. A query of concrete tool-ish words ("delete issue", "post message") outperforms a natural-language sentence. A restricted tool is left out of the tools Elaichi offers a client and cannot be called. Search still names it, flagged restricted, with no schema. So an engineer can see what to ask for, rather than wrongly concluding the connector never had that capability.
execute_tool is a naming indirection only. It unwraps to the underlying tool name and arguments it was given, and meets exactly the same gates as a direct call. There is no separate execution path and no extra privilege. read_tool is the same shape with added refusals baked in, so it only ever runs connected reads. The practical consequence for a manager is this. A client-side permission limited to execute_tool covers every connected-tool call made through it, across every connected app, because one tool name carries them all. Per-app and per-tool control has to be set in Elaichi's rules, not in the client's own permission UI.
The endpoint allows 120 requests per 60 seconds per access token, a vendor-specific limit, and the brake on an agent stuck in a retry loop. Over it, Elaichi answers HTTP 429 with Retry-After: 60.
What the audit log holds after a week of use
Elaichi writes one audit row for each connected-tool call that reaches execution and then runs, fails, or is refused there. That holds whether the call came from Claude Code, another MCP client, or a Code Mode program. In Elaichi, one entry for each connected-tool call that reaches execution (a call refused earlier writes none). The MCP client's id is on the row too, so a Claude Code call reads differently from a Cursor one in the log.
Some things are not in there, and a manager planning an evidence story should know this up front. A call refused before execution writes no row at all. That covers a tool a restriction withheld, a missing OAuth scope, an unknown tool name, and a rate limit hit. Argument values are not recorded, with one exception: the path argument that names the target object, kept as a target id. Elaichi retains 90 days of audit history by default. A longer history means forwarding to your own log service, and logging destinations are gated to the Black plan, with Datadog the only delivery target currently supported.
One naming detail confuses people reading the console. Connected-tool calls, MCP ones included, sit in the audit log's assistant category. They are recorded under several action names, depending on how the call ran, so filter by category rather than by one action string. The mcp category holds grant events, such as an MCP client connecting for the first time. Which tools AI agents call: read the audit log walks through a real week of rows.
What this does not solve
Elaichi's MCP endpoint has no prompt-injection defense of its own, and structurally cannot build one at this layer. Nothing on that path scans tool results for injected instructions or second-guesses a call's intent. The endpoint only ever sees a tool name and arguments from a tools/call request. It never has the user's original prompt to judge the call against. That defense has to live in the client (Claude Code, Cursor, and the rest), or in a layer that sees the full conversation, not in a tool-call gateway.
If your engineering team is three people, all of you are admins in Jira and GitHub already, and nobody is asking for an access review, local config files with personal tokens are genuinely a smaller problem than standing up a control plane. The honest threshold is the point where you cannot answer "which tools did the agents call last week" and somebody has started asking. That somebody is a security review, a customer, or a departing-employee checklist. When you don't need an MCP gateway yet makes this case at more length.
If what you want is scheduled, unattended automation rather than engineers asking questions in a terminal, this is the wrong shape. Elaichi's MCP sign-in supports only the authorization-code grant with PKCE, plus refresh. A new grant always needs a person to approve a consent screen in a browser. Nothing in this setup lets a client obtain a grant unattended, which rules out cron-triggered agents and CI-pipeline agents as-is.
If your blocker is that engineers already have personal Claude subscriptions, and your actual worry is what they paste into them rather than what a connected agent calls, the problem is upstream of MCP entirely and this post doesn't address it. Engineers using ChatGPT at work: block or scope it covers that separate case.
If the fit is right, the next practical step is the first-week setup checklist.