[{"data":1,"prerenderedAt":1392},["ShallowReactive",2],{"navigation_docs":3,"-hosts-mcp":52,"-hosts-mcp-surround":1333,"host-views":1338},[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":36,"body":54,"description":1327,"extension":1328,"links":1329,"meta":1330,"navigation":130,"path":37,"seo":1331,"stem":38,"__hash__":1332},"docs\u002F2.hosts\u002F01.mcp.md",{"type":55,"value":56,"toc":1312},"minimark",[57,61,279,294,327,332,358,399,411,422,426,456,463,480,484,507,529,533,560,567,571,593,614,617,635,639,649,656,662,747,766,772,776,789,920,941,944,955,965,980,988,1003,1091,1108,1127,1130,1176,1180,1186,1234,1247,1252,1282,1302,1308],[58,59],"host-facts",{"name":60},"mcp",[62,63,69],"pre",{"className":64,"code":65,"filename":66,"language":67,"meta":68,"style":68},"language-ts shiki shiki-themes tools tools tools","import { createMcpServer } from \"@agntn\u002Ftools\u002Fmcp\";\nimport { StdioServerTransport } from \"@modelcontextprotocol\u002Fserver\u002Fstdio\";\nimport { tools } from \".\u002Ftools.ts\";\n\nconst server = createMcpServer(\n  {\n    name: \"mypackage\",\n    version: \"1.0.0\",\n    title: \"My Package\",\n    description: \"Turns titles into slugs.\",\n    icons: [{ src: \"https:\u002F\u002Fexample.com\u002Fmypackage\u002Ficon.svg\", mimeType: \"image\u002Fsvg+xml\", sizes: [\"any\"] }],\n    websiteUrl: \"https:\u002F\u002Fexample.com\u002Fmypackage\",\n  },\n  tools,\n);\nawait server.connect(new StdioServerTransport());\n","server.ts","ts","",[70,71,72,95,110,125,132,151,157,169,180,191,202,226,237,243,249,255],"code",{"__ignoreMap":68},[73,74,77,81,85,88,92],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"skH_V","import",[73,82,84],{"class":83},"s38Sx"," { createMcpServer } ",[73,86,87],{"class":79},"from",[73,89,91],{"class":90},"shU9J"," \"@agntn\u002Ftools\u002Fmcp\"",[73,93,94],{"class":83},";\n",[73,96,98,100,103,105,108],{"class":75,"line":97},2,[73,99,80],{"class":79},[73,101,102],{"class":83}," { StdioServerTransport } ",[73,104,87],{"class":79},[73,106,107],{"class":90}," \"@modelcontextprotocol\u002Fserver\u002Fstdio\"",[73,109,94],{"class":83},[73,111,113,115,118,120,123],{"class":75,"line":112},3,[73,114,80],{"class":79},[73,116,117],{"class":83}," { tools } ",[73,119,87],{"class":79},[73,121,122],{"class":90}," \".\u002Ftools.ts\"",[73,124,94],{"class":83},[73,126,128],{"class":75,"line":127},4,[73,129,131],{"emptyLinePlaceholder":130},true,"\n",[73,133,135,138,141,144,148],{"class":75,"line":134},5,[73,136,137],{"class":79},"const",[73,139,140],{"class":83}," server ",[73,142,143],{"class":79},"=",[73,145,147],{"class":146},"sK71F"," createMcpServer",[73,149,150],{"class":83},"(\n",[73,152,154],{"class":75,"line":153},6,[73,155,156],{"class":83},"  {\n",[73,158,160,163,166],{"class":75,"line":159},7,[73,161,162],{"class":83},"    name: ",[73,164,165],{"class":90},"\"mypackage\"",[73,167,168],{"class":83},",\n",[73,170,172,175,178],{"class":75,"line":171},8,[73,173,174],{"class":83},"    version: ",[73,176,177],{"class":90},"\"1.0.0\"",[73,179,168],{"class":83},[73,181,183,186,189],{"class":75,"line":182},9,[73,184,185],{"class":83},"    title: ",[73,187,188],{"class":90},"\"My Package\"",[73,190,168],{"class":83},[73,192,194,197,200],{"class":75,"line":193},10,[73,195,196],{"class":83},"    description: ",[73,198,199],{"class":90},"\"Turns titles into slugs.\"",[73,201,168],{"class":83},[73,203,205,208,211,214,217,220,223],{"class":75,"line":204},11,[73,206,207],{"class":83},"    icons: [{ src: ",[73,209,210],{"class":90},"\"https:\u002F\u002Fexample.com\u002Fmypackage\u002Ficon.svg\"",[73,212,213],{"class":83},", mimeType: ",[73,215,216],{"class":90},"\"image\u002Fsvg+xml\"",[73,218,219],{"class":83},", sizes: [",[73,221,222],{"class":90},"\"any\"",[73,224,225],{"class":83},"] }],\n",[73,227,229,232,235],{"class":75,"line":228},12,[73,230,231],{"class":83},"    websiteUrl: ",[73,233,234],{"class":90},"\"https:\u002F\u002Fexample.com\u002Fmypackage\"",[73,236,168],{"class":83},[73,238,240],{"class":75,"line":239},13,[73,241,242],{"class":83},"  },\n",[73,244,246],{"class":75,"line":245},14,[73,247,248],{"class":83},"  tools,\n",[73,250,252],{"class":75,"line":251},15,[73,253,254],{"class":83},");\n",[73,256,258,261,264,267,270,273,276],{"class":75,"line":257},16,[73,259,260],{"class":79},"await",[73,262,263],{"class":83}," server.",[73,265,266],{"class":146},"connect",[73,268,269],{"class":83},"(",[73,271,272],{"class":79},"new",[73,274,275],{"class":146}," StdioServerTransport",[73,277,278],{"class":83},"());\n",[280,281,282,285,286,289,290,293],"p",{},[70,283,284],{},"createMcpServer(info, tools)"," gives you an unconnected server. One ",[70,287,288],{},"tools\u002Flist"," handler, one ",[70,291,292],{},"tools\u002Fcall"," handler, every tool in the list. Connect it to stdio, to Streamable HTTP or to a pair in memory for tests. This site does the last one, in your tab.",[280,295,296,299,300,303,304,307,308,311,312,315,316,319,320,323,324,326],{},[70,297,298],{},"info"," needs a ",[70,301,302],{},"name"," and a ",[70,305,306],{},"version",", that's it. Want a nicer label? ",[70,309,310],{},"title"," is the name a client can put on screen and ",[70,313,314],{},"websiteUrl"," is where it can link. A connector card wants more than a name, though. ",[70,317,318],{},"description"," gives it a line and ",[70,321,322],{},"icons"," give it a face. It all goes out exactly as you wrote it. ",[70,325,302],{}," is still the word in the unknown tool error, so the pretty one never ends up there.",[328,329,331],"h2",{"id":330},"what-the-client-sees","What the client sees",[280,333,334,335,338,339,342,343,345,346,349,350,353,354,357],{},"The schema goes out exactly as you wrote it, ",[70,336,337],{},"additionalProperties: false"," and ",[70,340,341],{},"pattern"," included. A tool's ",[70,344,310],{}," goes out twice, beside the name and in the annotations. Why twice? Claude Code reads only the second one, so without it your nice ",[70,347,348],{},"Decode WIF"," shows up as ",[70,351,352],{},"keys_wif_decode",". The rest of the annotations come from ",[70,355,356],{},"effect",":",[359,360,361,371,380,390],"ul",{},[362,363,364,367,368],"li",{},[70,365,366],{},"readOnlyHint"," when the effect is ",[70,369,370],{},"read",[362,372,373,376,377],{},[70,374,375],{},"destructiveHint"," when it's ",[70,378,379],{},"destructive",[362,381,382,385,386,389],{},[70,383,384],{},"idempotentHint"," from ",[70,387,388],{},"idempotent",", which defaults to reads",[362,391,392,385,395,398],{},[70,393,394],{},"openWorldHint",[70,396,397],{},"openWorld",", off unless you say so",[280,400,401,402,404,405,407,408,410],{},"Gave a tool its own ",[70,403,322],{},"? They ride along in ",[70,406,288],{},". No icons, no ",[70,409,322],{}," key. Pi, OMP and the AI SDK draw no pictures, so they never see them.",[280,412,413,414,417,418,421],{},"By default ",[70,415,416],{},"details"," stays home and ",[70,419,420],{},"structuredContent"," is never set. Clients that see structured output tend to prefer it over the text and hide the readable answer, so the text is the answer. Everything the next call needs goes in it.",[328,423,425],{"id":424},"when-a-program-reads-the-answer-too","When a program reads the answer too",[280,427,428,429,431,432,435,436,439,440,442,443,445,446,448,449,451,452,455],{},"Sometimes the reader isn't a model. ChatGPT's mention search answers with ",[70,430,420],{},". So does its settings page, and there the read tool must declare an ",[70,433,434],{},"outputSchema",". Give the tool an ",[70,437,438],{},"output"," schema. ",[70,441,288],{}," lists it as ",[70,444,434],{},". Every successful call sends ",[70,447,416],{}," as ",[70,450,420],{},", right next to the text. A returned ",[70,453,454],{},"isError"," still goes out as text alone.",[280,457,458,459,462],{},"The details pass that schema before they leave. Renamed a field and forgot the schema? The client gets ",[70,460,461],{},"\u003Ctool> returned details that don't match its output schema",", one line per problem. Not data that lies about its own shape. Better you hear it than ChatGPT.",[280,464,465,468,469,472,473,338,476,479],{},[70,466,467],{},"meta"," goes out as the tool's ",[70,470,471],{},"_meta",", as written. That's where ",[70,474,475],{},"ui",[70,477,478],{},"openai\u002Fextensions"," live. Pi, OMP and the AI SDK never see it.",[328,481,483],{"id":482},"why-not-mcpserver","Why not McpServer",[280,485,486,487,490,491,494,495,498,499,502,503,506],{},"SDK v2's ",[70,488,489],{},"McpServer.registerTool"," would take the schema. It also answers a call to ",[70,492,493],{},"toString",", ",[70,496,497],{},"constructor"," or ",[70,500,501],{},"__proto__"," with \"Tool toString disabled\", because its lookup reaches ",[70,504,505],{},"Object.prototype",". It echoes a tool name with a newline and an escape code straight back to the client. It glues validation problems into one line and passes a thrown message through with its newline intact.",[280,508,509,510,513,514,517,518,520,521,524,525,528],{},"So the adapter sits on the low-level ",[70,511,512],{},"Server",", which the SDK marks ",[70,515,516],{},"@deprecated",", and does those parts itself. An unknown name, ",[70,519,493],{}," included, gets ",[70,522,523],{},"Unknown \u003Cserver> tool: \"\u003Cname>\""," with the name escaped. Validation gets one line per problem. A throw becomes ",[70,526,527],{},"\u003Ctool> failed: \u003Cmessage>"," on one line.",[328,530,532],{"id":531},"progress","Progress",[280,534,535,536,538,539,542,543,545,546,549,550,553,554,448,556,559],{},"Ask for progress, get progress. When a ",[70,537,292],{}," carries ",[70,540,541],{},"_meta.progressToken",", each ",[70,544,531],{}," line from the tool goes out as ",[70,547,548],{},"notifications\u002Fprogress",", with ",[70,551,552],{},"total"," when the tool knows it. No token, no callback. The tool sees ",[70,555,531],{},[70,557,558],{},"undefined"," and doesn't waste a breath on it.",[280,561,562,563,566],{},"A notification that fails to send lands in ",[70,564,565],{},"server.onerror",". The answer still arrives, and that's the part the model actually needs.",[328,568,570],{"id":569},"questions","Questions",[280,572,573,574,577,578,585,586,588,589,592],{},"A client that declares ",[70,575,576],{},"elicitation"," gets ",[579,580,582],"a",{"href":581},"\u002Fguide\u002Fdefining-tools#ask-the-person-not-the-model",[70,583,584],{},"ask",". Any other client gets ",[70,587,558],{}," there. A tool that calls it with ",[70,590,591],{},"?."," just carries on.",[280,594,595,596,599,600,603,604,607,608,610,611,613],{},"Under the hood it's the 2026 way. The call answers ",[70,597,598],{},"input_required"," with one ",[70,601,602],{},"elicitation\u002Fcreate"," inside. The answers so far ride along in ",[70,605,606],{},"requestState",". On a 2025 connection the SDK plays the client's part. It sends a real ",[70,609,602],{}," and runs the handler again. Same code, both revisions. Each answer is checked against its form. One that doesn't fit gets asked again, a tampered ",[70,612,606],{}," too. Same for an answer whose question changed in between. A yes to \"Apply to A?\" never turns into a yes to B.",[280,615,616],{},"Why no signature on that state? Whoever can forge it can forge the reply next to it. Nothing to protect.",[280,618,619,620,622,623,626,627,630,631,634],{},"Claude Code declares ",[70,621,576],{},", so it gets the form. In ",[70,624,625],{},"claude -p"," there's nobody to fill it, and the tool reads ",[70,628,629],{},"cancel",". An ",[70,632,633],{},"Elicitation"," hook can answer for the person there.",[328,636,638],{"id":637},"errors","Errors",[280,640,641,642,644,645,648],{},"A bad input, a returned ",[70,643,454],{}," and a thrown error all come back as ",[70,646,647],{},"isError: true"," with text. Nothing escapes as a JSON-RPC error, so the model always gets something to read and a reason to try again.",[328,650,652,653,655],{"id":651},"no-server-in-sight","No ",[70,654,512],{}," in sight",[280,657,658,659,661],{},"Some transports don't want a ",[70,660,512],{}," at all. They bring their own JSON-RPC and ask for a listing and a handler, that's it. So the two halves come out on their own:",[62,663,666],{"className":64,"code":664,"filename":665,"language":67,"meta":68,"style":68},"import { callTool, listTools } from \"@agntn\u002Ftools\u002Fmcp\";\nimport { tools } from \".\u002Ftools.ts\";\n\nconst listing = listTools(tools);\nconst answer = await callTool({ name: \"mypackage\" }, tools, \"mypackage_lookup\", { word: \"marmot\" });\n","answers.ts",[70,667,668,681,693,697,712],{"__ignoreMap":68},[73,669,670,672,675,677,679],{"class":75,"line":76},[73,671,80],{"class":79},[73,673,674],{"class":83}," { callTool, listTools } ",[73,676,87],{"class":79},[73,678,91],{"class":90},[73,680,94],{"class":83},[73,682,683,685,687,689,691],{"class":75,"line":97},[73,684,80],{"class":79},[73,686,117],{"class":83},[73,688,87],{"class":79},[73,690,122],{"class":90},[73,692,94],{"class":83},[73,694,695],{"class":75,"line":112},[73,696,131],{"emptyLinePlaceholder":130},[73,698,699,701,704,706,709],{"class":75,"line":127},[73,700,137],{"class":79},[73,702,703],{"class":83}," listing ",[73,705,143],{"class":79},[73,707,708],{"class":146}," listTools",[73,710,711],{"class":83},"(tools);\n",[73,713,714,716,719,721,724,727,730,732,735,738,741,744],{"class":75,"line":134},[73,715,137],{"class":79},[73,717,718],{"class":83}," answer ",[73,720,143],{"class":79},[73,722,723],{"class":79}," await",[73,725,726],{"class":146}," callTool",[73,728,729],{"class":83},"({ name: ",[73,731,165],{"class":90},[73,733,734],{"class":83}," }, tools, ",[73,736,737],{"class":90},"\"mypackage_lookup\"",[73,739,740],{"class":83},", { word: ",[73,742,743],{"class":90},"\"marmot\"",[73,745,746],{"class":83}," });\n",[280,748,749,752,753,755,756,752,759,761,762,765],{},[70,750,751],{},"listTools(tools)"," is what ",[70,754,288],{}," answers. ",[70,757,758],{},"callTool(info, tools, name, args, context)",[70,760,292],{}," answers, unknown names and bad input included. It never throws. Want the abort signal or progress to reach the executor? Pass ",[70,763,764],{},"{ signal, progress }"," last.",[280,767,768,771],{},[70,769,770],{},"createMcpServer"," is built on these two. Your stdio server and your docs page can't drift apart anymore, because there's nothing left to copy by hand.",[328,773,775],{"id":774},"on-h3","On H3",[280,777,778,784,785,788],{},[579,779,783],{"href":780,"rel":781},"https:\u002F\u002Fmcp.h3.dev",[782],"nofollow","h3-mcp"," speaks MCP over HTTP on its own, no SDK involved. It wants a list of its tool definitions, and ",[70,786,787],{},"toH3Tools"," hands it one:",[62,790,792],{"className":64,"code":791,"filename":66,"language":67,"meta":68,"style":68},"import { H3, serve } from \"h3\";\nimport { defineMcpHandler } from \"h3-mcp\";\nimport { toH3Tools } from \"@agntn\u002Ftools\u002Fh3\";\nimport { tools } from \".\u002Ftools.ts\";\n\nconst app = new H3().all(\n  \"\u002Fmcp\",\n  defineMcpHandler({ name: \"mypackage\", version: \"1.0.0\", tools: toH3Tools({ name: \"mypackage\" }, tools) }),\n);\nserve(app);\n",[70,793,794,808,822,836,848,852,875,882,908,912],{"__ignoreMap":68},[73,795,796,798,801,803,806],{"class":75,"line":76},[73,797,80],{"class":79},[73,799,800],{"class":83}," { H3, serve } ",[73,802,87],{"class":79},[73,804,805],{"class":90}," \"h3\"",[73,807,94],{"class":83},[73,809,810,812,815,817,820],{"class":75,"line":97},[73,811,80],{"class":79},[73,813,814],{"class":83}," { defineMcpHandler } ",[73,816,87],{"class":79},[73,818,819],{"class":90}," \"h3-mcp\"",[73,821,94],{"class":83},[73,823,824,826,829,831,834],{"class":75,"line":112},[73,825,80],{"class":79},[73,827,828],{"class":83}," { toH3Tools } ",[73,830,87],{"class":79},[73,832,833],{"class":90}," \"@agntn\u002Ftools\u002Fh3\"",[73,835,94],{"class":83},[73,837,838,840,842,844,846],{"class":75,"line":127},[73,839,80],{"class":79},[73,841,117],{"class":83},[73,843,87],{"class":79},[73,845,122],{"class":90},[73,847,94],{"class":83},[73,849,850],{"class":75,"line":134},[73,851,131],{"emptyLinePlaceholder":130},[73,853,854,856,859,861,864,867,870,873],{"class":75,"line":153},[73,855,137],{"class":79},[73,857,858],{"class":83}," app ",[73,860,143],{"class":79},[73,862,863],{"class":79}," new",[73,865,866],{"class":146}," H3",[73,868,869],{"class":83},"().",[73,871,872],{"class":146},"all",[73,874,150],{"class":83},[73,876,877,880],{"class":75,"line":159},[73,878,879],{"class":90},"  \"\u002Fmcp\"",[73,881,168],{"class":83},[73,883,884,887,889,891,894,896,899,901,903,905],{"class":75,"line":171},[73,885,886],{"class":146},"  defineMcpHandler",[73,888,729],{"class":83},[73,890,165],{"class":90},[73,892,893],{"class":83},", version: ",[73,895,177],{"class":90},[73,897,898],{"class":83},", tools: ",[73,900,787],{"class":146},[73,902,729],{"class":83},[73,904,165],{"class":90},[73,906,907],{"class":83}," }, tools) }),\n",[73,909,910],{"class":75,"line":182},[73,911,254],{"class":83},[73,913,914,917],{"class":75,"line":193},[73,915,916],{"class":146},"serve",[73,918,919],{"class":83},"(app);\n",[280,921,922,923,926,927,930,931,934,935,937,938,940],{},"Each entry is what ",[70,924,925],{},"listTools"," lists, and its handler is ",[70,928,929],{},"callTool",". So the text, the validation lines and the cleaned up errors match the stdio server word for word. The request's abort signal reaches ",[70,932,933],{},"execute",", and ",[70,936,531],{}," turns into ",[70,939,548],{}," when the client sent a token.",[280,942,943],{},"The schema goes to h3-mcp as plain JSON Schema. h3-mcp doesn't validate that, it just passes the arguments on, and the core checks them like on every other host.",[280,945,946,947,949,950,448,952,954],{},"Questions work on the 2026 revision only. h3-mcp has no 2025 fallback for ",[70,948,598],{},", so an older request gets ",[70,951,584],{},[70,953,558],{},".",[280,956,957,958,961,962,964],{},"One difference. An unknown tool name never reaches the adapter. h3-mcp answers it itself, with a JSON-RPC ",[70,959,960],{},"Tool not found"," error instead of the ",[70,963,454],{}," text.",[280,966,967,970,971,338,973,976,977,954],{},[70,968,969],{},"@agntn\u002Ftools\u002Fh3"," imports only types from h3-mcp. Install ",[70,972,783],{},[70,974,975],{},"h3",", and the MCP SDK stays out of your ",[70,978,979],{},"node_modules",[328,981,983,984,987],{"id":982},"on-a-docus-mcp-route","On a Docus ",[70,985,986],{},"\u002Fmcp"," route",[280,989,990,991,998,999,1002],{},"Docus serves MCP through ",[579,992,995],{"href":993,"rel":994},"https:\u002F\u002Fmcp-toolkit.nuxt.dev",[782],[70,996,997],{},"@nuxtjs\u002Fmcp-toolkit",". It wants its own tool definitions, and ",[70,1000,1001],{},"toToolkitTools"," builds them:",[62,1004,1007],{"className":64,"code":1005,"filename":1006,"language":67,"meta":68,"style":68},"import { toToolkitTools } from \"@agntn\u002Ftools\u002Ftoolkit\";\nimport { tools } from \"mypackage\u002Ftools\";\n\nexport default defineMcpHandler({\n  name: \"mypackage\",\n  version: \"1.0.0\",\n  tools: toToolkitTools({ name: \"mypackage\" }, tools),\n});\n","server\u002Fmcp\u002Findex.ts",[70,1008,1009,1023,1036,1040,1054,1063,1072,1086],{"__ignoreMap":68},[73,1010,1011,1013,1016,1018,1021],{"class":75,"line":76},[73,1012,80],{"class":79},[73,1014,1015],{"class":83}," { toToolkitTools } ",[73,1017,87],{"class":79},[73,1019,1020],{"class":90}," \"@agntn\u002Ftools\u002Ftoolkit\"",[73,1022,94],{"class":83},[73,1024,1025,1027,1029,1031,1034],{"class":75,"line":97},[73,1026,80],{"class":79},[73,1028,117],{"class":83},[73,1030,87],{"class":79},[73,1032,1033],{"class":90}," \"mypackage\u002Ftools\"",[73,1035,94],{"class":83},[73,1037,1038],{"class":75,"line":112},[73,1039,131],{"emptyLinePlaceholder":130},[73,1041,1042,1045,1048,1051],{"class":75,"line":127},[73,1043,1044],{"class":79},"export",[73,1046,1047],{"class":79}," default",[73,1049,1050],{"class":146}," defineMcpHandler",[73,1052,1053],{"class":83},"({\n",[73,1055,1056,1059,1061],{"class":75,"line":134},[73,1057,1058],{"class":83},"  name: ",[73,1060,165],{"class":90},[73,1062,168],{"class":83},[73,1064,1065,1068,1070],{"class":75,"line":153},[73,1066,1067],{"class":83},"  version: ",[73,1069,177],{"class":90},[73,1071,168],{"class":83},[73,1073,1074,1077,1079,1081,1083],{"class":75,"line":159},[73,1075,1076],{"class":83},"  tools: ",[73,1078,1001],{"class":146},[73,1080,729],{"class":83},[73,1082,165],{"class":90},[73,1084,1085],{"class":83}," }, tools),\n",[73,1087,1088],{"class":75,"line":171},[73,1089,1090],{"class":83},"});\n",[280,1092,1093,1094,1096,1097,1099,1100,1102,1103,338,1105,1107],{},"Same deal as on H3. Each entry lists what ",[70,1095,925],{}," lists and answers through ",[70,1098,929],{},", so the text and the error lines match stdio word for word. The abort signal reaches ",[70,1101,933],{},". ",[70,1104,438],{},[70,1106,467],{}," ride along too, which hand copies of this route never bothered with.",[280,1109,1110,1111,1114,1115,1118,1119,1122,1123,1126],{},"Now the ugly part. The toolkit hands every schema to SDK v1's ",[70,1112,1113],{},"McpServer",", and that one takes Zod or nothing. So each schema turns into a ",[70,1116,1117],{},"z.looseObject({})"," that lists your JSON Schema as written and waves any object through. The core does the real checking, like everywhere else. Zod has no public hook for this, so the adapter pokes at ",[70,1120,1121],{},"_zod",". Still ugly? Sure. But now it's ugly in one place, and the tests go red first when Zod moves things. Install ",[70,1124,1125],{},"zod"," next to the toolkit, it's a peer.",[280,1128,1129],{},"A few things the toolkit does its own way:",[359,1131,1132,1143,1153,1166],{},[362,1133,1134,1135,1138,1139,1142],{},"SDK v1 adds ",[70,1136,1137],{},"$schema"," to every listed schema and ",[70,1140,1141],{},"execution: { taskSupport: \"forbidden\" }"," to every tool. Harmless, just don't be surprised.",[362,1144,1145,1146,1148,1149,1152],{},"Tool ",[70,1147,322],{}," stay home. The toolkit never passes them to the SDK. Server icons go in ",[70,1150,1151],{},"defineMcpHandler"," as usual.",[362,1154,1155,1156,1159,1160,338,1163,1165],{},"Progress needs sessions. The stateless default answers in plain JSON, so a notification has nowhere to ride. Set ",[70,1157,1158],{},"mcp: { sessions: { enabled: true } }"," in ",[70,1161,1162],{},"nuxt.config.ts",[70,1164,531],{}," shows up like on stdio.",[362,1167,1168,1169,1171,1172,1175],{},"An unknown name never reaches the adapter. SDK v1 answers it, and yes, it answers ",[70,1170,493],{}," with \"Tool toString disabled\". The bug from ",[579,1173,483],{"href":1174},"#why-not-mcpserver",", one major version older.",[975,1177,1179],{"id":1178},"can-it-ask-the-person","Can it ask the person?",[280,1181,1182,1183,357],{},"It can. Hand it the toolkit's own ",[70,1184,1185],{},"useMcpElicitation",[62,1187,1189],{"className":64,"code":1188,"filename":1006,"language":67,"meta":68,"style":68},"export default defineMcpHandler({\n  name: \"mypackage\",\n  version: \"1.0.0\",\n  tools: toToolkitTools({ name: \"mypackage\" }, tools, { elicitation: useMcpElicitation }),\n});\n",[70,1190,1191,1201,1209,1217,1230],{"__ignoreMap":68},[73,1192,1193,1195,1197,1199],{"class":75,"line":76},[73,1194,1044],{"class":79},[73,1196,1047],{"class":79},[73,1198,1050],{"class":146},[73,1200,1053],{"class":83},[73,1202,1203,1205,1207],{"class":75,"line":97},[73,1204,1058],{"class":83},[73,1206,165],{"class":90},[73,1208,168],{"class":83},[73,1210,1211,1213,1215],{"class":75,"line":112},[73,1212,1067],{"class":83},[73,1214,177],{"class":90},[73,1216,168],{"class":83},[73,1218,1219,1221,1223,1225,1227],{"class":75,"line":127},[73,1220,1076],{"class":83},[73,1222,1001],{"class":146},[73,1224,729],{"class":83},[73,1226,165],{"class":90},[73,1228,1229],{"class":83}," }, tools, { elicitation: useMcpElicitation }),\n",[73,1231,1232],{"class":75,"line":134},[73,1233,1090],{"class":83},[280,1235,1236,1237,1239,1240,1242,1243,1246],{},"SDK v1 has never heard of ",[70,1238,598],{},". So the question goes out as a real ",[70,1241,602],{}," on the call's own stream, and the call just waits for it. One run per call, like in Pi. Only ",[70,1244,1245],{},"supports(\"form\")"," comes from the composable. The form is ours, built and checked the same way as on stdio.",[280,1248,1249,1250,357],{},"It needs two switches in ",[70,1251,1162],{},[62,1253,1255],{"className":64,"code":1254,"filename":1162,"language":67,"meta":68,"style":68},"export default defineNuxtConfig({\n  mcp: { sessions: { enabled: true } },\n  nitro: { experimental: { asyncContext: true } },\n});\n",[70,1256,1257,1268,1273,1278],{"__ignoreMap":68},[73,1258,1259,1261,1263,1266],{"class":75,"line":76},[73,1260,1044],{"class":79},[73,1262,1047],{"class":79},[73,1264,1265],{"class":146}," defineNuxtConfig",[73,1267,1053],{"class":83},[73,1269,1270],{"class":75,"line":97},[73,1271,1272],{"class":83},"  mcp: { sessions: { enabled: true } },\n",[73,1274,1275],{"class":75,"line":112},[73,1276,1277],{"class":83},"  nitro: { experimental: { asyncContext: true } },\n",[73,1279,1280],{"class":75,"line":127},[73,1281,1090],{"class":83},[280,1283,1284,1285,1288,1289,1291,1292,1294,1295,1298,1299,1301],{},"Forget sessions and every request gets a fresh server that never saw ",[70,1286,1287],{},"initialize",". It can't know the client fills forms, so ",[70,1290,584],{}," stays ",[70,1293,558],{},". Quiet, and instant. No request hanging somewhere. Forget ",[70,1296,1297],{},"asyncContext"," and it gets loud. ",[70,1300,1185],{}," can't find the request, so every call answers \"Nitro request context is not available\". Annoying? Sure. But it names the flag.",[280,1303,1304,1305,1307],{},"The question follows the call's abort signal, so a cancelled call takes its form back. Unless the client runs SDK v1 and it's the session's first request. That client skips a cancel for request id 0, so the form stays up with nobody waiting for it. SDK v2 clients get it right. One more catch. SDK v1 gives every request 60 seconds, a form too. A person who thinks longer makes ",[70,1306,584],{}," throw.",[1309,1310,1311],"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);}",{"title":68,"searchDepth":97,"depth":97,"links":1313},[1314,1315,1316,1317,1318,1319,1320,1322,1323],{"id":330,"depth":97,"text":331},{"id":424,"depth":97,"text":425},{"id":482,"depth":97,"text":483},{"id":531,"depth":97,"text":532},{"id":569,"depth":97,"text":570},{"id":637,"depth":97,"text":638},{"id":651,"depth":97,"text":1321},"No Server in sight",{"id":774,"depth":97,"text":775},{"id":982,"depth":97,"text":1324,"children":1325},"On a Docus \u002Fmcp route",[1326],{"id":1178,"depth":112,"text":1179},"createMcpServer builds an MCP server on SDK v2 for any list of tools. With the error handling the SDK's own McpServer gets wrong","md",null,{},{"title":36,"description":1327},"cVbgsy-8_dAsQYV0hJllRvaLKkhSS8qT7SB9RsAev60",[1334,1336],{"title":34,"path":30,"stem":31,"description":1335,"children":-1},"The four hosts one tool definition reaches. What each adapter gives its host and which peer it needs",{"title":40,"path":41,"stem":42,"description":1337,"children":-1},"registerPiTools puts every tool on Pi with its prompt snippet and guidelines. And throws on failure because older Pi only listens to throws",[1339,1354,1369,1381],{"key":60,"rows":1340,"registration":1351,"fields":1352},[1341,1343,1346,1349],{"label":310,"value":1342},"Text Slug",{"label":1344,"value":1345,"accent":130},"hints","readOnly, idempotent",{"label":1347,"value":1348},"inputSchema","text, separator",{"label":1350,"value":647},"failure","{\n  \"name\": \"text_slug\",\n  \"title\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"required\": [\n      \"text\"\n    ],\n    \"additionalProperties\": false\n  },\n  \"annotations\": {\n    \"title\": \"Text Slug\",\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"idempotentHint\": true,\n    \"openWorldHint\": false\n  }\n}",{"name":302,"label":310,"schema":1347,"effect":1353,"prompt":1329,"run":1329},"annotations",{"key":1355,"rows":1356,"registration":1366,"fields":1367},"pi",[1357,1359,1362,1364],{"label":1358,"value":1342},"label",{"label":1360,"value":1361,"accent":130},"prompt","snippet, 1 rule",{"label":1363,"value":1348},"parameters",{"label":1350,"value":1365},"thrown","{\n  \"name\": \"text_slug\",\n  \"label\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"promptSnippet\": \"Use text_slug to turn a title into a URL slug.\",\n  \"promptGuidelines\": [\n    \"Pass separator '_' for identifiers. The default '-' is for URLs.\"\n  ],\n  \"parameters\": {\n    \"type\": \"object\",\n    \"required\": [\n      \"text\"\n    ],\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"additionalProperties\": false\n  },\n  \"execute\": \"[function]\"\n}",{"name":302,"label":1358,"schema":1363,"effect":1329,"prompt":1368,"run":933},"promptSnippet",{"key":1370,"rows":1371,"registration":1379,"fields":1380},"omp",[1372,1373,1375,1377],{"label":1358,"value":1342},{"label":1374,"value":370,"accent":130},"approval",{"label":1363,"value":1376},"Type.Unsafe(json)",{"label":1350,"value":1378},"isError or thrown","{\n  \"name\": \"text_slug\",\n  \"label\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"parameters\": {\n    \"unsafe\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"text\"\n      ],\n      \"properties\": {\n        \"text\": {\n          \"type\": \"string\",\n          \"minLength\": 1,\n          \"maxLength\": 200,\n          \"pattern\": \"\\\\S\",\n          \"description\": \"Title to slug\"\n        },\n        \"separator\": {\n          \"enum\": [\n            \"-\",\n            \"_\",\n            \".\"\n          ],\n          \"description\": \"Joins the words. '-' when omitted\"\n        }\n      },\n      \"additionalProperties\": false\n    }\n  },\n  \"approval\": \"read\",\n  \"execute\": \"[function]\",\n  \"renderCall\": \"[function]\",\n  \"renderResult\": \"[function]\"\n}",{"name":302,"label":1358,"schema":1363,"effect":1374,"prompt":1329,"run":933},{"key":1382,"rows":1383,"registration":1390,"fields":1391},"ai",[1384,1385,1387,1389],{"label":310,"value":1342},{"label":1347,"value":1386,"accent":130},"jsonSchema()",{"label":438,"value":1388},"details + text",{"label":1350,"value":1365},"{\n  \"title\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"required\": [\n      \"text\"\n    ],\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"additionalProperties\": false\n  },\n  \"execute\": \"[function]\",\n  \"toModelOutput\": \"[function]\"\n}",{"name":1329,"label":310,"schema":1347,"effect":1329,"prompt":1329,"run":933},1791624637639]