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

@arcadeai/mcp-remote

v0.1.0

Published

Local stdio-to-remote bridge for Arcade MCP gateways, with managed OAuth and local file materialization.

Readme

@arcadeai/mcp-remote

A local stdio bridge to an Arcade MCP gateway.

Arcade serves MCP gateways over Streamable HTTP. Some MCP clients only speak the stdio transport, and, more importantly, a local process can do work that a cloud gateway cannot: it can read files from the machine. This bridge runs locally, presents a stdio MCP server to your client, and relays every request to the remote gateway over HTTP.

The headline capability this unlocks is local file materialization: the model emits a short file path, the bridge reads the bytes and substitutes them before the request leaves the machine, and files returned by tools are written to disk and handed back as a path. Large base64 blobs never enter the model's context window.

Status

The bridge relays MCP traffic to the gateway, authenticates with browser-based OAuth (Dynamic Client Registration + PKCE, silent refresh), rewrites file-carrying tool parameters so the model passes a local path (uploads), and materializes files returned by tools to disk so the model gets a path instead of a blob (downloads). On macOS the OAuth tokens are kept in the Keychain; file reads are guarded by a credential-directory blocklist and an optional allowlist.

Not yet done: the arcade connect --local integration and the public npm publish (the package is prepared but intentionally unpublished).

Usage

Point your MCP client at the bridge. The first time it connects to a gateway you have not authorized yet, the bridge opens your browser to sign in, caches the tokens, and then serves; every later start is silent. (Set ARCADE_AUTO_LOGIN=0 to disable this and require an explicit login first.)

To authorize ahead of time, so the first spawn never has to wait on you, run once in a terminal:

npx @arcadeai/mcp-remote login <gateway-slug-or-url>

Either way, point your MCP client at the bridge:

// in your MCP client config (e.g. claude_desktop_config.json)
{
  "mcpServers": {
    "arcade": {
      "command": "npx",
      "args": ["-y", "@arcadeai/mcp-remote", "<gateway-slug-or-url>"]
    }
  }
}

The single argument is either a gateway slug (expanded to https://api.arcade.dev/mcp/<slug>) or a full gateway URL.

To sign out: npx @arcadeai/mcp-remote logout [<gateway-slug-or-url>] (omit the argument to clear all cached credentials).

Headless hosts

On a machine with no browser, add --no-browser (or set ARCADE_NO_BROWSER=1) to login. It prints the authorization URL instead of opening a browser; open that URL on any machine that can reach the loopback callback, for example by forwarding the callback port over SSH:

ssh -L 9908:localhost:9908 you@remote-host

The device-code grant is not offered, because the Arcade authorization server does not support it.

Authentication

By default the bridge uses OAuth tokens cached by login, refreshing the access token silently when it expires. No API key is required, so project members who cannot mint API keys can still connect.

Alternatively, pass a bearer credential via the environment. This overrides the cached login and is handy for CI. The Engine accepts it in Authorization for both shapes:

| Variable | Meaning | | ----------------- | ------------------------------------------------------------------- | | ARCADE_API_KEY | API key. Sent as Authorization: Bearer <key>. | | ARCADE_USER_ID | Paired with ARCADE_API_KEY (sent as the Arcade-User-Id header). | | ARCADE_TOKEN | An OAuth access token. Sent as Authorization: Bearer <token>. |

Token storage

On macOS the OAuth tokens are stored in the login Keychain (through the security tool, so there is no native module to compile), and the on-disk file keeps only the non-secret client registration. On other platforms, or when ARCADE_USE_KEYCHAIN=0, tokens are written to credentials.json (0600) in the cache directory instead. A file written before the Keychain was in use still works and is migrated on the next token write.

Other environment variables

| Variable | Default | Meaning | | ------------------------------ | ------------------------ | ------------------------------------------ | | ARCADE_ENGINE_URL | https://api.arcade.dev | Engine base used to expand a slug. | | ARCADE_OAUTH_CALLBACK_PORT | 9908 | Loopback port for the login redirect. | | ARCADE_LOGIN_TIMEOUT_SECONDS | 600 | How long login waits for the callback. | | ARCADE_MCP_REMOTE_DIR | ~/.arcade/mcp-remote | Where the client registration (and, without a keychain, tokens) is stored (0600).| | ARCADE_USE_KEYCHAIN | on (macOS) | Set 0 to keep tokens in the file instead of the OS keychain. | | ARCADE_NO_BROWSER | (unset) | Set 1 for headless login (same as --no-browser). | | ARCADE_DOWNLOAD_DIR | ~/Downloads/arcade | Where files returned by tools are saved (also --download-dir). | | ARCADE_ALLOWED_DIRS | (unset) | Colon-separated dirs uploads may read from; unset means anywhere but credential dirs. | | ARCADE_MAX_FILE_BYTES | 26214400 (25 MB) | Max in-memory upload/download size. | | ARCADE_MAX_DOWNLOAD_BYTES | 262144000 (250 MB) | Max assembled chunked-download size. | | ARCADE_DOWNLOAD_CHUNKED | on | Set 0 to disable chunked-download assembly. | | ARCADE_PUBLIC_UPLOAD_BASE | (unset) | Public base URL that lets URL-fetching tools accept a local file path. | | ARCADE_UPLOAD_PORT | 9909 | Local port the upload host binds (behind your tunnel). | | MCP_REMOTE_LOG_LEVEL | info | debug | info | warn | error. |

All diagnostics are written to stderr; stdout carries only MCP protocol traffic, as the stdio transport requires.

How it works

MCP client  <--stdio-->  @arcadeai/mcp-remote  <--Streamable HTTP-->  Arcade gateway

The bridge connects outbound to the gateway as an MCP client, reads its capabilities and identity during initialization, and then presents an inbound stdio server that mirrors them. Most requests and notifications pass through untransformed; tools/list and tools/call are the seam where file parameters are rewritten (uploads) and returned files are materialized (downloads).

For auth, the outbound client carries an OAuth bearer token. On a 401 the SDK follows RFC 9728 discovery from the gateway to its authorization server, dynamically registers a public client (PKCE S256), and, during login, drives the browser + loopback authorization-code flow. In serve mode it only ever uses the cached tokens (refreshing silently); it never opens a browser.

File uploads

When a tool takes a file as bytes, the model should never see or produce base64. On tools/list the proxy detects file-carrying parameters and, at tools/call, reads the file locally and substitutes its contents before forwarding. The model only ever emits a short local path.

Detection favors precision; an ambiguous parameter is left untouched (today's behavior, base64 through the model):

  • Curated registry for known shapes: Gmail attachments[].source (the item already accepts a file:// URI, which becomes a data: URI, with filename/mime_type filled from the file), and base64 workbook/attachment parameters (presented to the model as a local-path parameter instead).
  • Guarded heuristic for new tools: a string parameter is rewritten only when its name looks like a bytes field (*_base64, file_content*, file_bytes, attachment*) and its description carries a base64/binary signal. This excludes plain-text content parameters.
  • Explicit marker: a parameter with contentEncoding: base64 is treated as authoritative.

Pass an absolute path or a file:// URI. Files up to 25 MB are read (override with ARCADE_MAX_FILE_BYTES), and file contents are never logged.

Uploading to tools that fetch a URL

Some tools don't accept bytes at all: they take a public URL that the gateway fetches server-side (for example GoogleDrive_UploadFile's source_url, Resend_SendEmail attachments, Fireflies_UploadRecording). A remote gateway can't reach a file on your machine, so this is opt-in: set ARCADE_PUBLIC_UPLOAD_BASE to a URL that reaches this machine (an SSH tunnel or an ngrok/cloudflared tunnel to ARCADE_UPLOAD_PORT, default 9909). With it set, you may pass a local path to those tools and the bridge hosts the file under an unguessable token and substitutes the public URL; the local server binds 127.0.0.1 only, so nothing is exposed except through your tunnel. Without it set, passing a local path returns a clear error explaining how to enable hosting.

File downloads

When a tool returns a file, the proxy saves it to the download directory (~/Downloads/arcade/ by default, or --download-dir / ARCADE_DOWNLOAD_DIR) and replaces it with the local path, so the bytes never reach the model's context. It handles three shapes in a tool result:

  • a resource_link URL — downloaded (best-effort, unauthenticated) and saved;
  • a data:<mime>;base64,... URI anywhere in the result (a text block or a structured field);
  • a base64 value under a bytes-looking field name, with the extension resolved from a sibling filename, then magic bytes, then .bin;
  • an http(s) URL under a media/file field (image_url, file_url, download_url, a bare image/photo, a nested *_image_url), which the proxy fetches and saves. Page and profile URLs are deliberately excluded so the proxy does not fetch every URL a tool returns.

Some tools return a large file as a "fetch me in chunks" reference rather than inline bytes (Google Drive's DownloadFile does this above ~5 MB, pointing at DownloadFileChunk). The proxy recognizes that shape, pulls every chunk through the gateway, streams them into one file on disk, and returns the same local-path result a small file's direct download gets, so the model makes a single call and never sees the chunking. Assembled downloads are capped at ARCADE_MAX_DOWNLOAD_BYTES (250 MB default).

A small per-tool registry adds the file producers whose output arrives under a field the generic rules miss (for example Figma_ExportImage, Firecrawl_ScrapeUrl, GoogleSlides_GetSlideThumbnail); incidental images like avatars and listing thumbnails are deliberately left alone. When a file URL is on the gateway's own host, it's fetched with the bearer; every other host is fetched unauthenticated, so the token never leaks. Provider-private attachment URLs (Microsoft Graph, Zendesk, Confluence, Linear) therefore can't be materialized by the bridge, since it holds the Arcade token, not the provider's; those need a gateway-side tool that returns bytes.

Anything the proxy can't confidently identify is left untouched. Set ARCADE_DOWNLOAD_MATERIALIZE=0 to disable all of it, ARCADE_DOWNLOAD_LINKS=0 to stop fetching resource_link and media URLs, or ARCADE_DOWNLOAD_CHUNKED=0 to disable chunked-download assembly.

File access policy

Reads for uploads are confined. A built-in blocklist always refuses credential directories (~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.config/gcloud, and the proxy's own ~/.arcade), and symlinks are resolved before the check so a link cannot smuggle a read past it. Set ARCADE_ALLOWED_DIRS (colon-separated) to additionally confine reads to specific roots. Downloads are written only inside the download directory; a gateway-supplied filename is reduced to its basename so it cannot escape.

Development

npm install
npm run build     # tsc -> dist/
npm test          # unit + end-to-end passthrough against an in-process gateway

The end-to-end tests stand up real servers in-process: one spawns the built bridge as a child over stdio and asserts that initialize, tools/list, tools/call, and error propagation all cross the bridge intact; another runs a mock authorization server and secured gateway to exercise the full OAuth login (DCR + PKCE + loopback), silent serve, and silent token refresh; a third drives the bridge against an echo gateway to confirm that base64 parameters are rewritten to paths and that file bytes (raw base64 and data: URIs) are substituted before forwarding; and a fourth confirms that files returned by tools (data: URIs, base64 fields, and resource_link URLs) are saved to disk and handed back as paths. Unit tests cover MIME typing, the credential store's Keychain split (with an injected backend, so the real keychain is never touched), the streaming download size cap, and chunked-download assembly (with an injected tool caller, so no gateway is needed).

License

Proprietary - Arcade Software License Agreement v1.0. See LICENSE. Copyright (c) 2025 Arcade Technologies, Inc. All rights reserved.