/** * `@chrischall/mcp-utils/healthcheck` — the credential-style healthcheck * factory. * * It lives in its OWN subpath rather than beside the bridge factory in * `/fetchproxy` because that module imports `@fetchproxy/server`, an optional * peer. Most connectors this helper is for — API-key and OAuth ones like * splitwise, gemini and freshbooks — have no fetchproxy dependency at all, and * importing it from `/fetchproxy` failed at runtime with * `Cannot find package '@fetchproxy/server'`. Nothing here touches fetchproxy. */ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; /** * Ladder arms for {@link registerCredentialHealthcheckTool}. Ordered by the * question each answers: is there a credential at all, did the far side accept * it, and did the round-trip work. */ export type CredentialHealthcheckArm = 'ok' | 'no_credential' | 'credential_rejected' | 'timeout' | 'http' | 'transport' | 'unknown'; /** What a consumer's resolver reports. NEVER the credential value itself. */ export interface CredentialState { /** * Which source supplied the credential — `'env'`, `'fetchproxy'`, `'cache'`, * a connector field name, etc. `null` means nothing resolved, which short- * circuits the probe. */ source: string | null; /** * Non-secret facts worth reporting: age, expiry, account label, which * district was selected. This is echoed into the tool result verbatim, so * it MUST NOT carry the credential, any part of it, or anything that would * identify it beyond a label — a healthcheck is the tool people paste into * chats when something is broken. */ detail?: Record; } /** * Options for {@link registerCredentialHealthcheckTool} — the credential-side * twin of `RegisterBridgeHealthcheckToolArgs` (in `../fetchproxy/`; not an * `{@link}` because this module deliberately cannot import that one). The per-connector bits are * `prefix`, `hostLabel`, the optional `probePath`, and the two functions that * reach the outside world (`resolveCredential`, `probeFn`). */ export interface RegisterCredentialHealthcheckToolArgs { /** * The `McpServer` to register the tool on — the same type * `RegisterBridgeHealthcheckToolArgs` takes. NOT a structural * `{ registerTool }` shape: `McpServer.registerTool` is generic over its * schema arguments, so a loose signature with `config: unknown` is not * assignable from the real method and every caller fails to typecheck. */ server: McpServer; /** Tool-name prefix; the tool is `${prefix}_healthcheck`. */ prefix: string; /** Display host for the probe URL and hint copy, e.g. `'api.freshbooks.com'`. */ hostLabel: string; /** Optional path, for display only: the probe URL is `https://`. */ probePath?: string; /** * Resolve the credential the way the real tools do — same cache, same * fallback order — so a passing healthcheck means real tools work. * * Throwing reports the throw's message and `resolved: false`. The ARM is * `no_credential` by default — a resolver that cannot produce one has * answered the question — but {@link * RegisterCredentialHealthcheckToolArgs.classifyThrown} is consulted first, * so a resolver that failed for some other reason (a bridge that is down, a * rejected password) can say so instead of being told to set variables that * are already set. */ resolveCredential: () => Promise; /** One authenticated round-trip. Only called when a credential resolved. */ probeFn: () => Promise; /** * Classify a thrown error into an arm, and optionally override the hint and * carry structured detail. Consulted for a `probeFn` failure AND for a * `resolveCredential` failure. * * The resolver case is the one worth knowing about: a resolver fails for * reasons that are not "no credential" — a browser bridge that is down, an * upstream that rejected a password, a store that will not decrypt — and * without a classification all of those answer with the `no_credential` * arm's advice, which tells someone to set variables that are already set. * Returning `undefined` (or omitting this) keeps that fallback. * * A classification never changes `credential.resolved`: nothing resolved * either way, and the classification explains why. */ classifyThrown?: (err: unknown) => { kind: string; hint?: string; detail?: Record; } | undefined; /** Per-arm copy overrides. */ hints?: Partial>; } /** * The JSON body `${prefix}_healthcheck` returns for a credential-style * connector, mirroring `BridgeHealthcheckResult`'s envelope: `ok`, the * per-subject block (here `credential` rather than `bridge`), the `probe` * measurements, an optional typed `error`, and always a human-readable `hint`. * * `credential` never carries the credential itself — only the source label and * whatever non-secret `detail` the resolver chose to report. */ export interface CredentialHealthcheckResult { ok: boolean; credential: { source: string | null; resolved: boolean; detail?: Record; }; probe: { url?: string; elapsed_ms: number; status?: number; }; error?: { kind: string; message: string; detail?: Record; }; hint: string; } /** * Register `${prefix}_healthcheck` for a connector whose health is about a * CREDENTIAL rather than a browser bridge — OAuth connectors, API-key * connectors, and the fetchproxy MCPs that only BOOTSTRAP a token and then * talk to an API directly. * * It exists because those three failures are indistinguishable today and have * different fixes: nothing minted a credential, something minted one the far * side rejects, and the far side is simply down. The bridge helper * (`registerBridgeHealthcheckTool`, in `../fetchproxy/`) answers the equivalent question for * MCPs where every request rides the bridge. * * The probe is SKIPPED when no credential resolved — probing without one * produces a 401 that reads like a rejected credential and points at the wrong * fix. */ export declare function registerCredentialHealthcheckTool(args: RegisterCredentialHealthcheckToolArgs): void; //# sourceMappingURL=index.d.ts.map