Skip to content

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

@kamilio

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions