Commit Graph

163 Commits

Author SHA1 Message Date
Peter Steinberger
c1b58296db
feat: support file-backed call arguments (#213) 2026-06-18 07:54:31 +02:00
Qi Zhang
4813cdfe7a
fix(cli): keep CloudBase authentication alive (#193)
Keep CloudBase device-code polling alive after `AUTH_PENDING` while preserving explicit ephemeral lifecycle overrides.

Co-authored-by: Qi Zhang <sevenzhang51@gmail.com>
2026-06-09 22:07:47 -07:00
Sebastian B Otaegui
3e27b64021
fix(runtime): preserve disableOAuth across headless paths (#198)
Some checks failed
CI / build (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / build (${{ matrix.os }}) (macos-15) (push) Has been cancelled
CI / build (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
pages / Deploy docs (push) Has been cancelled
* feat(runtime): add `disableOAuth` connect option (cache-friendly OAuth suppression)

Closes #197.

Long-running headless callers (daemons, scheduled jobs, CI workers) need
to suppress the interactive OAuth flow without losing connection caching.
The only existing knob — `maxOAuthAttempts: 0` — couples those two concerns
because `useCache` is gated on `options.maxOAuthAttempts === undefined`.
Daemons that wrap `connect` to force `maxOAuthAttempts: 0` end up spawning
a fresh transport per `callTool`/`listTools` and `runtime.close()` cannot
reap any of them.

Add an additive `disableOAuth: boolean` option that suppresses OAuth at
the transport layer (short-circuits `shouldEstablishOAuth` and
`maybePromoteHttpDefinition`) but preserves caching. The cache entry
metadata gains a `disableOAuth` field so connections established with
the flag don't share a slot with connections that could refresh into an
OAuth flow — switching the flag between calls evicts and re-establishes,
mirroring the existing `allowCachedAuth` mismatch path.

Backward compatibility:

* `maxOAuthAttempts: 0` keeps its legacy escape-the-cache contract
  unchanged. Existing callers see no behavior change.
* `skipCache: true` keeps its behavior unchanged.
* `disableOAuth` defaults to undefined; only opt-in changes behavior.

Also export `ConnectOptions` from `runtime.ts` and add the parameter to
the `Runtime.connect` interface signature — the implementation already
accepted options at runtime but the interface only exposed
`connect(server)`, so callers couldn't pass options through the type
system. (Pre-existing gap surfaced by adding the new test coverage.)

Tests added to `tests/runtime-integration.test.ts`:

* `reuses cached connection when disableOAuth: true is passed` — two
  calls return the same ClientContext, `close()` reaps it.
* `maxOAuthAttempts: 0 still bypasses the cache (existing contract
  preserved)` — regression guard.
* `evicts and re-establishes the cached client when disableOAuth flag
  changes` — the core eviction semantic.

`pnpm test` (709 pass / 3 skip), `pnpm lint`, `pnpm typecheck` all
green.

* fix(runtime): preserve disableOAuth across helper calls

* fix(daemon): forward disableOAuth through keep-alive paths

* feat(cli): expose disableOAuth for headless commands

* fix(runtime): preserve cached slot across connect(disableOAuth) → callTool/listTools

Addresses PR #198 review comment r3366238654.

The documented headless setup is:

    await runtime.connect(server, { disableOAuth: true });
    await runtime.callTool(server, 'foo', { ... });

The first call stored the cache slot with `allowCachedAuth: undefined`,
but `callTool()` internally calls `this.connect(server, {
allowCachedAuth: true, disableOAuth: <effective>: true })` and the
cache-match check treated the two options shapes as structurally
different:

    existing.allowCachedAuth (undefined)
       !== options.allowCachedAuth (true)
       && options.allowCachedAuth !== undefined
    => MISMATCH => evict + reopen transport

Every first callTool / listTools after a pre-connect spawned a fresh
transport, defeating the pooling guarantee that motivated the
disableOAuth option in the first place. Same shape affected `listTools`
(which defaults `allowCachedAuth: options.allowCachedAuth ?? true`).

Fix: normalize at the connect() entrypoint. A `disableOAuth: true`
caller has no path to interactive OAuth, so cached-token application
is the only auth they can ever use — default `allowCachedAuth: true`
when the caller didn't pick a side. Explicit `false` is honored
(header-only / anonymous callers). The normalized value flows through
both the cache lookup and the cache write so subsequent internal
callers compose without eviction.

Two regression tests added to `tests/runtime-integration.test.ts`:

  - `preserves the cached client across connect(disableOAuth:true) →
    callTool() (no implicit eviction)`
  - `preserves the cached client across connect(disableOAuth:true) →
    listTools() (no implicit eviction)`

Both call `runtime.connect(disableOAuth:true)`, then invoke the
internal-cached path (callTool or listTools), then re-call
`runtime.connect(disableOAuth:true)` and assert the resulting
ClientContext is `=== ` the first one. Both tests fail without this
fix (the second connect returns a new ClientContext because the first
was evicted).

`pnpm test` 723 pass / 3 skip / 0 fail. `pnpm lint` + `pnpm
typecheck` clean. No push.

* docs(examples): add headless-pooling-demo for disableOAuth verification

Demonstrates the three patterns under the new `disableOAuth` option
against a local mock MCP server (no real auth). Reproducible artifact
for PR #198 review proof.

Patterns demonstrated:

* Legacy `maxOAuthAttempts: 0` (uncached): 5 connect() calls produce
  5 distinct ClientContexts. Existing contract preserved.
* `disableOAuth: true` on every connect: 5 calls produce 1
  ClientContext. Cache reuse under cache-friendly suppression.
* Documented headless setup — pre-connect(disableOAuth: true) +
  5 callTool() — proves the pre-connected slot survives the implicit
  internal connect path. Directly demonstrates the fix from b0e3e2e.

Run: `pnpm tsx examples/headless-pooling-demo.ts`

Sample output is intentionally redacted to no PII / no secrets: a local
http://127.0.0.1:<random-port>/mcp server with a public `add` tool.

* style(examples): oxfmt headless-pooling-demo (CI fix)

* fix(server-proxy): thread disableOAuth through schema-discovery listTools

Addresses PR #198 review comment r3366307210 (clawsweeper proxy gap).

The Proxy returned by `createServerProxy` calls `ensureMetadata()` on
every tool invocation, which fires `runtime.listTools(server, {
includeSchema: true })` for schema discovery. That call ran BEFORE the
proxy parsed the caller's options bag, so a `proxy.tool({ ... }, {
disableOAuth: true })` invocation on an OAuth server with no cached
schema could still trigger an interactive OAuth flow during metadata
fetch — defeating the no-browser guarantee the option was meant to
provide.

Fix:

* Pre-scan callArgs once for `disableOAuth: true` before invoking
  `ensureMetadata`. The scan is a single linear pass over the
  already-present argument list and short-circuits on the first match.
* Extend `ensureMetadata(toolName, { disableOAuth? })` and forward the
  flag to the underlying `runtime.listTools(serverName, { includeSchema:
  true, disableOAuth: true })` call.
* The schema-fetch path that was vulnerable now inherits the same
  no-OAuth posture as the eventual `runtime.callTool` invocation. End-
  to-end no-browser guarantee is preserved across the proxy interface.

Regression test in `tests/server-proxy.test.ts`:

  > threads disableOAuth through schema discovery so
  > proxy.tool({disableOAuth:true}) cannot trigger OAuth during
  > metadata fetch

Asserts BOTH:
- `runtime.listTools` called with `{ includeSchema: true, disableOAuth:
  true }`
- `runtime.callTool` called with the eventual tool args and
  `disableOAuth: true`

Locks the contract on both halves so a future refactor that re-introduces
the gap on either side will fail loudly.

Full suite: 724 pass / 3 skipped / 0 fail. `pnpm check` (format + lint
+ typecheck) clean.

* refactor(cli): drop --disable-oauth alias; keep only --no-oauth

The PR originally exposed two CLI names for the same intent:
--disable-oauth (mirroring the JS option `disableOAuth: true`) and
--no-oauth (the GNU-style boolean opt-out). Two names for one
behavior is noise — documentation has to mention both, users have to
learn both, and they invite drift.

--no-oauth is the right shape for a per-invocation boolean opt-out:
- Matches the dominant unix convention (git --no-verify, npm --no-save,
  bun --no-cache, curl --no-progress-meter).
- Shorter to type.
- Composes naturally with other flags in scripts.

The JS option name stays `disableOAuth: boolean` — that's the right
shape for a JS option (verb+noun, no Boolean-negation prefix
ambiguity), and the JS and CLI naming conventions are genuinely
different domains.

Removed CLI registrations + help text + internal forwarding for
--disable-oauth across:
- src/cli/call-arguments.ts (FLAG_HANDLERS registration)
- src/cli/call-command.ts (internal listArgs forwarding, 2 sites)
- src/cli/call-help.ts (help text)
- src/cli/list-command.ts (help text)
- src/cli/list-flags.ts (token check)
- src/cli/resource-command.ts (token check + help text)
- docs/cli-reference.md (3 references)

Renamed test cases that exclusively exercised --disable-oauth to
exercise --no-oauth instead, preserving regression coverage:
- tests/call-arguments.test.ts
- tests/cli-list-flags.test.ts
- tests/cli-resource-command.test.ts

The internal cache-key fragment `disable-oauth:` in
src/cli/tool-cache.ts is kept — it mirrors the JS option name (which
stays `disableOAuth`), not the CLI flag.

Tests: 724 passed, 3 skipped, 0 failed.
Lint: 0 warnings, 0 errors.
Typecheck: clean.

* fix(runtime): forward disableOAuth through callOnce

* chore: update dependencies

* fix(server-proxy): preserve schema-owned option fields

* fix(runtime): isolate OAuth cache variants safely

* fix(server-proxy): isolate schema discovery posture

* fix(server-proxy): preserve OAuth posture during discovery

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-06-08 16:11:23 -07:00
Peter Steinberger
0fb13581fb docs(serve): document per-server HTTP endpoints 2026-06-08 12:16:38 -07:00
LDMB123
2bf7a5eab2
fix(replay): rewrite response ids during replay (#192)
Some checks failed
CI / build (${{ matrix.os }}) (macos-15) (push) Has been cancelled
CI / build (${{ matrix.os }}) (ubuntu-latest) (push) Has been cancelled
CI / build (${{ matrix.os }}) (windows-latest) (push) Has been cancelled
pages / Deploy docs (push) Has been cancelled
* feat(record): capture MCP call streams to NDJSON and replay deterministically

mcporter record <session> wraps the runtime transport and appends every
JSON-RPC request, response, and notification to a per-session NDJSON file
under ~/.mcporter/recordings/. mcporter replay <session> reconstructs an
in-memory transport from the recording and matches requests by method +
deep-equal params, returning the recorded response without contacting
the live server.

Use cases:
- Reproduce MCP-backed agent bugs offline (no live Linear quota, no
  Vercel API rate limits)
- Build test fixtures from real call sequences
- Share a session for a postmortem without sharing credentials

The format is plain JSON-RPC over NDJSON with a small _meta field
(direction, server, timestamp). No proprietary blob. Env-var passthrough
(MCPORTER_RECORD=<name>, MCPORTER_REPLAY=<name>) lets the existing
runtime constructor wrap any transport when set.

* fix(replay): attach cause to wrapped errors to satisfy preserve-caught-error lint

* fix(replay): rewrite response ids during replay

* fix(replay): harden record replay modes

Clear conflicting record/replay env vars when spawning wrapped commands, force those commands off the daemon fast path, truncate each recording file at session start, and fail replay close when recorded requests remain unused.

* fix(cli): preserve wrapped command flags

Stop global flag extraction at -- so record/replay wrappers do not consume child command flags, and drop the release-owned changelog entry from the PR diff.

* fix(replay): propagate cleanup failures through cli

Ensure replay-mode transport close failures escape normal runtime and CLI cleanup after best-effort shutdown has completed. Add runtime and CLI regressions for partial recordings that leave requests unreplayed.

* fix: harden record replay runtime paths

* test: align replay fixtures with windows home

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-31 08:52:02 +01:00
Peter Steinberger
815016a008
docs: position README banner
Some checks are pending
CI / build (${{ matrix.os }}) (windows-latest) (push) Waiting to run
CI / build (${{ matrix.os }}) (macos-15) (push) Waiting to run
CI / build (${{ matrix.os }}) (ubuntu-latest) (push) Waiting to run
2026-05-28 20:48:14 +01:00
Peter Steinberger
fb3f041339
docs: add README banner 2026-05-28 19:43:30 +01:00
Peter Steinberger
67e3f5250f
fix: fall back to legacy config after empty xdg home (#185) 2026-05-21 22:15:13 +01:00
Peter Steinberger
de7c811271
feat: add list health check flags (#183) 2026-05-20 20:55:21 +01:00
Peter Steinberger
a1201d1955
fix: support daemon idle timeout config 2026-05-20 17:34:42 +01:00
Asim Arshad
8c63bbe81e docs: clarify headless OAuth process lifetime 2026-05-17 00:36:39 +01:00
Peter Steinberger
46cc31cafe
fix: harden generated cli bundles 2026-05-14 19:22:37 +01:00
Peter Steinberger
7f1e9a8ce0
feat: support refreshable bearer stdio auth 2026-05-14 18:29:43 +01:00
Peter Steinberger
3e06e582ef
fix: add HTTP fetch compatibility mode 2026-05-14 17:31:36 +01:00
Peter Steinberger
2171c1f209
Merge pull request #171 from feniix/feat/headless-oauth-no-browser
feat: add headless OAuth browser suppression
2026-05-14 16:48:57 +01:00
Peter Steinberger
8d962fbd79
Merge remote-tracking branch 'origin/main' into feat/mcporter-serve
# Conflicts:
#	CHANGELOG.md
2026-05-14 13:40:29 +01:00
Peter Steinberger
eee954e4a1
fix: patch chrome-devtools auto-connect hang 2026-05-14 12:51:16 +01:00
zm2231
6879a69f49 feat: add mcporter serve bridge 2026-05-13 20:26:12 -04:00
Sebastian Otaegui
7ddb433479
docs: document headless OAuth auth flow 2026-05-12 23:59:45 -03:00
Peter Steinberger
a64e29b4fe
feat: add headless OAuth vault seeding
Some checks are pending
CI / build (macos-latest) (push) Waiting to run
CI / build (ubuntu-latest) (push) Waiting to run
CI / build (windows-latest) (push) Waiting to run
pages / Deploy docs (push) Waiting to run
2026-05-09 14:55:44 +01:00
Peter Steinberger
ea91086273
fix: resolve config env placeholders 2026-05-09 13:14:09 +01:00
Peter Steinberger
c0e251babe
fix: harden live mcp cli paths 2026-05-09 12:23:33 +01:00
Peter Steinberger
b3e1c7c314
fix: extend oauth browser timeout 2026-05-08 03:56:38 +01:00
Peter Steinberger
7d345bc7db
docs: add static GitHub Pages site
Some checks failed
CI / build (macos-latest) (push) Waiting to run
CI / build (ubuntu-latest) (push) Waiting to run
CI / build (windows-latest) (push) Waiting to run
pages / Deploy docs (push) Has been cancelled
2026-05-06 05:56:02 +01:00
Peter Steinberger
6ed98602ef
docs: clarify Homebrew release verification 2026-05-04 10:10:01 +01:00
Peter Steinberger
bb6e64617a
fix: honor xdg directories 2026-05-04 08:14:31 +01:00
Peter Steinberger
eb8986cd10
docs: clarify daemon isolation 2026-05-04 08:05:35 +01:00
Peter Steinberger
dd33721d89
feat: support static oauth clients 2026-05-04 08:03:13 +01:00
Peter Steinberger
caa00dd3a4
docs: document agent skill pattern 2026-05-04 07:54:40 +01:00
Peter Steinberger
75dba26173
feat: add resource read command 2026-05-04 07:50:26 +01:00
Peter Steinberger
db6a199cd5
feat: route generated keep-alive CLIs through daemon 2026-05-04 07:42:34 +01:00
Peter Steinberger
07ac8ea4c0
fix: start bun daemon children through nohup 2026-05-04 07:32:27 +01:00
Peter Steinberger
0e50f2b564
feat: add compact list signatures
Co-authored-by: yuhp <yu.haip@gmail.com>
2026-05-04 06:52:34 +01:00
Peter Steinberger
5d8e64d5d5
feat: surface server instructions in list 2026-05-04 06:22:18 +01:00
Peter Steinberger
a64bdda3f7
feat: support per-tool list schemas 2026-05-04 06:19:31 +01:00
Peter Steinberger
d9eda97abe
fix: fail oauth when authorization url is missing 2026-05-04 06:16:48 +01:00
Peter Steinberger
88237703e2
fix: let list reuse cached oauth tokens 2026-05-04 06:00:11 +01:00
Peter Steinberger
c2c256db7e
fix: persist ad-hoc oauth promotion 2026-05-04 05:58:15 +01:00
Peter Steinberger
3f4f8dc317
feat: support ad-hoc HTTP headers 2026-05-04 05:49:25 +01:00
Peter Steinberger
41fa8cb06e
fix: wrap long-flag array values 2026-05-04 05:12:57 +01:00
Peter Steinberger
25e68730a9
fix: restore call long-flag arguments 2026-05-04 05:11:27 +01:00
Peter Steinberger
6e597cf59f
fix: handle CLI generation edge cases 2026-05-04 05:05:12 +01:00
Peter Steinberger
7ffbd52bf7
fix: respect stdio cwd config 2026-05-04 05:05:10 +01:00
Peter Steinberger
7bfc4736aa feat: add per-server tool filtering 2026-04-18 21:52:18 +01:00
Peter Steinberger
34fe35e46c
style: apply oxfmt formatting 2026-04-18 19:33:22 +01:00
Peter Steinberger
efed4d4383
build: migrate lint formatting tooling 2026-04-18 19:33:08 +01:00
Peter Steinberger
4daadfc08f fix: preserve schema string call args 2026-04-18 19:22:38 +01:00
Peter Steinberger
b69dd07dba
test: refresh live deepwiki smoke 2026-03-29 09:40:39 +09:00
Peter Steinberger
d7cfc29bff
fix: reject unknown call flags (#35) 2026-03-28 21:33:15 +00:00
Saatvik Arya
1dc2e70061 feat: add JSON Schema generation for config files
- Add .describe() to Zod schemas for rich documentation
- Create generate-json-schema.ts using Zod v4's toJSONSchema()
- Add generate:schema npm script
- Generate mcporter.schema.json at repo root
- Document $schema usage in docs/config.md
2026-03-03 01:07:35 +00:00