/**
 * @fileoverview Unified Context interface for tool and resource handlers.
 * Replaces the split `appContext` (RequestContext) + `sdkContext` (RequestHandlerExtra)
 * pattern with a single object that provides logging, storage, protocol capabilities,
 * and cancellation.
 * @module src/core/context
 */
import { invalidRequest, JsonRpcErrorCode, McpError, } from '../types-global/errors.js';
/**
 * @internal
 *
 * Builds a runtime `fail` function for a given contract. Looks up the entry
 * by `reason` and constructs an `McpError` with the declared code, the
 * caller's message (or the contract's `when` text as a fallback), and
 * `data.reason` auto-populated from the contract — *not* from caller-supplied
 * data, which is spread first and then overwritten so the contract reason
 * wins. This keeps `data.reason` a stable observability identifier.
 *
 * **`data.retryable` precedence.** When the contract entry declares `retryable`,
 * it is used as the base default. Caller-supplied `data.retryable` (per-occurrence
 * override) is then spread on top, so a throw site can still override the
 * contract default for that one occurrence. `reason` is always forced last.
 * Only the `retryable` key is injected when the entry actually declares it —
 * entries without the field produce no `retryable` key on `data`.
 *
 * Net effect: `withRetry`'s default predicate receives `data.retryable === false`
 * automatically for any contract entry that declares `retryable: false`, so
 * deterministic failures fail fast without a per-throw-site flag.
 *
 * If the reason isn't in the contract — a JS caller or stale contract slipping
 * past the `TypedFail` type-system guard — `createFail` returns an
 * `McpError(InternalError)` with diagnostic data (`{ reason, declaredReasons }`)
 * rather than throwing, so the call site can `throw` it like any other error.
 */
export function createFail(errors) {
    const byReason = new Map();
    for (const entry of errors)
        byReason.set(entry.reason, entry);
    return (reason, message, data, options) => {
        const entry = byReason.get(reason);
        if (!entry) {
            // Reason isn't in the contract. The TypedFail type prevents this at
            // compile time, but a JS caller (or a stale contract) can still hit it.
            // Surface the bug clearly rather than silently picking a code.
            return new McpError(JsonRpcErrorCode.InternalError, `ctx.fail() called with unknown reason '${reason}' — not declared in errors[].`, { reason, declaredReasons: [...byReason.keys()] }, options);
        }
        // Precedence (last write wins):
        //   1. contract `retryable` as base default (only when the entry declares it)
        //   2. caller-supplied `data` (per-occurrence override, spread over the default)
        //   3. contract `reason` forced last (never overridable — observability invariant)
        const retryableBase = entry.retryable !== undefined ? { retryable: entry.retryable } : undefined;
        return new McpError(entry.code, message ?? entry.when, { ...retryableBase, ...data, reason }, options);
    };
}
/**
 * Builds a runtime `recoveryFor` resolver for a given contract. Looks up the
 * entry by `reason` and returns the canonical wire shape `{ recovery: { hint } }`,
 * safe to spread into factory `data` or `ctx.fail`'s data argument.
 *
 * Returns `{}` when the reason isn't declared — at runtime this protects JS
 * callers and stale contracts; at compile time `TypedRecoveryFor<R>` constrains
 * the reason to declared values. The `{}` fallback also keeps the bare-Context
 * resolver (no contract attached) safe to spread without optional chaining.
 *
 * @internal
 */
export function createRecoveryFor(errors) {
    const byReason = new Map();
    for (const entry of errors)
        byReason.set(entry.reason, entry);
    return (reason) => {
        const entry = byReason.get(reason);
        if (!entry)
            return {};
        return { recovery: { hint: entry.recovery } };
    };
}
/**
 * Attaches a typed `fail` and `recoveryFor` helper to `ctx` when the definition
 * declares an error contract. The bare `Context.recoveryFor` (always present
 * via `createContext`) is overwritten with a contract-aware resolver. Used by
 * the tool and resource handler factories so all call sites stay identical.
 *
 * @internal
 */
export function attachTypedFail(ctx, errors) {
    if (!errors || errors.length === 0)
        return ctx;
    return Object.assign(ctx, {
        fail: createFail(errors),
        recoveryFor: createRecoveryFor(errors),
    });
}
// ---------------------------------------------------------------------------
// Enrichment runtime — accumulator wiring (internal)
// ---------------------------------------------------------------------------
/**
 * Symbol stashing the per-request {@link EnrichmentStore} on a Context. A symbol
 * key keeps it invisible to `JSON.stringify`, `Object.keys`, and consumers — the
 * store is framework plumbing the handler factory reads, never wire data.
 * @internal
 */
const ENRICHMENT_STORE = Symbol('mcp.enrichmentStore');
/** Fresh, empty enrichment accumulator. @internal */
export function createEnrichmentStore() {
    return { values: {}, kinds: new Map() };
}
/**
 * Builds the always-present `ctx.enrich` callable bound to a store. The bare
 * call merges arbitrary fields; the field-helpers write conventional keys and
 * tag their trailer render kind.
 * @internal
 */
export function createEnrich(store) {
    const enrich = ((fields) => {
        Object.assign(store.values, fields);
    });
    enrich.notice = (text) => {
        store.values.notice = text;
        store.kinds.set('notice', 'notice');
    };
    enrich.total = (count) => {
        store.values.totalCount = count;
        store.kinds.set('totalCount', 'total');
    };
    enrich.echo = (query) => {
        store.values.effectiveQuery = query;
        store.kinds.set('effectiveQuery', 'echo');
    };
    enrich.delta = ({ field, before, after }) => {
        store.values[field] = { before, after };
        store.kinds.set(field, 'delta');
    };
    enrich.truncated = ({ shown, cap, ceiling, guidance }) => {
        store.values.truncated = true;
        store.values.shown = shown;
        store.values.cap = cap;
        if (ceiling !== undefined)
            store.values.truncationCeiling = ceiling;
        // Route through notice (last-wins — callers with multiple notice sources compose one string)
        const text = guidance ??
            `Results capped at ${cap}; showing ${shown}. Raise the cap or narrow with filters.`;
        store.values.notice = text;
        store.kinds.set('notice', 'notice');
    };
    return enrich;
}
/** Stashes the store on a Context under the private symbol (non-enumerable). @internal */
export function stashEnrichmentStore(ctx, store) {
    Object.defineProperty(ctx, ENRICHMENT_STORE, {
        value: store,
        enumerable: false,
        writable: false,
        configurable: false,
    });
}
/**
 * Reads the enrichment store wired by `createContext` (or a test mock). Returns
 * `undefined` when none was stashed — defensive, though every factory-built
 * Context has one.
 * @internal
 */
export function readEnrichmentStore(ctx) {
    return ctx[ENRICHMENT_STORE];
}
// ---------------------------------------------------------------------------
// Content runtime — accumulator wiring (internal)
// ---------------------------------------------------------------------------
/**
 * Symbol stashing the per-request {@link ContentStore} on a Context. Like the
 * enrichment store, a symbol key keeps it invisible to `JSON.stringify`,
 * `Object.keys`, and consumers — framework plumbing the handler factory reads,
 * never wire data.
 * @internal
 */
const CONTENT_STORE = Symbol('mcp.contentStore');
/** Fresh, empty content accumulator. @internal */
export function createContentStore() {
    return { blocks: [] };
}
/**
 * Builds the always-present `ctx.content` callable bound to a store. The bare
 * call pushes a raw block; the helpers push the conventional media blocks.
 * @internal
 */
