{"_id":"@arcadeai/mcp-remote","name":"@arcadeai/mcp-remote","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@arcadeai/mcp-remote","version":"0.1.0","description":"Local stdio-to-remote bridge for Arcade MCP gateways, with managed OAuth and local file materialization.","type":"module","bin":{"mcp-remote":"dist/index.js"},"engines":{"node":">=18"},"scripts":{"clean":"rm -rf dist","build":"tsc -p tsconfig.json && chmod +x dist/index.js","dev":"tsc -w -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","test":"ARCADE_USE_KEYCHAIN=0 node --test test/*.test.mjs","prepack":"npm run clean && npm run build"},"dependencies":{"@modelcontextprotocol/sdk":"^1.29.0"},"devDependencies":{"@types/node":"^22.10.0","typescript":"^5.7.0"},"keywords":["mcp","arcade","stdio","proxy","model-context-protocol"],"license":"SEE LICENSE IN LICENSE","author":{"name":"Arcade AI"},"repository":{"type":"git","url":"git+https://github.com/ArcadeAI/mcp-remote.git"},"homepage":"https://github.com/ArcadeAI/mcp-remote#readme","bugs":{"url":"https://github.com/ArcadeAI/mcp-remote/issues"},"publishConfig":{"access":"public"},"_id":"@arcadeai/mcp-remote@0.1.0","gitHead":"0706e828d228a96a54641f08702d56ffe348bae2","_nodeVersion":"22.13.0","_npmVersion":"11.3.0","dist":{"integrity":"sha512-mrUMdtceW48Bk/wzVrGi+0cOuun2n6l358HFiU67jGIIisqIZI6C5sOgUEHt0RVyMUJP0hZMbDCdbPGtVh/P4g==","shasum":"4e267c06b27d129602a47b44c9557f3ec0cf8e9c","tarball":"https://registry.npmjs.org/@arcadeai/mcp-remote/-/mcp-remote-0.1.0.tgz","fileCount":79,"unpackedSize":209712,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDyWAxS8IMjexPDs/4LSJwNLfEgdEf7YCahI1OowvKXrQIhANruutFWgwsB78Z9vhms9QyYZFZXp30MqMgXfNG/oJbH"}]},"_npmUser":{"name":"ericgustin","email":"eric@arcade.dev"},"directories":{},"maintainers":[{"name":"pmdroid","email":"me@pascal.sh"},{"name":"ericgustin","email":"eric@arcade.dev"},{"name":"sdserrano","email":"sdserranog@gmail.com"},{"name":"evantahler","email":"evan@evantahler.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mcp-remote_0.1.0_1783964731764_0.7918741657107464"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T17:45:31.674Z","0.1.0":"2026-07-13T17:45:31.909Z","modified":"2026-07-13T17:45:32.098Z"},"maintainers":[{"name":"pmdroid","email":"me@pascal.sh"},{"name":"ericgustin","email":"eric@arcade.dev"},{"name":"sdserrano","email":"sdserranog@gmail.com"},{"name":"evantahler","email":"evan@evantahler.com"}],"description":"Local stdio-to-remote bridge for Arcade MCP gateways, with managed OAuth and local file materialization.","homepage":"https://github.com/ArcadeAI/mcp-remote#readme","keywords":["mcp","arcade","stdio","proxy","model-context-protocol"],"repository":{"type":"git","url":"git+https://github.com/ArcadeAI/mcp-remote.git"},"author":{"name":"Arcade AI"},"bugs":{"url":"https://github.com/ArcadeAI/mcp-remote/issues"},"license":"SEE LICENSE IN LICENSE","readme":"# @arcadeai/mcp-remote\n\nA local **stdio** bridge to an Arcade MCP gateway.\n\nArcade serves MCP gateways over Streamable HTTP. Some MCP clients only speak the\nstdio transport, and, more importantly, a local process can do work that a cloud\ngateway cannot: it can read files from the machine. This bridge runs locally,\npresents a stdio MCP server to your client, and relays every request to the\nremote gateway over HTTP.\n\nThe headline capability this unlocks is **local file materialization**: the\nmodel emits a short file path, the bridge reads the bytes and substitutes them\nbefore the request leaves the machine, and files returned by tools are written to\ndisk and handed back as a path. Large base64 blobs never enter the model's\ncontext window.\n\n## Status\n\nThe bridge relays MCP traffic to the gateway, authenticates with browser-based\nOAuth (Dynamic Client Registration + PKCE, silent refresh), rewrites\nfile-carrying tool parameters so the model passes a local path (uploads), and\nmaterializes files returned by tools to disk so the model gets a path instead of\na blob (downloads). On macOS the OAuth tokens are kept in the Keychain; file\nreads are guarded by a credential-directory blocklist and an optional allowlist.\n\nNot yet done: the `arcade connect --local` integration and the public npm\npublish (the package is prepared but intentionally unpublished).\n\n## Usage\n\nPoint your MCP client at the bridge. The first time it connects to a gateway you\nhave not authorized yet, the bridge opens your browser to sign in, caches the\ntokens, and then serves; every later start is silent. (Set `ARCADE_AUTO_LOGIN=0`\nto disable this and require an explicit `login` first.)\n\nTo authorize ahead of time, so the first spawn never has to wait on you, run once\nin a terminal:\n\n```bash\nnpx @arcadeai/mcp-remote login <gateway-slug-or-url>\n```\n\nEither way, point your MCP client at the bridge:\n\n```jsonc\n// in your MCP client config (e.g. claude_desktop_config.json)\n{\n  \"mcpServers\": {\n    \"arcade\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@arcadeai/mcp-remote\", \"<gateway-slug-or-url>\"]\n    }\n  }\n}\n```\n\nThe single argument is either a gateway **slug** (expanded to\n`https://api.arcade.dev/mcp/<slug>`) or a full gateway **URL**.\n\nTo sign out: `npx @arcadeai/mcp-remote logout [<gateway-slug-or-url>]` (omit the\nargument to clear all cached credentials).\n\n### Headless hosts\n\nOn a machine with no browser, add `--no-browser` (or set `ARCADE_NO_BROWSER=1`)\nto `login`. It prints the authorization URL instead of opening a browser; open\nthat URL on any machine that can reach the loopback callback, for example by\nforwarding the callback port over SSH:\n\n```bash\nssh -L 9908:localhost:9908 you@remote-host\n```\n\nThe device-code grant is not offered, because the Arcade authorization server\ndoes not support it.\n\n### Authentication\n\nBy default the bridge uses OAuth tokens cached by `login`, refreshing the access\ntoken silently when it expires. No API key is required, so project members who\ncannot mint API keys can still connect.\n\nAlternatively, pass a bearer credential via the environment. This **overrides**\nthe cached login and is handy for CI. The Engine accepts it in `Authorization`\nfor both shapes:\n\n| Variable          | Meaning                                                             |\n| ----------------- | ------------------------------------------------------------------- |\n| `ARCADE_API_KEY`  | API key. Sent as `Authorization: Bearer <key>`.                     |\n| `ARCADE_USER_ID`  | Paired with `ARCADE_API_KEY` (sent as the `Arcade-User-Id` header). |\n| `ARCADE_TOKEN`    | An OAuth access token. Sent as `Authorization: Bearer <token>`.     |\n\n### Token storage\n\nOn macOS the OAuth tokens are stored in the login Keychain (through the\n`security` tool, so there is no native module to compile), and the on-disk file\nkeeps only the non-secret client registration. On other platforms, or when\n`ARCADE_USE_KEYCHAIN=0`, tokens are written to `credentials.json` (0600) in the\ncache directory instead. A file written before the Keychain was in use still\nworks and is migrated on the next token write.\n\n### Other environment variables\n\n| Variable                       | Default                  | Meaning                                    |\n| ------------------------------ | ------------------------ | ------------------------------------------ |\n| `ARCADE_ENGINE_URL`            | `https://api.arcade.dev` | Engine base used to expand a slug.         |\n| `ARCADE_OAUTH_CALLBACK_PORT`   | `9908`                   | Loopback port for the `login` redirect.    |\n| `ARCADE_LOGIN_TIMEOUT_SECONDS` | `600`                    | How long `login` waits for the callback.   |\n| `ARCADE_MCP_REMOTE_DIR`        | `~/.arcade/mcp-remote`   | Where the client registration (and, without a keychain, tokens) is stored (0600).|\n| `ARCADE_USE_KEYCHAIN`          | on (macOS)               | Set `0` to keep tokens in the file instead of the OS keychain. |\n| `ARCADE_NO_BROWSER`            | (unset)                  | Set `1` for headless `login` (same as `--no-browser`).         |\n| `ARCADE_DOWNLOAD_DIR`          | `~/Downloads/arcade`     | Where files returned by tools are saved (also `--download-dir`). |\n| `ARCADE_ALLOWED_DIRS`          | (unset)                  | Colon-separated dirs uploads may read from; unset means anywhere but credential dirs. |\n| `ARCADE_MAX_FILE_BYTES`        | `26214400` (25 MB)       | Max in-memory upload/download size.        |\n| `ARCADE_MAX_DOWNLOAD_BYTES`    | `262144000` (250 MB)     | Max assembled chunked-download size.       |\n| `ARCADE_DOWNLOAD_CHUNKED`      | on                       | Set `0` to disable chunked-download assembly. |\n| `ARCADE_PUBLIC_UPLOAD_BASE`    | (unset)                  | Public base URL that lets URL-fetching tools accept a local file path. |\n| `ARCADE_UPLOAD_PORT`           | `9909`                   | Local port the upload host binds (behind your tunnel). |\n| `MCP_REMOTE_LOG_LEVEL`         | `info`                   | `debug` \\| `info` \\| `warn` \\| `error`.    |\n\nAll diagnostics are written to **stderr**; stdout carries only MCP protocol\ntraffic, as the stdio transport requires.\n\n## How it works\n\n```\nMCP client  <--stdio-->  @arcadeai/mcp-remote  <--Streamable HTTP-->  Arcade gateway\n```\n\nThe bridge connects outbound to the gateway as an MCP client, reads its\ncapabilities and identity during initialization, and then presents an inbound\nstdio server that mirrors them. Most requests and notifications pass through\nuntransformed; `tools/list` and `tools/call` are the seam where file parameters\nare rewritten (uploads) and returned files are materialized (downloads).\n\nFor auth, the outbound client carries an OAuth bearer token. On a 401 the SDK\nfollows RFC 9728 discovery from the gateway to its authorization server,\ndynamically registers a public client (PKCE S256), and, during `login`, drives\nthe browser + loopback authorization-code flow. In serve mode it only ever uses\nthe cached tokens (refreshing silently); it never opens a browser.\n\n## File uploads\n\nWhen a tool takes a file as bytes, the model should never see or produce base64.\nOn `tools/list` the proxy detects file-carrying parameters and, at `tools/call`,\nreads the file locally and substitutes its contents before forwarding. The model\nonly ever emits a short local path.\n\nDetection favors precision; an ambiguous parameter is left untouched (today's\nbehavior, base64 through the model):\n\n- **Curated registry** for known shapes: Gmail `attachments[].source` (the item\n  already accepts a `file://` URI, which becomes a `data:` URI, with\n  `filename`/`mime_type` filled from the file), and base64 workbook/attachment\n  parameters (presented to the model as a local-path parameter instead).\n- **Guarded heuristic** for new tools: a string parameter is rewritten only when\n  its name looks like a bytes field (`*_base64`, `file_content*`, `file_bytes`,\n  `attachment*`) **and** its description carries a base64/binary signal. This\n  excludes plain-text `content` parameters.\n- **Explicit marker**: a parameter with `contentEncoding: base64` is treated as\n  authoritative.\n\nPass an absolute path or a `file://` URI. Files up to 25 MB are read (override\nwith `ARCADE_MAX_FILE_BYTES`), and file contents are never logged.\n\n### Uploading to tools that fetch a URL\n\nSome tools don't accept bytes at all: they take a **public URL** that the\ngateway fetches server-side (for example `GoogleDrive_UploadFile`'s `source_url`,\n`Resend_SendEmail` attachments, `Fireflies_UploadRecording`). A remote gateway\ncan't reach a file on your machine, so this is opt-in: set\n`ARCADE_PUBLIC_UPLOAD_BASE` to a URL that reaches this machine (an SSH tunnel or\nan `ngrok`/`cloudflared` tunnel to `ARCADE_UPLOAD_PORT`, default 9909). With it\nset, you may pass a local path to those tools and the bridge hosts the file\nunder an unguessable token and substitutes the public URL; the local server\nbinds `127.0.0.1` only, so nothing is exposed except through your tunnel.\nWithout it set, passing a local path returns a clear error explaining how to\nenable hosting.\n\n## File downloads\n\nWhen a tool returns a file, the proxy saves it to the download directory\n(`~/Downloads/arcade/` by default, or `--download-dir` / `ARCADE_DOWNLOAD_DIR`)\nand replaces it with the local path, so the bytes never reach the model's\ncontext. It handles three shapes in a tool result:\n\n- a `resource_link` URL — downloaded (best-effort, unauthenticated) and saved;\n- a `data:<mime>;base64,...` URI anywhere in the result (a text block or a\n  structured field);\n- a base64 value under a bytes-looking field name, with the extension resolved\n  from a sibling filename, then magic bytes, then `.bin`;\n- an `http(s)` URL under a media/file field (`image_url`, `file_url`,\n  `download_url`, a bare `image`/`photo`, a nested `*_image_url`), which the\n  proxy fetches and saves. Page and profile URLs are deliberately excluded so\n  the proxy does not fetch every URL a tool returns.\n\nSome tools return a large file as a \"fetch me in chunks\" reference rather than\ninline bytes (Google Drive's `DownloadFile` does this above ~5 MB, pointing at\n`DownloadFileChunk`). The proxy recognizes that shape, pulls every chunk through\nthe gateway, streams them into one file on disk, and returns the same local-path\nresult a small file's direct download gets, so the model makes a single call and\nnever sees the chunking. Assembled downloads are capped at `ARCADE_MAX_DOWNLOAD_BYTES`\n(250 MB default).\n\nA small per-tool registry adds the file producers whose output arrives under a\nfield the generic rules miss (for example `Figma_ExportImage`,\n`Firecrawl_ScrapeUrl`, `GoogleSlides_GetSlideThumbnail`); incidental images like\navatars and listing thumbnails are deliberately left alone. When a file URL is\non the gateway's own host, it's fetched with the bearer; every other host is\nfetched unauthenticated, so the token never leaks. Provider-private attachment\nURLs (Microsoft Graph, Zendesk, Confluence, Linear) therefore can't be\nmaterialized by the bridge, since it holds the Arcade token, not the provider's;\nthose need a gateway-side tool that returns bytes.\n\nAnything the proxy can't confidently identify is left untouched. Set\n`ARCADE_DOWNLOAD_MATERIALIZE=0` to disable all of it, `ARCADE_DOWNLOAD_LINKS=0`\nto stop fetching `resource_link` and media URLs, or `ARCADE_DOWNLOAD_CHUNKED=0`\nto disable chunked-download assembly.\n\n## File access policy\n\nReads for uploads are confined. A built-in blocklist always refuses credential\ndirectories (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.kube`, `~/.docker`,\n`~/.config/gcloud`, and the proxy's own `~/.arcade`), and symlinks are resolved\nbefore the check so a link cannot smuggle a read past it. Set `ARCADE_ALLOWED_DIRS`\n(colon-separated) to additionally confine reads to specific roots. Downloads are\nwritten only inside the download directory; a gateway-supplied filename is reduced\nto its basename so it cannot escape.\n\n## Development\n\n```bash\nnpm install\nnpm run build     # tsc -> dist/\nnpm test          # unit + end-to-end passthrough against an in-process gateway\n```\n\nThe end-to-end tests stand up real servers in-process: one spawns the built\nbridge as a child over stdio and asserts that initialize, `tools/list`,\n`tools/call`, and error propagation all cross the bridge intact; another runs a\nmock authorization server and secured gateway to exercise the full OAuth login\n(DCR + PKCE + loopback), silent serve, and silent token refresh; a third drives\nthe bridge against an echo gateway to confirm that base64 parameters are rewritten\nto paths and that file bytes (raw base64 and `data:` URIs) are substituted before\nforwarding; and a fourth confirms that files returned by tools (`data:` URIs,\nbase64 fields, and `resource_link` URLs) are saved to disk and handed back as\npaths. Unit tests cover MIME typing, the credential store's Keychain split (with\nan injected backend, so the real keychain is never touched), the streaming\ndownload size cap, and chunked-download assembly (with an injected tool caller,\nso no gateway is needed).\n\n## License\n\nProprietary - Arcade Software License Agreement v1.0. See [LICENSE](LICENSE).\nCopyright (c) 2025 Arcade Technologies, Inc. All rights reserved.\n","readmeFilename":"README.md","_rev":"1-91c0d8d009233801f1883a6953eff58d"}