[{"data":1,"prerenderedAt":1103},["ShallowReactive",2],{"navigation_docs":3,"-guide-cli":52,"-guide-cli-surround":1098},[4,28],{"title":5,"path":6,"stem":7,"children":8,"icon":27},"Guide","\u002Fguide","1.guide\u002F01.index",[9,11,15,19,23],{"title":10,"path":6,"stem":7},"Getting Started",{"title":12,"path":13,"stem":14},"Defining Tools","\u002Fguide\u002Fdefining-tools","1.guide\u002F02.defining-tools",{"title":16,"path":17,"stem":18},"Validation and Errors","\u002Fguide\u002Fvalidation","1.guide\u002F03.validation",{"title":20,"path":21,"stem":22},"CLI","\u002Fguide\u002Fcli","1.guide\u002F04.cli",{"title":24,"path":25,"stem":26},"Moving a Package Over","\u002Fguide\u002Fmigrating","1.guide\u002F05.migrating","i-lucide-book-open",{"title":29,"path":30,"stem":31,"children":32,"icon":51},"Hosts","\u002Fhosts","2.hosts\u002F00.index",[33,35,39,43,47],{"title":34,"path":30,"stem":31},"Every host",{"title":36,"path":37,"stem":38},"MCP","\u002Fhosts\u002Fmcp","2.hosts\u002F01.mcp",{"title":40,"path":41,"stem":42},"Pi","\u002Fhosts\u002Fpi","2.hosts\u002F02.pi",{"title":44,"path":45,"stem":46},"OMP","\u002Fhosts\u002Fomp","2.hosts\u002F03.omp",{"title":48,"path":49,"stem":50},"AI SDK","\u002Fhosts\u002Fai-sdk","2.hosts\u002F04.ai-sdk","i-lucide-table",{"id":53,"title":54,"body":55,"description":1089,"extension":1090,"links":1091,"meta":1092,"navigation":1094,"path":21,"seo":1095,"stem":22,"__hash__":1097},"docs\u002F1.guide\u002F04.cli.md","The CLI",{"type":56,"value":57,"toc":1073},"minimark",[58,71,191,198,207,214,219,224,257,261,288,321,332,335,341,345,352,412,426,432,435,441,447,451,458,484,494,500,524,528,541,552,558,561,567,589,593,612,616,626,632,643,653,656,660,674,724,731,735,738,791,804,808,819,834,840,850,854,867,928,937,1005,1031,1047,1069],[59,60,61,62,66,67,70],"p",{},"Your tools already know their arguments. The schema says which ones are required, what type each one is and which values an enum takes. So why write a CLI by hand? ",[63,64,65],"code",{},"runCli"," reads all of that and gives every tool its own command. No CLI library underneath, just ",[63,68,69],{},"util.parseArgs"," from Node.",[72,73,79],"pre",{"className":74,"code":75,"filename":76,"language":77,"meta":78,"style":78},"language-ts shiki shiki-themes tools tools tools","import { runCli } from \"@agntn\u002Ftools\u002Fcli\";\nimport { slugTool } from \".\u002Ftools.ts\";\n\nawait runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  mcp: true,\n});\n","src\u002Fcli.ts","ts","",[63,80,81,104,119,126,139,151,162,173,179,185],{"__ignoreMap":78},[82,83,86,90,94,97,101],"span",{"class":84,"line":85},"line",1,[82,87,89],{"class":88},"skH_V","import",[82,91,93],{"class":92},"s38Sx"," { runCli } ",[82,95,96],{"class":88},"from",[82,98,100],{"class":99},"shU9J"," \"@agntn\u002Ftools\u002Fcli\"",[82,102,103],{"class":92},";\n",[82,105,107,109,112,114,117],{"class":84,"line":106},2,[82,108,89],{"class":88},[82,110,111],{"class":92}," { slugTool } ",[82,113,96],{"class":88},[82,115,116],{"class":99}," \".\u002Ftools.ts\"",[82,118,103],{"class":92},[82,120,122],{"class":84,"line":121},3,[82,123,125],{"emptyLinePlaceholder":124},true,"\n",[82,127,129,132,136],{"class":84,"line":128},4,[82,130,131],{"class":88},"await",[82,133,135],{"class":134},"sK71F"," runCli",[82,137,138],{"class":92},"({\n",[82,140,142,145,148],{"class":84,"line":141},5,[82,143,144],{"class":92},"  name: ",[82,146,147],{"class":99},"\"slugs\"",[82,149,150],{"class":92},",\n",[82,152,154,157,160],{"class":84,"line":153},6,[82,155,156],{"class":92},"  version: ",[82,158,159],{"class":99},"\"1.0.0\"",[82,161,150],{"class":92},[82,163,165,168,171],{"class":84,"line":164},7,[82,166,167],{"class":92},"  description: ",[82,169,170],{"class":99},"\"Slugs from titles\"",[82,172,150],{"class":92},[82,174,176],{"class":84,"line":175},8,[82,177,178],{"class":92},"  tools: [slugTool],\n",[82,180,182],{"class":84,"line":181},9,[82,183,184],{"class":92},"  mcp: true,\n",[82,186,188],{"class":84,"line":187},10,[82,189,190],{"class":92},"});\n",[59,192,193,194,197],{},"Point your ",[63,195,196],{},"bin"," at the built file and that's it:",[72,199,205],{"className":200,"code":202,"filename":203,"language":204,"meta":78},[201],"language-text","$ slugs slug --text \"Crème brûlée\"\ncreme-brulee\n","terminal","text",[63,206,202],{"__ignoreMap":78},[59,208,209,210,213],{},"It's the one Node part of the package. The core and the host adapters run in a browser tab, this one reads ",[63,211,212],{},"process.argv",".",[215,216,218],"h2",{"id":217},"how-a-tool-turns-into-a-command","How a tool turns into a command",[220,221,223],"h3",{"id":222},"where-does-the-command-name-come-from","Where does the command name come from?",[59,225,226,227,230,231,234,235,230,238,241,242,245,246,249,250,253,254,213],{},"From the tool name, minus the package prefix. ",[63,228,229],{},"text_slug"," becomes ",[63,232,233],{},"slug",", ",[63,236,237],{},"hashes_digest_identify",[63,239,240],{},"digest-identify",". Want another word? The ",[63,243,244],{},"cli"," hint on ",[63,247,248],{},"defineTool"," takes a ",[63,251,252],{},"command"," and a few ",[63,255,256],{},"aliases",[220,258,260],{"id":259},"flags-straight-from-the-schema","Flags straight from the schema",[59,262,263,264,267,268,271,272,275,276,279,280,283,284,287],{},"Every property is a flag, in kebab case. ",[63,265,266],{},"maxWords"," and ",[63,269,270],{},"max_words"," both turn into ",[63,273,274],{},"--max-words",". Strings go in as they are. Numbers have to look like numbers: ",[63,277,278],{},"--max-words 2"," works, ",[63,281,282],{},"--max-words 1.5"," gets refused instead of rounded down. And ",[63,285,286],{},"0x10","? Just a typo here. Objects and arrays come in as JSON:",[72,289,293],{"className":290,"code":291,"filename":203,"language":292,"meta":78,"style":78},"language-bash shiki shiki-themes tools tools tools","$ slugs batch --titles '[\"Hello World\", \"Crème brûlée\"]' --max-words 2 --no-lower\n","bash",[63,294,295],{"__ignoreMap":78},[82,296,297,300,303,306,309,312,315,318],{"class":84,"line":85},[82,298,299],{"class":134},"$",[82,301,302],{"class":99}," slugs",[82,304,305],{"class":99}," batch",[82,307,308],{"class":99}," --titles",[82,310,311],{"class":99}," '[\"Hello World\", \"Crème brûlée\"]'",[82,313,314],{"class":99}," --max-words",[82,316,317],{"class":99}," 2",[82,319,320],{"class":99}," --no-lower\n",[59,322,323,324,327,328,331],{},"A boolean answers to ",[63,325,326],{},"--lower"," and to ",[63,329,330],{},"--no-lower",". Say both and the command refuses, it doesn't pick a winner. Same for an unknown option, a missing value or a word too many. Nothing on the line is quietly skipped.",[59,333,334],{},"Then the core validates, like on every other host. A typo here gets the same treatment a model's typo gets:",[72,336,339],{"className":337,"code":338,"filename":203,"language":204,"meta":78},[201],"$ slugs slug --text Hello --sep _\nInvalid arguments: unknown option \"--sep\"; takes --text, --separator, --json\n$ slugs slug --text Hello --separator \"~\"\nInvalid arguments at \u002Fseparator: must be one of -, _, .\n",[63,340,338],{"__ignoreMap":78},[220,342,344],{"id":343},"positionals-and-stdin","Positionals and stdin",[59,346,347,348,351],{},"Typing ",[63,349,350],{},"--text"," every time gets old fast. Two more hints fix that:",[72,353,356],{"className":74,"code":354,"filename":355,"language":77,"meta":78,"style":78},"export const slugTool = defineTool({\n  name: \"text_slug\",\n  \u002F\u002F …\n  cli: { positional: [\"text\"], stdin: [\"text\"] },\n});\n","tools.ts",[63,357,358,377,386,392,408],{"__ignoreMap":78},[82,359,360,363,366,369,372,375],{"class":84,"line":85},[82,361,362],{"class":88},"export",[82,364,365],{"class":88}," const",[82,367,368],{"class":92}," slugTool ",[82,370,371],{"class":88},"=",[82,373,374],{"class":134}," defineTool",[82,376,138],{"class":92},[82,378,379,381,384],{"class":84,"line":106},[82,380,144],{"class":92},[82,382,383],{"class":99},"\"text_slug\"",[82,385,150],{"class":92},[82,387,388],{"class":84,"line":121},[82,389,391],{"class":390},"scIB-","  \u002F\u002F …\n",[82,393,394,397,400,403,405],{"class":84,"line":128},[82,395,396],{"class":92},"  cli: { positional: [",[82,398,399],{"class":99},"\"text\"",[82,401,402],{"class":92},"], stdin: [",[82,404,399],{"class":99},[82,406,407],{"class":92},"] },\n",[82,409,410],{"class":84,"line":141},[82,411,190],{"class":92},[59,413,414,417,418,421,422,425],{},[63,415,416],{},"positional"," takes properties in order, as plain words. ",[63,419,420],{},"stdin"," lets a string property read standard input when you pass ",[63,423,424],{},"-",":",[72,427,430],{"className":428,"code":429,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"Hello, World\" --separator _\nhello_world\n$ echo \"Pipe me in\" | slugs slug -\npipe-me-in\n",[63,431,429],{"__ignoreMap":78},[59,433,434],{},"Stdin has to be valid UTF-8. A lenient decode would swap bad bytes for U+FFFD, and a hash tool would happily hash something you never sent. So invalid input is an error. Got real binary data? That's a job for a command of your own, see below.",[59,436,437,440],{},[63,438,439],{},"--help"," is built from the same fields. Descriptions come from the schema, the enum values too:",[72,442,445],{"className":443,"code":444,"language":204,"meta":78},[201],"Turn a title into a lowercase URL slug. (slugs slug v1.0.0)\n\nUSAGE slugs slug [OPTIONS] \u003CTEXT>\n\nARGUMENTS\n\n  TEXT  Title to slug (- reads stdin) (required)\n\nOPTIONS\n\n  --separator=\u003C-|_|.>  Joins the words. '-' when omitted\n  --json               Print the details as JSON instead of the text\n  -h, --help           Show this help\n",[63,446,444],{"__ignoreMap":78},[220,448,450],{"id":449},"short-flags-and-the-rest-of-the-line","Short flags and the rest of the line",[59,452,453,454,457],{},"Quotes around every title get old too. So does ",[63,455,456],{},"--separator",". Two hints bring back what every CLI written by hand had:",[72,459,461],{"className":74,"code":460,"filename":355,"language":77,"meta":78,"style":78},"cli: { rest: \"text\", stdin: [\"text\"], short: { separator: \"s\" } },\n",[63,462,463],{"__ignoreMap":78},[82,464,465,468,470,473,475,478,481],{"class":84,"line":85},[82,466,467],{"class":92},"cli: { rest: ",[82,469,399],{"class":99},[82,471,472],{"class":92},", stdin: [",[82,474,399],{"class":99},[82,476,477],{"class":92},"], short: { separator: ",[82,479,480],{"class":99},"\"s\"",[82,482,483],{"class":92}," } },\n",[59,485,486,489,490,493],{},[63,487,488],{},"rest"," takes every word the positionals leave over and joins them with single spaces. ",[63,491,492],{},"short"," gives an option an alias of one letter:",[72,495,498],{"className":496,"code":497,"filename":203,"language":204,"meta":78},[201],"$ slugs slug Hello big world -s _\nhello_big_world\n",[63,499,497],{"__ignoreMap":78},[59,501,502,503,279,506,509,510,512,513,516,517,520,521,523],{},"Only a whole word counts as a short flag, though. ",[63,504,505],{},"-s _",[63,507,508],{},"-s_"," is an unknown option. Pedantic? A bit. But ",[63,511,69],{}," reads ",[63,514,515],{},"-separator _"," as ",[63,518,519],{},"-s eparator",", and next to a greedy ",[63,522,488],{}," one dash too few shouldn't turn into some other call. Here it fails and tells you why.",[220,525,527],{"id":526},"text-that-starts-with-a-dash","Text that starts with a dash",[59,529,530,531,534,535,537,538,213],{},"Is ",[63,532,533],{},"-.-."," an option? ",[63,536,69],{}," thinks so. In Morse it's a C. Every PGP armor line opens with five dashes, and a temperature can be ",[63,539,540],{},"-12",[59,542,543,544,234,546,234,548,551],{},"So a word counts as an option only when it spells one as a whole: ",[63,545,456],{},[63,547,330],{},[63,549,550],{},"-s",". Any other dashed word takes the next free positional:",[72,553,556],{"className":554,"code":555,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"-12 below zero\"\n12-below-zero\n",[63,557,555],{"__ignoreMap":78},[59,559,560],{},"Free means the plain words left room for it. Once they fill every positional, a dashed word is an unknown option again. A typo after the title fails, it doesn't end up in your slug:",[72,562,565],{"className":563,"code":564,"filename":203,"language":204,"meta":78},[201],"$ slugs slug Hello --seperator _\nInvalid arguments: unknown option \"--seperator\"; takes --separator (-s), --json\n",[63,566,564],{"__ignoreMap":78},[59,568,569,571,572,575,576,267,579,582,583,585,586,213],{},[63,570,488],{}," counts as one positional here. ",[63,573,574],{},"slugs slug -12 below zero"," fails, because ",[63,577,578],{},"below",[63,580,581],{},"zero"," already took the spot. Quote the title and it's one word again. Text that does spell a flag, like ",[63,584,550],{}," itself, goes after ",[63,587,588],{},"--",[220,590,592],{"id":591},"when-does-a-broken-hint-show-up","When does a broken hint show up?",[59,594,595,596,599,600,603,604,607,608,611],{},"At startup, before any command runs. A hint naming a property the tool doesn't have, two properties fighting over one flag, a boolean called ",[63,597,598],{},"noCache",", a required positional after an optional one, a short flag that tries to take ",[63,601,602],{},"-h",". Each one fails in ",[63,605,606],{},"createCli"," with a ",[63,609,610],{},"ToolDefinitionError",". Not on the first user who types the wrong thing.",[215,613,615],{"id":614},"what-comes-out","What comes out",[59,617,618,619,622,623,425],{},"The command prints the tool's text, the same text a model reads. Want data instead? ",[63,620,621],{},"--json"," prints ",[63,624,625],{},"details",[72,627,630],{"className":628,"code":629,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"Hello\" --json\n{\n  \"slug\": \"hello\",\n  \"words\": 1\n}\n",[63,631,629],{"__ignoreMap":78},[59,633,634,635,638,639,642],{},"A returned ",[63,636,637],{},"isError"," goes to stderr with exit code 1. So ",[63,640,641],{},"slugs slug \"!!!\""," fails like a proper command, scripts included.",[59,644,645,646,648,649,652],{},"A thrown error is a different story. Tell ",[63,647,65],{}," which errors you throw on purpose through ",[63,650,651],{},"expected",", and those print as one line with exit code 1. Anything else keeps its stack trace. A bug should look like a bug.",[59,654,655],{},"Errors get the same treatment as on the other hosts: one line each, so a newline in an echoed argument can't forge a second one. Escape codes and bidi tricks don't reach your terminal, not even through the tool's own text. And there are no colors at all, so nothing breaks in a pipe.",[215,657,659],{"id":658},"commands-only-the-cli-gets","Commands only the CLI gets",[59,661,662,663,666,667,670,671,425],{},"Some things belong on the command line and nowhere else. A ",[63,664,665],{},"version"," with build details, a ",[63,668,669],{},"doctor",", a reader for binary stdin. Write them as tool definitions and pass them as ",[63,672,673],{},"commands",[72,675,677],{"className":74,"code":676,"filename":76,"language":77,"meta":78,"style":78},"await runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  commands: [separatorsCommand],\n});\n",[63,678,679,687,695,703,711,715,720],{"__ignoreMap":78},[82,680,681,683,685],{"class":84,"line":85},[82,682,131],{"class":88},[82,684,135],{"class":134},[82,686,138],{"class":92},[82,688,689,691,693],{"class":84,"line":106},[82,690,144],{"class":92},[82,692,147],{"class":99},[82,694,150],{"class":92},[82,696,697,699,701],{"class":84,"line":121},[82,698,156],{"class":92},[82,700,159],{"class":99},[82,702,150],{"class":92},[82,704,705,707,709],{"class":84,"line":128},[82,706,167],{"class":92},[82,708,170],{"class":99},[82,710,150],{"class":92},[82,712,713],{"class":84,"line":141},[82,714,178],{"class":92},[82,716,717],{"class":84,"line":153},[82,718,719],{"class":92},"  commands: [separatorsCommand],\n",[82,721,722],{"class":84,"line":164},[82,723,190],{"class":92},[59,725,726,727,730],{},"MCP, Pi, OMP and the AI SDK never see them. They parse, validate and print exactly like the tools. One with the name of a generated command takes its place, ",[63,728,729],{},"mcp"," included.",[220,732,734],{"id":733},"what-about-bytes-going-out","What about bytes going out?",[59,736,737],{},"The CLI ends every text with a newline. Lovely for a slug. Quietly fatal for a gzip file, which comes out one byte too long. Nobody notices until a checksum does. So a command that writes bytes writes them itself and answers with no content blocks at all:",[72,739,741],{"className":74,"code":740,"language":77,"meta":78,"style":78},"cli: { json: false },\nexecute({ file }) {\n  process.stdout.write(gunzipSync(readFileSync(file)));\n  return { content: [], details: null };\n},\n",[63,742,743,748,756,778,786],{"__ignoreMap":78},[82,744,745],{"class":84,"line":85},[82,746,747],{"class":92},"cli: { json: false },\n",[82,749,750,753],{"class":84,"line":106},[82,751,752],{"class":134},"execute",[82,754,755],{"class":92},"({ file }) {\n",[82,757,758,761,764,767,770,772,775],{"class":84,"line":121},[82,759,760],{"class":92},"  process.stdout.",[82,762,763],{"class":134},"write",[82,765,766],{"class":92},"(",[82,768,769],{"class":134},"gunzipSync",[82,771,766],{"class":92},[82,773,774],{"class":134},"readFileSync",[82,776,777],{"class":92},"(file)));\n",[82,779,780,783],{"class":84,"line":128},[82,781,782],{"class":88},"  return",[82,784,785],{"class":92}," { content: [], details: null };\n",[82,787,788],{"class":84,"line":141},[82,789,790],{"class":92},"},\n",[59,792,793,794,797,798,800,801,803],{},"No blocks, nothing printed, not even the newline. An empty text block still prints its empty line, so you only get silence when you ask for it. ",[63,795,796],{},"json: false"," takes ",[63,799,621],{}," away, since bytes followed by JSON is neither. A returned ",[63,802,637],{}," works like on any other command.",[220,805,807],{"id":806},"can-it-guess-what-i-meant","Can it guess what I meant?",[59,809,810,811,814,815,818],{},"If you let it. ",[63,812,813],{},"default"," names the command for an empty line. ",[63,816,817],{},"fallback"," takes any first word that isn't a command:",[72,820,822],{"className":74,"code":821,"language":77,"meta":78,"style":78},"fallback: \"slug\",\n",[63,823,824],{"__ignoreMap":78},[82,825,826,829,832],{"class":84,"line":85},[82,827,828],{"class":92},"fallback: ",[82,830,831],{"class":99},"\"slug\"",[82,833,150],{"class":92},[72,835,838],{"className":836,"code":837,"filename":203,"language":204,"meta":78},[201],"$ slugs \"Hello, World\"\nhello-world\n",[63,839,837],{"__ignoreMap":78},[59,841,842,843,845,846,849],{},"Just remember it's greedy. Leave ",[63,844,729],{}," out and ",[63,847,848],{},"slugs mcp"," slugs the word \"mcp\". Correct, and completely useless.",[215,851,853],{"id":852},"an-mcp-server-on-the-side","An MCP server on the side",[59,855,856,859,860,862,863,866],{},[63,857,858],{},"mcp: true"," adds an ",[63,861,729],{}," command. It's ",[63,864,865],{},"createMcpServer"," over stdio, and the server introduces itself with the CLI's name, version and help line:",[72,868,873],{"className":869,"code":870,"filename":871,"language":872,"meta":78,"style":78},"language-json shiki shiki-themes tools tools tools","{\n  \"mcpServers\": {\n    \"slugs\": { \"command\": \"slugs\", \"args\": [\"mcp\"] }\n  }\n}\n",".mcp.json","json",[63,874,875,880,888,918,923],{"__ignoreMap":78},[82,876,877],{"class":84,"line":85},[82,878,879],{"class":92},"{\n",[82,881,882,885],{"class":84,"line":106},[82,883,884],{"class":134},"  \"mcpServers\"",[82,886,887],{"class":92},": {\n",[82,889,890,893,896,899,902,904,906,909,912,915],{"class":84,"line":121},[82,891,892],{"class":134},"    \"slugs\"",[82,894,895],{"class":92},": { ",[82,897,898],{"class":134},"\"command\"",[82,900,901],{"class":92},": ",[82,903,147],{"class":99},[82,905,234],{"class":92},[82,907,908],{"class":134},"\"args\"",[82,910,911],{"class":92},": [",[82,913,914],{"class":99},"\"mcp\"",[82,916,917],{"class":92},"] }\n",[82,919,920],{"class":84,"line":128},[82,921,922],{"class":92},"  }\n",[82,924,925],{"class":84,"line":141},[82,926,927],{"class":92},"}\n",[59,929,930,931,933,934,425],{},"A help line makes a sad connector card, though. Got a real pitch and an icon? Hand ",[63,932,729],{}," the whole server info instead of ",[63,935,936],{},"true",[72,938,940],{"className":74,"code":939,"filename":76,"language":77,"meta":78,"style":78},"import { serverInfo } from \".\u002Fserver-info.ts\";\n\nawait runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  mcp: serverInfo,\n});\n",[63,941,942,956,960,968,976,984,992,996,1001],{"__ignoreMap":78},[82,943,944,946,949,951,954],{"class":84,"line":85},[82,945,89],{"class":88},[82,947,948],{"class":92}," { serverInfo } ",[82,950,96],{"class":88},[82,952,953],{"class":99}," \".\u002Fserver-info.ts\"",[82,955,103],{"class":92},[82,957,958],{"class":84,"line":106},[82,959,125],{"emptyLinePlaceholder":124},[82,961,962,964,966],{"class":84,"line":121},[82,963,131],{"class":88},[82,965,135],{"class":134},[82,967,138],{"class":92},[82,969,970,972,974],{"class":84,"line":128},[82,971,144],{"class":92},[82,973,147],{"class":99},[82,975,150],{"class":92},[82,977,978,980,982],{"class":84,"line":141},[82,979,156],{"class":92},[82,981,159],{"class":99},[82,983,150],{"class":92},[82,985,986,988,990],{"class":84,"line":153},[82,987,167],{"class":92},[82,989,170],{"class":99},[82,991,150],{"class":92},[82,993,994],{"class":84,"line":164},[82,995,178],{"class":92},[82,997,998],{"class":84,"line":175},[82,999,1000],{"class":92},"  mcp: serverInfo,\n",[82,1002,1003],{"class":84,"line":181},[82,1004,190],{"class":92},[59,1006,1007,1008,1014,1015,1017,1018,1021,1022,1024,1025,1028,1029,213],{},"It's the same ",[1009,1010,1011],"a",{"href":37},[63,1012,1013],{},"McpServerInfo"," that ",[63,1016,865],{}," takes. So the ",[63,1019,1020],{},"\u002Fmcp"," on your site and ",[63,1023,848],{}," can share one object and say hello the same way. The CLI's own ",[63,1026,1027],{},"description"," stays where it belongs, in ",[63,1030,439],{},[59,1032,1033,1034,1037,1038,1040,1041,1044,1045,213],{},"One binary, two jobs. The server serves ",[63,1035,1036],{},"tools"," only, your own ",[63,1039,673],{}," stay on the command line. It needs ",[63,1042,1043],{},"@modelcontextprotocol\u002Fserver"," installed, and loads it only when someone actually runs ",[63,1046,729],{},[59,1048,1049,1050,1053,1054,1057,1058,1061,1062,1064,1065,1068],{},"Progress is the one thing that differs. A plain command has nowhere to show it, so ",[63,1051,1052],{},"progress"," is ",[63,1055,1056],{},"undefined"," there and your ",[63,1059,1060],{},"progress?.()"," calls stay quiet. Through ",[63,1063,848],{}," it's a real MCP server, so a client that sends a ",[63,1066,1067],{},"progressToken"," gets every line.",[1070,1071,1072],"style",{},"html pre.shiki code .skH_V, html code.shiki .skH_V{--shiki-light:var(--shiki-token-keyword);--shiki-default:var(--shiki-token-keyword);--shiki-dark:var(--shiki-token-keyword)}html pre.shiki code .s38Sx, html code.shiki .s38Sx{--shiki-light:var(--ui-text-highlighted);--shiki-default:var(--ui-text-highlighted);--shiki-dark:var(--ui-text-highlighted)}html pre.shiki code .shU9J, html code.shiki .shU9J{--shiki-light:var(--shiki-token-string);--shiki-default:var(--shiki-token-string);--shiki-dark:var(--shiki-token-string)}html pre.shiki code .sK71F, html code.shiki .sK71F{--shiki-light:var(--shiki-token-function);--shiki-default:var(--shiki-token-function);--shiki-dark:var(--shiki-token-function)}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .scIB-, html code.shiki .scIB-{--shiki-light:var(--shiki-token-comment);--shiki-default:var(--shiki-token-comment);--shiki-dark:var(--shiki-token-comment)}",{"title":78,"searchDepth":106,"depth":106,"links":1074},[1075,1083,1084,1088],{"id":217,"depth":106,"text":218,"children":1076},[1077,1078,1079,1080,1081,1082],{"id":222,"depth":121,"text":223},{"id":259,"depth":121,"text":260},{"id":343,"depth":121,"text":344},{"id":449,"depth":121,"text":450},{"id":526,"depth":121,"text":527},{"id":591,"depth":121,"text":592},{"id":614,"depth":106,"text":615},{"id":658,"depth":106,"text":659,"children":1085},[1086,1087],{"id":733,"depth":121,"text":734},{"id":806,"depth":121,"text":807},{"id":852,"depth":106,"text":853},"Turn the same tool list into a command line. Flags come from the schema and MCP rides along","md",null,{"icon":1093},"i-lucide-square-terminal",{"title":20},{"title":1096,"description":1089},"A command line from the tools you already have","8up0nvwxi5UOMuM3OyUG7mVCcU_6teUw6IEkCl2zq9o",[1099,1101],{"title":16,"path":17,"stem":18,"description":1100,"children":-1},"How the core checks every call and how each host reports a failure. And why error text never gets a second line",{"title":24,"path":25,"stem":26,"description":1102,"children":-1},"Take a package that declares its tools four times and bring it down to one definition. Step by step with the traps marked",1791393749721]