export function createContentCollect(store) {
    const content = ((block) => {
        store.blocks.push(block);
    });
    content.image = (data, mimeType) => {
        store.blocks.push({ type: 'image', data, mimeType });
    };
    content.audio = (data, mimeType) => {
        store.blocks.push({ type: 'audio', data, mimeType });
    };
    return content;
}
/** Stashes the store on a Context under the private symbol (non-enumerable). @internal */
export function stashContentStore(ctx, store) {
    Object.defineProperty(ctx, CONTENT_STORE, {
        value: store,
        enumerable: false,
        writable: false,
        configurable: false,
    });
}
/**
 * Reads the content store wired by `createContext` (or a test mock). Returns
 * `undefined` when none was stashed — defensive, though every factory-built
 * Context has one.
 * @internal
 */
export function readContentStore(ctx) {
    return ctx[CONTENT_STORE];
}
/**
 * Constructs a Context from internal dependencies.
 * Called by handler factories — not exposed to consumers.
 * @internal
 */
export function createContext(deps) {
    const { appContext, logger: pinoLogger, storage, signal } = deps;
    // Default tenantId to 'default' when no auth pipeline is expected to populate it:
    //   - stdio: single-client by nature, no auth middleware runs
    //   - HTTP + MCP_AUTH_MODE=none: single-tenant by design (auth=none means
    //     "no identity check, sharing is intentional")
    // Preserves fail-closed for HTTP + jwt/oauth: a token missing the `tid` claim
    // must NOT silently share state across distinct authenticated callers.
    const isStdio = process.env.MCP_TRANSPORT_TYPE?.toLowerCase() !== 'http';
    const isAuthDisabled = (process.env.MCP_AUTH_MODE ?? 'none').toLowerCase() === 'none';
    const effectiveContext = appContext.tenantId
        ? appContext
        : isStdio || isAuthDisabled
            ? { ...appContext, tenantId: 'default' }
            : appContext;
    const log = createContextLogger(pinoLogger, effectiveContext);
    const state = createContextState(storage, effectiveContext, signal);
    const progress = deps.taskCtx
        ? createContextProgress(deps.taskCtx.store, deps.taskCtx.taskId)
        : undefined;
    // Per-request enrichment accumulator + always-present `enrich` (mirrors
    // ctx.log/ctx.state — created here, callable from handler and service layer).
    // Stashed on ctx under a private symbol for the handler factory to read after
    // the handler returns; tools with no `enrichment` block accumulate harmlessly
    // (values are stripped by the effective-output parse).
    const enrichmentStore = createEnrichmentStore();
    // Per-request content-block accumulator + always-present `ctx.content`. Blocks
    // collected here are prepended to `content[]` by the handler factory and never
    // reach `structuredContent` (mirrors the enrichment store's stash/read wiring).
    const contentStore = createContentStore();
    const ctx = {
        requestId: effectiveContext.requestId,
        timestamp: effectiveContext.timestamp,
        log,
        state,
        signal,
        tenantId: effectiveContext.tenantId,
        sessionId: deps.sessionId,
        traceId: appContext.traceId,
        spanId: appContext.spanId,
        auth: appContext.auth,
        elicit: deps.elicit,
        notifyPromptListChanged: deps.notifyPromptListChanged,
        notifyResourceListChanged: deps.notifyResourceListChanged,
        notifyResourceUpdated: deps.notifyResourceUpdated,
        notifyToolListChanged: deps.notifyToolListChanged,
        progress,
        uri: deps.uri,
        content: createContentCollect(contentStore),
        enrich: createEnrich(enrichmentStore),
        // No-op resolver for definitions without a contract. `attachTypedFail`
        // overwrites with a contract-aware resolver when `errors[]` is declared.
        // Always-present so service code can spread `...ctx.recoveryFor('x')`
        // without optional chaining, regardless of whether the calling tool
        // declared a contract.
        recoveryFor: () => ({}),
    };
    stashEnrichmentStore(ctx, enrichmentStore);
    stashContentStore(ctx, contentStore);
    return ctx;
}
// ---------------------------------------------------------------------------
// ContextLogger implementation
// ---------------------------------------------------------------------------
function createContextLogger(appLogger, appContext) {
    // Build a RequestContext enriched with extra data for each log call.
    // Our Logger accepts (msg, RequestContext?) — the extra data fields are
    // spread into the context's index signature.
    const enriched = (data) => data ? { ...appContext, ...data } : appContext;
    return {
        debug(msg, data) {
            appLogger.debug(msg, enriched(data));
        },
        info(msg, data) {
            appLogger.info(msg, enriched(data));
        },
        notice(msg, data) {
            appLogger.notice(msg, enriched(data));
        },
        warning(msg, data) {
            appLogger.warning(msg, enriched(data));
        },
        error(msg, error, data) {
            if (error) {
                appLogger.error(msg, error, enriched(data));
            }
            else {
                appLogger.error(msg, enriched(data));
            }
        },
    };
}
// ---------------------------------------------------------------------------
// ContextState implementation
// ---------------------------------------------------------------------------
function createContextState(storage, appContext, signal) {
    const requireContext = () => {
        if (!appContext.tenantId) {
            throw invalidRequest('tenantId required for state operations. With MCP_AUTH_MODE=jwt|oauth, the token must include a "tid" claim. Stdio and HTTP+MCP_AUTH_MODE=none default tenantId to "default" automatically.');
        }
        return appContext;
    };
    return {
        async get(key, schema) {
            signal.throwIfAborted();
            const result = await storage.get(key, requireContext());
            if (result === null || result === undefined)
                return null;
            return schema ? schema.parse(result) : result;
        },
        async set(key, value, opts) {
            signal.throwIfAborted();
            await storage.set(key, value, requireContext(), opts?.ttl !== undefined ? { ttl: opts.ttl } : undefined);
        },
        async delete(key) {
            signal.throwIfAborted();
            await storage.delete(key, requireContext());
        },
        deleteMany(keys) {
            signal.throwIfAborted();
            return storage.deleteMany(keys, requireContext());
        },
        getMany(keys) {
            signal.throwIfAborted();
            return storage.getMany(keys, requireContext());
        },
        async setMany(entries, opts) {
            signal.throwIfAborted();
            await storage.setMany(entries, requireContext(), opts?.ttl !== undefined ? { ttl: opts.ttl } : undefined);
        },
        async list(prefix, opts) {
            signal.throwIfAborted();
            const ctx = requireContext();
            const listOpts = {};
            if (opts?.cursor)
                listOpts.cursor = opts.cursor;
            if (opts?.limit !== undefined)
                listOpts.limit = opts.limit;
            const result = await storage.list(prefix ?? '', ctx, listOpts);
            const keys = result.keys;
            const items = [];
            if (keys.length > 0) {
                // Use pre-fetched values when the provider supplies them, otherwise fetch.
                let values;
                if (result.values) {
                    values = result.values;
                }
                else {
                    signal.throwIfAborted();
                    values = await storage.getMany(keys, ctx);
                }
                for (const key of keys) {
                    const value = values.get(key);
                    if (value !== undefined) {
                        items.push({ key, value });
                    }
                }
            }
            const out = { items };
            if (result.nextCursor)
                out.cursor = result.nextCursor;
            return out;
        },
    };
}
// ---------------------------------------------------------------------------
// ContextProgress implementation
// ---------------------------------------------------------------------------
function createContextProgress(store, taskId) {
    let total = 0;
    let completed = 0;
    return {
        setTotal(n) {
            total = n;
            completed = 0;
            return Promise.resolve();
        },
        async increment(amount = 1) {
            completed = Math.min(completed + amount, total || completed + amount);
            const percentage = total > 0 ? Math.round((completed / total) * 100) : undefined;
            await store.updateTaskStatus(taskId, 'working', percentage !== undefined ? `${percentage}% complete` : undefined);
        },
        async update(message) {
            await store.updateTaskStatus(taskId, 'working', message);
        },
    };
}
//# sourceMappingURL=context.js.map