Skip to content

test(mcp): add OAuth smoke test and dummy-server e2e coverage - #447

Merged
christso merged 4 commits into
mainfrom
feat/mcp-remote
Aug 3, 2026
Merged

test(mcp): add OAuth smoke test and dummy-server e2e coverage#447
christso merged 4 commits into
mainfrom
feat/mcp-remote

Conversation

@christso

@christso christso commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The existing HTTP MCP proxy OAuth client (src/core/mcp-http-stdio-proxy.ts, exposed via allagents mcp proxy <url>) had no test coverage beyond one Windows-browser-launcher unit test. This PR adds:

  1. A self-contained smoke test that runs the full OAuth -> list-tools -> call-tool flow against a local dummy MCP+OAuth server by default (zero config, zero external network dependency), with an optional override to point it at any real OAuth-protected server instead.
  2. A standalone dev-server script for general local development against allagents mcp, reusing the same dummy server.
  3. A fully local dummy MCP+OAuth server so the OAuth flow (initial auth, cached-token reuse, token refresh) has automated, CI-safe e2e coverage.
  4. A pre-existing, unrelated dependency fix discovered while building this (see "Bundled fix" below).

User-facing impact

None for the published CLI. scripts/ is dev-only tooling (not in package.json's files, never shipped in the npm package), and the only change to shipped code (src/core/mcp-http-stdio-proxy.ts) is:

  • A new ALLAGENTS_MCP_OAUTH_NO_BROWSER env var (test-only escape hatch — skips the real OS browser-open; the authorization URL is still always printed either way, so nothing is hidden from a real user even if this were somehow set).
  • hashServerUrl and AUTH_URL_LOG_PREFIX are now exported (for test reuse only, no behavior change).

Default allagents mcp proxy behavior for real users is unchanged.

What was delivered

  • scripts/smoke-mcp-oauth.ts — with no arguments, spins up the local dummy MCP+OAuth server itself and runs the full connect -> OAuth -> list tools -> call tool flow against it end-to-end, printing the response. OAuth completes via a plain fetch() to the dummy IdP's auto-approving /authorize endpoint (the same "simulate the browser with curl" trick the e2e tests use) — no login screen, no external network, no setup required. Pass a URL explicitly (bun run smoke:mcp-oauth <url> [question]) to instead run the original real-OAuth path against any OAuth-protected server you have access to (opens a real browser for you to complete login).
  • scripts/dev-mcp-server.ts — standalone script that starts the same dummy MCP+OAuth server and keeps it running (Ctrl+C to stop), so it can be used for local development against allagents mcp generally, not just this smoke test.
  • tests/helpers/dummy-mcp-oauth-server.ts — a local stand-in OAuth IdP (PKCE, dynamic client registration, auto-approving /authorize) plus a protected MCP endpoint (RFC 9728 metadata, bearer-token enforcement, multi-session support) — entirely on 127.0.0.1, no external dependency.
  • tests/e2e/mcp-proxy-oauth.test.ts — 3 scenarios against the dummy server:
    1. First connection completes OAuth and calls a tool.
    2. A second connection reuses the cached token with zero new /authorize calls.
    3. An expired access token is refreshed automatically (refresh-token grant), still zero new /authorize calls.
  • tests/helpers/mcp-proxy-client.ts — shared stdio-client helper used by the smoke script, dev server example, and the e2e tests, so all of them exercise the identical production code path.
  • Bundled fix: downgraded chalk v5→v4 (+ package.json overrides) to resolve a pre-existing, intermittent TypeError: require() async module ... unsupported crash — a Bun/cmd-ts CJS↔ESM interop issue (cmd-ts internally require()s chalk, but chalk v5 is ESM-only). This was hitting ~50% of dev-path CLI invocations (bun run src/cli/index.ts, the same path both existing and new e2e tests spawn), including the already-merged mcp-add-proxy.test.ts. Confirmed 0/10 crashes after the fix vs ~5/10 before; does not affect the built dist/index.js binary real users run (was already unaffected).

Manual test steps (reproduction)

bun install
bun run build   # optional, only if you want to sanity-check dist too

# 1. Self-contained smoke test -- no setup, no external server needed
bun run smoke:mcp-oauth
# -> starts a local dummy server, completes OAuth automatically, lists
#    tools, calls one, and prints a fixture response

# 1b. Optional: validate against a real OAuth-protected MCP server you
#     have access to (requires completing OAuth in your own browser)
bun run smoke:mcp-oauth https://your-mcp-server.example.com "how to rename a company branch"
# Run it again immediately after -- it should NOT prompt for OAuth again
# (cached token reuse), confirming the token/client-registration cache works.

# 2. Standalone dev server, for poking at with `allagents mcp` directly
bun run dev:mcp-server
# (in another terminal) allagents mcp add local-dev http://127.0.0.1:<port> --proxy

# 3. Automated e2e OAuth coverage (no browser, fully local, no external URL)
bun test tests/e2e/mcp-proxy-oauth.test.ts

# 4. Full suite + typecheck + lint
bun test
bun run typecheck
bun run lint

Expected: all pass, no browser windows open during steps 1/2/3/4, no require() async module crashes anywhere.

Review

Went through a 9-persona automated code review (correctness, security, reliability, adversarial, testing, maintainability, project-standards, agent-native, learnings) on the initial implementation. No P0/P1 findings. 6 fixes were applied directly (truncated-URL parsing on stderr chunk boundaries, shared log-prefix constant instead of duplication, explicit logging when the no-browser test escape hatch is active, child-process cleanup on connect failure, clean error response instead of an unhandled rejection in the dummy server, error handling on a fire-and-forget test fetch). Remaining open items (all P2/P3, none blocking):

  • tests/helpers/dummy-mcp-oauth-server.ts: stop() doesn't close sessions left mid-handshake (test-only cleanup edge case).
  • No unit tests yet for scripts/smoke-mcp-oauth.ts's argument-building/tool-selection heuristics.
  • Dummy IdP's invalid_request / PKCE-failure error branches aren't exercised by any test.
  • Child-process stderr isn't surfaced in the thrown error if connect() fails (would help debug a hung/crashed proxy in test failures).

Follow-up recommendation (not in this PR)

Compared this implementation against geelen/mcp-remote's approach to reducing repeated OAuth flows. We already avoid its two biggest footguns (version-scoped credential directory that wipes tokens on every upgrade; expires_in-only token bookkeeping with no proactive refresh). The one gap that concretely matters for allagents specifically: no cross-process coordination. Since allagents syncs one MCP server entry into Claude Code, Copilot/VS Code, and Cursor, a cold start after sync/install spawns separate mcp proxy processes per client — each would trigger its own browser popup for the same server on first use. mcp-remote's lockfile + long-poll rendezvous pattern is the direct fix, but is a distinct, separately-scoped change from this PR.

The existing HTTP MCP proxy OAuth client (mcp-http-stdio-proxy.ts) had
no coverage beyond one browser-launcher unit test. Add:

- scripts/smoke-mcp-oauth.ts: an interactive smoke test that connects
  to a real OAuth-protected remote MCP server via `allagents mcp
  proxy`, lists its tools, and asks a question -- for manually
  confirming the OAuth flow against a live private server.
- tests/helpers/dummy-mcp-oauth-server.ts: a local, CI-safe stand-in
  OAuth IdP + MCP server (PKCE, dynamic client registration,
  auto-approving authorization) so the OAuth flow can be exercised
  without a human or network access to a real server.
- tests/e2e/mcp-proxy-oauth.test.ts: covers first-connection OAuth,
  cached-token reuse on a second connection, and automatic token
  refresh on an expired access token -- all with zero browser/human
  interaction required.
- tests/helpers/mcp-proxy-client.ts: shared helper connecting an SDK
  Client to the proxy over stdio, used by both the smoke script and
  the e2e tests so both exercise the same production code path.

mcp-http-stdio-proxy.ts: export hashServerUrl and AUTH_URL_LOG_PREFIX
for test reuse, and add an ALLAGENTS_MCP_OAUTH_NO_BROWSER escape hatch
so e2e tests never spawn a real OS browser.
cmd-ts's compiled CJS output does require("chalk") internally, but
chalk v5 is pure ESM. Under Bun this intermittently throws
"TypeError: require() async module ... is unsupported" -- observed on
~50% of dev-path CLI invocations (bun run src/cli/index.ts), including
in already-merged e2e tests, though not in the built dist binary real
users run.

Force chalk to v4.1.2 everywhere via a package.json `overrides` entry,
since cmd-ts declares its own "chalk": "^5.4.1" dependency independent
of this project's own version pin -- changing only this project's
direct dependency would leave a nested v5 copy for cmd-ts to crash on.
Confirmed 0 crashes across 10 repeated CLI invocations after the fix
(previously ~5/10), and the full test suite (including e2e) passes
consistently across repeated runs.
The script defaulted to a private company MCP endpoint. The whole
point of this tooling is to not depend on any internal/company URL —
that's what the dummy server + e2e suite are for. Require --url-style
positional input instead, with a clear usage message.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying allagents with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7b801ab
Status: ✅  Deploy successful!
Preview URL: https://647e6408.allagents.pages.dev
Branch Preview URL: https://feat-mcp-remote.allagents.pages.dev

View logs

Requiring an explicit URL was correct in spirit (don't hardcode a
private endpoint) but missed the actual point: the smoke test should
work entirely on localhost by default, with zero external dependency
and zero setup.

- scripts/smoke-mcp-oauth.ts: with no URL argument, spins up
  tests/helpers/dummy-mcp-oauth-server.ts itself and runs the full
  connect -> OAuth -> list-tools -> call-tool flow against it. OAuth
  completes via a plain fetch() to the dummy IdP's auto-approving
  /authorize endpoint -- the same "simulate the browser with curl"
  trick the e2e tests already use, no real login screen involved.
  Passing an explicit URL still runs the original real-OAuth path
  (opens a real browser) unchanged, for validating against an actual
  private server when you have one.
- scripts/dev-mcp-server.ts: new standalone script that starts the
  same dummy server and keeps it running (Ctrl+C to stop), so it can
  be used for general local development against `allagents mcp` --
  not just this smoke test.
@christso
christso merged commit 2babcc2 into main Aug 3, 2026
5 checks passed
@christso
christso deleted the feat/mcp-remote branch August 3, 2026 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant