npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@computec/mcp-remote

v0.1.35

Published

Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth

Readme

mcp-remote-appengine

A CompuTec fork of mcp-remote by Glen Maddern, taken by way of punkpeye/mcp-remote. Distributed under the MIT License; the original work is Copyright (c) 2025 Cloudflare, Inc. See LICENSE for the full text and the retained upstream notice.

This fork is maintained independently of upstream — please do not report issues with it to the upstream projects.

Changes in this fork:

  • Access Token is now automatically refreshed 5 minutes before the expiry
  • I have internally changed the reference library @modelcontextprotocol/sdk to reflect issue https://github.com/geelen/mcp-remote/issues/128
  • --static-oauth-client-info-base64 option added where json parameter is now encoded as base64

OAuth Token Refresh Enhancements

Overview

The NodeOAuthClientProvider has been enhanced with automatic token refresh capabilities to prevent authentication failures due to expired access tokens.

Key Improvements

1. Token Expiry Read From the Token Itself

  • Validity comes from the JWT exp claim — the only expiry the resource server acts on
  • issued_at + expires_in bookkeeping remains as a fallback, for opaque (non-JWT) tokens
  • A 60-second skew tolerance counts in favour of validity; refresh starts 300 seconds before expiry
  • A token that arrives already expired is reported as such and withheld, instead of spinning on 401

2. Automatic Token Refresh

  • When tokens() is called and the access token is expired, it automatically attempts to refresh using the refresh token
  • Uses the MCP SDK's refreshAuthorization function
  • Seamlessly handles the OAuth metadata discovery and token exchange

3. Enhanced Token Validation

  • New checkTokenValidity() method for diagnostic purposes
  • Improved debug logging with expiration details
  • Better error handling for invalid or malformed tokens

4. Graceful Degradation

  • If a token refresh fails, the proxy first re-reads the shared token store — a concurrent instance may have refreshed successfully — and only then reports that re-authentication is required. Token files are never deleted on a failed refresh, so one instance's failure cannot destroy the refresh token other sessions seed from.
  • The shared token store is ~/.mcp-auth/shared/ (or $MCP_REMOTE_CONFIG_DIR/shared/). It is deliberately not under mcp-remote-<version>/, which is where it used to live: a version-scoped store made every upgrade look like a first run and forced a browser login. Tokens left behind by an older release are read once and copied into the shared store, so upgrading does not cost a login. ~/.mcp-auth/mcp-remote-<version>/sessions/<id>/ still holds per-session state and can be deleted freely; deleting shared/ is what logs you out.
  • Maintains backward compatibility with existing token files

5. Path‑aware OAuth discovery builds an invalid URL

  • If Authorization server has realm in path the path builded in mcp is incorrect

6. No silent hangs (0.1.24)

Fixes the failure mode where the proxy stopped answering entirely, without an error, until it was restarted:

  • Every request gets an answer. A rejected upstream send(), a closed upstream transport, or an unanswered request now produces a JSON-RPC error for the client instead of a stderr line and an orphaned request id.
  • Proactive refresh actually runs. The refresh decision is made on the time remaining, not on expires_in (the total lifetime), which never crossed the buffer and left the feature dead. A background timer refreshes one buffer ahead of expiry, so an idle proxy no longer drifts past the refresh token's own lifetime and into an interactive re-login it cannot complete — and one buffer ahead is the only moment it is armed. A token already inside the buffer gets no timer, because the only one that could be armed is a zero-delay one, and an issuer that clamps the lifetime to a session about to end (Keycloak returns expires_in: 1) turns that into a spin.
  • One refresh at a time. Concurrent callers join a single in-flight refresh instead of racing with the same refresh token, which a rotating auth server rejects for all but the first.
  • The login is taken over from an instance that cannot finish it. Deferring to whoever holds the auth lock is right only while that instance can still complete the login. A live process keeps answering the liveness probe whether or not anyone is still looking at the window it opened, so the wait ends after two minutes. What the next instance then opens is the window its own pre-flight held back: the authorization URL is built once, by the SDK, before any coordination has happened, and it is kept rather than dropped so that the takeover has something to show. A lock is only ever released by the instance that still owns it, so the handover does not strand the session that took it.
  • Credential files are replaced atomically. The shared store is read by every session on the machine, and writeFile truncates before it fills; a reader in that window got half a file.
  • Bounded OAuth calls. Metadata discovery and token refresh have a timeout, so an auth server that accepts the connection and never answers can no longer stall every proxied request.
  • Nothing is left holding credentials. Besides the session directories below, a startup sweep removes the temporary files an atomic write leaves behind when the process writing one is killed before it can rename it into place — each of those is a full copy of the token file.
  • Session directories are cleaned up. Own directory on shutdown, plus orphans left by processes that no longer exist.

Timeout flags

Both are documented with the rest of the options under Command-line options.

Command-line options

npx -y @computec/mcp-remote <server-url> [callback-port] [options]

<server-url> is required. [callback-port], if given, is the local port the OAuth callback listens on; leave it out and a port is derived from the server URL, reused from an earlier registration, or picked from what is free. Pinning a port that disagrees with an existing client registration deletes that registration and forces the client to register again.

