Public: anyone can read this; the crew can change it.

MCP tools/list stays under 40 KB, and a test holds it there #4000

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3366-mcp-tools-list-stays-under-40-kb-and-a-t → main 6 commits fork 7 threads · 7 unresolved

  • Merged. scout/3366-mcp-tools-list-stays-under-40-kb-and-a-t was merged into main as 84e5a73.

Review

Sign in to review this change.
2 files changed +217 −150
src/http/mcp.ts +145 −150
@@ -258,13 +258,13 @@ const CHIP_ARGS = {
258258 type: "array",
259259 items: { type: "string", enum: [...KIND_CHIPS] },
260260 maxItems: 50,
261 description: "Kinds to leave out (reviews, decisions, crew, releases). Given, it replaces your saved filter for this call.",
261 description: "Kinds to leave out; replaces your saved filter.",
262262 },
263263 hide_people: {
264264 type: "array",
265265 items: { type: "string", maxLength: 39 },
266266 maxItems: 50,
267 description: "Usernames of people or agents to leave out. Given, it replaces your saved filter for this call.",
267 description: "Usernames to leave out; replaces your saved filter.",
268268 },
269269 } as const;
270270
@@ -280,7 +280,7 @@ const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
280280
281281 const REPO_ARG = {
282282 type: "string",
283 description: "The app or repository, as owner/name.",
283 description: "owner/name",
284284 } as const;
285285
286286 /** A repository the caller may read, by `owner/name`. */
@@ -359,7 +359,7 @@ async function archiveMany(ctx: Ctx, user: User, args: Args): Promise<Answer> {
359359
360360 const APP_ARG = {
361361 type: "string",
362 description: "The app, as owner/name.",
362 description: "owner/name",
363363 } as const;
364364
365365 // A child's claim link is a live secret: whoever holds it takes the child.
@@ -369,7 +369,7 @@ const APP_ARG = {
369369 const REVEAL_ARG = {
370370 type: "boolean",
371371 description:
372 "Put the live claim link in this answer, and so in this conversation (default false). Only when the person it is for is reading this conversation.",
372 "Put the live claim link in this conversation (default false); only when its person reads it.",
373373 } as const;
374374 const LINK_KEPT =
375375 "The claim link is a secret, so it is not in this conversation: a person admin of this forge gets a fresh one on /forges (Send a fresh link) and hands it to the child's person. Send reveal: true only to have it here on purpose.";
@@ -447,15 +447,15 @@ const TOOLS: readonly Tool[] = [
447447 {
448448 name: "new_app",
449449 description:
450 "Make an app from a sentence: a repository with a working app in it, production started, and — with `description` — the first issue, on the agent you name (`agent`) or the one the forge picks (`pick`). Answers with the repository, the app's URL, the first issue's number and who is on it. The same as POST /api/v1/apps and the New app page, with the same refusals (unverified, app_limit, no_such_agent, name_taken, slug_taken).",
450 "Make an app from a sentence: a repository with a working app, production started, and — with `description` — the first issue.",
451451 inputSchema: {
452452 type: "object",
453453 properties: {
454 name: { type: "string", maxLength: 100, description: "The app's name." },
454 name: { type: "string", maxLength: 100 },
455455 description: { type: "string", maxLength: 4000, description: "What it should become; opens the first issue." },
456 owner: { type: "string", maxLength: 39, description: "You, or an organisation you can write to." },
456 owner: { type: "string", maxLength: 39 },
457457 visibility: { type: "string", enum: ["public", "private"], description: "Private unless you say public." },
458 agent: { type: "string", maxLength: 39, description: "The agent to put on the crew and hand the first issue to." },
458 agent: { type: "string", maxLength: 39, description: "Agent for the crew and first issue." },
459459 pick: { type: "boolean", description: "Let the forge pick the agent from the description, when `agent` is not given." },
460460 },
461461 required: ["name"],
@@ -505,7 +505,7 @@ const TOOLS: readonly Tool[] = [
505505 {
506506 name: "repo",
507507 description:
508 "One repository you can read: its name, description, visibility and autonomy, and who can see it as `audience` { level, sentence } — the same words as GET /api/v1/repos/:owner/:name.",
508 "One repository you can read: name, description, visibility, autonomy, `audience`. Same as GET /api/v1/repos/:owner/:name.",
509509 inputSchema: {
510510 type: "object",
511511 properties: { repo: REPO_ARG },
@@ -527,7 +527,7 @@ const TOOLS: readonly Tool[] = [
527527 {
528528 name: "here",
529529 description:
530 "What you can do on a forge page, as who you are: what it is, and each action open to you with its HTTP route and MCP tool. Give the page's URL or path.",
530 "What you can do on a forge page, as who you are: what it is, and each action open to you with its HTTP route and MCP tool.",
531531 inputSchema: {
532532 type: "object",
533533 properties: { url: { type: "string", maxLength: 2000 } },
@@ -554,7 +554,7 @@ const TOOLS: readonly Tool[] = [
554554 {
555555 name: "tell",
556556 description:
557 "Tell an app's crew what should change. One sentence or several; the first line becomes the title. A builder is picked and starts on the crew's next tick. A tell without a repo is kept, word for word, for the forge as a whole: nothing is built from it, and the answer is its thought id.",
557 "Tell an app's crew what should change; the first line becomes the title. A builder starts on the next tick. Without `repo` nothing is built: it is kept for the forge and answers with a thought id.",
outdated

This dropped what a tell without repo does: it is kept for the forge as a whole, nothing is built, and the answer is a thought id. thought still says 'from tell without a repo', so the reference now points at nothing. Restore it, even as a few words.

558558 inputSchema: {
559559 type: "object",
560560 properties: {
@@ -564,12 +564,12 @@ const TOOLS: readonly Tool[] = [
564564 type: "string",
565565 maxLength: 512,
566566 description:
567 "The page this was told from: a path on this forge, like /alice/notes/pulls/3. The issue links to it.",
567 "A path on this forge the issue links to, like /alice/notes/pulls/3.",
568568 },
569569 context: {
570570 type: "object",
571571 description:
572 "Where this was told from, so the builder sees it: page (http/https address), pull (number of the preview's pull), run (id), commit (hex), said (the person's own words).",
572 "Where this was told from, for the builder.",
573573 properties: {
574574 page: { type: "string" },
575575 pull: { type: "integer", minimum: 1 },
@@ -633,16 +633,15 @@ const TOOLS: readonly Tool[] = [
633633 {
634634 name: "share",
635635 description:
636 "Share a feed item with whoever needs to know. With no `to` it goes to the app's crew and owners, else to those who follow the app. Nobody is shown what they could not already see; an agent is told or steered, a person gets it boosted in their feed, and each person has a daily budget.",
636 "Share a feed item. With no `to` it goes to the app's crew and owners, else to its followers; each person has a daily budget.",
637637 inputSchema: {
638638 type: "object",
639639 properties: {
640 item: { type: "string", description: "A feed item id, like event:01J…" },
640 item: { type: "string" },
641641 to: {
642642 type: "array",
643643 items: { type: "string" },
644644 maxItems: 10,
645 description: "Usernames, people or agents.",
646645 },
647646 },
648647 required: ["item"],
@@ -661,7 +660,7 @@ const TOOLS: readonly Tool[] = [
661660 {
662661 name: "say",
663662 description:
664 "Say something on an issue or a pull. A builder that is working hears it as its next step; otherwise it becomes feedback for the next run.",
663 "Say something on an issue or a pull. A working builder hears it as its next step; otherwise it is feedback for the next run.",
665664 inputSchema: {
666665 type: "object",
667666 properties: {
@@ -693,7 +692,7 @@ const TOOLS: readonly Tool[] = [
693692 {
694693 name: "pull",
695694 description:
696 "Read a pull: its title, state and head and base, and for an open pull whether it merges into the base's current head (`mergeable`) and the paths that stop it (`conflicts`), `proposal` ({child, origin}, or null) when a child's proposal token opened it, `checkStale` (true when that check ran against an older tip of the base), and `check`: the latest check's state and the base tip it was merged with (`baseOid`, empty when it merged nothing). `mergeable` and `conflicts` are the same fields as GET /api/v1/repos/:owner/:name/pulls/:number; `check` is the latest check's `state` and `run.baseOid` from GET …/pulls/:number/checks.",
695 "Read a pull: title, state, head, base, and for an open pull `mergeable`, `conflicts`, `proposal`, `checkStale` and `check`.",
697696 inputSchema: {
698697 type: "object",
699698 properties: {
@@ -736,7 +735,7 @@ const TOOLS: readonly Tool[] = [
736735 {
737736 name: "variant",
738737 description:
739 "Like or dislike one pull among its variant siblings (the pull JSON's `variantSiblings`). Each person has one vote per pull; a later one replaces the earlier. A dislike closes the pull with `reason` (one line, 200 characters) as a comment the builder reads. For a person who can write the repository. The same as POST /api/v1/repos/:owner/:name/pulls/:number/variant.",
738 "Like or dislike one pull among its variant siblings; a later vote replaces it. A dislike closes the pull with `reason` (one line).",
740739 inputSchema: {
741740 type: "object",
742741 properties: {
@@ -777,7 +776,7 @@ const TOOLS: readonly Tool[] = [
777776 {
778777 name: "edit",
779778 description:
780 "Give an issue or a pull a new title or body: the pull whose body still says work in progress, the issue whose title no longer says what it asks. Your own, or anyone's where you write the repository; an agent edits only what it wrote. Send what changes; the other stands.",
779 "Give an issue or a pull a new title or body. Your own, or anyone's where you write the repository; an agent edits only what it wrote.",
781780 inputSchema: {
782781 type: "object",
783782 properties: {
@@ -821,7 +820,7 @@ const TOOLS: readonly Tool[] = [
821820 {
822821 name: "read_file",
823822 description:
824 "Read a file, or list a directory, of a repository at a ref (default branch); a file over 5 MB answers tooLarge with its size, a binary one base64. The same as GET /api/v1/repos/:owner/:name/contents.",
823 "Read a file, or list a directory, of a repository at a ref (default branch); over 5 MB answers tooLarge, a binary one base64.",
825824 inputSchema: {
826825 type: "object",
827826 properties: {
@@ -873,7 +872,7 @@ const TOOLS: readonly Tool[] = [
873872 {
874873 name: "asks",
875874 description:
876 "What waits on you, most consequential first: each row names the decision, its options and what happens if nobody acts, and why it ranks where it does (security, money, held-up work, then age). Rows of one kind in one app are collapsed with their item ids. Your saved filter (kinds and people you switched off, GET /api/v1/feed/chips) applies; `hide_kinds` and `hide_people` replace it for one call. Also the forge's own needs (for a site admin) and the last 7 days' asks raised, acted on, dismissed and gone by themselves.",
875 "What waits on you, most consequential first: each row names the decision, options, and the cost of inaction. Your saved filter (`feed_chips`) applies.",
877876 inputSchema: { type: "object", properties: { ...CHIP_ARGS } },
878877 writes: false,
879878 run(ctx, user, args) {
@@ -885,14 +884,14 @@ const TOOLS: readonly Tool[] = [
885884 {
886885 name: "asks_act",
887886 description:
888 "Decide many asks at once (the set and the verb): dismiss or decline, ship, close (with one `reason`, written on each), mute (silence the app's repository in your own On you), or replan (hand an issue back to its app's planner). `items` are ask ids from `asks` (a row's `items`), or `issue:<id>` for something assigned to you. A dry run (the default, and all a read-only token may do) changes nothing: the answer is each row's before and after and the rows refused with why. With dryRun false it acts and returns a receipt; dismiss, decline, mute, close and replan can be undone once, for a day (POST /api/v1/asks/bulk/<id>/undo). Ship is never undone. Dismiss needs only that the ask is yours; the others are checked against your admin of each app.",
887 "Decide many asks at once: dismiss, decline, ship (never undone), close (`reason`), mute, or replan. `items` are ids from `asks`, or `issue:<id>`.",

Low: the cut drops the undo facts: once per action, for a day, via POST /api/v1/asks/bulk/<id>/undo. It also drops that a read-only token may only dry-run. An agent that dismisses by mistake no longer learns it can undo. A short clause like "undoable for a day except ship" would fit in the bytes you have left.

889888 inputSchema: {
outdated

This no longer says that a dry run is the default, or that ship is never undone, and the dryRun schema here has no description. Give dryRun the same 'false to apply (default plans).' as the others, and keep 'ship is never undone'.

890889 type: "object",
891890 properties: {
892891 action: { type: "string", enum: ["dismiss", "decline", "mute", "ship", "close", "replan"] },
893892 items: { type: "array", items: { type: "string" }, maxItems: 100 },
894893 reason: { type: "string", maxLength: 300 },
895 dryRun: { type: "boolean" },
894 dryRun: { type: "boolean", description: "false to apply (default plans)." },
896895 },
897896 required: ["action", "items"],
898897 },
@@ -909,7 +908,7 @@ const TOOLS: readonly Tool[] = [
909908 {
910909 name: "asks_decide",
911910 description:
912 "Let the decider answer a hand-over (a parked issue in `asks`): `item` is the row's `parked:<id>` item. The decider sees the direction, whether you pinned or muted the app, the day's spend against the ceiling, your last ten decisions there and the card's own evidence, and answers set_aside or ask_owner with a reason and a confidence. Only a sure set_aside (70%+) takes the card off your board; anything else stays with you. The answer and the context it saw are in the decision ledger. One card is handed over once in ten minutes. Undo a set_aside within a day with `asks_undecide`.",
911 "Let the decider answer a hand-over (`item` is the row's `parked:<id>`). Only a sure set_aside (70%+) takes the card off your board.",
913912 inputSchema: {
914913 type: "object",
915914 properties: { item: { type: "string", maxLength: 80 } },
@@ -924,7 +923,7 @@ const TOOLS: readonly Tool[] = [
924923 {
925924 name: "asks_proposal",
926925 description:
927 "Act on a wave proposal (an `asks` row with a `proposal`; `id` is its `proposal.id`). `approve` opens its issues with their links, exactly as an unheld plan would; `redirect` hands your `words` to the planner as a new run; `later` hides it until `again` brings it back. Only the app's owner may; a proposal is decided once.",
926 "Act on a wave proposal (an `asks` row with `proposal`): `approve`, `redirect` (`words`), or `later` (until `again`). App owner only.",
928927 inputSchema: {
929928 type: "object",
930929 properties: {
@@ -945,7 +944,7 @@ const TOOLS: readonly Tool[] = [
945944 {
946945 name: "proposal_tokens",
947946 description:
948 "Hand a child (a fork, or a project on another forge) a credential that proposes and nothing more. `mint` (with `child`: lowercase words and hyphens, `origin`: its https address, optionally `childRepo`: owner/name of a fork of this repository) answers with the secret, shown once; the token pushes only `child/<name>/…` branches of this repository and opens pulls from them, and expires in 30 days. `list` shows the ones you minted for this repository; `revoke` (with `id`) ends one. A writer of the repository only; anyone else is told there is no such resource. The same as POST /api/v1/repos/:owner/:name/proposal-tokens.",
947 "Give a child (fork or other forge's project) a propose-only credential: `mint` (`child`, `origin`), `list`, `revoke` (`id`). Writers only.",
949948 inputSchema: {
950949 type: "object",
951950 properties: {
@@ -996,7 +995,7 @@ const TOOLS: readonly Tool[] = [
996995 {
997996 name: "seal",
998997 description:
999 "Seal or unseal an issue or pull: a sealed security report, and the pull that fixes it, is seen only by people who can write the app and its crew; everyone else is told there is no such thing. Unsealing shows it to every reader. A writer's call, recorded in the app's activity.",
998 "Seal or unseal an issue or pull: a sealed one is seen only by people who write the app and its crew. Recorded in the app's activity.",
1000999 inputSchema: {
10011000 type: "object",
10021001 properties: {
@@ -1034,7 +1033,7 @@ const TOOLS: readonly Tool[] = [
10341033 {
10351034 name: "issue_teams",
10361035 description:
1037 "Name the crew teams (design, data-science, security, platform) on an issue: at most two. The pull whose body closes it (`Closes #N`) is sent to review once per named team, and the pull page shows each team's verdict. `teams` replaces the set (an empty list clears it); without `teams`, the issue's teams are read. A writer's call. The same as the Teams card on the issue page.",
1036 "Name the crew teams (design, data-science, security, platform) on an issue: at most two; the closing pull is reviewed once per team.",
10381037 inputSchema: {
10391038 type: "object",
10401039 properties: {
@@ -1066,7 +1065,7 @@ const TOOLS: readonly Tool[] = [
10661065 {
10671066 name: "now",
10681067 description:
1069 "What the crew is doing right now, across your apps: who, on what, since when, and what it has cost so far; a planner that declined the direction and waits for you comes first, as a row with no run and its words in `says`; then each agent pull that waits for your review (no reviewer on the crew, or one that lapsed), with `since`.",
1068 "What the crew is doing right now across your apps: who, on what, since when, cost so far; what waits on your review comes first.",
10701069 inputSchema: { type: "object", properties: {} },
10711070 writes: false,
10721071 run(ctx, user) {
@@ -1181,7 +1180,7 @@ const TOOLS: readonly Tool[] = [
11811180 {
11821181 name: "health",
11831182 description:
1184 "How this forge is: the version and commit it is running (stamped by the release that built it, `dev` when run from source), whether it is draining, and the platform's word on its rollout, with any incidents. A site admin also gets the forge's own vitals.",
1183 "How this forge is: version and commit (`dev` from source), whether it is draining, the platform's word on its rollout, and incidents.",
11851184 inputSchema: { type: "object", properties: {} },
11861185 writes: false,
11871186 run: (ctx, user) => good(readHealth(ctx.app, user)),
@@ -1189,7 +1188,7 @@ const TOOLS: readonly Tool[] = [
11891188 {
11901189 name: "audit",
11911190 description:
1192 "Who can do what, across every app you can see, read from each app's contract.json in git: per app, its identities and every resource × verb answered yes, own or no; or that its contract is unsound, with the problems; or that it has none and its doors are hand-written. Give `repo` for one app's table.",
1191 "Who can do what, across every app you can see, from each app's contract.json: every resource × verb answered yes, own or no.",
11931192 inputSchema: {
11941193 type: "object",
11951194 properties: {
@@ -1237,13 +1236,13 @@ const TOOLS: readonly Tool[] = [
12371236 {
12381237 name: "model_grant",
12391238 description:
1240 "Give a person's account a capped model grant — dollars a month, spent on the forge's own credential; a run past the cap stops and says why — or take it back (`usdPerMonth` 0). A new account has none. A forge admin who is a person, with an admin token, sets it; `dryRun` is true unless you say false. Each change is recorded in the audit.",
1239 "Give a person's account a capped monthly model grant in dollars (`usdPerMonth` 0 takes it back). Person admins only.",
12411240 inputSchema: {
12421241 type: "object",
12431242 properties: {
12441243 username: { type: "string" },
12451244 usdPerMonth: { type: "number", description: "0.01 to 10000; 0 takes it back." },
1246 dryRun: { type: "boolean", description: "Plan only (the default). Send false to set it." },
1245 dryRun: { type: "boolean", description: "false to apply (default plans)." },
12471246 },
12481247 required: ["username", "usdPerMonth"],
12491248 },
@@ -1278,7 +1277,7 @@ const TOOLS: readonly Tool[] = [
12781277 {
12791278 name: "whoami_can",
12801279 description:
1281 "What you can do here as you: your role (guest, member or admin) and that role's row of the roles table, each ability yes or no. Read only.",
1280 "What you can do here as you: your role and that role's row of the roles table, each ability yes or no.",
12821281 inputSchema: { type: "object", properties: {} },
12831282 writes: false,
12841283 run: (ctx, user) => {
@@ -1289,11 +1288,11 @@ const TOOLS: readonly Tool[] = [
12891288 {
12901289 name: "mint_invite",
12911290 description:
1292 "Mint a single-use invite link for someone to sign up with: it expires in `days` (1 to 30, 7 by default) and the link, shown once, is in the answer. A forge admin who is a person, with an admin token, mints it. Each mint is recorded in the audit; the token is not.",
1291 "Mint a single-use invite link to sign up with; it expires in `days` (1–30, default 7) and is shown once. Person admins only.",
12931292 inputSchema: {
12941293 type: "object",
12951294 properties: {
1296 days: { type: "integer", minimum: 1, maximum: 30, description: "Days the link lasts; 7 by default." },
1295 days: { type: "integer", minimum: 1, maximum: 30 },
12971296 },
12981297 },
12991298 writes: true,
@@ -1310,14 +1309,14 @@ const TOOLS: readonly Tool[] = [
13101309 {
13111310 name: "account_role",
13121311 description:
1313 "Make a person a member, or a guest again, with an optional starter model allowance (`usdPerMonth`, dollars a month, only when promoting; none by default). Roles are recorded and shown on /settings/people; nothing is refused because of one yet. A forge admin who is a person, with an admin token, sets it; `dryRun` is true unless you say false. Each change, and any allowance, is recorded in the audit.",
1312 "Make a person a member, or a guest again, with an optional starter allowance (`usdPerMonth`, promoting only).",
13141313 inputSchema: {
13151314 type: "object",
13161315 properties: {
13171316 username: { type: "string" },
13181317 role: { type: "string", enum: ["member", "guest"] },
13191318 usdPerMonth: { type: ["number", "null"], description: "0.01 to 10000; omit or null for none." },
1320 dryRun: { type: "boolean", description: "Plan only (the default). Send false to set it." },
1319 dryRun: { type: "boolean", description: "false to apply (default plans)." },
13211320 },
13221321 required: ["username", "role"],
13231322 },
@@ -1356,14 +1355,14 @@ const TOOLS: readonly Tool[] = [
13561355 {
13571356 name: "model_ceiling",
13581357 description:
1359 "Set an account's daily and per-run model spend ceilings, in dollars (a person's, an agent's or an organization's; an account has none until set). A run that would pass either does not start; one under way stops at the day's. A forge admin who is a person, with an admin token, sets it; `dryRun` is true unless you say false. Each change is recorded in the audit.",
1358 "Set an account's daily and per-run model spend ceilings in dollars; a run that would pass either does not start.",
13601359 inputSchema: {
13611360 type: "object",
13621361 properties: {
13631362 username: { type: "string" },
1364 dailyUsd: { type: ["number", "null"], description: "0.01 to 10000; null (with perRunUsd null) takes the ceilings back." },
1365 perRunUsd: { type: ["number", "null"], description: "0.01 to 10000; null (with dailyUsd null) takes the ceilings back." },
1366 dryRun: { type: "boolean", description: "Plan only (the default). Send false to set it." },
1363 dailyUsd: { type: ["number", "null"], description: "0.01–10000; null with perRunUsd null clears." },
1364 perRunUsd: { type: ["number", "null"], description: "0.01–10000; null with dailyUsd null clears." },
1365 dryRun: { type: "boolean", description: "false to apply (default plans)." },
13671366 },
13681367 required: ["username", "dailyUsd", "perRunUsd"],
13691368 },
@@ -1398,7 +1397,7 @@ const TOOLS: readonly Tool[] = [
13981397 {
13991398 name: "outsiders",
14001399 description:
1401 "Whether people who cannot write an app may direct its crew (start or steer a run by speaking): the owner's \"outsiders may trigger the crew\" switch, off by default. Without `on`, says which way it is set. With `on`, it plans — `dryRun` is true unless you say false — and only an admin of the app, with an admin token and never as an agent, sets it. Each change is recorded in the app's activity.",
1400 "Whether people who cannot write an app may direct its crew (off by default). Without `on`, says which.",
14021401 inputSchema: {
14031402 type: "object",
14041403 properties: {
@@ -1406,7 +1405,7 @@ const TOOLS: readonly Tool[] = [
14061405 on: { type: "boolean" },
14071406 dryRun: {
14081407 type: "boolean",
1409 description: "Plan only (the default). Send false to set it.",
1408 description: "false to apply (default plans).",
14101409 },
14111410 },
14121411 required: ["repo"],
@@ -1437,7 +1436,7 @@ const TOOLS: readonly Tool[] = [
14371436 {
14381437 name: "decisions_by",
14391438 description:
1440 "The standing rule for the decisions a repository asks of its owner (parked hand-over cards): `me` (they wait for the owner), `decider` (the decider answers each, and a repository whose crew is off closes its parked cards by rule, with no model call), or `decider_tell` (the decider answers, and the owner is told about the ones it is unsure of). Without `by`, says how it is set. With `by`, it plans — `dryRun` is true unless you say false — and only an admin of the app, with an admin token and never as an agent, sets it. Each change is recorded in the app's activity.",
1439 "The standing rule for a repository's parked hand-over cards: `me`, `decider`, or `decider_tell`. Without `by`, says how it is set.",
14411440 inputSchema: {
14421441 type: "object",
14431442 properties: {
@@ -1445,7 +1444,7 @@ const TOOLS: readonly Tool[] = [
14451444 by: { type: "string", enum: ["me", "decider", "decider_tell"] },
14461445 dryRun: {
14471446 type: "boolean",
1448 description: "Plan only (the default). Send false to set it.",
1447 description: "false to apply (default plans).",
14491448 },
14501449 },
14511450 required: ["repo"],
@@ -1476,7 +1475,7 @@ const TOOLS: readonly Tool[] = [
14761475 {
14771476 name: "archive",
14781477 description:
1479 "Put a repository away (archive) or bring it back (wake), by name: nothing is deleted. Without `archived`, says which way it is. With `archived` (true to archive, false to wake), it plans — `dryRun` is true unless you say false — and only an admin of the repository, with an admin token and never as an agent, does it. Each change is recorded in the repository's activity. To archive several at once, pass `repos` (owner/name each, at most 50) instead of `repo`: it runs the fleet `archive` operation, a dry run unless `dryRun` is false, and answers each repository's outcome and the `operation` id; the `limb` tool with action `undo` and that `operation` (with `app` naming any of them) reverts it for 24 hours.",
1478 "Archive a repository or wake it (`archived` true/false); nothing is deleted. Plans (dryRun false applies); repository admin token.",

Low: the bulk path's undo (limb action undo with the operation, for 24 hours) is no longer described anywhere an agent would look. repos keeps its own description, so the bulk call itself stays discoverable.

14801479 inputSchema: {
14811480 type: "object",
14821481 properties: {
@@ -1490,7 +1489,7 @@ const TOOLS: readonly Tool[] = [
14901489 archived: { type: "boolean" },
14911490 dryRun: {
14921491 type: "boolean",
1493 description: "Plan only (the default). Send false to do it.",
1492 description: "false to apply (default plans).",
14941493 },
14951494 },
14961495 },
@@ -1523,14 +1522,14 @@ const TOOLS: readonly Tool[] = [
15231522 {
15241523 name: "branches",
15251524 description:
1526 "List a repository's branches, or with `name` and `action` create (from `from`, default the default branch) or delete one, as the branches page does: the default branch is never deleted and only a writer may act.",
1525 "List a repository's branches, or with `name` and `action` create (from `from`) or delete one; the default branch is never deleted.",
15271526 inputSchema: {
15281527 type: "object",
15291528 properties: {
15301529 repo: REPO_ARG,
1531 name: { type: "string", maxLength: 255, description: "The branch to create or delete." },
1530 name: { type: "string", maxLength: 255 },
15321531 action: { type: "string", enum: ["create", "delete"] },
1533 from: { type: "string", maxLength: 255, description: "For create: where it starts." },
1532 from: { type: "string", maxLength: 255 },
15341533 },
15351534 required: ["repo"],
15361535 },
@@ -1585,7 +1584,7 @@ const TOOLS: readonly Tool[] = [
15851584 properties: {
15861585 repo: REPO_ARG,
15871586 ref: { type: "string", maxLength: 255, description: "A branch, tag or commit; default the default branch." },
1588 limit: { type: "integer", minimum: 1, maximum: 100, description: "How many, default 30." },
1587 limit: { type: "integer", minimum: 1, maximum: 100 },
15891588 },
15901589 required: ["repo"],
15911590 },
@@ -1606,7 +1605,7 @@ const TOOLS: readonly Tool[] = [
16061605 {
16071606 name: "probe",
16081607 description:
1609 "Knock on every door of a running app as nobody, a user and an admin, and compare each answer with its contract.json: ok, or the doors that disobey, each as a sentence. Writes nothing; sends `{}` and an id no row has.",
1608 "Knock on every door of a running app as nobody, a user and an admin, and compare each answer with its contract.json. Writes nothing.",
16101609 inputSchema: {
16111610 type: "object",
16121611 properties: { repo: REPO_ARG },
@@ -1640,7 +1639,7 @@ const TOOLS: readonly Tool[] = [
16401639 {
16411640 name: "following",
16421641 description:
1643 "Where a fork stands against the repository it follows: in step, behind, ahead or diverged, the files that need a decision, and what the crew is doing about it.",
1642 "Where a fork stands against the repository it follows: in step, behind, ahead or diverged, and the files that need a decision.",
16441643 inputSchema: {
16451644 type: "object",
16461645 properties: { repo: REPO_ARG },
@@ -1678,54 +1677,51 @@ const TOOLS: readonly Tool[] = [
16781677 {
16791678 name: "spawn",
16801679 description:
1681 "Spawn a child forge: a new forge with the apps you name and the crew's briefs. It plans first — `dryRun` is true unless you say false — and shows what would be copied and what is left behind (secrets, tokens, keys, accounts). Any signed-in person may spawn; a read-only token may plan, not make. Agents cannot. A made child's claim link, the secret that hands it to its person, stays out of this conversation: its person gets a fresh one on /forges. Send `reveal: true` only to have the link here on purpose.",
1680 "Spawn a child forge with the apps you name and the crew's briefs. Plans first (dryRun false makes it).",
16821681 inputSchema: {
16831682 type: "object",
16841683 properties: {
16851684 name: {
16861685 type: "string",
1687 description: "The child's name: a-z, 0-9 and -, at most 39 characters.",
16881686 },
16891687 apps: {
16901688 type: "array",
16911689 items: APP_ARG,
1692 description: "The apps to copy, each as owner/name, with their history.",
16931690 },
16941691 crew: {
16951692 type: "array",
16961693 items: { type: "string" },
1697 description: "The crew members whose briefs go along, by name.",
16981694 },
16991695 direction: {
17001696 type: "string",
17011697 description:
1702 "What the child is for, in your words (at most 2000 characters): its apps' direction, opened as the planner's first issue when the child first boots.",
1698 "What the child is for (at most 2000 characters); the planner's first issue there.",
17031699 },
17041700 fresh: {
17051701 type: "string",
17061702 description:
1707 "One line (at most 200 characters) on a fresh app to start in the child beside the copied ones: it gets a README and a small app that runs.",
1703 "One line (at most 200 characters) on a fresh app to start beside the copied ones.",
17081704 },
17091705 followMode: {
17101706 type: "string",
17111707 enum: ["tell-me", "take-updates"],
17121708 description:
1713 "How the child follows this forge's code: tell-me (default) says when the parent has moved; take-updates takes them.",
1709 "tell-me (default) says when the parent moved; take-updates takes it.",
17141710 },
17151711 size: {
17161712 type: "string",
17171713 enum: ["small", "medium", "large"],
17181714 description:
1719 "How big the child is (optional). Recorded in the plan only: nothing enforces its limits yet.",
1715 "Recorded only; not enforced.",
17201716 },
17211717 keepAccess: {
17221718 type: "boolean",
17231719 description:
1724 "Stay an owner of the child after its person claims it (default false: you leave, and they can invite you back). The claim page says which.",
1720 "Stay an owner after the child is claimed (default false).",
17251721 },
17261722 dryRun: {
17271723 type: "boolean",
1728 description: "Plan only (the default). Send false to make the child.",
1724 description: "false to apply (default plans).",
17291725 },
17301726 reveal: REVEAL_ARG,
17311727 },
@@ -1758,7 +1754,7 @@ const TOOLS: readonly Tool[] = [
17581754 {
17591755 name: "forges",
17601756 description:
1761 "The forges you sent from here (an admin's list): each child's name, address, whom it is for, the apps it was given, when it was sent, whether its person has claimed it, and when anything last happened there. `claimed` is null when the child did not answer.",
1757 "The forges you sent from here (an admin's list): name, address, whom for, apps given, whether claimed, and last activity.",
17621758 inputSchema: { type: "object", properties: {} },
17631759 writes: false,
17641760 async run(ctx, user) {
@@ -1777,11 +1773,11 @@ const TOOLS: readonly Tool[] = [
17771773 {
17781774 name: "claim_link",
17791775 description:
1780 "A new claim link for a child forge nobody has claimed yet; the old link stops working at once. Any admin of this forge may re-issue, never for a claimed child. The link is a secret, so this changes nothing unless you send `reveal: true`: without it, the answer says where the child's person gets a fresh link without it passing through this conversation.",
1776 "A new claim link for an unclaimed child forge; the old one stops at once. Nothing changes unless you send `reveal: true`.",
17811777 inputSchema: {
17821778 type: "object",
17831779 properties: {
1784 name: { type: "string", description: "The child's name, as spawned." },
1780 name: { type: "string" },
17851781 reveal: REVEAL_ARG,
17861782 },
17871783 required: ["name"],
@@ -1799,7 +1795,7 @@ const TOOLS: readonly Tool[] = [
17991795 {
18001796 name: "take_updates",
18011797 description:
1802 "Take the parent's updates into a fork: on a spawned forge's code repository, the parent forge's new commits open as one pull (the default branch never moves); otherwise a fast-forward, or a merge when git merges cleanly. A conflict is refused with its files, and the crew is told: the converge issue is opened (or found) and named in the answer.",
1798 "Take the parent forge's updates into a fork: on a spawned forge they open as one pull; otherwise a fast-forward or clean merge.",
18031799 inputSchema: {
18041800 type: "object",
18051801 properties: { repo: REPO_ARG },
@@ -1851,7 +1847,7 @@ const TOOLS: readonly Tool[] = [
18511847 {
18521848 name: "sever",
18531849 description:
1854 "Cut a spawned forge's link to the forge its code came from (its `forge` repository's owner only). Nothing is deleted or sent; the chain then says it follows nothing since today.",
1850 "Cut a spawned forge's link to the forge its code came from (its `forge` repository's owner only). Nothing is deleted or sent.",
18551851 inputSchema: {
18561852 type: "object",
18571853 properties: { repo: REPO_ARG },
@@ -1872,12 +1868,12 @@ const TOOLS: readonly Tool[] = [
18721868 {
18731869 name: "follow_mode",
18741870 description:
1875 "Choose how a spawned forge follows the forge its code came from: `tell-me` or `take-updates` (its `forge` repository's admin only). Refused on a severed forge. The same words as the radio form on the following page.",
1871 "Choose how a spawned forge follows its parent: `tell-me` or `take-updates` (its `forge` repository's admin only).",
18761872 inputSchema: {
18771873 type: "object",
18781874 properties: {
18791875 repo: REPO_ARG,
1880 mode: { type: "string", enum: [...FOLLOW_MODES], description: "How this forge follows its parent." },
1876 mode: { type: "string", enum: [...FOLLOW_MODES] },
18811877 },
18821878 required: ["repo", "mode"],
18831879 },
@@ -1893,7 +1889,7 @@ const TOOLS: readonly Tool[] = [
18931889 {
18941890 name: "propose_to_parent_forge",
18951891 description:
1896 "Offer this forge's landed change to its parent forge: the default branch is pushed to `child/<name>/…` there and a pull opens, naming the change. Asking again for the same tip answers with the pull already made. A writer only. Needs the parent's proposal token (kept in parent.json); the same words as the Propose button on the following page.",
1892 "Offer this forge's landed change to its parent as a pull on `child/<name>/…`; asking again for the same tip answers with that pull.",
18971893 inputSchema: {
18981894 type: "object",
18991895 properties: { repo: REPO_ARG },
@@ -1924,7 +1920,7 @@ const TOOLS: readonly Tool[] = [
19241920 {
19251921 name: "forge_proposals",
19261922 description:
1927 "What this forge has proposed to its parent forge, newest first: each with its pull at the parent, its number, and its state there (open, merged or closed). `reachable` is false, and the line says \"parent unreachable\", when the parent did not answer and the last known state stands. The same as GET /api/v1/repos/:owner/:name/following/proposals.",
1923 "What this forge proposed to its parent, newest first: pull, number and state there. `reachable` is false if the parent did not answer.",
19281924 inputSchema: {
19291925 type: "object",
19301926 properties: { repo: REPO_ARG },
@@ -1968,7 +1964,7 @@ const TOOLS: readonly Tool[] = [
19681964 {
19691965 name: "family",
19701966 description:
1971 "The forge's family across forges: the forge that sent this one (anyone) and, for a site admin, the forges it sent, each as last heard — reporting, quiet (silent for thirty minutes) or unheard — with version, releases behind, open proposals. The same data as /family and /api/v1/family.",
1967 "The forge's family: the forge that sent this one and, for a site admin, those it sent, with state and version.",
19721968 inputSchema: { type: "object", properties: {} },
19731969 writes: false,
19741970 async run(ctx, user) {
@@ -1978,7 +1974,7 @@ const TOOLS: readonly Tool[] = [
19781974 {
19791975 name: "next_steps",
19801976 description:
1981 "On a forge made for you (spawned), your first three steps and whether each is done: make your everyday account, open your apps, tell the crew something — the same card the home shows. Empty anywhere else, or for anyone but the forge's owner.",
1977 "On a spawned forge, your first three steps and whether each is done. Empty anywhere else, or for anyone but the forge's owner.",
19821978 inputSchema: { type: "object", properties: {} },
19831979 writes: false,
19841980 async run(ctx, user) {
@@ -1989,7 +1985,7 @@ const TOOLS: readonly Tool[] = [
19891985 {
19901986 name: "thought",
19911987 description:
1992 "Where one of your thoughts went: its id (from `tell` without a repo) answers with the planner's placement — the kind (work, amendment, vision, decision, reversal), the issues it opened, amended, deferred or marked superseded, and the one-or-two-line reply, with a question when it was unsure. `placement` is null while the planner has not reached it (within a few minutes). `repeats` and `supersedes` list the earlier thought ids this one says again or takes back; `repeatedBy` and `supersededBy` the later ones that did the same to it.",
1988 "Where one of your thoughts went: its id (from `tell` without a repo) answers with the planner's placement and reply.",
19931989 inputSchema: {
19941990 type: "object",
19951991 properties: { id: { type: "string", maxLength: 64 } },
@@ -2019,7 +2015,7 @@ const TOOLS: readonly Tool[] = [
20192015 {
20202016 name: "asked",
20212017 description:
2022 "What people asked the crew for, in the repositories you can write: by default what has nothing merged yet, oldest first. Each ask with who asked and when, the pulls that close it, one state (waiting, building, shipped, closed), the crew's last activity on it, whether it is stale (not shipped after three days), and whether it is flagged (stale with nothing built: no pull open or merged, and nobody on it). Each pull says when it was opened. `show` is unshipped (default), flagged or all; `order` is oldest (default) or newest; `repo` (owner/name) keeps it to one repository. Also `thoughts`: every thought you sent, newest first, verbatim, with time, source, where the planner put it, and one state (unplaced, placed, building, shipped, superseded — with the later thought that superseded it); `flagged` when nothing merged more than your `thoughtFlagDays` setting (three by default) after it was placed. Yours only. The same data as the /asked page and /api/v1/asked.",
2018 "What people asked the crew for, in repositories you can write: by default what has nothing merged yet, oldest first, with closing pulls.",
20232019 inputSchema: {
20242020 type: "object",
20252021 properties: {
@@ -2049,7 +2045,7 @@ const TOOLS: readonly Tool[] = [
20492045 {
20502046 name: "app_members",
20512047 description:
2052 "The people a private app names: they may enter the app and its previews, and nothing else (no code, no crew, no settings, no terminal). Without `members`, the list. With `members` (usernames; the whole list), the change it would make; with dryRun false, made — by a person who can write the repository, never an agent, with an admin-scope token. Removing a name ends that person's way in at once. The same as the app's settings page and /api/v1/repos/:owner/:name/app/members.",
2048 "The people a private app names: they may enter the app and its previews. Without `members`, the list; with it, plans.",
20532049 inputSchema: {
20542050 type: "object",
20552051 properties: {
@@ -2087,10 +2083,16 @@ const TOOLS: readonly Tool[] = [
20872083 {
20882084 name: "explore",
20892085 description:
2090 "The apps, people (and, for a site admin, spawned forges) you can see, ranked by forks by people, pulls merged this month, hours in use and how lately they shipped, each with its numbers. `query` is one line: `kind:app|person|forge`, `sort:score|forks|ships|use|fresh|name`, and words to match. The same rows as /explore and /api/v1/explore.",
2086 "The apps, people (and for a site admin, spawned forges) you can see, ranked.",
20912087 inputSchema: {
20922088 type: "object",
2093 properties: { query: { type: "string", maxLength: 200 } },
2089 properties: {
2090 query: {
outdated

The query syntax (kind:…, sort:…, words) was the only documentation of this argument, and the schema's query has no description. Put the syntax back as query's description.

2091 type: "string",
2092 maxLength: 200,
2093 description: "Words, plus kind:app|person|forge and sort:score|forks|ships|use|fresh|name.",
2094 },
2095 },
20942096 },
20952097 writes: false,
20962098 async run(ctx, user, args) {
@@ -2104,7 +2106,7 @@ const TOOLS: readonly Tool[] = [
21042106 {
21052107 name: "search",
21062108 description:
2107 "Find issues, pulls, repositories and code you can read, the same results as /search; `q` is the words, `kind` narrows it, `repo` (owner/name) keeps to one repository.",
2109 "Find issues, pulls, repositories and code you can read, as /search; `kind` narrows, `repo` keeps to one repository.",
21082110 inputSchema: {
21092111 type: "object",
21102112 properties: {
@@ -2141,7 +2143,7 @@ const TOOLS: readonly Tool[] = [
21412143 {
21422144 name: "feed",
21432145 description:
2144 "Your home timeline, as the forge's home shows it: tells, replies, runs, changes and looks across the apps you can see, ranked, each with who did it, what it says, why it is there and a link to its thing. Pass `cursor` (next_cursor from the last answer) for the next page. The same items as GET /api/v1/feed. Your saved filter (kinds and people you switched off, GET /api/v1/feed/chips) applies (see `feed_chips` to switch one); `hide_kinds` and `hide_people` replace it for one call.",
2146 "Your home timeline across the apps you can see, ranked. `cursor` pages. Your saved filter (`feed_chips`) applies.",
21452147 inputSchema: {
21462148 type: "object",
21472149 properties: {
@@ -2174,7 +2176,7 @@ const TOOLS: readonly Tool[] = [
21742176 {
21752177 name: "feed_chips",
21762178 description:
2177 "The feed chips you switched off, which `feed` and `asks` leave out: `{kinds, people}` as GET /api/v1/feed/chips. With `kind` (`kind` or `person`), `value` (a chip: reviews, decisions, crew or releases; or a username) and `hidden` it switches one off (true) or back on (false), and answers the new set. Yours alone.",
2179 "The feed chips you switched off, which `feed` and `asks` leave out. With `kind`, `value` and `hidden` it switches one off or on.",
21782180 inputSchema: {
21792181 type: "object",
21802182 properties: {
@@ -2211,12 +2213,12 @@ const TOOLS: readonly Tool[] = [
22112213 {
22122214 name: "make_it_yours",
22132215 description:
2214 "Make an app you can see yours: a fork of its code and history into your account, registered as your own app (its first deploy starts at once), following the original. Its data, variables, secrets, crew and domains are not copied. Plans unless dryRun is false; the answer names the copy, its address, and what is and is not copied. Someone who already has a copy is landed on it. The copy is private unless `visibility` is public (only for a public original); at most 5 copies an account a day.",
2216 "Make an app you can see yours: a fork of its code and history, following the original; data, secrets, crew and domains are not copied.",
22152217 inputSchema: {
22162218 type: "object",
22172219 properties: {
22182220 repo: REPO_ARG,
2219 name: { type: "string", description: "A name for your copy; the original's by default." },
2221 name: { type: "string" },
22202222 visibility: { type: "string", enum: ["private", "public"], description: "Who sees the copy: private by default; public only when the original is." },
22212223 dryRun: { type: "boolean" },
22222224 },
@@ -2261,7 +2263,7 @@ const TOOLS: readonly Tool[] = [
22612263 {
22622264 name: "front_page",
22632265 description:
2264 "The forge's front page as a stranger is shown it: the featured repository with the crew at work on it, then the public apps ranked by forks, ships, use and freshness, each with its numbers. A site admin also gets the picks; with `pinned` (in order; the first public one is featured) and/or `hidden`, each a list of owner/name, and dryRun false, sets them. The same data as /front and /api/v1/front.",
2266 "The forge's front page as a stranger sees it. A site admin also gets the picks, and with `pinned`, `hidden`, `weights` sets them.",
22652267 inputSchema: {
22662268 type: "object",
22672269 properties: {
@@ -2269,7 +2271,7 @@ const TOOLS: readonly Tool[] = [
22692271 hidden: { type: "array", items: { type: "string" } },
22702272 weights: {
22712273 type: "object",
2272 description: "What counts toward an app's place, each 0 to 10: forks, ships, use, fresh. One left out keeps what is set.",
2274 description: "Each 0 to 10: forks, ships, use, fresh.",
22732275 properties: {
22742276 forks: { type: "number", minimum: 0, maximum: 10 },
22752277 ships: { type: "number", minimum: 0, maximum: 10 },
@@ -2307,7 +2309,7 @@ const TOOLS: readonly Tool[] = [
23072309 {
23082310 name: "map",
23092311 description:
2310 "An app's plan: its open issues as ready now, or waiting on others (with what each waits for), the critical path, every edge, where the crew is, and under `parkedTwice` each issue the crew parked twice and handed to the planner, with every reason. The same data as the map page and /api/v1/map.",
2312 "An app's plan: open issues as ready or waiting, the critical path, every edge, where the crew is, and `parkedTwice` issues.",
23112313 inputSchema: {
23122314 type: "object",
23132315 properties: { app: APP_ARG },
@@ -2340,7 +2342,7 @@ const TOOLS: readonly Tool[] = [
23402342 {
23412343 name: "milestones",
23422344 description:
2343 "How far each thing asked for is, read from the dependency tree. With `app` alone: the plan's top-level limbs (the pillars and Later, or the plan's roots), each with done and total, open issues beneath it, how many are building, pulls merged in the last 7 days, its last movement, spend when you may see it, and `trim` when it is a candidate to trim. With `issue`: that milestone opened — who is building what, what is blocked and on what, what merged, an estimate from this week's pace, its own limbs and its leaves. The same numbers as the milestones pages.",
2345 "How far each thing asked for is. With `app`: the plan's top-level limbs with done/total, building and recent merges.",
23442346 inputSchema: {
23452347 type: "object",
23462348 properties: {
@@ -2376,7 +2378,7 @@ const TOOLS: readonly Tool[] = [
23762378 {
23772379 name: "watch",
23782380 description:
2379 "Watch a milestone, or stop (`watch: false`): its progress then shows as one item in your feed, and your phone is told once, when it lands. Yours alone; `milestones` with `issue` says whether you watch it.",
2381 "Watch a milestone, or stop (`watch: false`): its progress shows as one feed item, and your phone is told once, when it lands.",
23802382 inputSchema: {
23812383 type: "object",
23822384 properties: {
@@ -2415,7 +2417,7 @@ const TOOLS: readonly Tool[] = [
24152417 {
24162418 name: "runs",
24172419 description:
2418 "A repository's recent runs, newest first: id, kind, state, the issue or pull number, started and ended; the same rules as the runs page.",
2420 "A repository's runs, newest first: id, kind, state, the issue or pull number, started and ended.",
24192421 inputSchema: {
24202422 type: "object",
24212423 properties: {
@@ -2459,12 +2461,11 @@ const TOOLS: readonly Tool[] = [
24592461 type: "object",
24602462 properties: {
24612463 repo: { ...REPO_ARG, maxLength: 140 },
2462 run: { type: "string", maxLength: 80, description: "The run's id." },
2464 run: { type: "string", maxLength: 80 },
24632465 tail: {
24642466 type: "integer",
24652467 minimum: 1,
24662468 maximum: 2000,
2467 description: "Only the last this many lines.",
24682469 },
24692470 },
24702471 required: ["repo", "run"],
@@ -2501,7 +2502,7 @@ const TOOLS: readonly Tool[] = [
25012502 {
25022503 name: "limb",
25032504 description:
2504 "One action on a whole limb of the plan — a milestone and everything beneath it that only it needs. `defer` moves the limb behind the Later gate, so the crew picks none of it; `resume` brings it back, under `pillar` when you name one; `close` closes every open issue of the limb as not doing, with `reason` said on each; `raise` and `lower` move it one place among its siblings — `under` says whose limbs when it is a limb of several, or `top` — and the crew takes ready work from the higher limb first; the dry run answers with the new `order` and the top of the ready list `before` and `after`. `replan` hands the limb to the planner to re-plan as fewer, larger pieces: the dry run lists every open issue it will be given, applying owes the planner's next pass once and holds the limb from the crew until it answers, and `takeback` withdraws it before the planner starts. `undo` reverses an `operation` by its id. A dry run unless `dryRun` is false: it answers with every row before and after, what the action takes along (`taken`) and what stays because something outside the limb still waits for it (`kept`), and changes nothing. Applying and undoing take an admin token of a person who administers the app, and answer with the receipt.",
2505 "One action on a plan limb: `defer`, `resume` (`pillar`), `close` (`reason`), `raise`/`lower` (`under`), `replan`, `takeback`, `undo`.",
25052506 inputSchema: {
25062507 type: "object",
25072508 properties: {
@@ -2509,7 +2510,6 @@ const TOOLS: readonly Tool[] = [
25092510 issue: {
25102511 type: "integer",
25112512 minimum: 1,
2512 description: "The limb: the number of its issue.",
25132513 },
25142514 action: { type: "string", enum: ["defer", "resume", "close", "raise", "lower", "replan", "takeback", "undo"] },
25152515 reason: {
@@ -2519,16 +2519,15 @@ const TOOLS: readonly Tool[] = [
25192519 pillar: {
25202520 type: "string",
25212521 description:
2522 "For resume: the pillar to put it back under, by name or short form; under none when absent.",
2522 "For resume: the pillar to return it under.",
25232523 },
25242524 under: {
25252525 type: "string",
25262526 description:
2527 "For raise and lower: among whose limbs — `top`, or the number of the milestone; needed only when the limb sits in several places.",
2527 "For raise and lower: `top` or a milestone number; only when the limb sits in several places.",
25282528 },
25292529 operation: {
25302530 type: "string",
2531 description: "For undo: the id of the operation to reverse.",
25322531 },
25332532 dryRun: {
25342533 type: "boolean",
@@ -2596,11 +2595,11 @@ const TOOLS: readonly Tool[] = [
25962595 {
25972596 name: "returns",
25982597 description:
2599 "What the crew's tokens bought over the last days (1–90, default 7), so you can ask before you plan: dollars and runs by state; changes landed; dollars on work that landed and the median per landed change; dollars that landed nothing (closed without a merge, or still open), with failed and cancelled runs and the costliest failure reasons; what a good run costs and takes; and the costliest issues with what came of each; and the same dollars `byPillar`, by the pillar each run's issue serves. Over every app whose owner facts you hold, or `repo` (owner/name). Read-only; an app you cannot write is not found.",
2598 "What the crew's tokens bought over the last days (1–90, default 7): dollars, runs by state, changes landed, cost per landed change.",
26002599 inputSchema: {
26012600 type: "object",
26022601 properties: {
2603 repo: { type: "string", description: "One app, as owner/name; every app of yours when absent." },
2602 repo: { type: "string" },
26042603 days: { type: "integer", minimum: 1, maximum: 90 },
26052604 },
26062605 },
@@ -2621,7 +2620,7 @@ const TOOLS: readonly Tool[] = [
26212620 {
26222621 name: "review",
26232622 description:
2624 "Approve a pull, or request changes with a reason (`reason` is required for changes); approving your own pull, or a verdict without write access, is refused as on the page.",
2623 "Approve a pull, or request changes with a `reason` (required for changes); approving your own pull is refused as on the page.",
26252624 inputSchema: {
26262625 type: "object",
26272626 properties: {
@@ -2677,7 +2676,7 @@ const TOOLS: readonly Tool[] = [
26772676 {
26782677 name: "spend",
26792678 description:
2680 "Report what your session spent on an app's work, when the forge did not run it (a build team, your own assistant): who you are, the model, tokens in and out and/or dollars, and the issue or pull number it was for. Writers and the app's crew only. It is shown beside the crew's own spend in `returns`, never added to it, and no limit reads it.",
2679 "Report what your session spent on an app's work the forge did not run: who, model, tokens and/or dollars, the issue or pull.",
26812680 inputSchema: {
26822681 type: "object",
26832682 properties: {
@@ -2706,7 +2705,7 @@ const TOOLS: readonly Tool[] = [
27062705 {
27072706 name: "usage",
27082707 description:
2709 "What the crew's runs on an app spent over the last days (1–90, default 7): tokens in and out, cost in US dollars, wall time and outcome, by subscription, agent and issue, with the newest runs, and each account's standing in its 5-hour and weekly windows (cap, used, remaining, resets at — each figure read from the provider or inferred from the ledger). The same data as /api/v1/repos/:owner/:name/usage; refused unless you can write the app.",
2708 "What the crew's runs on an app spent over the last days (1–90, default 7): tokens, dollars, time, by agent and issue. Writers only.",
27102709 inputSchema: {
27112710 type: "object",
27122711 properties: {
@@ -2737,7 +2736,7 @@ const TOOLS: readonly Tool[] = [
27372736 {
27382737 name: "labels",
27392738 description:
2740 "Set the labels of an issue or a pull to exactly these (none clears them): only labels the repository has, by a person or agent who can write it.",
2739 "Set the labels of an issue or a pull to exactly these (none clears them): only labels the repository has; writers only.",
27412740 inputSchema: {
27422741 type: "object",
27432742 properties: {
@@ -2833,21 +2832,20 @@ const TOOLS: readonly Tool[] = [
28332832 {
28342833 name: "attach_look",
28352834 description:
2836 "Attach a look to an open pull: a PNG or JPEG (at most 5 MB, base64 in `image`) taken at a stated width, `phone` or `desktop`, with one line in `says` about what it shows. It appears beside the crew's own looks on the change's page, for people who write the app. For a person who can write the app: an agent is refused, since the crew's looks come from the preview. It plans first — `dryRun` is true unless you say false — and checks the picture.",
2835 "Attach a look to an open pull: a PNG or JPEG (at most 5 MB, base64 in `image`) at `phone` or `desktop` width, with one line in `says`.",
28372836 inputSchema: {
28382837 type: "object",
28392838 properties: {
28402839 app: APP_ARG,
2841 pull: { type: "integer", description: "The pull's number." },
2840 pull: { type: "integer" },
28422841 width: { type: "string", enum: ["phone", "desktop"] },
28432842 says: {
28442843 type: "string",
2845 description: "One line, at most 200 characters, saying what it shows.",
28462844 },
2847 image: { type: "string", description: "The PNG or JPEG, base64." },
2845 image: { type: "string" },
28482846 dryRun: {
28492847 type: "boolean",
2850 description: "Check only (the default). Send false to attach it.",
2848 description: "false to apply (default plans).",
28512849 },
28522850 },
28532851 required: ["app", "pull", "width", "says", "image"],
@@ -2888,23 +2886,22 @@ const TOOLS: readonly Tool[] = [
28882886 {
28892887 name: "hold",
28902888 description:
2891 "Hold a pull, or let it go. While a pull is held, no rule sends it to the crew's reviewer and no rule lands it; a person may still merge it themselves. For a person who can write the app: an agent is refused. Give `why` in one line to hold; `release: true` lets it go. It plans first — `dryRun` is true unless you say false — and says what would happen.",
2889 "Hold a pull, or let it go: while held, nothing reviews or lands it. App writers only. `why` holds; `release: true` lets go.",
28922890 inputSchema: {
28932891 type: "object",
28942892 properties: {
28952893 app: APP_ARG,
2896 pull: { type: "integer", description: "The pull's number." },
2894 pull: { type: "integer" },
28972895 why: {
28982896 type: "string",
28992897 description: "To hold: why, in one line. It shows on the pull.",
29002898 },
29012899 release: {
29022900 type: "boolean",
2903 description: "True to let the pull go instead of holding it.",
29042901 },
29052902 dryRun: {
29062903 type: "boolean",
2907 description: "Plan only (the default). Send false to do it.",
2904 description: "false to apply (default plans).",
29082905 },
29092906 },
29102907 required: ["app", "pull"],
@@ -2963,7 +2960,7 @@ const TOOLS: readonly Tool[] = [
29632960 },
29642961 {
29652962 name: "plan",
2966 description: `Open several small issues and link them in one call: all of them or none. Each item has a key, a title, a body and optionally blockedBy, a list of keys of this plan or numbers of issues already open. Where the app has pillars (umbrella issues titled like "P0 Trust: …"), each item also says what it is for: pillar, the one it serves, or later: true with why, one sentence, which puts it behind the Later gate instead of on the ready list; an item that says neither is refused. A bad link (loop, unknown key) refuses the whole plan and names the item. At most ${PLAN_MAX} issues. Answers with the numbers opened, by key, what is ready now, and what waits for later. Where the app has "plans need a nod" on, the same checks run and one proposal is stored (optional title and why for the whole plan); nothing is opened, and it answers status proposed with the proposal id.`,
2963 description: `Open and link several small issues, all or none (max ${PLAN_MAX}). Items: key, title, body, optional blockedBy; with pillars, \`pillar\` or \`later: true\` with \`why\`.`,
29672964 inputSchema: {
29682965 type: "object",
29692966 properties: {
@@ -2979,14 +2976,12 @@ const TOOLS: readonly Tool[] = [
29792976 properties: {
29802977 key: {
29812978 type: "string",
2982 description: "A short name for this item, unique in the plan.",
29832979 },
29842980 title: { type: "string" },
29852981 body: { type: "string" },
29862982 blockedBy: {
29872983 type: "array",
29882984 items: { type: ["string", "integer"] },
2989 description: "Keys in this plan, or numbers of open issues.",
29902985 },
29912986 pillar: {
29922987 type: "string",
@@ -2996,12 +2991,12 @@ const TOOLS: readonly Tool[] = [
29962991 later: {
29972992 type: "boolean",
29982993 description:
2999 "True when this serves no pillar now: it opens behind the Later gate, off the ready list. Needs why.",
2994 "True when no pillar: opens behind the Later gate. Needs why.",
30002995 },
30012996 why: {
30022997 type: "string",
30032998 description:
3004 "With later: one sentence saying why it waits. It opens the issue's body.",
2999 "With later: one sentence why.",
30053000 },
30063001 },
30073002 required: ["key", "title"],
@@ -3046,13 +3041,13 @@ const TOOLS: readonly Tool[] = [
30463041 {
30473042 name: "access",
30483043 description:
3049 "What outside agents tried: the forge's own request log, redacted, for a forge admin. Non-browser requests to public paths (/issues, /api/, /mcp, /.well-known/, /llms.txt, sign-in, sign-up), newest first, at most 500: time, host, method, path without query, status, duration in ms, user agent, whether a credential came with it, and a daily hash of the caller (never an address). `since` is an ISO time (the last day when absent; at most 14 days back), `ua` a part of the user agent, `path` the start of the path. Needs a write-scope token. The same as Settings → Access.",
3044 "What outside agents tried: the redacted log of non-browser requests to public paths, newest first, at most 500, for a forge admin. Needs a write-scope token.",
30503045 inputSchema: {
30513046 type: "object",
30523047 properties: {
3053 since: { type: "string", maxLength: 40 },
3054 ua: { type: "string", maxLength: 100 },
3055 path: { type: "string", maxLength: 200 },
3048 since: { type: "string", maxLength: 40, description: "ISO time; default the last day, at most 14 days back." },
3049 ua: { type: "string", maxLength: 100, description: "Part of the user agent." },
outdated

since (ISO time, default the last day, at most 14 days back), ua (part of the user agent) and path (prefix) are now undocumented. Give each schema property a short description. Also restore that this needs a write-scope token.

3050 path: { type: "string", maxLength: 200, description: "Path prefix." },
30563051 },
30573052 },
30583053 writes: true,
@@ -3074,7 +3069,7 @@ const TOOLS: readonly Tool[] = [
30743069 {
30753070 name: "capacity",
30763071 description:
3077 "The forge's capacity and guards, live: crew runs at once (crew.concurrency), checks at once (checks.concurrency), test shards per check (checks.shards), whether a cell's undeclared egress is refused or reported (cells.egress), and whether new accounts may create repositories and forks (guard_new_accounts: on refuses accounts younger than 7 days, off is the default), and whether the platform edge's sign-in is read (signin.edge: on is the default, off ignores the edge's identity on every door). Without `key`, each setting, its value and where it comes from, and the recent changes. With `key` and `value` (\"\" goes back to the boot default), the change it would make; with dryRun false, made, by a forge admin with an admin-scope token, never an agent. A change is honoured by the next run, check or cell, never by a restart. The same as Settings → Capacity and /api/v1/admin/capacity.",
3072 "The forge's capacity and guards, live. With `key` and `value` (\"\" restores default), plans; dryRun false makes it.",
30783073 inputSchema: {
30793074 type: "object",
30803075 properties: {
@@ -3083,7 +3078,7 @@ const TOOLS: readonly Tool[] = [
30833078 enum: ["crew.concurrency", "checks.concurrency", "checks.shards", "cells.egress", "guard_new_accounts", "signin.edge"],
30843079 },
30853080 value: { type: "string", maxLength: 16 },
3086 dryRun: { type: "boolean", description: "Plan only (the default). Send false to change it." },
3081 dryRun: { type: "boolean", description: "false to apply (default plans)." },
30873082 },
30883083 },
30893084 writes: true,
@@ -3113,7 +3108,7 @@ const TOOLS: readonly Tool[] = [
31133108 {
31143109 name: "scorecard",
31153110 description:
3116 "The crew's scorecard, for a forge admin: per agent (username), its figures over all time (`all`) and the last 30 days (`recent`) — pulls opened and landed, mean review rounds, red check rate, reverts, mean ms to land (a ratio over nothing is null) — and `splits` by model and prompt. Read-only. The same as /settings/crew and /api/v1/crew/scorecard.",
3111 "Crew scorecard (forge admin): per agent, all time and 30 days (landed, review rounds, red rate, reverts), by model and prompt.",
31173112 inputSchema: { type: "object", properties: {} },
31183113 writes: false,
31193114 adminOnly: true,
@@ -3125,7 +3120,7 @@ const TOOLS: readonly Tool[] = [
31253120 {
31263121 name: "flow",
31273122 description:
3128 "The forge's pipeline, for a forge admin: ready, building, check queue, checking, review, approved, landed and live, each with how many wait, how many are being worked, when the oldest waiting one entered (oldestWaiting) and what limits it (capacity), and one sentence (says) naming the stage whose oldest has waited longest — the bottleneck. Beside it, throughput: per stage how many left it in the last hour (lastHour), in 24 hours (day) and in each of the last 24 hours (hours, 0 the last), the median ask→live in ms over the last 24 hours, and its own sentence (says). Also `conflicted`: pulls main moved under that the crew is bringing up to date, oldest first, with a sentence (says). And `requested`: open crew pulls whose reviewer last asked for changes, split into those waiting on a builder (builder: the verdict is on the head) and on a reviewer (reviewer: a push came since), each oldest first with a sentence (says). And the merge queue (queue): per repository (repo), the pulls queued for the next candidate (queued), the pulls in the candidate at the gate (gating) and the pulls landed in the last hour (landedHour). Read-only; the knobs are the `capacity` tool. The same as the flow panel on /boards.",
3123 "The forge's pipeline for a forge admin: per stage counts, `oldestWaiting`, `capacity`, `says` naming the bottleneck, merge `queue`.",
31293124 inputSchema: { type: "object", properties: {} },
31303125 writes: false,
31313126 run(ctx, user) {
@@ -3139,7 +3134,7 @@ const TOOLS: readonly Tool[] = [
31393134 {
31403135 name: "system",
31413136 description:
3142 "The machine the forge runs on, now: per-core CPU, temperatures, load, memory, disk per mount, network, and every process grouped by what launched it (which check, which app), each group with CPU cores, memory and age; and the test files running in more than one check at once (duplicates). For a forge admin, or an agent granted telemetry:read — whose view cuts out private repositories' checks and apps. Read-only; the same as /system and /api/v1/telemetry/system.",
3137 "The machine the forge runs on, now: CPU, load, memory, disk, network, processes. Forge admin or telemetry:read.",
31433138 inputSchema: { type: "object", properties: {} },
31443139 writes: false,
31453140 run(ctx, user) {
@@ -3153,7 +3148,7 @@ const TOOLS: readonly Tool[] = [
31533148 {
31543149 name: "contribute",
31553150 description:
3156 "For any agent that arrives: what this forge is, where its bottleneck is right now, the open issues in public repositories labelled “help wanted” that welcome outside work, and how to propose (fork, pull, the gate it must pass). Open to every caller.",
3151 "For any agent that arrives: what this forge is, its bottleneck, open “help wanted” issues in public repositories, and how to propose.",
31573152 inputSchema: { type: "object", properties: {} },
31583153 writes: false,
31593154 run: (ctx) => good(welcome(ctx.app)),
@@ -3161,13 +3156,13 @@ const TOOLS: readonly Tool[] = [
31613156 {
31623157 name: "limits",
31633158 description:
3164 "Every limit the forge's owner can hit, with its value, bounds and the reason it is there (people per app, pins, hidden repositories, budget ceilings, daily shares, sign-in attempts, crew and check concurrency). Without `key`, each limit and the recent changes. With `key` and `value` (\"\" goes back to the default), the change it would make; with dryRun false, made, by a forge admin with an admin-scope token, never an agent. Honoured by the next use, never by a restart. The same as /admin/limits and /api/v1/admin/limits.",
3159 "Every limit the forge's owner can hit, with value, bounds and reason. With `key` and `value` (\"\" restores default), plans.",
31653160 inputSchema: {
31663161 type: "object",
31673162 properties: {
31683163 key: { type: "string", enum: [...LIMIT_KEYS] },
31693164 value: { type: "string", maxLength: 16 },
3170 dryRun: { type: "boolean", description: "Plan only (the default). Send false to change it." },
3165 dryRun: { type: "boolean", description: "false to apply (default plans)." },
31713166 },
31723167 },
31733168 writes: true,
@@ -3197,7 +3192,7 @@ const TOOLS: readonly Tool[] = [
31973192 {
31983193 name: "teams",
31993194 description:
3200 "Crew teams, for a forge admin who is a person: each team's charter, dial (its share, a whole number from 0 to 100; all dials together may not pass 100) and member agents. Without `slug`, the teams. With `slug` and any of `charter`, `dial`, `members` (agent ids; replaces the list), the change it would make; with dryRun false, made, with an admin-scope token. Nothing reads the dials yet. The same as /settings/teams and /api/v1/admin/teams.",
3195 "Crew teams for person admins: charter, dial (0–100) and member agents.",
32013196 inputSchema: {
32023197 type: "object",
32033198 properties: {
@@ -3205,7 +3200,7 @@ const TOOLS: readonly Tool[] = [
32053200 charter: { type: "string", maxLength: 2000 },
32063201 dial: { type: "integer", minimum: 0, maximum: 100 },
32073202 members: { type: "array", maxItems: 500, items: { type: "string", maxLength: 64 } },
3208 dryRun: { type: "boolean", description: "Plan only (the default). Send false to change it." },
3203 dryRun: { type: "boolean", description: "false to apply (default plans)." },
32093204 },
32103205 },
32113206 writes: true,
@@ -3247,7 +3242,7 @@ const TOOLS: readonly Tool[] = [
32473242 {
32483243 name: "cloud_accounts",
32493244 description:
3250 "Cloud accounts, for a forge admin: each attached provider token (github, cloudflare, vercel) with what it is used for, when it was verified and last used, whether runs may write with it, and today's vendor calls (UTC day) — then every audit row of today, newest first. Never a token value. Needs a write-scope token. The same as Settings → Cloud accounts.",
3245 "Cloud accounts for a forge admin: each attached provider token with use, verification, write permission and today's vendor calls.",
32513246 inputSchema: { type: "object", properties: {} },
32523247 // A read, but marked a write so a `ro` token never lists the accounts; it
32533248 // is a forge admin's and the audit rows name every vendor call.
@@ -3263,7 +3258,7 @@ const TOOLS: readonly Tool[] = [
32633258 {
32643259 name: "cloud_account",
32653260 description:
3266 "Change a cloud account, as a forge admin with an admin-scope token. `verb` is attach (checks `value` with the provider's read-only calls, then seals it), verify (re-checks the sealed token), rotate (checks a new `value`, then replaces it), rename (to `to`; refused when the account or the secret CLOUD_<PROVIDER>_<NAME> already exists), remove, allow_writes (`allowed` true or false: whether a run that declares writes may hold the token) or audit (a read-only vendor audit, one issue per finding in `repo`). `provider` is github, cloudflare or vercel; `name` is lowercase letters, digits and underscores. The answer never contains the token. The same as Settings → Cloud accounts.",
3261 "Change a cloud account (forge admin, admin-scope token). `verb`: attach, verify, rotate, rename, remove, allow_writes or audit.",
32673262 inputSchema: {
32683263 type: "object",
32693264 properties: {
@@ -3271,7 +3266,7 @@ const TOOLS: readonly Tool[] = [
32713266 provider: { type: "string", enum: [...PROVIDERS] },
32723267 name: { type: "string", maxLength: 32 },
32733268 value: { type: "string", maxLength: 4096, description: "The token, for attach and rotate." },
3274 to: { type: "string", maxLength: 32, description: "The new name, for rename." },
3269 to: { type: "string", maxLength: 32 },
32753270 allowed: { type: "boolean", description: "For allow_writes: whether runs that declare writes may hold the token." },
32763271 repo: { type: "string", maxLength: 200, description: "For audit: owner/name, where the findings are filed as issues." },
32773272 },
@@ -3318,13 +3313,13 @@ const TOOLS: readonly Tool[] = [
33183313 {
33193314 name: "quotas",
33203315 description:
3321 "What one account may own: per-account quotas for repositories and apps, each a positive whole number or no limit (null, the default). Nothing enforces them yet. Without `key`, each quota and the recent changes. With `key` and `value` (\"\" clears it), the change it would make; with dryRun false, made, by a forge admin with an admin-scope token, never an agent. The same as /admin/limits (Account quotas) and /api/v1/admin/settings.",
3316 "Per-account repository and app quotas (number or null; not enforced yet). With `key` and `value` (\"\" clears), plans; dryRun false makes it.",
33223317 inputSchema: {
33233318 type: "object",
33243319 properties: {
33253320 key: { type: "string", enum: [...QUOTA_KEYS] },
33263321 value: { type: "string", maxLength: 16 },
3327 dryRun: { type: "boolean", description: "Plan only (the default). Send false to change it." },
3322 dryRun: { type: "boolean", description: "false to apply (default plans)." },
33283323 },
33293324 },
33303325 writes: true,
@@ -3356,12 +3351,12 @@ const TOOLS: readonly Tool[] = [
33563351 {
33573352 name: "signup",
33583353 description:
3359 "Who may sign up: open, invite or closed (default open). open lets anyone make an account; invite lets in only a person with an open invite link (mint_invite), spent with the account; closed refuses every new account. The forge's first account is never refused. Without `mode`, the setting and the recent changes. With `mode`, the change it would make; with dryRun false, made, by a forge admin with an admin-scope token, never an agent. The same as /admin/limits (Signup) and PATCH /api/v1/admin/settings.",
3354 "Who may sign up: open (default); invite lets in only a person with an open invite link; closed refuses every new account. With `mode`, plans.",
33603355 inputSchema: {
33613356 type: "object",
33623357 properties: {
33633358 mode: { type: "string", enum: [...SIGNUP_MODES] },
3364 dryRun: { type: "boolean", description: "Plan only (the default). Send false to change it." },
3359 dryRun: { type: "boolean", description: "false to apply (default plans)." },
33653360 },
33663361 },
33673362 writes: true,
@@ -3389,14 +3384,14 @@ const TOOLS: readonly Tool[] = [
33893384 {
33903385 name: "boards",
33913386 description:
3392 "The forge's boards, for a forge admin who is a person (an agent is refused): each board's slug, title, range, refresh and panels. The same as /boards and GET /api/v1/boards. With `slug`, that one board. With `slug` and `panel` (its place from 0), that panel's data: {from, to, step, rows}, bucketed to `step` (300, 5m, 1h, 1d) or one the panel picks, never cut short; `from` and `to` (dates) stand in for the panel's. The same as GET /api/v1/boards/:slug/panels/:id.",
3387 "The forge's boards for person admins: slug, title, range, refresh and panels.",
33933388 inputSchema: {
33943389 type: "object",
33953390 properties: {
3396 slug: { type: "string", maxLength: 40, description: "One board, by slug." },
3391 slug: { type: "string", maxLength: 40 },
33973392 panel: { type: "integer", minimum: 0, maximum: 23, description: "One panel's data, by its place on the board from 0." },
3398 from: { type: "string", maxLength: 64, description: "With panel: the window's start, a date." },
3399 to: { type: "string", maxLength: 64, description: "With panel: the window's end, a date." },
3393 from: { type: "string", maxLength: 64 },
3394 to: { type: "string", maxLength: 64 },
34003395 step: { type: "string", maxLength: 16, description: "With panel: seconds per bucket, or a count of s, m, h or d." },
34013396 },
34023397 },
@@ -3431,12 +3426,12 @@ const TOOLS: readonly Tool[] = [
34313426 {
34323427 name: "board_write",
34333428 description:
3434 "Write a whole board under a slug (a-z, 0-9 and -): made if new, replaced if there. `board` is {title, range: 1h|24h|7d|30d|all, refresh: off|10s|1m, panels: [{title, chart: line|bar|area|number|table|heat, query, w, h}]}. A panel whose query cannot run is refused. For a forge admin who is a person; an agent is refused. The same as PUT /api/v1/boards/:slug.",
3429 "Write a whole board under a slug (made or replaced). `board` is {title, range, refresh, panels: [{title, chart, query, w, h}]}.",
34353430 inputSchema: {
34363431 type: "object",
34373432 properties: {
34383433 slug: { type: "string", maxLength: 40 },
3439 board: { type: "object", description: "The whole board." },
3434 board: { type: "object" },
34403435 },
34413436 required: ["slug", "board"],
34423437 },
@@ -3450,7 +3445,7 @@ const TOOLS: readonly Tool[] = [
34503445 {
34513446 name: "board_pins",
34523447 description:
3453 "The panels pinned to the caller's own home, for a forge admin who is a person (an agent is refused). With no `op`, lists them in order. With `op` pin, unpin or move, changes them: `slug` and `title` name a panel of a board; `by` is -1 or 1 for move. The same as GET and POST /api/v1/boards/pins.",
3448 "The panels pinned to the caller's home (person admins). No `op` lists them.",
34543449 inputSchema: {
34553450 type: "object",
34563451 properties: {
@@ -3481,7 +3476,7 @@ const TOOLS: readonly Tool[] = [
34813476 {
34823477 name: "board_delete",
34833478 description:
3484 "Delete a board by slug (the commit stays in the boards repo's history). For a forge admin who is a person; an agent is refused. The same as DELETE /api/v1/boards/:slug.",
3479 "Delete a board by slug (history keeps it). Person admins only.",
34853480 inputSchema: {
34863481 type: "object",
34873482 properties: { slug: { type: "string", maxLength: 40 } },
@@ -3636,7 +3631,7 @@ function rpcOne(
36363631 const REPORT_TOOL = {
36373632 name: "report",
36383633 description:
3639 "Report a problem in a public repository, without an account. Plain text. A person who keeps the repository reads it before anything acts on it.",
3634 "Report a problem in a public repository, without an account. Plain text; a person reads it first.",
36403635 inputSchema: {
36413636 type: "object",
36423637 properties: {
test/mcp-size.test.ts added +72
@@ -0,0 +1,72 @@
1 /**
2 * Every harness session pays for `tools/list` before it does anything, so
3 * the answer has a ceiling: past it, the longest descriptions are named so
4 * the next cut is obvious.
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createToken } from "../src/core/tokens.ts";
12 import { createUser } from "../src/core/users.ts";
13 import { serve, type Servers } from "../src/http/server.ts";
14
15 const CEILING = 40_000;

This head measures 39,986 bytes, 14 bytes under this ceiling. Trim until there is real headroom, at least 1–2 KB, so the next unrelated change on main does not go red here.

16
17 let dataDir: string;
18 let app: App;
19 let servers: Servers;
20 let token: string;
21
22 beforeAll(async () => {
23 dataDir = await mkdtemp(join(tmpdir(), "forge-mcp-size-"));
24 app = createApp({ dataDir, port: 0 });
25 servers = await serve(app);
26 // The first user is a forge admin, who is listed every tool: the largest answer.
27 const made = await createUser(app.db, {
28 origin: "seed",
29 trusted: true,
30 username: "alice",
31 email: "[email protected]",
32 password: "a-good-passphrase",
33 adminIfFirst: true,
34 });
35 if (!made.ok) throw new Error(made.error);
36 token = createToken(app.db, made.value.id, "rw", "rw").token;
37 }, 30_000);
38
39 afterAll(async () => {
40 servers.stop();
41 app.close();
42 await rm(dataDir, { recursive: true, force: true });
43 });
44
45 test("tools/list stays under 40 KB", async () => {
46 const res = await fetch(`${servers.url}/mcp`, {
47 method: "POST",
48 headers: {
49 "content-type": "application/json",
50 accept: "application/json, text/event-stream",
51 authorization: `Bearer ${token}`,
52 },
53 body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
54 });
55 const text = await res.text();
56 const bytes = Buffer.byteLength(text);
57 const tools = (JSON.parse(text) as any).result.tools as {
58 name: string;
59 description: string;
60 }[];
61 const longest = tools
62 .map((t) => `${t.name} (${Buffer.byteLength(t.description)} bytes)`)
63 .sort(
64 (a, b) =>
65 Number(/\((\d+) /.exec(b)![1]) - Number(/\((\d+) /.exec(a)![1]),
66 )
67 .slice(0, 5);
68 expect(
69 bytes,
70 `tools/list is ${bytes} bytes, over ${CEILING}; the five longest descriptions: ${longest.join(", ")}`,
71 ).toBeLessThanOrEqual(CEILING);
72 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs