inspector/clients/web/src/lib/environmentFactory.ts

76 lines
2.7 KiB
TypeScript

import type { InspectorClientEnvironment } from "@inspector/core/mcp/index.js";
import {
createRemoteTransport,
createRemoteFetch,
createRemoteLogger,
} from "@inspector/core/mcp/remote/index.js";
import { BrowserNavigation } from "@inspector/core/auth/browser/index.js";
import type { RedirectUrlProvider } from "@inspector/core/auth/index.js";
import { getWebRemoteOAuthStorage } from "./remoteOAuthStorage.js";
export interface WebEnvironmentResult {
environment: InspectorClientEnvironment;
logger: InspectorClientEnvironment["logger"];
}
/**
* Assemble an `InspectorClientEnvironment` for the browser:
* - transport / fetch / logger all routed through the in-process Hono
* backend at `window.location.origin` (the `clients/web/server`
* dev-backend wires this in `/api/*`).
* - OAuth storage uses `RemoteOAuthStorage` (shared `oauth.json` via
* `/api/storage/oauth`); navigation uses `BrowserNavigation`.
*
* Returns both the assembled environment and the logger so callers can share
* the same pino instance for any direct logging they need to do, instead of
* reaching back through the client.
*
* `authToken` is supplied by `App.tsx` (`getAuthToken()`) and forwarded to
* remote transport/fetch/logger and `getWebRemoteOAuthStorage` so every
* `/api/*` call (including `/api/storage/oauth`) carries `x-mcp-remote-auth`
* when the backend requires it.
*
* `onBeforeOAuthRedirect` runs synchronously immediately before the OAuth
* full-page redirect (see `BrowserNavigation`). The app uses it to flush the
* pre-redirect Network log to backend storage so the auth handshake's first
* half (discovery + Dynamic Client Registration) survives the navigation.
*/
export function createWebEnvironment(
authToken: string | undefined,
redirectUrlProvider: RedirectUrlProvider,
onBeforeOAuthRedirect?: (authorizationUrl: URL) => void,
): WebEnvironmentResult {
const baseUrl = `${window.location.protocol}//${window.location.host}`;
// Passing `window.fetch` directly raises "Illegal invocation" because the
// function loses its `this` binding when extracted off `window`. Wrap so
// the call site preserves the global receiver.
const fetchFn: typeof fetch = (...args) => globalThis.fetch(...args);
const logger = createRemoteLogger({
baseUrl,
authToken,
fetchFn,
});
const environment: InspectorClientEnvironment = {
transport: createRemoteTransport({
baseUrl,
authToken,
fetchFn,
}),
fetch: createRemoteFetch({
baseUrl,
authToken,
fetchFn,
}),
logger,
oauth: {
storage: getWebRemoteOAuthStorage(authToken),
navigation: new BrowserNavigation(undefined, onBeforeOAuthRedirect),
redirectUrlProvider,
},
};
return { environment, logger };
}