atlassian-proxy
by DzmHub
README.md
# atlassian-proxy
Local stdio MCP proxy that sits in front of Atlassian's hosted Rovo MCP
server (`https://mcp.atlassian.com/v1/mcp`) and trims it down for Claude
Code.
```
Claude Code (stdio) -> index.js -> mcp-remote (subprocess, OAuth) -> https://mcp.atlassian.com/v1/mcp
```
What it does:
- Exposes only the tools listed in `ALLOWED_TOOLS` in `index.js` (the ones
`jira-ops`/`jira-reviewer` actually use) — everything else upstream is
hidden from `tools/list`.
- Auto-injects `cloudId`, `contentFormat`, `responseContentFormat` into every
call and strips those params from the schema Claude sees, so Claude never
has to know about them.
- Recursively strips noisy keys (`avatarUrls`, `self`, `iconUrl`, `_links`,
`expand`, `browseUrl`, ...) from every tool result before returning it.
## Requirements
- Node.js (any version supporting native ESM / `type: "module"`).
- `mcp-remote` installed via `npm install` (`index.js` spawns its local
`node_modules/mcp-remote/dist/proxy.js` directly with `node` — no `npx`
involved, see "mcp-remote version pinning" below).
## Install
First check whether Node.js is already installed — if it is, and
`node_modules` is already present in this folder, `npm install` is not
needed:
```
node -v
```
If that fails (Node not found), install Node.js first, then:
```
cd C:\Users\Dz\oktanetest\atlassian-proxy
npm install
```
This installs `@modelcontextprotocol/sdk` and `mcp-remote` from
`package.json`.
## Run standalone (manual smoke test)
```
node index.js
```
It speaks MCP over stdio, so running it directly just blocks waiting for a
client on stdin. On startup it spawns the locally installed
`node_modules/mcp-remote/dist/proxy.js` (via `node`, pinned to the exact
version in `package.json`) as a child process, which opens a browser window
for Atlassian OAuth login on first use. Once authorized, `mcp-remote` caches
the token, so subsequent runs don't need to re-authenticate. `[atlassian-proxy]
ready` on stderr means it connected to upstream and is serving the filtered
tool list.
## Register with Claude Code
Register this proxy under the name `atlassian-proxy`, not `atlassian` — that
keeps it from colliding with the real hosted Atlassian/Rovo MCP server if
that one is also registered under `atlassian`.
Add it as a stdio MCP server for the project, e.g. in that project's Claude
Code config (`mcpServers` block):
```json
{
"atlassian-proxy": {
"type": "stdio",
"command": "node",
"args": ["<path-to-atlassian-proxy>\\index.js"],
"env": {}
}
}
```
If the real `atlassian` MCP server is also registered, disable it (don't
remove it) so only this proxy's filtered tools are active: run `/mcp` in
Claude Code, select the `atlassian` server, and choose the disable option.
Re-enable it there later if you ever need the full, unfiltered upstream
tool set back.
Restart/reconnect Claude Code (or the MCP connection) after registering so
it picks up the new server. Tools from this proxy will appear under the
`atlassian-proxy` prefix (e.g. `mcp__atlassian-proxy__getJiraIssue`),
already limited to `ALLOWED_TOOLS` and stripped of `cloudId`/
`contentFormat`/`responseContentFormat`.
## Token savings stats
Since Claude Code runs this proxy hidden and its stderr isn't visible in
chat, savings are written to files instead:
- `logs/token-savings.log` — JSONL, one line per `tools/list`/`tools/call`
event (`tokensSavedIn`, `tokensSavedOut`, running totals).
- `logs/stats-summary.txt` — overwritten after every event with the current
running totals.
Two numbers are tracked (both are estimates — char-count / 4, not a real
tokenizer, since no Claude tokenizer is wired in):
- **Kept out of Claude's context** (`tokensSavedIn`): tool-list trimming
(`ALLOWED_TOOLS`) plus noise stripped from tool results (`stripNoise`).
- **Claude didn't have to generate** (`tokensSavedOut`): the constant params
(`cloudId`, `contentFormat`, `responseContentFormat`) the proxy injects on
Claude's behalf.
To watch it live, open a separate terminal and tail the summary file:
```powershell
Get-Content logs\stats-summary.txt -Wait
```
## Troubleshooting
- **Hangs with no `ready` message**: the spawned `mcp-remote` (see "Run
standalone" above) is likely waiting on the OAuth browser flow — check for
a browser window/prompt.
- **Tool not available in Claude**: confirm the tool name is in
`ALLOWED_TOOLS` in `index.js`; anything not listed is filtered out of
`tools/list` and rejected on `tools/call`.
### Stale/expired auth (401 Unauthorized, or `could not resolve cloudId from getAccessibleAtlassianResources`)
Claude Code's `/mcp` reconnect only restarts the `node index.js` stdio
process — it does **not** force re-auth. The actual OAuth token lives one
layer down, cached on disk by the `mcp-remote` child process at
`~/.mcp-auth/mcp-remote-<version>/<server-hash>_tokens.json`, keyed by the
upstream server URL. If that token is stale (expired, revoked, or was
already stale before the `node` process even restarted), every tool call
fails with `401`/`could not resolve cloudId` regardless of how many times
you reconnect in Claude Code, because reconnecting never touches that
cache.
Full recipe, in order:
1. **Clear the cache**: `rm -rf ~/.mcp-auth` (Windows: `C:\Users\<you>\.mcp-auth`).
This wipes the cached client registration, PKCE verifier, and token for
every `mcp-remote`-backed server on the machine — safe, it's pure
OAuth-client state, nothing else depends on it.
2. **Re-authenticate standalone, outside Claude Code**, in its own terminal —
use the same local `mcp-remote` script `index.js` spawns, not a bare `npx
mcp-remote` (an unpinned `npx` call can resolve a different version than
`package.json` pins, writing the token to a different
`~/.mcp-auth/mcp-remote-<version>/` folder than `index.js` reads from —
see "mcp-remote version pinning" below):
```
cd C:\Users\Dz\oktanetest\atlassian-proxy
node node_modules\mcp-remote\dist\proxy.js https://mcp.atlassian.com/v1/mcp --host 127.0.0.1
```
Let this run in the foreground (or a background shell you can watch) and
complete the browser login at your own pace. **Do not** try to
re-authenticate by reconnecting `atlassian-proxy` inside Claude Code
first — Claude Code's own connection-setup timeout races the interactive
OAuth flow, since `index.js`'s `main()` doesn't open its own stdio side
until the entire upstream `mcp-remote` connect (including you clicking
through the browser) resolves. If Claude Code gives up first, it kills
`index.js` and its `mcp-remote` child mid-handshake — the browser may
still show "Authorization successful!", but the token exchange gets cut
off before `tokens.json` is written (you're left with only
`client_info.json`/`code_verifier.txt` in `~/.mcp-auth`, no token — check
for that if this happens). Running `mcp-remote` standalone first removes
that race entirely; once it prints `Proxy established successfully`
(exit code 0), the token is safely cached.
3. **Only then** reconnect `atlassian-proxy` in Claude Code (`/mcp`). It
spawns the same `mcp-remote`, finds the now-valid cached token, and
connects immediately with no browser step.
**`--host 127.0.0.1`** in `index.js`'s spawn args (not upstream `mcp-remote`
default) is required on Windows: `mcp-remote`'s local OAuth callback server
binds `127.0.0.1` only, but its default redirect_uri hostname is
`localhost`. On a Windows machine where `localhost` resolves to `::1`
first (check with `Resolve-DnsName localhost`), the browser's post-login
redirect hits an address nothing is listening on — the callback page shows
"Unable to connect" and the auth code is stranded. Do not remove this flag.
**`EADDRINUSE` on the callback port** (e.g. `listen EADDRINUSE:
127.0.0.1:3736`) after a previous auth attempt was interrupted: `npx` on
Windows doesn't always kill the full child process tree when its parent is
cancelled, so a previous `mcp-remote` can be left orphaned, still holding
the port. Find and kill it before retrying:
```powershell
Get-NetTCPConnection -LocalPort <port> | Select-Object OwningProcess
Stop-Process -Id <pid> -Force
```
### mcp-remote version pinning (fixed 2026-09-02)
`mcp-remote` namespaces its OAuth token cache by its own version
(`~/.mcp-auth/mcp-remote-<version>/...`), so if the version that actually
runs drifts between launches, a perfectly valid token in one version's
folder is invisible to a differently-versioned process — it finds an empty
folder, decides `UnauthorizedError`, and restarts the full interactive OAuth
flow. That flow then races Claude Code's ~30s MCP connect timeout (see
"Stale/expired auth" above), which is what actually surfaces as a connection
failure.
This bit twice:
- **2026-08-25**: `index.js` originally spawned unpinned `npx -y mcp-remote
...`, so every launch installed whatever was current `latest` on the
registry. mcp-remote had just published `0.2.1`; every new launch pulled
it fresh, and the token from the previous version's launch never carried
over. Fixed at the time by hardcoding the version in the spawn args:
`npx -y mcp-remote@0.2.1 ...`.
- **2026-09-02**: even with that pin, `npx -y mcp-remote@0.2.1` still
resolves the version from npx's own cache/registry state on every launch,
independent of what's pinned in this project's `package.json` (which had
drifted to a `^0.1.38` range, currently installing `0.1.38`). One launch
observed during debugging resolved to yet a third version (`0.1.37`),
matching neither the `index.js` pin nor `package.json`.
**Fix**: `index.js` no longer shells out to `npx` at all. It spawns
`node_modules/mcp-remote/dist/proxy.js` directly via `node` (see `main()`),
and `package.json` pins `"mcp-remote"` to an exact version (`0.2.1`, no
`^`). The version that runs is now determined solely by
`package.json`/`package-lock.json` in this repo — no npx resolution, no
registry lookup, no dependency on npx's local cache state. Bumping the
mcp-remote version going forward means editing `package.json` and running
`npm install`; `index.js` needs no change.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues