Skip to main content
Glama
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.

Maintenance

ActivityMaintained
ResponsivenessNo issues