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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5032-mcp-tools-list-s-argument-descriptions-s → main 6 commits fork 8 threads · 8 unresolved
3 files
changed +82
−32
docs/MCP.md +20
| @@ -122,6 +122,26 @@ a failure names the bytes over and the longest descriptions. | ||
| 122 | 122 | Answers are JSON in a text block; most carry a `says` sentence short enough |
| 123 | 123 | to relay as it is. |
| 124 | 124 | |
| 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 | ||
| 125 | 145 | ## Connecting |
| 126 | 146 | |
| 127 | 147 | 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 = { | ||
| 320 | 320 | type: "array", |
| 321 | 321 | items: { type: "string", enum: [...KIND_CHIPS] }, |
| 322 | 322 | maxItems: 50, |
| 323 | description: "Kinds to leave out; replaces your saved filter.", | |
| 323 | description: "Kinds to hide.", | |
| 324 | 324 | }, |
| 325 | 325 | hide_people: { |
| 326 | 326 | type: "array", |
| 327 | 327 | items: { type: "string", maxLength: 39 }, |
| 328 | 328 | maxItems: 50, |
| 329 | description: "Usernames to leave out; replaces your saved filter.", | |
| 329 | description: "Usernames to hide.", | |
| 330 | 330 | }, |
| 331 | 331 | } as const; |
| 332 | 332 | |
| @@ -435,7 +435,7 @@ const APP_ARG = { type: "string" } as const; | ||
| 435 | 435 | const REVEAL_ARG = { |
| 436 | 436 | type: "boolean", |
| 437 | 437 | 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.", | |
| 439 | 439 | } as const; |
| 440 | 440 | const LINK_KEPT = |
| 441 | 441 | "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[] = [ | ||
| 671 | 671 | type: "string", |
| 672 | 672 | maxLength: 512, |
| 673 | 673 | 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.", | |
| 675 | 675 | }, |
| 676 | 676 | context: { |
| 677 | 677 | type: "object", |
| 678 | 678 | description: |
| 679 | "Where this was told from, for the builder.", | |
| 679 | "Where it was told.", | |
| 680 | 680 | properties: { |
| 681 | 681 | page: { type: "string" }, |
| 682 | 682 | pull: { type: "integer", minimum: 1 }, |
| @@ -1543,7 +1543,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 1543 | 1543 | repo: { |
| 1544 | 1544 | ...REPO_ARG, |
| 1545 | 1545 | description: |
| 1546 | "One app, as owner/name; every app you can see when absent.", | |
| 1546 | "owner/name; all when absent.", | |
| 1547 | 1547 | }, |
| 1548 | 1548 | }, |
| 1549 | 1549 | }, |
| @@ -1589,7 +1589,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 1589 | 1589 | type: "object", |
| 1590 | 1590 | properties: { |
| 1591 | 1591 | 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." }, | |
| 1593 | 1593 | dryRun: { type: "boolean", description: "false to apply" }, |
| 1594 | 1594 | }, |
| 1595 | 1595 | required: ["username", "usdPerMonth"], |
| @@ -1693,7 +1693,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 1693 | 1693 | properties: { |
| 1694 | 1694 | username: { type: "string" }, |
| 1695 | 1695 | 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." }, | |
| 1697 | 1697 | dryRun: { type: "boolean", description: "false to apply" }, |
| 1698 | 1698 | }, |
| 1699 | 1699 | required: ["username", "role"], |
| @@ -1947,7 +1947,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 1947 | 1947 | type: "array", |
| 1948 | 1948 | items: { type: "string" }, |
| 1949 | 1949 | maxItems: MAX_ARCHIVE_REPOS, |
| 1950 | description: "Several repositories, as owner/name, to archive in one operation.", | |
| 1950 | description: "owner/name list, one operation.", | |
| 1951 | 1951 | }, |
| 1952 | 1952 | archived: { type: "boolean" }, |
| 1953 | 1953 | dryRun: { |
| @@ -2377,29 +2377,29 @@ const TOOLS: readonly Tool[] = [ | ||
| 2377 | 2377 | direction: { |
| 2378 | 2378 | type: "string", |
| 2379 | 2379 | description: |
| 2380 | "What the child is for; its first issue.", | |
| 2380 | "Its purpose; first issue.", | |
| 2381 | 2381 | }, |
| 2382 | 2382 | fresh: { |
| 2383 | 2383 | type: "string", |
| 2384 | 2384 | description: |
| 2385 | "A fresh app beside the copied ones.", | |
| 2385 | "A fresh app too.", | |
| 2386 | 2386 | }, |
| 2387 | 2387 | followMode: { |
| 2388 | 2388 | type: "string", |
| 2389 | 2389 | enum: ["tell-me", "take-updates"], |
| 2390 | 2390 | description: |
| 2391 | "tell-me (default) or take-updates.", | |
| 2391 | "tell-me or take-updates.", | |
| 2392 | 2392 | }, |
| 2393 | 2393 | size: { |
| 2394 | 2394 | type: "string", |
| 2395 | 2395 | enum: ["small", "medium", "large"], |
| 2396 | 2396 | description: |
| 2397 | "Recorded only; not enforced.", | |
| 2397 | "Recorded only.", | |
| 2398 | 2398 | }, |
| 2399 | 2399 | keepAccess: { |
| 2400 | 2400 | type: "boolean", |
| 2401 | 2401 | description: |
| 2402 | "Stay an owner once claimed.", | |
| 2402 | "Stay an owner.", | |
| 2403 | 2403 | }, |
| 2404 | 2404 | dryRun: { |
| 2405 | 2405 | type: "boolean", |
| @@ -2897,7 +2897,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 2897 | 2897 | query: { |
| 2898 | 2898 | type: "string", |
| 2899 | 2899 | maxLength: 200, |
| 2900 | description: "Words, plus kind:app|person|forge and sort:score|forks|ships|use|fresh|name.", | |
| 2900 | description: "Words; kind:, sort: filters.", | |
| 2901 | 2901 | }, |
| 2902 | 2902 | }, |
| 2903 | 2903 | }, |
| @@ -3131,7 +3131,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3131 | 3131 | hidden: { type: "array", items: { type: "string" } }, |
| 3132 | 3132 | weights: { |
| 3133 | 3133 | type: "object", |
| 3134 | description: "Each 0 to 10: forks, ships, use, fresh.", | |
| 3134 | description: "0–10: forks, ships, use, fresh.", | |
| 3135 | 3135 | properties: { |
| 3136 | 3136 | forks: { type: "number", minimum: 0, maximum: 10 }, |
| 3137 | 3137 | ships: { type: "number", minimum: 0, maximum: 10 }, |
| @@ -3211,7 +3211,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3211 | 3211 | type: "integer", |
| 3212 | 3212 | minimum: 1, |
| 3213 | 3213 | description: |
| 3214 | "The number of an issue that waits for others; the limbs when absent.", | |
| 3214 | "Issue number; limbs if absent.", | |
| 3215 | 3215 | }, |
| 3216 | 3216 | }, |
| 3217 | 3217 | required: ["app"], |
| @@ -3246,11 +3246,11 @@ const TOOLS: readonly Tool[] = [ | ||
| 3246 | 3246 | issue: { |
| 3247 | 3247 | type: "integer", |
| 3248 | 3248 | minimum: 1, |
| 3249 | description: "The number of an issue that waits for others.", | |
| 3249 | description: "Issue number.", | |
| 3250 | 3250 | }, |
| 3251 | 3251 | watch: { |
| 3252 | 3252 | type: "boolean", |
| 3253 | description: "false to stop watching; anything else watches.", | |
| 3253 | description: "false to stop.", | |
| 3254 | 3254 | }, |
| 3255 | 3255 | }, |
| 3256 | 3256 | required: ["app", "issue"], |
| @@ -3286,7 +3286,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3286 | 3286 | type: "integer", |
| 3287 | 3287 | minimum: 1, |
| 3288 | 3288 | maximum: 50, |
| 3289 | description: "How many, 1 to 50; 20 when absent.", | |
| 3289 | description: "1–50, default 20.", | |
| 3290 | 3290 | }, |
| 3291 | 3291 | }, |
| 3292 | 3292 | required: ["repo"], |
| @@ -3372,15 +3372,26 @@ const TOOLS: readonly Tool[] = [ | ||
| 3372 | 3372 | minimum: 1, |
| 3373 | 3373 | }, |
| 3374 | 3374 | 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 | }, | |
| 3378 | 3389 | operation: { |
| 3379 | 3390 | type: "string", |
| 3380 | 3391 | }, |
| 3381 | 3392 | dryRun: { |
| 3382 | 3393 | type: "boolean", |
| 3383 | description: "false to apply; anything else plans and changes nothing.", | |
| 3394 | description: "false to apply", | |
| 3384 | 3395 | }, |
| 3385 | 3396 | }, |
| 3386 | 3397 | required: ["app", "action"], |
| @@ -3894,7 +3905,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3894 | 3905 | pull: { type: "integer" }, |
| 3895 | 3906 | why: { |
| 3896 | 3907 | type: "string", |
| 3897 | description: "To hold: why, in one line. It shows on the pull.", | |
| 3908 | description: "Why, one line.", | |
| 3898 | 3909 | }, |
| 3899 | 3910 | release: { |
| 3900 | 3911 | type: "boolean", |
| @@ -3985,11 +3996,11 @@ const TOOLS: readonly Tool[] = [ | ||
| 3985 | 3996 | }, |
| 3986 | 3997 | pillar: { |
| 3987 | 3998 | type: "string", |
| 3988 | description: 'Pillar served: "P0" or "P0 Trust".', | |
| 3999 | description: 'Pillar, "P0".', | |
| 3989 | 4000 | }, |
| 3990 | 4001 | later: { |
| 3991 | 4002 | type: "boolean", |
| 3992 | description: "No pillar: needs why.", | |
| 4003 | description: "No pillar; needs why.", | |
| 3993 | 4004 | }, |
| 3994 | 4005 | why: { |
| 3995 | 4006 | type: "string", |
| @@ -4092,8 +4103,8 @@ const TOOLS: readonly Tool[] = [ | ||
| 4092 | 4103 | inputSchema: { |
| 4093 | 4104 | type: "object", |
| 4094 | 4105 | 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." }, | |
| 4097 | 4108 | path: { type: "string", maxLength: 200, description: "Path prefix." }, |
| 4098 | 4109 | }, |
| 4099 | 4110 | }, |
| @@ -4357,10 +4368,10 @@ const TOOLS: readonly Tool[] = [ | ||
| 4357 | 4368 | verb: { type: "string", enum: ["attach", "verify", "rotate", "rename", "remove", "allow_writes", "audit"] }, |
| 4358 | 4369 | provider: { type: "string", enum: [...PROVIDERS] }, |
| 4359 | 4370 | 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." }, | |
| 4361 | 4372 | to: { type: "string", maxLength: 32 }, |
| 4362 | 4373 | 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." }, | |
| 4364 | 4375 | }, |
| 4365 | 4376 | required: ["verb", "provider", "name"], |
| 4366 | 4377 | }, |
| @@ -4756,7 +4767,7 @@ const REPORT_TOOL = { | ||
| 4756 | 4767 | body: { type: "string", maxLength: BODY_MAX }, |
| 4757 | 4768 | page: { |
| 4758 | 4769 | type: "string", |
| 4759 | description: "The http(s) address where the problem was seen.", | |
| 4770 | description: "Address where seen.", | |
| 4760 | 4771 | }, |
| 4761 | 4772 | }, |
| 4762 | 4773 | required: ["repo", "title"], |
test/mcp-size.test.ts +19
| @@ -26,6 +26,8 @@ const HEADROOM_CEILING = 37_000; | ||
| 26 | 26 | // One sentence a tool: past this, a description is a paragraph and belongs in |
| 27 | 27 | // docs/MCP.md, which keeps the detail. |
| 28 | 28 | const DESCRIPTION_CEILING = 120; |
| 29 | // No argument description is longer than this. | |
| 30 | const PROPERTY_MAX = 60; | |
| 29 | 31 | |
| 30 | 32 | let dataDir: string; |
| 31 | 33 | let app: App; |
| @@ -70,8 +72,25 @@ test("tools/list stays under 40 KB, with 3 KB of headroom (37,000 bytes)", async | ||
| 70 | 72 | const tools = (JSON.parse(text) as any).result.tools as { |
| 71 | 73 | name: string; |
| 72 | 74 | description: string; |
| 75 | inputSchema: unknown; | |
| 73 | 76 | annotations?: { readOnlyHint?: boolean }; |
| 74 | 77 | }[]; |
| 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 |
||
| 92 | 36_400, | |
| 93 | ); | |
| 75 | 94 | // readOnlyHint defaults to false; spelling the default out on every write |
| 76 | 95 | // tool cost 2 KB, so only a read tool carries annotations. |
| 77 | 96 | expect(tools.filter((t) => t.annotations?.readOnlyHint === false).map((t) => t.name)).toEqual([]); |
new above
offline
Low:
hide_kinds/hide_peopledropped "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.