UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

1,858 lines • 93.8 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved.
import { WorksTools } from "./works.js";
import { PeopleTools } from "./people.js";
import { ApprovalsTools } from "./approvals.js";
import { GroupsTools } from "./groups.js";
import { ShareSendTools } from "./share-send.js";
import { RecordingsTools } from "./recordings.js";
import { SearchTools } from "./search.js";
import { LicensingTools } from "./licensing.js";
import { BookingsTools } from "./bookings.js";
import { ShowsTools } from "./shows.js";
import { CreditsTools } from "./credits.js";
import { AudioFilesTools } from "./audio-files.js";
import { MultimediaTools } from "./multimedia.js";
import { AgreementsTools } from "./agreements.js";
import { MemoryTools } from "./memory.js";
import { SkillsTools } from "./skills.js";
import { EnrichmentTools } from "./enrichment.js";
import { BulkTools } from "./bulk.js";
import { ExportTools } from "./exports.js";
import { DuplicatesTools, coerceMergeEntityType } from "./duplicates.js";
import { DirectoryTools } from "./directory.js";
import { CollaboratorsTools } from "./collaborators.js";
import { CollaborationsTools } from "./collaborations.js";
import { UsersTools } from "./users.js";
import { ImportDocumentTools } from "./import-documents.js";
import { UploadTools } from "./uploads.js";
import { FilesTools } from "./files.js";
import { ReleaseNotesTools } from "./release-notes.js";
import { DocumentTools } from "./documents.js";
import { ImportTools } from "./import.js";
import { SendTools } from "./send.js";
import { ComparisonTools } from "./comparisons.js";
import { AssetsTools } from "./assets.js";
import { SessionsTools } from "./sessions.js";
import { AnalyticsTools } from "./analytics.js";
import { NotificationsTools } from "./notifications.js";
import { NotesTools } from "./notes.js";
import { ProjectsTools } from "./projects.js";
import { ReleasesTools } from "./releases.js";
import { ReleaseRichTools } from "./release-rich.js";
import { CalendarTools } from "./calendar.js";
import { SplitSheetsTools } from "./split-sheets.js";
import { AgreementTypesTools } from "./agreement-types.js";
import { expandAliases, getAliasFor, isDeprecatedMeta, TOOL_ALIASES, } from "./deprecations.js";
import { DashboardTools } from "./dashboard.js";
import { SettingsTools } from "./settings.js";
import { FeedbackTools } from "./feedback.js";
import { StorageConfigTools } from "./storage-config.js";
import { SubscriptionTools } from "./subscription.js";
import { OnboardingTools } from "./onboarding.js";
import { AgentHygieneTools } from "./agent-hygiene.js";
import { ReportIssueTools } from "./report-issue.js";
import { MyReportedIssuesTools } from "./my-reported-issues.js";
import { MyRecentQuestionsTools } from "./my-recent-questions.js";
import { AgentIdentityTools } from "./agent-identity.js";
import { RoyaltiesTools } from "./royalties.js";
import { RightsTools } from "./rights.js";
import { StatementsTools } from "./statements.js";
import { SelectionsTools } from "./selections.js";
import { ShareLinksTools } from "./share-links.js";
import { SharingTools } from "./sharing.js";
import { ExplainabilityTools } from "./explainability.js";
import { AccessSimulateTools } from "./access-simulate.js";
import { DisputesTools } from "./disputes.js";
import { CustodyTools } from "./custody.js";
import { ConsentTools } from "./consent.js";
import { IntegrityTools } from "./integrity.js";
import { RecordingCustodyTools } from "./recording-custody.js";
import { RecordingSamplesTools } from "./recording-samples.js";
import { RegisterTools } from "./register.js";
import { PurchasesTools } from "./purchases.js";
import { TelegramTools } from "./telegram.js";
import { PublishersTools } from "./publishers.js";
import { LabelsTools } from "./labels.js";
import { PartyClaimsTools } from "./party-claims.js";
import { ProfileClaimsTools } from "./profile-claims.js";
import { AuthTools } from "./auth.js";
import { SignupTools } from "./signup.js";
import { AppTools } from "./app-tools.js";
import { AuditTools } from "./audit.js";
import { DiscoveryTools } from "./discovery.js";
import { SyncPlacementsTools } from "./sync-placements.js";
import { readCredentials } from "@withpica/mcp-utils";
import { formatError, ToolExecutionError } from "@withpica/mcp-utils";
import { guardToolResult } from "@withpica/mcp-utils";
import { narrowToolResultForRefs } from "@withpica/mcp-utils";
import { getToolMetadata } from "./metadata.js";
import { getRecoveryHint } from "./recovery-hints.js";
import { generateConfirmationToken, validateAndConsumeToken, } from "@withpica/mcp-utils";
import { rejectUnknownSections } from "./section-validation.js";
import { countSignedSplitSheets, countSignedSplitSheetsForWorks, signedSplitSheetLineForWork, signedSplitSheetLineForWorks, } from "./signed-split-sheet-preview.js";
import { buildSessionState, incrementSessionMutations, resetSinceLastBriefing, } from "@withpica/mcp-utils";
/**
 * ADR-230 — derive the MCP-standard 3-value risk classification from the
 * declared tier. Sourced once at registration and at audit-write time so
 * the contract that hints, audit row, and confirmation requirement all
 * agree is mechanical.
 */
export function tierToRiskLevel(tier) {
    switch (tier) {
        case "read":
            return "safe";
        case "draft":
        case "write":
            return "mutating";
        case "destructive":
            return "destructive";
    }
}
/**
 * ADR-230 — derive the required API-key scope from the declared tier.
 *
 *  - `read`        → `read:<resource>` (write:* satisfies via `hasScope`).
 *  - `draft|write` → `write:<resource>`.
 *  - `destructive` → `destructive:*` (admin satisfies via `hasScope`); the
 *                    write:<resource> scope is also required for the
 *                    underlying write surface.
 *
 * Returns the *additional* scope tier beyond per-resource. Resource scope
 * is composed at call-site by `lib/services/mcp-scopes.ts`.
 */
export function tierToScopeKind(tier) {
    switch (tier) {
        case "read":
            return "read";
        case "draft":
        case "write":
            return "write";
        case "destructive":
            return "destructive";
    }
}
/**
 * Build a tool description with metadata injected.
 * Format: "[category] original description"
 */
export function injectMetadataIntoDescription(description, metadata) {
    // Category tag helps the agent understand tool grouping.
    // Risk prefixes removed — annotations (readOnlyHint, destructiveHint) handle this structurally.
    // Behavioral guidance (tell user what you're doing) is in server instructions (one copy).
    return `[${metadata.category}] ${description}`;
}
export const CATEGORY_DISPLAY = {
    catalog: "manage your catalog",
    enrichment: "enrich your metadata",
    business: "handle business and agreements",
    discovery: "search and explore",
    media: "manage photos, audio, and video",
    comms: "send and share",
    settings: "manage your account and team",
};
/**
 * Lift error attribution (code + message) off an `isError` ToolResult for
 * audit telemetry. Prefers `structuredContent.error_code`/`error_message`;
 * falls back to parsing the first text content item as a JSON error
 * envelope (`error_code`/`error` + `error_message`/`message`) — the shape
 * the pica_execute dispatcher and fail-quiet wrappers return. Without the
 * fallback those errors landed in mcp telemetry with a NULL code, so the
 * ops_issues pipeline (which groups on a code) never saw them — 26 opaque
 * ChatGPT dispatcher failures in 14 days, invisible to triage. Exported
 * for unit testing.
 */
export function extractErrorAttribution(result) {
    let code;
    let message;
    const sc = result?.structuredContent;
    if (sc && typeof sc === "object") {
        if (typeof sc.error_code === "string")
            code = sc.error_code;
        if (typeof sc.error_message === "string")
            message = sc.error_message;
    }
    if (!code) {
        const firstText = result?.content?.find((c) => c.type === "text" && typeof c.text === "string")?.text;
        if (firstText && firstText.trimStart().startsWith("{")) {
            try {
                const envelope = JSON.parse(firstText);
                const envCode = envelope.error_code ?? envelope.error;
                if (typeof envCode === "string")
                    code = envCode;
                const envMessage = envelope.error_message ?? envelope.message;
                if (!message && typeof envMessage === "string")
                    message = envMessage;
            }
            catch {
                // Not a JSON envelope — the raw text is still what the agent saw;
                // captured below instead of leaving the row message-less.
            }
        }
    }
    // Last resort (excellence agent-surface audit 2026-07-16): 7 external
    // execute-path errors landed with NULL error_message — whatever the agent
    // saw, ops couldn't. If attribution found no structured message, stamp
    // the first text content (truncated) so telemetry always carries the
    // agent-visible error.
    if (!message) {
        const rawText = result?.content?.find((c) => c.type === "text" && typeof c.text === "string")?.text;
        if (rawText)
            message = rawText.slice(0, 500);
    }
    return { code, message };
}
/**
 * Sanitize tool parameters for `mcp_audit_log.parameters`. Pure function
 * so it's directly testable; the registry's private method delegates here.
 *
 * Security audit 2026-05-11 P2. The audit log is queryable by anyone with
 * team-portal access. Tools like `pica_sign_in`, `pica_share_links_*`,
 * and `team_comms_send` previously landed bare emails / share tokens /
 * OAuth refresh tokens in the log because the only redaction was a
 * `delete sanitized.confirmation_token` + length truncation.
 *
 * Deny-list is keyed off the param name (case-insensitive substring
 * match) — covers `password`, `api_key`, `token`, `secret`,
 * `authorization`, plus `confirmation_token` via the `token` substring.
 * Sister keys like `refresh_token` / `id_token` / `access_token` /
 * `share_token` / `client_secret` also match.
 *
 * Email values (regardless of param key) get the local-part masked to
 * the first two characters: `jane@example.com` → `ja***@example.com`.
 * Locals ≤ 2 chars become all-asterisk.
 *
 * Strings > 500 chars are truncated to 500 + `...[truncated]`.
 *
 * Exported under `_internal` so test files can exercise the function
 * without instantiating a `ToolRegistry`.
 */
function sanitizeAuditParams(args) {
    const sanitized = { ...args };
    const denyKeySubstrings = [
        "password",
        "api_key",
        "token",
        "secret",
        "authorization",
    ];
    for (const [key, value] of Object.entries(sanitized)) {
        const lowerKey = key.toLowerCase();
        if (denyKeySubstrings.some((needle) => lowerKey.includes(needle))) {
            sanitized[key] = "[redacted]";
            continue;
        }
        if (typeof value === "string" &&
            /^[^\s@]{1,64}@[^\s@.]+(?:\.[^\s@.]+)+$/.test(value)) {
            const atIdx = value.indexOf("@");
            const local = value.slice(0, atIdx);
            const domain = value.slice(atIdx);
            const maskedLocal = local.length <= 2
                ? "*".repeat(local.length)
                : `${local.slice(0, 2)}***`;
            sanitized[key] = `${maskedLocal}${domain}`;
            continue;
        }
        if (typeof value === "string" && value.length > 500) {
            sanitized[key] = value.substring(0, 500) + "...[truncated]";
        }
    }
    return sanitized;
}
export const _internal = { sanitizeAuditParams };
/**
 * Authoring metadata that must never cross the wire.
 *
 * `ToolDefinition` is two things wearing one type: the MCP `Tool` a client
 * consumes, and the compile-time declarations our own lint reads. `listTools()`
 * spreads `...definition` three times, so every field on it — including the
 * second kind — was being published to every connected agent.
 *
 * `vernacular_reason` is the reason this exists. It is prose written for us,
 * and on ~30 tools it read "Specialist context; not yet covered by the Creator
 * Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4
 * substrate writes." No exported surface names an ADR: an ADR number is an
 * internal coordinate, it means nothing to the reader, and it invites an agent
 * to repeat it back to a user as if it were vocabulary they should know.
 *
 * Nothing downstream reads these off the wire — `scripts/lint-mcp-tools.ts`
 * parses source, and every test reads `tool.definition` from the registry
 * directly. `annotationOverrides` is dropped for a plainer reason: it has
 * already been merged into `annotations` by the time we get here, so on the
 * wire it is a duplicate of a field the client is also being handed.
 *
 * Deliberately NOT stripped: `tier`, `scope`, `audiences`, `workflows`. Those
 * are short machine slugs a client can act on, not prose about our roadmap.
 */
const INTERNAL_ONLY_FIELDS = [
    "vernacular_kind",
    "vernacular_reason",
    "annotationOverrides",
];
/** Drop authoring-only fields from a definition on its way to a client. */
function toWireDefinition(definition) {
    const wire = { ...definition };
    for (const field of INTERNAL_ONLY_FIELDS)
        delete wire[field];
    return wire;
}
/**
 * Tools whose effect leaves PICA — email, invites, Telegram, the send hub.
 * Pattern-derived so a new tool inherits the classification without an edit.
 * Feeds BOTH the `createsExternalSideEffects` extended annotation and the
 * MCP-spec `openWorldHint`, which is the same claim in the spec's words.
 */
const EXTERNAL_SIDE_EFFECT_PATTERNS = [
    "telegram",
    "_send_",
    "_send",
    "send_message",
    "send_resend",
    "team_comms_send",
    "_notify_user",
    "_invite",
];
function hasExternalSideEffects(toolName) {
    return EXTERNAL_SIDE_EFFECT_PATTERNS.some((p) => toolName.includes(p));
}
/**
 * The four MCP-spec annotation hints plus `title`, derived from one place.
 *
 * Before this existed the derivation lived inline in `listTools()`, so the
 * layered-discovery surface (`pica_tool_details` → `buildToolDetail` in
 * discovery.ts) fell back to the tool's *declared* `annotations` — which most
 * tools never declare, so it answered `annotations: null` for the large
 * majority of the registry while the flat surface answered real hints for the
 * same tool (measured 2026-08-24). Connector clients only ever see the layered
 * surface, so they could not tell a delete from a read.
 *
 * ADR-230 — declared `tier` is the source of truth for the two hints that
 * describe AUTHORITY. Declared `annotations.readOnlyHint` /
 * `annotations.destructiveHint` are NOT read: they would let a declaration
 * contradict the tier that actually drives the confirmation token and the
 * `destructive:*` scope. `metadata.risk` is a quiet fallback for a tool that
 * somehow bypassed lint Rule 13.
 *
 * Two hints a declaration CAN set, because neither describes authority and
 * nothing enforces against either:
 *
 * - `openWorldHint` — a declaration may RAISE it (never lower it); the name
 *   patterns in `EXTERNAL_SIDE_EFFECT_PATTERNS` raise it on their own.
 * - `idempotentHint` — a declared value WINS outright (ruling 2026-08-24).
 *   The tier-derived value is a guess from `TOOL_METADATA.retry_safe` plus
 *   "destructive is never idempotent", and it was wrong on four tools whose
 *   authors had said otherwise in the definition — `pica_storage_disconnect`
 *   is destructive-tier and its own description reads "Idempotent — calling
 *   on an already-PICA-managed org is a no-op". The author knows; the
 *   heuristic does not.
 *
 * `annotationOverrides` still merges last, so it beats all of the above.
 *
 * Agreement between the two surfaces is pinned by "listTools() and
 * pica_tool_details agree on the four core hints" in
 * `src/__tests__/tools/tool-details-contract.test.ts`; the precedence rules
 * themselves are pinned by the synthetic-definition cases in the same file.
 * `scripts/ci/check-agent-readability.mjs` (`tier-annotation-mismatch`)
 * applies the identical rule when it looks for declarations the derivation
 * silently discards.
 */
export function deriveAnnotations(definition) {
    const metadata = getToolMetadata(definition.name);
    const declaredTier = definition.tier;
    const openWorldHint = hasExternalSideEffects(definition.name) ||
        definition.annotations?.openWorldHint === true;
    // Advisory-hint overrides (the pica_execute dispatcher is the only current
    // user) merged last so they win over the tier-derived hints.
    const overrides = definition.annotationOverrides ?? {};
    // A declared idempotentHint wins over the tier-derived guess (see header).
    // `{}` when undeclared, so the spread is a no-op rather than an `undefined`
    // that would clobber the computed value.
    const declaredIdempotent = typeof definition.annotations?.idempotentHint === "boolean"
        ? { idempotentHint: definition.annotations.idempotentHint }
        : {};
    if (declaredTier && metadata) {
        return {
            title: metadata.display_name,
            readOnlyHint: declaredTier === "read",
            destructiveHint: declaredTier === "destructive",
            idempotentHint: declaredTier !== "destructive" && metadata.retry_safe,
            openWorldHint,
            ...declaredIdempotent,
            ...overrides,
        };
    }
    if (metadata) {
        return {
            title: metadata.display_name,
            readOnlyHint: metadata.risk === "safe",
            destructiveHint: metadata.risk === "destructive",
            idempotentHint: metadata.risk !== "destructive" && metadata.retry_safe,
            openWorldHint,
            ...declaredIdempotent,
            ...overrides,
        };
    }
    // No TOOL_METADATA entry — the lobby meta-tools (sign_in/out, discover,
    // tool_details, execute). Source from declared tier; default to read when
    // tier is somehow missing (lint blocks; defensive only).
    const tierForHints = declaredTier ?? "read";
    return {
        title: definition.name,
        readOnlyHint: tierForHints === "read",
        destructiveHint: tierForHints === "destructive",
        idempotentHint: false,
        openWorldHint,
        ...declaredIdempotent,
        ...overrides,
    };
}
export class ToolRegistry {
    tools;
    pica;
    config;
    reinitializeCallback;
    signOutCallback;
    auditLogger;
    callerContext;
    /** Caller-specific "can run it here" judgement — see setToolVisibility. */
    toolVisibility;
    constructor(pica, config, reinitializeCallback, callerContext, signOutCallback) {
        this.pica = pica;
        this.config = config;
        this.reinitializeCallback = reinitializeCallback;
        this.signOutCallback = signOutCallback;
        this.callerContext = callerContext ?? {
            callerIdentity: "unknown",
            transport: "stdio",
        };
        this.tools = new Map();
        this.registerAllTools();
    }
    setAuditLogger(logger) {
        this.auditLogger = logger;
    }
    setCallerContext(context) {
        this.callerContext = context;
    }
    /**
     * Install the caller's "can run it on this connection" judgement so
     * discovery ranks and flags with the predicate the dispatcher enforces.
     * The HTTP route passes `allowedToolNames.has` (ADR-248: audiences ∋ http
     * AND scope held) — and only while `isInnerGateEnforced()`
     * (lib/services/mcp-inner-gate-shadow.ts) is true, because in
     * `PICA_EXECUTE_INNER_GATE_MODE=shadow` pica_execute lets a scope-gated
     * inner tool run and a "not callable" flag would then be wrong in the
     * worst direction. Without a predicate, `isToolVisibleOnSurface` falls
     * back to audience only: a tool is visible when its declared audiences
     * include the caller's surface (lobby-mode → "lobby", else the
     * transport). 2026-08-26: a ChatGPT caller was ranked five finance-gated
     * tools first and read one's schema three times before leaving.
     */
    setToolVisibility(predicate) {
        this.toolVisibility = predicate;
    }
    isToolVisibleOnSurface(name) {
        if (this.toolVisibility)
            return this.toolVisibility(name);
        const def = this.tools.get(name)?.definition;
        if (!def)
            return false;
        const surface = this.config?.lobbyMode
            ? "lobby"
            : this.callerContext.transport;
        return def.audiences?.includes(surface) ?? false;
    }
    /**
     * Read clientInfo from the MCP `initialize` handshake. SDK populates this
     * on the per-request `Server` for HTTP and on the long-lived `Server` for
     * stdio; either way getClientVersion() returns the same shape after the
     * handshake completes. Returns empty when ctx is absent or the handshake
     * hasn't run (lobby-mode dispatches that bypass the per-request transport).
     *
     * Stamped onto audit rows as provenance only — never used as a permission
     * boundary, since clientInfo is self-declared by the client.
     */
    extractClientInfo(ctx) {
        const info = ctx?.server?.getClientVersion?.();
        if (!info)
            return {};
        return {
            client_name: typeof info.name === "string" ? info.name : undefined,
            client_version: typeof info.version === "string" ? info.version : undefined,
        };
    }
    /**
     * Register all available tools
     */
    registerAllTools() {
        // Auth tools — always registered (lobby + authenticated mode)
        if (this.config) {
            const authTools = new AuthTools(this.config, this.reinitializeCallback || (() => { }), this.signOutCallback || (() => { }));
            authTools.getTools().forEach((tool) => {
                this.tools.set(tool.definition.name, tool);
            });
            // ADR-211 P1 — Signup tool. Registered in lobby alongside the auth
            // pair so unauthenticated callers (stdio without API key, HTTP
            // without Bearer) have a discoverable signup verb. Stays registered
            // post-auth too — the tool itself is harmless once you're signed in
            // (mints a link for a new account, not your existing one) and that
            // matches the ADR-211 § Primitive A 2026-04-30 amendment which
            // makes Phase 1 lobby additive (3 tools), not replacement.
            //
            // Transport is read from callerContext so stdio + HTTP transports
            // stamp their JWTs correctly without per-call config plumbing.
            const signupTools = new SignupTools({
                apiUrl: this.config.picaApiUrl,
                transport: this.callerContext.transport,
            });
            signupTools.getTools().forEach((tool) => {
                this.tools.set(tool.definition.name, tool);
            });
        }
        // In lobby mode, only auth + signup tools are available
        if (this.config?.lobbyMode || !this.pica) {
            return;
        }
        const pica = this.pica;
        // Works tools
        const worksTools = new WorksTools(pica);
        worksTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // People tools
        const peopleTools = new PeopleTools(pica);
        peopleTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Approvals tools — pica_approval_status (human-in-the-loop poll)
        const approvalsTools = new ApprovalsTools(pica);
        approvalsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Groups tools (ADR-223)
        const groupsTools = new GroupsTools(pica);
        groupsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Share-send tool (ADR-231)
        const shareSendTools = new ShareSendTools(pica);
        shareSendTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Recordings tools
        const recordingsTools = new RecordingsTools(pica);
        recordingsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Search tools
        const searchTools = new SearchTools(pica);
        searchTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Licensing tools
        const licensingTools = new LicensingTools(pica);
        licensingTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Bookings tools (ADR-289 Wave C1) — internal-team booking enquiries
        const bookingsTools = new BookingsTools(pica);
        bookingsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Shows tools (ADR-313) — live shows and tour credits
        const showsTools = new ShowsTools(pica);
        showsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Credits tools
        const creditsTools = new CreditsTools(pica);
        creditsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Audio Files tools
        const audioFilesTools = new AudioFilesTools(pica);
        audioFilesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Multimedia tools
        const multimediaTools = new MultimediaTools(pica);
        multimediaTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Agreements tools
        const agreementsTools = new AgreementsTools(pica);
        agreementsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Memory tools
        const memoryTools = new MemoryTools(pica);
        memoryTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Skills tools (ADR-140 Phase 2b — SEP-2640 bridge tool lane)
        // ADR-271 Phase 2: pass org-authored extra skills from caller context
        const skillsTools = new SkillsTools(this.callerContext?.extraSkills ?? []);
        skillsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Enrichment tools
        const enrichmentTools = new EnrichmentTools(pica);
        enrichmentTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Bulk operations tools
        const bulkTools = new BulkTools(pica);
        bulkTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Export tools
        const exportTools = new ExportTools(pica);
        exportTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Duplicates tools
        const duplicatesTools = new DuplicatesTools(pica);
        duplicatesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Directory tools
        const directoryTools = new DirectoryTools(pica);
        directoryTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Collaborator tools
        const collaboratorsTools = new CollaboratorsTools(pica);
        collaboratorsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // ADR-236 — cross-org credit visibility for recipients
        const collaborationsTools = new CollaborationsTools(pica);
        collaborationsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // ADR-237 — opt-in public handles + bio + avatar (cross-org discovery)
        const usersTools = new UsersTools(pica);
        usersTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Upload tools (generic file upload — images, videos, documents)
        const uploadTools = new UploadTools(pica);
        uploadTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // File delivery tools — outbound sharing with recipient accountability
        // and revocation. Wraps /admin/files/deliver + /deliver/[id]/revoke.
        const filesTools = new FilesTools(pica);
        filesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Release notes — agent-facing platform changelog. Reads CHANGELOG.md
        // embedded at build time. Disambiguated from catalog "what's new" by
        // name + description NOT-FOR disclaimer.
        const releaseNotesTools = new ReleaseNotesTools(pica);
        releaseNotesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Document tools (AI analysis)
        const documentTools = new DocumentTools(pica);
        documentTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Bulk import tools (CSV import pipeline)
        const importTools = new ImportTools(pica);
        importTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Comparison tools (ADR-116 Phase 2: enrichment + registration comparison)
        const comparisonTools = new ComparisonTools(pica);
        comparisonTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Send Hub tools (messages, invites, file requests, audio shares)
        const sendTools = new SendTools(pica);
        sendTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Import document tools
        const importDocumentTools = new ImportDocumentTools(pica);
        importDocumentTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Physical assets tools (equipment, instruments, studio gear)
        const assetsTools = new AssetsTools(pica);
        assetsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Work sessions tools (studio session tracking)
        const sessionsTools = new SessionsTools(pica);
        sessionsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Analytics tools (carbon, provenance, diligence)
        const analyticsTools = new AnalyticsTools(pica);
        analyticsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Notifications tools
        const notificationsTools = new NotificationsTools(pica);
        notificationsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Notes tools (catalog notes on works, people, general observations)
        const notesTools = new NotesTools(pica);
        notesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Team management tools removed 2026-06-11 (founder decision, ADR-248):
        // /api/admin/team/* is session-only by design — every bearer/MCP caller
        // received a hard 401. Team membership stays human-only via /settings.
        // Projects tools (albums, EPs, compilations — grouping works)
        const projectsTools = new ProjectsTools(pica);
        projectsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Releases tools (albums, EPs, singles with dates, labels, UPCs)
        const releasesTools = new ReleasesTools(pica);
        releasesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // ADR-208 Phase 3 Primitive D — release rich render (image + markdown)
        const releaseRichTools = new ReleaseRichTools(pica);
        releaseRichTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Calendar tools
        const calendarTools = new CalendarTools(pica);
        calendarTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Split sheets & recording splits tools
        const splitSheetsTools = new SplitSheetsTools(pica);
        splitSheetsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Agreement types tools (templates, producer agreements, work-for-hire)
        const agreementTypesTools = new AgreementTypesTools(pica);
        agreementTypesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Dashboard & discovery tools
        const dashboardTools = new DashboardTools(pica);
        dashboardTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Settings tools (credits history, storage, org profile)
        const settingsTools = new SettingsTools(pica);
        settingsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Feedback tools (ADR-184 slice 8 — pica_submit_feedback)
        const feedbackTools = new FeedbackTools(pica);
        feedbackTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Storage config tools (ADR-217 — Primitives B + C; Primitive A
        // pica_storage_status lives in settings.ts because it shipped earlier
        // alongside the original settings cluster).
        const storageConfigTools = new StorageConfigTools(pica);
        storageConfigTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Subscription tools — the three this class registers:
        // pica_subscription_status and pica_subscription_manage (ADR-210 Phase 2:
        // the explicit billing read + action surface complementing the ambient
        // _meta.session_state.billing_slice from Phase 1), plus pica_billing_pay
        // (spec 2026-08-24 WS-B: mints a pay link for what is owed).
        const subscriptionTools = new SubscriptionTools(pica);
        subscriptionTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Onboarding tools — pica_acknowledge_onboarding gates OAuth-arrived
        // users until the agent walks them through identity/security/intent.
        const onboardingTools = new OnboardingTools(pica);
        onboardingTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Agent hygiene tools — pica_introduce_self gates OAuth-Bearer +
        // raw-API-key callers until they declare identity (ADR-247 PR (a)).
        // Cookie + grant callers are carved out at the router level.
        const agentHygieneTools = new AgentHygieneTools(pica);
        agentHygieneTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Report-issue tool (ADR-196 Phase 3 — pica_report_issue)
        const reportIssueTools = new ReportIssueTools(pica);
        reportIssueTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // My-reported-issues tool (ADR-196 Phase 4a — pica_my_reported_issues)
        // — closed-loop counterpart to pica_report_issue. Org-scoped polling.
        const myReportedIssuesTools = new MyReportedIssuesTools(pica);
        myReportedIssuesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // My-recent-questions tool (ADR-226 Phase 5 — pica_my_recent_questions)
        // — user-scoped read of the Phase 4 substrate (assistant_interactions
        // joined with mcp_sessions for client identity). Atlas-resolved entry
        // for "what did I ask the AI?".
        //
        // ADR-230 post-ship cleanup — the tier resolver is the registry's
        // own `getToolTier` shape (declared `tier` on each tool definition).
        // Lint Rule 13 makes tier required for every customer MCP tool, so
        // this resolver returns a populated value for all known tools and
        // `undefined` only for unknown / meta tools — `dominantTier` falls
        // back to "read" in that case.
        const myRecentQuestionsTools = new MyRecentQuestionsTools(pica, (name) => this.tools.get(name)?.definition.tier);
        myRecentQuestionsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Agent identity tools (ADR-185 Part 1 — session-auth-only MCP surface).
        // The three tools refuse grant-auth callers at the HTTP API layer;
        // the stdio path carries an API key so behaviour is identical either
        // way.
        const agentIdentityTools = new AgentIdentityTools(pica);
        agentIdentityTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Royalties & statements tools (ADR-139 Step 2)
        const royaltiesTools = new RoyaltiesTools(pica);
        royaltiesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Programmable rights (ADR-321 §9) — agents propose, humans confirm.
        const rightsTools = new RightsTools(pica);
        rightsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Statements ingestion tools (ADR-277 — agent-driven royalty statement ingest)
        const statementsTools = new StatementsTools(pica);
        statementsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Saved catalog selections tools (ADR-319)
        const selectionsTools = new SelectionsTools(pica);
        selectionsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Share links tools (ADR-139 Step 3)
        const shareLinksTools = new ShareLinksTools(pica);
        shareLinksTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Sharing trace tool (ADR-232 § Decision 2)
        const sharingTools = new SharingTools(pica);
        sharingTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Explainability tools (ADR-232 § Decision 3)
        const explainabilityTools = new ExplainabilityTools(pica);
        explainabilityTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Access simulator — read-only "who would see what?" preview.
        const accessSimulateTools = new AccessSimulateTools(pica);
        accessSimulateTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Disputes tools (ADR-139 Step 3)
        const disputesTools = new DisputesTools(pica);
        disputesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Custody tools (ADR-158 Plan C)
        const custodyTools = new CustodyTools(pica);
        custodyTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Consent readiness (ADR-300 — the read side of ADR-292's ledger).
        // Read-only: consent writes stay human-session only.
        const consentTools = new ConsentTools(pica);
        consentTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Registration integrity floor waive tool (spec 2026-07-09, Task 10 —
        // flagged spec deviation, the one new tool the floor program ships).
        const integrityTools = new IntegrityTools(pica);
        integrityTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Recording custody tools (ADR-262 — master-side control chain)
        const recordingCustodyTools = new RecordingCustodyTools(pica);
        recordingCustodyTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Recording sample provenance tools (ADR-259 — marketplace samples)
        const recordingSamplesTools = new RecordingSamplesTools(pica);
        recordingSamplesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Register-set tool (ADR-259 — one-call DAW set registration)
        const registerTools = new RegisterTools(pica);
        registerTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Purchases tools (credit checkout)
        const purchasesTools = new PurchasesTools(pica);
        purchasesTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Telegram tools (user notification channel)
        const telegramTools = new TelegramTools(pica);
        telegramTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Publishers tools (lookup + create)
        const publishersTools = new PublishersTools(pica);
        publishersTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Labels tools (read-only lookup; ADR-174 Decision 3)
        const labelsTools = new LabelsTools(pica);
        labelsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Party claims tools (ADR-268 WS1 — claim flow for unclaimed party orgs)
        const partyClaimsTools = new PartyClaimsTools(pica);
        partyClaimsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Profile claims tools (claim your profile: invite with a preview, check)
        const profileClaimsTools = new ProfileClaimsTools(pica);
        profileClaimsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // App tools (MCP Apps — interactive UI cards)
        const appTools = new AppTools(pica);
        appTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Audit tools (recent MCP tool executions — ADR-154 F6)
        // ADR-181 Phase 1: ported from the (now-retired)
        // @withpica/mcp-server-settings sub-server as part of the customer
        // MCP surface consolidation. The sub-server was the only place this
        // tool shipped before consolidation; every other sub-server tool
        // already existed in mcp-server/ so only pica_audit_list had to move.
        // Phase 3 (2026-04-19) tombstoned the sub-package on npm + registry
        // and deleted the source tree.
        const auditTools = new AuditTools(pica);
        auditTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // ADR-222 sync placements
        const syncPlacementsTools = new SyncPlacementsTools(pica);
        syncPlacementsTools.getTools().forEach((tool) => {
            this.tools.set(tool.definition.name, tool);
        });
        // Discovery meta-tools — registered last so pica_tool_details can look up
        // all tool definitions. Only active when discoveryMode is enabled.
        if (this.config?.discoveryMode) {
            const discoveryTools = new DiscoveryTools((name, args, ctx) => this.executeTool(name, args, ctx), (name) => this.tools.get(name), () => [...this.tools.keys()], (name) => this.isToolVisibleOnSurface(name));
            discoveryTools.getTools().forEach((tool) => {
                this.tools.set(tool.definition.name, tool);
            });
        }
        // Silent-alias expansion — register deprecated names as hidden clones that
        // route to their canonical. Must run last so every canonical exists.
        expandAliases(this.tools, TOOL_ALIASES);
    }
    /**
     * ADR-230 — declared tier lookup by tool name. Used by the HTTP MCP
     * dispatcher (`app/api/mcp/route.ts`) to enforce the elevated
     * `destructive:*` scope on top of the resource-level scope from
     * `lib/services/mcp-scopes`. Returns undefined for unknown tools or
     * tools that haven't declared tier yet (legacy tolerance).
     */
    getToolTier(name) {
        return this.tools.get(name)?.definition.tier;
    }
    /**
     * ADR-248 — declared resource scope (or `"always"`) lookup by tool name.
     * Used by the HTTP MCP dispatcher to compose the required scope set from
     * the tool's own declaration instead of a separate inverse map. Returns
     * undefined for unknown tools.
     */
    getToolScope(name) {
        return this.tools.get(name)?.definition.scope;
    }
    /**
     * ADR-248 — every registered tool's raw definition, WITHOUT the
     * `discoveryMode` filter that {@link listTools} applies and without the
     * description/annotation transformation. This is the source the HTTP
     * dispatcher folds over to derive the authenticated tool surface
     * (`audiences ∋ "http"` AND scope held) — it must see the full set even
     * in layered-discovery mode, where `listTools()` returns only the meta
     * tools. Reflects lobby/discovery REGISTRATION (what's in the registry),
     * just not the listing filter.
     */
    allDefinitions() {
        return Array.from(this.tools.values()).map((t) => t.definition);
    }
    /**
     * List all available tools with write-safety prefixes injected.
     * When discoveryMode is enabled, only the 5 handshake-visible tools are returned.
     * All other tools remain registered and callable via pica_execute.
     */
    listTools() {
        let toolsToList = Array.from(this.tools.values());
        if (this.config?.discoveryMode) {
            const META_TOOL_NAMES = new Set([
                "pica_sign_in",
                "pica_sign_out",
                "pica_discover",
                "pica_tool_details",
                "pica_execute",
            ]);
            toolsToList = toolsToList.filter((t) => META_TOOL_NAMES.has(t.definition.name));
        }
        // Hide deprecated (aliased) tools from the flat listing. They remain
        // registered and callable via executeTool so live callers don't break;
        // they are just no longer advertised. Layered discovery hides them via
        // discovery.ts CATEGORIES separately.
        toolsToList = toolsToList.filter((t) => !isDeprecatedMeta(getToolMetadata(t.definition.name)) &&
            getAliasFor(t.definition.name) === undefined);
        return toolsToList
            .map((tool) => {
            let definition = tool.definition;
            const metadata = getToolMetadata(definition.name);
            // ADR-230 — declared tier is the source of truth for hints + risk.
            // Lint Rule 13 enforces tier presence on every customer MCP tool;
            // metadata.risk is a quiet fallback for the unlikely case of a tool
            // that bypasses lint.
            const declaredTier = definition.tier;
            const isDestructive = declaredTier
                ? declaredTier === "destructive"
                : metadata?.risk === "destructive";
            // ADR-199 — write-tier tools may opt into the same confirmation token
            // via previewMode:"two_step_token" (e.g. pica_file_deliver). They must
            // advertise confirmation_token in their schema too, so schema-driven
            // clients know it's a legal parameter on the confirming call.
            const usesConfirmationToken = isDestructive || definition.previewMode === "two_step_token";
            if (usesConfirmationToken) {
                definition = {
                    ...definition,
                    inputSchema: {
                        ...definition.inputSchema,
                        properties: {
                            ...definition.inputSchema.properties,
                            confirmation_token: {
                                type: "string",
                                description: "Confirmation token from a previous call. Required to execute this operation. " +
                                    "Call this tool once without a token to get a preview and token, then call again with the token to confirm.",
                            },
                        },
                    },
                };
            }
            // Normalize _meta.ui.resourceUri → legacy "ui/resourceUri" key for older MCP Apps clients
            if (definition._meta?.ui?.resourceUri &&
                !definition._meta["ui/resourceUri"]) {
                definition = {
                    ...definition,
                    _meta: {
                        ...definition._meta,
                        "ui/resourceUri": definition._meta.ui.resourceUri,
                    },
                };
            }
            // ADR-230 — derive hints from declared tier when present (every
            // tool registered in mcp-server/src/tools/*.ts has tier as of
            // PR-1; the metadata branch stays as a defensive code path).
            if (declaredTier && metadata) {
                return {
                    ...definition,
                    description: injectMetadataIntoDescription(tool.definition.description, metadata),
                    annotations: {
                        ...deriveAnnotations(definition),
                        // ADR-199 extended annotation classes — read by ChatGPT/Claude.ai
                        // connectors directly. Derived from declared tier so the four
                        // surfaces (hint, riskLevel, audit row, scope) all agree.
                        ...this.deriveExtendedAnnotations(definition, metadata, undefined, declaredTier),
                    },
                };
            }
            if (metadata) {
                return {
                    ...definition,
                    description: injectMetadataIntoDescription(tool.definition.description, metadata),
                    annotations: {
                        ...deriveAnnotations(definition),
                        ...this.deriveExtendedAnnotations(definition, metadata),
                    },
                };
            }
            // ADR-230 — tools without a TOOL_METADATA entry (the 5 lobby
            // meta-tools — sign_in/out/discover/tool_details/execute) still
            // declare tier. Source hints from declared tier; default to read
            // when tier is somehow missing (lint blocks; defensive only).
            const tierForHints = declaredTier ?? "read";
            const annotations = {
                ...deriveAnnotations(definition),
                // ADR-199: derive the extended classes from declared tier so the
                // category/risk surface stays aligned with the rest of the
                // hint/audit/scope chain.
                ...this.deriveExtendedAnnotations(definition, null, undefined, tierForHints),
            };
            // No risk prefix needed — annotations handle this structurally
            return { ...definition, annotations };
            // Every branch above spreads `...definition`, so the strip is applied
            // once here rather than three times inside the map.
        })
            .map(toWireDefinition);
    }
    /**
     * ADR-199 — derive the extended annotation classes (riskLevel, category,
     * requiresConfirmation, createsExternalSideEffects, returnsPaginated,
     * requiresFollowUpInspection) from existing metadata + tool name patterns.
     *
     * Called from listTools() for every tool. Pure derivation: zero per-tool
     * edits required for the retrofit. Future tool authors can override by
     * setting fields directly on definition.annotations.
     */
    deriveExtendedAnnotations(definition, metadata, _classification, declaredTier) {
        const name = definition.name;
        // ADR-230 — declared tier is the source of truth. metadata.risk
        // remains a quiet fallback for any tool that bypasses lint Rule 13.
        // (The `_classification` parameter is retained as an unused
        // positional slot so legacy call signatures continue to compile;
        // its value is now ignored — classifyTool was removed in PR-3.)
        const risk = declaredTier
            ? tierToRiskLevel(declaredTier)
            : (metadata?.risk ?? "safe");
        // Side-effecting external systems: telegram, send-hub, comms, broadcasts.
        // Shared with deriveAnnotations() so this class and the spec's
        // `openWorldHint` cannot drift apart (EXTERNAL_SIDE_EFFECT_PATTERNS above).
        const createsExternalSideEffects = hasExternalSideEffects(name);
        // Paginated reads: every *_query and *_list tool.
        const returnsPaginated = /_query$|_list$|_search/.test(name);
        // Tools that mutate state are followed by a verifying inspect call when
        // the agent wants to confirm — the gap report flagged this as a useful
        // class for ChatGPT to surface in the UI.
        const requiresFollowUpInspection = risk === "mutating" || risk === "destructive";
        // ADR-230 — destructive tier always requires confirmation. ADR-199 — a
        // `write`-tier tool may opt into the same two-step confirmation token via
        // `previewMode: "two_step_token"` (e.g. pica_file_deliver: a write, but
        // recipient_email is typo-catastrophic), getting the friction without the
        // destructive:* scope. Both the advertised annotation (here) and the
        // executeTool gate honour this flag.
        const requiresConfirmation = definition.previewMode === "two_step_token" ||
            (declaredTier ? declaredTier === "destructive" : risk === "destructive");
        return {
            riskLevel: risk,
            category: metadata?.category,
            requiresConfirmation,
            createsExternalSideEffects,
            returnsPaginated,
            requiresFollowUpInspection,
        };
    }
    /**
     * Build a human-readable preview of what a destructive operation will affect.
     */
    async buildDestructivePreview(name, args) {
        const metadata = getToolMetadata(name);
        const displayName = metadata?.display_name || name;
        // buildDestructivePreview is only called during tool execution,
        // which is gated behind lobby mode check — pica is always available here.
        const pica = this.pica;
        try {
            switch (name) {
                case "pica_file_deliver": {
                    // Not destructive — a write that opts into the two-step token
                    // (ADR-199). The preview surfaces recipient_email because that is
                    // the typo-catastrophic field the confirmation exists to catch.
                    return {
                        action: "deliver file (email a download link)",
                        target: String(args.recipient_email ?? "(no recipient)"),
                        warning: "verify the recipient email is correct — this emails a time-limited download link for the file. the link is revocable (pica_file_revoke_delivery), but a download that already happened cannot be un-sent.",
                        display_name: displayName,
                    };
                }
                case "pica_works_delete": {
                    const [work, signedSheets] = await Promise.all([
                        pica.works.get(args.id).catch(() => null),
                        countSignedSplitSheets(pica, args.id).catch(() => null),
                    ]);
                    const title = work?.title || args.id;
                    const signedLine = signedSplitSheetLineForWork(signedSheets);
                    return {
                        action: "delete work",
                        target: title,
                        warning: "this will remove linked credits, agreement links, and audio files. recordings will be unlinked. this cannot be undone for works without verified identifiers." +
                            (signedLine ? ` ${signedLine}` : ""),
                        display_name: displayName,
                    };
                }
                case "pica_works_bulk_delete": {
                    const ids = Array.isArray(args.ids) ? args.ids : [];
                    const count = ids.length;
                    const signedLine = signedSplitSheetLineForWorks(await countSignedSplitSheetsForWorks(pica, ids));
                    return {
                        action: "bulk delete works",
                        target: `${count} works`,
                        warning: `this will delete ${count} work(s) and cascade to their credits and agreement links. this cannot be undone for works without verified identifiers.` +
                            (signedLine ? ` ${signedLine}` : ""),
                        display_name: displayName,
                    };
                }
                case "pica_works_bulk_move_to_performances": {
                    const count = args.ids?.length || 0;
                    return {
                        action: "bulk reclassify works → performances",
                        target: `${count} ${count === 1 ? "work" : "works"}`,
                        warning: `this creates ${count} new live_performance row(s) and removes the source work(s) from the catalog. for works with verified identifiers (ISRC/ISWC/MLC) the source is soft-deleted — credits and agreement links survive but are orphaned to the removed work; for unverified works the deletion is permanent. performance metadata is sparse by default and will need filling in.`,
                        display_name: displayName,
                    };
                }
                case "pica_people_delete": {
                    const person = await pica.people.get(args.id).catch(() => null);
                    const personName = person?.first_name
                        ? `${person.first_name} ${person.last_name || ""}`.trim()
                        : args.id;
                    return {
                        action: "remove person",
                        target: personName,
                        warning: "the person record will be soft-deleted. credits and agreements will remain linked.",
                        display_name: displayName,
                    };
                }
                case "pica_people_bulk_delete": {
                    const count = args.ids?.length || 0;
                    return {
                        action: "bulk delete people",
                        target: `${count} ${count === 1 ? "person" : "people"}`,
                        warning: `this will soft-delete ${count} person record(s). credits and agreements remain linked to the removed records.`,
                        display_name: displayName,
                    };
                }
                case "pica_people_split_identity": {
                    // ops 482424b7 — name who shares the identity and what moves, so
                    // the confirming human sees the blast radius before the token.
                    const preview = await pica.people
                        .splitIdentityPreview(args.id)
                        .catch(() => null);
                    const personName = preview?.person?.name || args.id;
                    const shared = Array.isArray(preview?.shared_with)
                        ? preview.shared_with.map((p) => p.name || p.id)
                        : [];
                    const moving = Array.isArray(args.move_identifiers)
                        ? args.move_identifiers
                        : [];
                    const relinkCount = Array.isArray(args.relink_split_ids)
                        ? args.relink_split_ids.length
                        : 0;
                    return {
                        action: "give a person their own identity",
                        target: shared.length
                            ? `${personName} (currently sharing an identity with ${shared.join(", ")})`
                            : personName,
                        warning: `${personName} gets a new global identity. ` +
                            (moving.length
                                ? `these identifiers move with them: ${moving.join(", ")}. `
                                : "no identifiers move — every identifier stays with the original identity. ") +
                            (relinkCount
                                ? `${relinkCount} unattributed split row(s) will be credited to them. `
                                : "") +
                            "custody records are not rewritten. the audit log keeps every moved value, but this is not undone automatically.",
                        display_name: displayName,
                    };
                }
                case "pica_agreements_delete": {
                    const agreementResult = await pica.agreements
                        .get(args.id)
                        .catch(() => null);
                    const title = agreementResult?.agreement?.title || args.id;
                    return {
                        action: "delete agreement",
                        target: title,
                        warning: "this will remove all linked work and party records. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_merge_duplicates": {
                    const loserCount = args.loser_ids?.length || 0;
                    let entityType = "entities";
                    try {
                        entityType = coerceMergeEntityType(args.entity_type).canonical;
                    }
                    catch {
                        // Unknown entity_type — fall back to generic noun in the warning
                        // copy. Tool execution itself will surface the validation error.
                    }
                    return {
                        action: `merge ${entityType}`,
                        target: `${loserCount} ${entityType} into winner`,
                        warning: `all credits, recordings, and agreements from ${loserCount} loser(s) will be moved to the winner. loser records will be permanently deleted. this cannot be undone.`,
                        display_name: displayName,
                    };
                }
                case "pica_rights_propose": {
                    // ADR-321 §9 — not a delete: the confirmation exists because this
                    // emails every party the rule affects. Nothing is active until each
                    // one confirms from their own email.
                    const work = await pica.works.get(args.work_id).catch(() => null);
                    const song = work?.title ? `"${work.title}"` : "the song";
                    const what = args.rule === "master"
                        ? `the master rule for a recording of ${song}`
                        : args.rule === "share"
                            ? `a ${String(args.kind ?? "share").replace(/_/g, " ")} share rule on ${song}`
                            : `the song rule for ${song}`;
                    return {
                        action: "propose a rights rule",
                        target: what,
                        warning: "this emails every party the rule affects and asks them to confirm. nothing is locked until every party confirms by email — PICA never confirms on anyone's behalf and no money moves.",
                        display_name: displayName,
                    };
                }
                case "pica_projects_delete": {
                    const project = await pica.projects?.get(args.id).catch(() => null);
                    const projectName = project?.name || args.id;
                    return {
                        action: "delete project",
                        target: projectName,
                        warning: "linked works will be unlinked but not deleted. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                // ── ADR-199 Stream 5b — 7 fall-through cases now specific ──
                case "pica_recordings_delete": {
                    const recording = await pica.recordings
                        ?.get?.(args.id)
                        .catch(() => null);
                    const title = recording?.title || args.id;
                    const isrc = recording?.isrc;
                    return {
                        action: "delete recording",
                        target: isrc ? `${title} (${isrc})` : title,
                        warning: "the recording will be removed from its work and any release. credits attached to this recording will be detached. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_share_links_delete": {
                    const link = await pica.shareLinks.get(args.id).catch(() => null);
                    const linkData = link?.data || link;
                    const linkName = linkData?.name || args.id;
                    return {
                        action: "delete share link",
                        target: linkName,
                        warning: "the link will become inaccessible immediately. recipients with the URL will see a 'no longer available' page. view / download history is removed. to keep the audit trail, prefer pica_share_links_update with is_active=false.",
                        display_name: displayName,
                    };
                }
                case "pica_agreement_types_delete": {
                    return {
                        action: "delete agreement type",
                        target: args.id,
                        warning: "the agreement type will be removed from the catalog. existing agreements that reference this type will keep their type string but lose the template definition. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_notes_delete": {
                    return {
                        action: "delete note",
                        target: args.id,
                        warning: "the note will be permanently removed. attachments referenced by the note are not deleted. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_physical_assets_delete": {
                    const asset = await pica.physicalAssets
                        ?.get?.(args.id)
                        .catch(() => null);
                    const assetName = asset?.name || asset?.title || args.id;
                    return {
                        action: "delete physical asset",
                        target: assetName,
                        warning: "the asset record and any linked recording / work associations will be removed. this cannot be undone — re-create from scratch if you need it back.",
                        display_name: displayName,
                    };
                }
                case "pica_memory_delete": {
                    return {
                        action: "delete memory entry",
                        target: args.id,
                        warning: "the memory entry will be permanently removed and will not appear in future agent context. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_delete_my_account": {
                    return {
                        action: "delete account",
                        target: "your account and organisation",
                        warning: "this is permanent. your auth user, organisation membership, API keys, OAuth tokens, agent grants, and notification preferences will all be removed. catalog data (works, recordings, credits) is retained per data-retention policy. this cannot be undone.",
                        display_name: displayName,
                    };
                }
                case "pica_revoke_agent_grant": {
                    const grantId = args.grant_id || args.id;
                    return {
                        action: "revoke agent grant",
                        target: grantId,
                        warning: "the grant token will stop authenticating immediately. any in-flight agent calls using this grant will fail at next request. this cannot be undone — issue a new grant if needed.",
                        display_name: displayName,
                    };
                }
                case "pica_collaborators_invite_revoke": {
                    const inviteId = args.invite_id || args.id;
                    return {
                        action: "revoke collaboration invite",
                        target: inviteId,
                        warning: "the invite will become unclaimable immediately. both the magic-link URL and the one-time code will return 'invite no longer valid'. an existing-user recipient will see a revocation event on their next agent prompt. this cannot be undone — issue a new invite if needed.",
                        display_name: displayName,
                    };
                }
                default: {
                    return {
                        action: displayName,
                        target: args.id || "unknown",
                        warning: "this cannot be undone.",
                        display_name: displayName,
                    };
                }
            }
        }
        catch {
            return {
                action: displayName,
                target: args.id || "unknown",
                warning: "this cannot be undone.",
                display_name: displayName,
            };
        }
    }
    /**
     * Sanitize tool parameters for audit logging — delegates to the
     * pure-function impl below so it can be exercised directly in tests.
     */
    sanitizeParams(args) {
        return sanitizeAuditParams(args);
    }
    /**
     * ADR-208 Primitive B — opaque per-org cache key.
     *
     * Stdio: `config.picaApiKey` is the org-bound API key from credentials,
     * unique per org. HTTP non-discovery: `callerContext.callerIdentity` is
     * `http:${authContext.keyId}` set by `app/api/mcp/route.ts`, also unique
     * per org. This is the cross-org cache isolation invariant required by
     * acceptance criterion 4 — two orgs sharing a worker process never share
     * a cacheKey. Returns `null` (and the wrapper skips session_state) when
     * neither identifier is present.
     */
    getSessionCacheKey() {
        const apiKey = this.config?.picaApiKey;
        if (apiKey)
            return apiKey;
        if (this.callerContext.callerIdentity &&
            this.callerContext.callerIdentity !== "unknown") {
            return this.callerContext.callerIdentity;
        }
        return null;
    }
    /**
     * ADR-208 Primitive B + ADR-208 Phase 2 + ADR-210 Phase 1 — fetch the
     * org-scoped counts + billing slice via `pica.catalogStats()`. The
     * endpoint composes the billing slice server-side via
     * `subscriptionService.getOrgBillingSlice` and the pending-uploads count
     * via the new `organisations.pending_uploads_count` column so stdio +
     * HTTP transports stay symmetric.
     *
     * `completeness_pct` is stubbed at 0 in Phase 1 and will be wired in a
     * Phase 1.5 follow-up. The stub keeps the shape contract green without
     * forcing a separate endpoint extension into this PR.
     *
     * Fail-soft: when `this.pica` is missing (HTTP-discovery without bearer
     * scope, mostly), every count is 0 and the billing slice falls back to
     * a conservative "trial" shape so the agent doesn't see misleading
     * "active" billing for an uninitialized context.
     */
    fetchSessionCounts = async () => {
        if (!this.pica) {
            return {
                works_count: 0,
                recordings_count: 0,
                completeness_pct: 0,
                pending_uploads: 0,
                billing_state: "trial",
                plan: "free_runway",
                current_tier: null,
                capacity_pct: 0,
            };
        }
        const stats = await this.pica.catalogStats();
        const billing = stats.billing_slice;
        // ADR-217 Primitive D — storage slice from /api/admin/catalog/stats.
        // Optional in the response so older servers without ADR-217 keep
        // working — when missing, omit the key entirely (vs fabricating a
        // misleading "pica_s3 / untested" envelope that would suppress an
        // agent's "you might want to configure storage" nudge).
        const storage = stats.storage_slice;
        const counts = {
            works_count: stats.works?.total ?? 0,
            recordings_count: stats.recordings?.total ?? 0,
            // Phase 1.5 stub — completeness_pct ships separately. Shape preserved.
            completeness_pct: 0,
            pending_uploads: stats.pending_uploads_count ?? 0,
            // ADR-210 Phase 1 — billing slice from /api/admin/catalog/stats. The
            // endpoint emits a "trial" fallback on its own billing-side outage; if
            // the field is missing entirely (older server pre-Phase-1), mirror the
            // same conservative shape locally rather than fabricate "active".
            billing_state: billing?.billing_state ?? "trial",
            plan: billing?.plan ?? "free_runway",
            current_tier: billing?.current_tier ?? null,
            capacity_pct: billing?.capacity_pct ?? 0,
            ...(storage
                ? {
                    storage_provider: {
                        provider: storage.provider,
                        is_byoc: storage.is_byoc,
                        last_test_status: storage.last_test_status,
                    },
                }
                : {}),
        };
        // ADR-218 Primitive A — propagate the optional notifications summary
        // when the catalog-stats response carries it. Older API servers
        // (pre-ADR-218) omit the field; we leave the envelope key off in that
        // case so the agent doesn't see a fabricated zero.
        if (stats.notifications) {
            counts.notifications = stats.notifications;
        }
        return counts;
    };
    /**
     * P3c — attach `_meta.schemas` (workflow-resource URIs) to a successful
     * workflow-tagged tool result. Mirrors `attachSessionState`'s merge
     * semantics: spread existing `_meta`, add the new key, leave other
     * keys (e.g. `session_state`) untouched.
     *
     * Applies to ALL workflow-tagged tools (Option X), including read
     * companions like `pica_sessions_query`, so the agent gets the schema
     * pointer no matter which verb they hit first. Skips when:
     *  - the tool's `workflows` is or contains only `"infrastructure"`
     *    (no schema resource exists), or
     *  - the result is an error (the workflow didn't progress).
     *
     * Fail-open like `attachSessionState`: derivation is pure today, but
     * any future throw must not turn a successful tool result into a
     * failure.
     */
    attachSchemaHints(result, workflows) {
        try {
            const tags = Array.isArray(workflows) ? workflows : [workflows];
            const schemas = tags
                .filter((t) => t !== "infrastructure")
                .map((t) => `pica://schemas/${t}`);
            if (schemas.length === 0)
                return;
            const existingMeta = result._meta ?? {};
            result._meta = {
                ...existingMeta,
                schemas,
            };
        }
        catch {
            // Match attachSessionState shape — fail-open. The hint is a
            // discoverability aid, never load-bearing for the tool's own work.
        }
    }
    /**
     * ADR-208 Primitive B — bump the session-mutation counters and attach
     * the resolved `_meta.session_state` to a successful write-tool result.
     * Mutates `result` in-place to keep the existing return contract.
     * Fail-open: any error here is swallowed — ambient state is best-effort
     * and must not turn a successful mutation into a tool failure.
     */
    async attachSessionState(result, toolName) {
        const cacheKey = this.getSessionCacheKey();
        if (!cacheKey)
            return;
        try {
            incrementSessionMutations(cacheKey);
            const sessionState = await buildSessionState(cacheKey, this.fetchSessionCounts);
            if (!sessionState)
                return;
            const existingMeta = result._meta ?? {};
            result._meta = {
                ...existingMeta,
                session_state: sessionState,
            };
        }
        catch {
            // Swallow — ADR-208 explicitly mandates fail-open. The counter has
            // already advanced; not attaching session_state on this turn just
            // means the agent gets the bumped counters on the next mutation.
            void toolName;
        }
    }
    /**
     * Execute a tool by name. `ctx.server` carries the transport-scoped
     * MCP `Server` reference so executors that need `server.elicitInput()`
     * (ADR-200 Phase 1) can reach it. Each per-request HTTP server is
     * different; passing it through positionally avoids registry-level
     * mutable state that would race between concurrent requests.
     */
    async executeTool(name, args, ctx) {
        const startTime = Date.now();
        const tool = this.tools.get(name);
        if (!tool) {
            throw new ToolExecutionError(`Tool not found: ${name}`);
        }
        // ops dfa810f8 — an unknown `sections` name used to match nothing in the
        // executor and answer a successful `{}`. Refuse it here, once, for every
        // tool that declares a closed `sections` list (./section-validation.ts).
        const sectionsRefusal = rejectUnknownSections(name, tool.definition.inputSchema, args);
        if (sectionsRefusal)
            return sectionsRefusal;
        // Task A0 — resolve the canonical identity for GATE PURPOSES ONLY. An
        // alias clone's `tool.executor` applies `adaptArgs` internally, but the
        // confirmation gate below (buildDestructivePreview / generate+validate
        // token) previously ran on the alias name + raw args, so a destructive
        // canonical aliased with `adaptArgs` would preview/tokenize the WRONG
        // arg shape. `gateName`/`gateArgs` fix that; the executor call further
        // down stays on the raw `name`/`args` (no double-adapt).
        const alias = getAliasFor(name);
        const gateName = alias?.canonical ?? name;
        const gateArgs = alias?.adaptArgs ? alias.adaptArgs(args) : args;
        const metadata = getToolMetadata(name);
        // ADR-230 — declared tier is the source of truth for the freshness +
        // confirmation gates and the audit-log surfaces. Fall back to
        // metadata.risk only if the tool somehow lacks tier (lint blocks
        // missing tier on creator + team + directory MCPs as of PR-2).
        const declaredTier = tool.definition.tier;
        const fallbackRisk = metadata?.risk ?? "safe";
        const auditTier = declaredTier ??
            (fallbackRisk === "safe"
                ? "read"
                : fallbackRisk === "destructive"
                    ? "destructive"
                    : "write");
        const auditRiskLevel = tierToRiskLevel(auditTier);
        const isDestructive = auditTier === "destructive";
        // ADR-199 — a `write`-tier tool may opt into the destructive two-step
        // confirmation token via previewMode, WITHOUT the destructive:* scope or
        // the 7-day freshness gate (both stay keyed on isDestructive below).
        const requiresTwoStepToken = tool.definition.previewMode === "two_step_token";
        const needsConfirmation = isDestructive || requiresTwoStepToken;
        // ── Session freshness gate (ADR-151) ──
        if (isDestructive && this.config && !this.config.lobbyMode) {
            const creds = readCredentials(this.config.credentialsPath);
            if (creds?.authenticated_at) {
                const authAge = Date.now() - new Date(creds.authenticated_at).getTime();
                const sevenDays = 7 * 24 * 60 * 60 * 1000;
                if (authAge > sevenDays) {
                    return {
                        content: [
                            {
                                type: "text",
                                text: 'this action requires re-verification — say "sign me in" to confirm your identity',
                            },
                        ],
                        isError: true,
                    };
                }
            }
        }
        // ── Confirmation gate (ADR-230 destructive tier + ADR-199 two_step_token) ──
        if (needsConfirmation) {
            const confirmationToken = args.confirmation_token;
            if (!confirmationToken) {
                // First call — generate preview and return confirmation challenge.
                // ADR-230 PR-4 — token state lives in Postgres for HTTP MCP under
                // Fluid Compute (or in-memory for stdio); both paths via the
                // active ConfirmationStore singleton, so callers stay unchanged.
                const token = await generateConfirmationToken(gateName, gateArgs);
                const preview = await this.buildDestructivePreview(gateName, gateArgs);
                const response = {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify({
                                status: "confirmation_required",
                                confirmation_token: token,
                                expires_in: 120,
                                preview,
                            }, null, 2),
                        },
                    ],
                    structuredContent: {
                        status: "confirmation_required",
                        confirmation_token: token,
                        expires_in: 120,
                        preview,
                    },
                };
                // Fire-and-forget audit for confirmation challenge
                this.auditLogger
                    ?.logToolExecution({
                    tool_name: name,
                    tool_category: metadata?.category || "unknown",
                    tier: auditTier,
                    risk_level: auditRiskLevel,
                    parameters: this.sanitizeParams(args),
                    result_status: "confirmation_required",
                    confirmation_token: token,
                    execution_time_ms: Date.now() - startTime,
                    caller_identity: this.callerContext.callerIdentity,
                    transport: this.callerContext.transport,
                    ...this.extractClientInfo(ctx),
                })
                    .catch(() => { }); // Never block on audit
                return response;
            }
            // Second call — validate token (Postgres-backed under Fluid Compute
            // since ADR-230 PR-4; the active store handles cross-instance state).
            const validation = await validateAndConsumeToken(confirmationToken, gateName, gateArgs);
            if (!validation.valid) {
                return {
                    content: [
                        {
                            type: "text",
                            text: JSON.stringify({
                                error: validation.reason?.includes("expired")
                                    ? "CONFIRMATION_EXPIRED"
                                    : "CONFIRMATION_INVALID",
                                message: validation.reason,
                                retry_safe: false,
                                suggestion: "start the operation again without the confirmation token",
                            }, null, 2),
                        },
                    ],
                    isError: true,
                };
            }
            // Token valid — fall through to execute
        }
        // ── Normal execution ──
        try {
            const result = await tool.executor(args, ctx);
            // Guard response size — envelope-aware (ops 41ff656c / 893e3e29): list
            // responses over the 100KB cap get whole-item trimming + a real resume
            // cursor (next_offset / recovery_hint) in both views; non-list shapes
            // keep the legacy text truncation.
            if (result?.content) {
                const schemaProps = tool.definition.inputSchema?.properties ?? {};
                guardToolResult(result, name, {
                    offsetSupported: "offset" in schemaProps,
                    offset: typeof args?.offset === "number" ? args.offset : 0,
                    limit: typeof args?.limit === "number" ? args.limit : undefined,
                });
            }
            // ADR-208 Primitive B — ambient session-state on write-tool results.
            // Read tools (`tier: "read"`) skip this entirely so the high-volume
            // read path stays cheap. `result.isError === true` also skips because
            // a failed mutation didn't change catalog state.
            const isWriteTool = auditRiskLevel !== "safe";
            if (isWriteTool && result && !result.isError) {
                await this.attachSessionState(result, name);
            }
            // P3c — schema-resource hint on every workflow-tagged tool result
            // (Option X — applies to read companions too so the agent gets the
            // pointer regardless of which verb they hit first). Runs after
            // `attachSessionState` so the two `_meta` keys co-exist via merge.
            // Skipped on errors (workflow didn't progress) and on infrastructure
            // tools (handled inside `attachSchemaHints`).
            if (result && !result.isError) {
                this.attachSchemaHints(result, tool.definition.workflows);
            }
            // Soft deprecation hint — when the invoked name is an alias, tell the agent
            // which canonical to prefer. Never errors; routing already happened.
            // `alias` was already resolved at the top of this method (gate-purposes
            // computation, Task A0) — same `name`, so reuse it instead of a second
            // getAliasFor lookup.
            if (alias && result && !result.isError) {
                result._meta = {
                    ...(result._meta ?? {}),
                    deprecation: {
                        use: alias.canonical,
                        removal_target: alias.removal_target,
                    },
                };
            }
            // ADR-208 acceptance criterion 5 — briefing run resets the
            // since_last_briefing counter. Handled in the wrapper so the briefing
            // tool itself stays oblivious to the cacheKey it doesn't own.
            if (name === "pica_dashboard_briefing" && result && !result.isError) {
                const cacheKey = this.getSessionCacheKey();
                if (cacheKey)
                    resetSinceLastBriefing(cacheKey);
            }
            // Fire-and-forget audit.
            // `isError: true` results (e.g. fail-quiet-to-ops wrappers that
            // caught an SDK error, validation rejections, or session-freshness
            // gates) get `result_status: "error"` here so mcp_audit_log
            // telemetry counts and the ops_issues auto-capture pipeline see
            // them. Executors can opt into structured error attribution by
            // setting `structuredContent.error_code` + `structuredContent.error_message`
            // — anything else just lands as an isError row with no detail,
            // which is fine for the input-validation case (ops_issues
            // upsertFromAuditError requires a code to group on, so generic
            // validation rejections stay out of the ops bucket by default).
            const resultIsError = result?.isError === true;
            const { code: resultErrorCode, message: resultErrorMessage } = resultIsError
                ? extractErrorAttribution(result)
                : { code: undefined, message: undefined };
            // 2026-09-01 stdio-relay wire change: forward the narrowed result
            // projection so the relay ingest can attribute import wins (created
            // work ids live in the RESULT for the import tools; their params
            // carry no id) and, since 2026-09-04, create wins (`createdId`).
            // narrowToolResultForRefs is the allow-list — null for every other
            // tool, so nothing else changes on the wire.
            const resultProjection = resultIsError
                ? null
                : narrowToolResultForRefs(name, args || {}, result);
            this.auditLogger
                ?.logToolExecution({
                tool_name: name,
                tool_category: metadata?.category || "unknown",
                tier: auditTier,
                risk_level: auditRiskLevel,
                parameters: this.sanitizeParams(args),
                result_status: resultIsError ? "error" : "success",
                ...(resultErrorCode ? { error_code: resultErrorCode } : {}),
                ...(resultErrorMessage ? { error_message: resultErrorMessage } : {}),
                execution_time_ms: Date.now() - startTime,
                caller_identity: this.callerContext.callerIdentity,
                transport: this.callerContext.transport,
                ...this.extractClientInfo(ctx),
                ...(resultProjection ? { result_projection: resultProjection } : {}),
            })
                .catch(() => { }); // Never block on audit
            return result;
        }
        catch (error) {
            const formatted = formatError(error);
            const parsed = JSON.parse(formatted.text);
            // Attach recovery hint if available for this tool + error code
            const hint = getRecoveryHint(name, parsed.error);
            if (hint) {
                parsed.suggestion = hint.suggestion;
                if (hint.next_tool) {
                    parsed.next_tool = hint.next_tool;
                }
            }
            // Fire-and-forget audit for errors
            this.auditLogger
                ?.logToolExecution({
                tool_name: name,
                tool_category: metadata?.category || "unknown",
                tier: auditTier,
                risk_level: auditRiskLevel,
                parameters: this.sanitizeParams(args),
                result_status: "error",
                error_code: parsed.error,
                // Stage A — forward the parsed body's message so mcp_audit_log
                // captures the underlying cause. Without this every error row had
                // error_message=null, leaving UNKNOWN_ERROR rows opaque.
                error_message: typeof parsed.message === "string" ? parsed.message : undefined,
                execution_time_ms: Date.now() - startTime,
                caller_identity: this.callerContext.callerIdentity,
                transport: this.callerContext.transport,
                ...this.extractClientInfo(ctx),
            })
                .catch(() => { }); // Never block on audit
            return {
                content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
                isError: true,
            };
        }
    }
}
//# sourceMappingURL=index.js.map