| Option | Default | What it does | |---|---|---| | --debug | off | Writes a detailed log per session to <store>/mcp-remote-<version>/sessions/<id>/<server-hash>_debug.log. Turn this on before reporting a login problem — the order of the lines is usually the whole diagnosis. | | --host <hostname> | localhost | Hostname used in the OAuth redirect_uri. Change it only if the browser cannot reach localhost. | | --transport <strategy> | http-first | One of http-first, sse-first, http-only, sse-only. An unrecognised value is ignored with a warning and the default stands. | | --allow-http | off | Permits a plain-HTTP server URL. Not needed for http://localhost or http://127.0.0.1, which are always allowed; without it any other http:// URL is refused. | | --header "Name: value" | none | Adds a header to every upstream request. Repeatable. ${VAR} in the value is replaced from the environment, so --header "Authorization: Bearer ${TOKEN}" keeps the secret out of the command line. A value that is not Name:value is ignored with a warning. | | --request-timeout <seconds> | 300 | Answers a request with a timeout error if the upstream stays silent. 0 disables. | | --oauth-timeout <seconds> | 30 | Ceiling for a single OAuth round trip — metadata discovery and token refresh. 0 disables. | | --resource <uri> | none | Sends resource=<uri> on the authorization request, for servers that scope tokens to an audience. | | --static-oauth-client-info <json\|@file> | none | Skips dynamic client registration and uses the client id and secret you supply. Either inline JSON or @path/to/file.json. | | --static-oauth-client-info-base64 <base64> | none | The same thing, base64-encoded, for hosts that mangle quotes in a command line. Wins if both are given (with a warning). Invalid base64 or JSON exits with an error rather than falling back. | | --static-oauth-client-metadata <json\|@file> | none | Overrides the client metadata sent during dynamic registration. Inline JSON or @file. |

A note on secrets. Anything passed on the command line is visible in the process list and is recorded verbatim by hosts that log their MCP server configuration. Prefer @file for --static-oauth-client-info, and ${VAR} for --header.

Environment variables

| Variable | Default | What it does | |---|---|---| | MCP_REMOTE_CONFIG_DIR | ~/.mcp-auth | Root of the credential store. Point it at a throwaway directory to test against a clean store without touching your real one — or at a second one to hold a second account, see below. | | MCP_REMOTE_SESSION_ID | the parent process id | Identifies the session whose directory holds per-session files. Set it explicitly when launching several instances from one parent, which would otherwise share an id. | | MCP_REMOTE_SESSION_ISOLATION | isolation on | 0 or false turns per-session isolation off, putting every session back in one shared directory. Any other value, including unset, leaves isolation on. |

Credentials that outlive a session — tokens.json, client_info.json — live in <store>/shared/ regardless, which is what lets an upgrade keep you logged in. Only per-session state (the PKCE verifier, the debug log) lives under the session directory.

Running two accounts against the same server

The credential slot is keyed by the server URL within one store root, and the token lives in <store>/shared/ precisely so that a new session does not cost you a login. That is also what decides which switch gives you a second account — and it is not the one most people reach for.

MCP_REMOTE_CONFIG_DIR is the switch that does it. A second store root means a second credential slot, a second lockfile and therefore a login window of its own:

"appengine-second-account": {
  "command": "npx",
  "args": ["-y", "@computec/mcp-remote@latest", "https://appengine.example/mcp"],
  "env": { "MCP_REMOTE_CONFIG_DIR": "/home/you/.mcp-auth-second" }
}

MCP_REMOTE_SESSION_ID is not. Per-session isolation keeps per-session state apart and stops an established session from adopting a token issued to somebody else — but a session that has no token of its own yet seeds from <store>/shared/, so a fresh session id on its own lands you silently back on whichever account logged in last.

Once each session does hold its own token they stay apart: a shared copy is adopted only when it is newer and belongs to the same user (iss|sub|azp), so a colleague's refresh never moves your session onto their data. The SAP company is chosen per MCP session (SetCompany) rather than carried by the token, so none of this affects which company you are working in.

Further reading

Platform documentation, including how AppEngine issues the OAuth client this proxy authenticates with: https://learn.computec.one/

Usage

The enhancements are transparent to existing code:

// Existing code continues to work unchanged
const provider = new NodeOAuthClientProvider(options)
const tokens = await provider.tokens() // Now automatically refreshes if needed

// New diagnostic capability
const validity = await provider.checkTokenValidity()
console.log('Token expires in:', validity.timeLeftSeconds, 'seconds')

Token Storage Format

Tokens are now stored with an additional issued_at field:

{
  "access_token": "...",
  "refresh_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "issued_at": 1643723400000
}

Debugging

Enable debug mode to see detailed token refresh information:

npx tsx proxy.ts https://your-server.com --debug

Debug logs include:

  • Token expiration calculations
  • Refresh attempt details
  • Success/failure notifications
  • Timing information

Error Handling

The system handles various failure scenarios:

  1. No refresh token: Clears expired tokens and forces re-auth
  2. Refresh endpoint unavailable: Falls back to full re-authentication
  3. Network failures: Retries based on underlying HTTP client behavior
  4. Invalid refresh token: Clears all tokens and forces re-auth

Migration

Existing token files will continue to work:

  • A stored token carrying a JWT exp claim is judged by it, whatever issued_at says
  • An opaque token stored without an issued_at cannot be dated, so it is treated as spent and refreshed on first use
  • First token refresh will add the issued_at timestamp
  • No manual migration required