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

MCP tools/list's argument descriptions say it in a few words, the detail in docs/MCP.md, and the answer shrinks by 1,000 bytes #5048

Merged opened this

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

Merged

scout/forge:scout/5032-mcp-tools-list-s-argument-descriptions-s → main 6 commits fork 8 threads · 8 unresolved

  • Merged. scout/5032-mcp-tools-list-s-argument-descriptions-s was merged into main as a13e379.

Review

Sign in to review this change.
3 files changed +82 −32
docs/MCP.md +20
@@ -122,6 +122,26 @@ a failure names the bytes over and the longest descriptions.
122122 Answers are JSON in a text block; most carry a `says` sentence short enough
123123 to relay as it is.
124124
125 ## Arguments, in full
126
127 `tools/list` keeps every argument description to 60 bytes (`test/mcp-size.test.ts`
128 enforces it); `dryRun` reads `false to apply` on every tool that takes it. The
129 longer sentences:
130
131 - `dryRun`: plans unless it is `false`; anything else changes nothing. `limb` applying takes an admin token.
132 - `tell.from`: a path on this forge the issue links to, like `/alice/notes/pulls/3`. `tell.context`: where this was told from, for the builder.
133 - `spawn.reveal`, `claim_link.reveal`: put the live claim link in the answer (default false); it is a secret, only when its person reads it. `spawn.fresh`: a fresh app beside the copied ones. `spawn.followMode`: `tell-me` (default) or `take-updates`. `spawn.size`: recorded only, not enforced. `spawn.keepAccess`: stay an owner once claimed.
134 - `commits.ref`: branch, tag or commit; the default branch when absent. `report.page`: the http(s) address where the problem was seen. `plan.issues.pillar`: the pillar served, as `"P0"` or `"P0 Trust"`; `later: true` means no pillar and needs `why`.
135 - `explore.query`: words, plus `kind:app|person|forge` and `sort:score|forks|ships|use|fresh|name`.
136 - `archive.repos`: several repositories, as `owner/name`, to archive in one operation.
137 - `milestones.issue`: the number of an issue that waits for others; the limbs when absent. `watch.issue` is the same number; `watch.watch` false stops watching, anything else watches.
138 - `limb.reason` (close): why it is not being done, in one sentence. `limb.pillar` (resume): the pillar to return it under. `limb.under` (raise, lower): `top` or a milestone number, only when the limb sits in several places.
139 - `asks.hide_kinds`, `asks.hide_people`, `feed.*`: leave out these kinds or usernames; replaces your saved filter.
140 - `audit.repo`: one app, as `owner/name`; every app you can see when absent.
141 - `model_ceiling.dailyUsd`/`perRunUsd`: 0.01–10000; both null clears; one null is refused. `model_grant.usdPerMonth`: 0 takes the grant back. `account_role.usdPerMonth`: omit or null for none.
142 - `hold.why`: why, in one line; it shows on the pull. `runs.limit`: how many, 1 to 50; 20 when absent.
143 - `boards.step`: with `panel`, seconds per bucket, or a count of `s`, `m`, `h` or `d`. `access.since`: ISO time; default a day, max 14 days.
144
125145 ## Connecting
126146
127147 Mint a token under **Settings → Access tokens** (`rw` to act, `ro` to watch),
src/http/mcp.ts +43 −32
@@ -320,13 +320,13 @@ const CHIP_ARGS = {
320320 type: "array",
outdated

Low: hide_kinds / hide_people dropped "replaces your saved filter". That is a side effect of the write: a model reading "Kinds to hide." may send one kind expecting to add to the saved filter and wipe the rest. Replaces saved hidden kinds. fits in 60 bytes. Worth doing in a follow-up; it doesn't block this pull.

321321 items: { type: "string", enum: [...KIND_CHIPS] },
322322 maxItems: 50,
323 description: "Kinds to leave out; replaces your saved filter.",
323 description: "Kinds to hide.",
324324 },
325325 hide_people: {
326326 type: "array",
327327 items: { type: "string", maxLength: 39 },
328328 maxItems: 50,
329 description: "Usernames to leave out; replaces your saved filter.",
329 description: "Usernames to hide.",
330330 },
331331 } as const;
332332
@@ -435,7 +435,7 @@ const APP_ARG = { type: "string" } as const;
435435 const REVEAL_ARG = {
436436 type: "boolean",
437437 description:
438 "Put the live claim link here (default false); only when its person reads it.",
438 "true puts the secret claim link here, for its person only.",
439439 } as const;
440440 const LINK_KEPT =
441441 "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.";
@@ -671,12 +671,12 @@ const TOOLS: readonly Tool[] = [
671671 type: "string",
672672 maxLength: 512,
673673 description:
674 "A path on this forge the issue links to, like /alice/notes/pulls/3.",
674 "Forge path to link, as /a/b/pulls/3.",
675675 },
676676 context: {
677677 type: "object",
678678 description:
679 "Where this was told from, for the builder.",
679 "Where it was told.",
680680 properties: {
681681 page: { type: "string" },
682682 pull: { type: "integer", minimum: 1 },
@@ -1543,7 +1543,7 @@ const TOOLS: readonly Tool[] = [
15431543 repo: {
15441544 ...REPO_ARG,
15451545 description:
1546 "One app, as owner/name; every app you can see when absent.",
1546 "owner/name; all when absent.",
15471547 },
15481548 },
15491549 },
@@ -1589,7 +1589,7 @@ const TOOLS: readonly Tool[] = [
15891589 type: "object",
15901590 properties: {
15911591 username: { type: "string" },
1592 usdPerMonth: { type: "number", description: "0.01 to 10000; 0 takes it back." },
1592 usdPerMonth: { type: "number", description: "0.01–10000; 0 revokes." },
15931593 dryRun: { type: "boolean", description: "false to apply" },
15941594 },
15951595 required: ["username", "usdPerMonth"],
@@ -1693,7 +1693,7 @@ const TOOLS: readonly Tool[] = [
16931693 properties: {
16941694 username: { type: "string" },
16951695 role: { type: "string", enum: ["member", "guest"] },
1696 usdPerMonth: { type: ["number", "null"], description: "0.01 to 10000; omit or null for none." },
1696 usdPerMonth: { type: ["number", "null"], description: "0.01–10000 or none." },
16971697 dryRun: { type: "boolean", description: "false to apply" },
16981698 },
16991699 required: ["username", "role"],
@@ -1947,7 +1947,7 @@ const TOOLS: readonly Tool[] = [
19471947 type: "array",
19481948 items: { type: "string" },
19491949 maxItems: MAX_ARCHIVE_REPOS,
1950 description: "Several repositories, as owner/name, to archive in one operation.",
1950 description: "owner/name list, one operation.",
19511951 },
19521952 archived: { type: "boolean" },
19531953 dryRun: {
@@ -2377,29 +2377,29 @@ const TOOLS: readonly Tool[] = [
23772377 direction: {
23782378 type: "string",
23792379 description:
2380 "What the child is for; its first issue.",
2380 "Its purpose; first issue.",
23812381 },
23822382 fresh: {
23832383 type: "string",
23842384 description:
2385 "A fresh app beside the copied ones.",
2385 "A fresh app too.",
23862386 },
23872387 followMode: {
23882388 type: "string",
23892389 enum: ["tell-me", "take-updates"],
23902390 description:
2391 "tell-me (default) or take-updates.",
2391 "tell-me or take-updates.",
23922392 },
23932393 size: {
23942394 type: "string",
23952395 enum: ["small", "medium", "large"],
23962396 description:
2397 "Recorded only; not enforced.",
2397 "Recorded only.",
23982398 },
23992399 keepAccess: {
24002400 type: "boolean",
24012401 description:
2402 "Stay an owner once claimed.",
2402 "Stay an owner.",
24032403 },
24042404 dryRun: {
24052405 type: "boolean",
@@ -2897,7 +2897,7 @@ const TOOLS: readonly Tool[] = [
28972897 query: {
28982898 type: "string",
28992899 maxLength: 200,
2900 description: "Words, plus kind:app|person|forge and sort:score|forks|ships|use|fresh|name.",
2900 description: "Words; kind:, sort: filters.",
29012901 },
29022902 },
29032903 },
@@ -3131,7 +3131,7 @@ const TOOLS: readonly Tool[] = [
31313131 hidden: { type: "array", items: { type: "string" } },
31323132 weights: {
31333133 type: "object",
3134 description: "Each 0 to 10: forks, ships, use, fresh.",
3134 description: "0–10: forks, ships, use, fresh.",
31353135 properties: {
31363136 forks: { type: "number", minimum: 0, maximum: 10 },
31373137 ships: { type: "number", minimum: 0, maximum: 10 },
@@ -3211,7 +3211,7 @@ const TOOLS: readonly Tool[] = [
32113211 type: "integer",
32123212 minimum: 1,
32133213 description:
3214 "The number of an issue that waits for others; the limbs when absent.",
3214 "Issue number; limbs if absent.",
32153215 },
32163216 },
32173217 required: ["app"],
@@ -3246,11 +3246,11 @@ const TOOLS: readonly Tool[] = [
32463246 issue: {
32473247 type: "integer",
32483248 minimum: 1,
3249 description: "The number of an issue that waits for others.",
3249 description: "Issue number.",
32503250 },
32513251 watch: {
32523252 type: "boolean",
3253 description: "false to stop watching; anything else watches.",
3253 description: "false to stop.",
32543254 },
32553255 },
32563256 required: ["app", "issue"],
@@ -3286,7 +3286,7 @@ const TOOLS: readonly Tool[] = [
32863286 type: "integer",
32873287 minimum: 1,
32883288 maximum: 50,
3289 description: "How many, 1 to 50; 20 when absent.",
3289 description: "1–50, default 20.",
32903290 },
32913291 },
32923292 required: ["repo"],
@@ -3372,15 +3372,26 @@ const TOOLS: readonly Tool[] = [
33723372 minimum: 1,
33733373 },
33743374 action: { type: "string", enum: ["defer", "resume", "close", "raise", "lower", "replan", "takeback", "undo"] },
3375 reason: { type: "string" },
3376 pillar: { type: "string" },
3377 under: { type: "string" },
3375 reason: {
3376 type: "string",
3377 description: "For close: why.",
3378 },
3379 pillar: {
3380 type: "string",
3381 description:
3382 "For resume.",
3383 },
3384 under: {
3385 type: "string",
3386 description:
3387 "`top` or a milestone number.",
3388 },
33783389 operation: {
33793390 type: "string",
33803391 },
33813392 dryRun: {
33823393 type: "boolean",
3383 description: "false to apply; anything else plans and changes nothing.",
3394 description: "false to apply",
33843395 },
33853396 },
33863397 required: ["app", "action"],
@@ -3894,7 +3905,7 @@ const TOOLS: readonly Tool[] = [
38943905 pull: { type: "integer" },
38953906 why: {
38963907 type: "string",
3897 description: "To hold: why, in one line. It shows on the pull.",
3908 description: "Why, one line.",
38983909 },
38993910 release: {
39003911 type: "boolean",
@@ -3985,11 +3996,11 @@ const TOOLS: readonly Tool[] = [
39853996 },
39863997 pillar: {
39873998 type: "string",
3988 description: 'Pillar served: "P0" or "P0 Trust".',
3999 description: 'Pillar, "P0".',
39894000 },
39904001 later: {
39914002 type: "boolean",
3992 description: "No pillar: needs why.",
4003 description: "No pillar; needs why.",
39934004 },
39944005 why: {
39954006 type: "string",
@@ -4092,8 +4103,8 @@ const TOOLS: readonly Tool[] = [
40924103 inputSchema: {
40934104 type: "object",
40944105 properties: {
4095 since: { type: "string", maxLength: 40, description: "ISO time; default a day, max 14 days." },
4096 ua: { type: "string", maxLength: 100, description: "Part of the user agent." },
4106 since: { type: "string", maxLength: 40, description: "ISO time; max 14 days." },
4107 ua: { type: "string", maxLength: 100, description: "User agent part." },
40974108 path: { type: "string", maxLength: 200, description: "Path prefix." },
40984109 },
40994110 },
@@ -4357,10 +4368,10 @@ const TOOLS: readonly Tool[] = [
43574368 verb: { type: "string", enum: ["attach", "verify", "rotate", "rename", "remove", "allow_writes", "audit"] },
43584369 provider: { type: "string", enum: [...PROVIDERS] },
43594370 name: { type: "string", maxLength: 32 },
4360 value: { type: "string", maxLength: 4096, description: "Token, for attach and rotate." },
4371 value: { type: "string", maxLength: 4096, description: "For attach, rotate." },
43614372 to: { type: "string", maxLength: 32 },
43624373 allowed: { type: "boolean", description: "For allow_writes." },
4363 repo: { type: "string", maxLength: 200, description: "For audit: owner/name." },
4374 repo: { type: "string", maxLength: 200, description: "For audit." },
43644375 },
43654376 required: ["verb", "provider", "name"],
43664377 },
@@ -4756,7 +4767,7 @@ const REPORT_TOOL = {
47564767 body: { type: "string", maxLength: BODY_MAX },
47574768 page: {
47584769 type: "string",
4759 description: "The http(s) address where the problem was seen.",
4770 description: "Address where seen.",
47604771 },
47614772 },
47624773 required: ["repo", "title"],
test/mcp-size.test.ts +19
@@ -26,6 +26,8 @@ const HEADROOM_CEILING = 37_000;
2626 // One sentence a tool: past this, a description is a paragraph and belongs in
2727 // docs/MCP.md, which keeps the detail.
2828 const DESCRIPTION_CEILING = 120;
29 // No argument description is longer than this.
30 const PROPERTY_MAX = 60;
2931
3032 let dataDir: string;
3133 let app: App;
@@ -70,8 +72,25 @@ test("tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes)", async
7072 const tools = (JSON.parse(text) as any).result.tools as {
7173 name: string;
7274 description: string;
75 inputSchema: unknown;
7376 annotations?: { readOnlyHint?: boolean };
7477 }[];
78 // The argument words are short; the detail lives in docs/MCP.md.
79 const long: string[] = [];
80 const walk = (tool: string, node: any, path: string): void => {
81 if (!node || typeof node !== "object") return;
82 for (const [k, v] of Object.entries<any>(node.properties ?? {})) {
83 if (typeof v?.description === "string" && Buffer.byteLength(v.description) > PROPERTY_MAX)
84 long.push(`${tool}.${path}${k} (${Buffer.byteLength(v.description)} bytes)`);
85 walk(tool, v, `${path}${k}.`);
86 }
87 walk(tool, node.items, path);
88 };
89 for (const t of tools) walk(t.name, t.inputSchema, "");
90 expect(long, `property descriptions over ${PROPERTY_MAX} bytes`).toEqual([]);
91 expect(bytes, "the argument descriptions were cut by 1,000 bytes").toBeLessThanOrEqual(

Minor: after the merge with main, the limit is 36,400 and the saving is no longer exactly 1,000 bytes. Rename the message to something like "argument descriptions stay cut" next time this file is touched, so a failure doesn't point at a number that isn't the target.

92 36_400,
93 );
7594 // readOnlyHint defaults to false; spelling the default out on every write
7695 // tool cost 2 KB, so only a read tool carries annotations.
7796 expect(tools.filter((t) => t.annotations?.readOnlyHint === false).map((t) => t.name)).toEqual([]);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs