Ctrl K
ComunidadGuías…

src/lib/mcp/scopes.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import type { McpScope } from "@prisma/client";

/**
 * The manual's sections, and the four jobs built out of them.
 *
 * Two things need these boundaries now — the manual's pages and a connection's
 * scope — and two copies of a range table is a drift waiting for a new tool to
 * land in the wrong one. `tools/gen-tool-docs.mjs` keeps only what is about
 * pages (file, title, icon, blurb) and asserts its keys against this list.
 *
 * Boundaries are first/last tool NAMES rather than indexes, so reordering the
 * array is caught rather than silently reshuffling both the manual and every
 * scope.
 *
 * Both literals below are PLAIN DATA — no function calls, no template strings,
 * no spreads — because tools/tool-source.mjs evaluates them as text. Keep them
 * that way.
 *
 * This file is not in src/lib/data/: it touches no database and takes no
 * userId, the same way src/lib/pipeline-fields.ts and src/lib/column-widths.ts
 * do not. It must stay importable by tools.ts with NO import back the other
 * way, or the module graph cycles.
 */
export const SECTIONS = [
  { key: "me", first: "search_me", last: "import_linkedin_archive" },
  { key: "resumes", first: "get_resume_format", last: "check_resume_fit" },
  { key: "letters", first: "prep_letter", last: "delete_letter" },
  { key: "pipeline", first: "pipeline_stats", last: "list_field_values" },
  { key: "crm", first: "list_companies", last: "schedule_contact_pings" },
  { key: "archive", first: "list_archive", last: "empty_archive" },
  { key: "accounts", first: "list_linked_accounts", last: "search_calendar" },
  { key: "connections", first: "get_digest_settings", last: "delete_connection" },
  { key: "admin", first: "admin_instance_stats", last: "admin_delete_variable" },
] as const;

export type SectionKey = (typeof SECTIONS)[number]["key"];

/**
 * The four tools every scope carries, whatever else it does not.
 *
 * Small on purpose, and read-only to a tool. The briefing names `search_me` as
 * the way in and would be lying in three scopes out of four without it;
 * `whoami` is how a narrowed client explains ITSELF — it returns the
 * connection's scope — which is the difference between "I can't do that" and
 * "that tool isn't served here, and here is how your person widens it", and its
 * `setup` is what a first conversation opens with; `list_connections` names
 * the other connections. Nothing that writes is ever core — which is
 * what makes READONLY's core fall out for free rather than needing an
 * exception.
 */
export const CORE_TOOLS = ["search_me", "whoami", "list_connections"] as const;

export const SCOPES = [
  {
    key: "FULL",
    label: "Everything",
    /** Empty sections means "no filter at all", not "no sections". */
    sections: [],
    extras: [],
    readOnly: false,
    blurb:
      "Every tool this account can reach. The default, and what every connection had before scopes existed.",
  },
  {
    key: "WRITING",
    label: "Me, resumes and letters",
    sections: ["me", "resumes", "letters"],
    /**
     * A document is aimed at a job at a company, and tailor_resume_for_application,
     * prep_letter and create_letter all take an applicationId somebody has to
     * look up first. get_company is already in ALWAYS_LOAD for the same reason.
     */
    extras: [
      "list_applications",
      "get_application",
      "list_companies",
      "get_company",
      // The undo path. These are filed under connections because that is where
      // the change log lives, but they are about WRITES — and a scope whose
      // whole job is writing roles and resumes, with no way back from an
      // update_role that ate somebody's background, is the exact footgun they
      // were built for. `--scope-audit` in tools/eval-tool-choice.mjs is what
      // found them sitting in FULL alone.
      "list_changes",
      "list_revisions",
      "restore_revision",
    ],
    readOnly: false,
    blurb:
      "Writes documents and the material behind them. Cannot move the pipeline, tag anybody or delete a company.",
  },
  {
    key: "PIPELINE",
    label: "The search",
    sections: ["pipeline", "crm", "archive", "accounts"],
    /**
     * list_resumes answers "which one did I send them" without opening the
     * resume tools; preview_digest is a pipeline read that happens to be filed
     * on the account page. The change log is here so this scope can say what
     * it did. restore_revision is NOT: nothing this scope writes is versioned
     * — only resumes and roles are — so there is nothing here for it to undo.
     */
    extras: ["list_resumes", "preview_digest", "list_changes"],
    readOnly: false,
    blurb:
      "Runs the search: applications, offers, interviews, people, tasks, the archive, mail and calendar.",
  },
  {
    key: "READONLY",
    /**
     * Every section, filtered to what writes nothing. Admin is excluded like
     * every other non-FULL scope.
     */
    label: "Read-only",
    sections: ["me", "resumes", "letters", "pipeline", "crm", "archive", "accounts", "connections"],
    extras: [],
    readOnly: true,
    blurb: "Reads anything, writes nothing. For a client you want looking but not touching.",
  },
] as const;

export const SCOPE_VALUES = ["FULL", "WRITING", "PIPELINE", "READONLY"] as const;

export const scopeByKey = new Map(SCOPES.map((scope) => [scope.key, scope]));

export function scopeLabel(scope: McpScope): string {
  return scopeByKey.get(scope)?.label ?? "Everything";
}

export function scopeBlurb(scope: McpScope): string {
  return scopeByKey.get(scope)?.blurb ?? "";
}