Skip to content

toolcraft: allow callers to inject runtime fetch/auth headers without patching globalThis.fetch #413

Description

@kamilio

Problem

While reviewing the OmniVoice Toolcraft CLI, I found a workaround that looks better fixed in Toolcraft/toolcraft-openapi itself.

OmniVoice needs to attach either a LAN-share PIN header (X-OmniVoice-Pin) or a bearer API key to every request sent by both generated OpenAPI commands and handwritten commands. Today the wrapper does this by monkeypatching globalThis.fetch at module load time:

  • OmniVoice-Studio/tools/omnivoice-toolcraft/src/auth.js:8 defines installOmniVoiceFetchAuth(...)
  • auth.js:16 replaces globalThis.fetch
  • auth.js:28 adds X-OmniVoice-Pin
  • auth.js:31 adds Authorization: Bearer ...
  • root.js:14 installs the patch before defineClientFromSpec(...)
  • bin.js:11 / bin.js:18 pass only services into runMCP / runCLI

That works, but it is process-global and order-sensitive. It also makes one client package mutate the fetch behavior of unrelated code in the same Node process.

Why it seems library-level

Toolcraft command handlers already receive ctx.fetch, and toolcraft-openapi runtime commands already call requestJson(..., fetch: ctx.fetch). So the plumbing is almost there.

The blocker is that the runtime entrypoints currently hard-code globalThis.fetch for the built-in handler context while also reserving fetch as a service name:

  • packages/toolcraft/src/cli.ts:72 reserves fetch
  • packages/toolcraft/src/cli.ts:2863 uses fetch: globalThis.fetch in normal runtime
  • packages/toolcraft/src/sdk.ts:21 reserves fetch
  • packages/toolcraft/src/sdk.ts:669 uses fetch: globalThis.fetch
  • packages/toolcraft/src/mcp.ts:34 reserves fetch
  • packages/toolcraft/src/mcp.ts:800 uses fetch: globalThis.fetch

Because fetch is reserved, a consumer cannot pass an authenticated fetch through services. Because the entrypoints do not expose a runtime fetch option, the consumer has to patch the global.

toolcraft-openapi auth is also currently token-oriented: AuthProvider.getToken() feeds Authorization: Bearer <token>. That does not cover APIs that require non-standard headers such as X-OmniVoice-Pin, short-lived request signing, or multiple auth headers.

Requested change

Provide a first-class way for Toolcraft consumers to customize the fetch used in command contexts across CLI, MCP, and SDK.

A minimal shape could be:

await runCLI(root, {
  services,
  fetch: authenticatedFetch
});

await runMCP(root, {
  services,
  fetch: authenticatedFetch
});

const sdk = createSDK(root, {
  services,
  fetch: authenticatedFetch
});

Then handlers and generated OpenAPI commands would see that as ctx.fetch. fetch can remain a reserved service name; it would just become an explicit runtime option rather than something consumers smuggle through services.

A complementary toolcraft-openapi improvement would be an auth/header provider abstraction, for example an auth provider method that can contribute request headers instead of only returning a bearer token. But exposing a runtime fetch option in Toolcraft core is enough to remove OmniVoice's global patch.

Acceptance criteria

  • CLI, MCP, and SDK entrypoints accept an optional runtime fetch implementation.
  • ctx.fetch uses the supplied fetch when provided, and defaults to globalThis.fetch otherwise.
  • Existing fixture/runtime fetch behavior keeps working.
  • toolcraft-openapi generated runtime commands automatically use the injected fetch because they already consume ctx.fetch.
  • Consumers can implement origin-scoped header injection without mutating globalThis.fetch.

Context from OmniVoice

Current workaround:

const originalFetch = globalThis.fetch;
globalThis.fetch = async (input, init = {}) => {
  const requestUrl = new URL(
    typeof input === "string" || input instanceof URL ? input : input.url,
    baseUrl
  );
  if (requestUrl.origin !== targetOrigin) {
    return originalFetch(input, init);
  }

  const headers = new Headers(init.headers || ...);
  if (pin && !headers.has("X-OmniVoice-Pin")) {
    headers.set("X-OmniVoice-Pin", pin);
  }
  if (apiKey && !headers.has("Authorization")) {
    headers.set("Authorization", `Bearer ${apiKey}`);
  }
  return originalFetch(input, { ...init, headers });
};

This would become a local authenticatedFetch passed to Toolcraft entrypoints instead of a global mutation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions