[{"data":1,"prerenderedAt":2296},["ShallowReactive",2],{"\u002Fblog\u002Fhow-to-build-an-mcp-server-2026\u002F":3,"\u002Fblog\u002Fhow-to-build-an-mcp-server-2026\u002F-related":2278,"\u002Fblog\u002Fhow-to-build-an-mcp-server-2026\u002F-resources":2291},{"id":4,"title":5,"author":6,"body":10,"canonical":2186,"category":2187,"checker":2186,"createdAt":2188,"description":2189,"disclosure":2186,"extension":2190,"faq":2191,"heroActions":2186,"howTo":2210,"lastVerified":2228,"location":2229,"meta":2230,"navigation":421,"ogImage":2186,"path":2236,"primaryKeyword":2237,"readingTime":2238,"related":2239,"seo":2243,"seoTitle":2244,"sources":2245,"stem":2268,"tags":2269,"takeaways":2270,"updatedAt":2188,"url":2276,"__hash__":2277},"blog\u002Fblog\u002Fhow-to-build-an-mcp-server-2026.md","How to Build an MCP Server in 2026: Step by Step with Code",{"name":7,"social":8},"Simon Dziak",{"twitter":9},"@SzymonDziak",{"type":11,"value":12,"toc":2173},"minimark",[13,21,47,52,65,87,90,94,105,122,126,137,193,212,216,227,320,330,334,344,1063,1066,1100,1107,1111,1123,1212,1227,1230,1261,1276,1280,1290,1379,1390,1393,1470,1483,1487,1501,1534,1558,1737,1743,1789,1805,1809,1832,1899,1913,1920,1926,1985,1989,1995,2129,2148,2152,2158,2162,2169],[14,15,16,20],"p",{},[17,18,19],"strong",{},"To build an MCP server, you register tools with an official SDK and serve them over stdio or Streamable HTTP."," With the TypeScript SDK v2, a remote server with two tools over an existing REST API is 52 lines. This guide builds that server, tests it with the MCP Inspector and adds sign-in. Everything follows the current spec revision, 2026-07-28.",[14,22,23,24,28,29,32,33,36,37,40,41,46],{},"Simon Dziak, App369’s one developer, ran every command and code block below on November 8, 2026. The versions were ",[25,26,27],"code",{},"@modelcontextprotocol\u002Fserver"," 2.4.0, ",[25,30,31],{},"@modelcontextprotocol\u002Fexpress"," 2.0.2, ",[25,34,35],{},"@modelcontextprotocol\u002Fnode"," 2.1.2, the Inspector 2.13.0 and Python ",[25,38,39],{},"mcp"," 2.3.0. If you are deciding which tools to expose and how customers sign in, start with ",[42,43,45],"a",{"href":44},"\u002Fblog\u002Fhow-to-add-an-mcp-server-to-your-saas-2026\u002F","how to add an MCP server to your SaaS",". This post is the code.",[48,49,51],"h2",{"id":50},"what-is-an-mcp-server","What is an MCP server?",[14,53,54,57,58,64],{},[17,55,56],{},"An MCP server is a program that offers tools, resources and prompts to AI applications over the Model Context Protocol."," The ",[42,59,63],{"href":60,"rel":61},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2026-07-28",[62],"nofollow","2026-07-28 specification"," defines three server features:",[66,67,68,75,81],"ul",{},[69,70,71,74],"li",{},[17,72,73],{},"Tools:"," functions for the AI model to execute.",[69,76,77,80],{},[17,78,79],{},"Resources:"," context and data, for the user or the AI model to use.",[69,82,83,86],{},[17,84,85],{},"Prompts:"," templated messages and workflows for users.",[14,88,89],{},"Messages are JSON-RPC 2.0. The client lives inside a host such as Claude, and the host asks the user before it calls a tool. Most servers built over an API only need tools, so this guide covers tools.",[48,91,93],{"id":92},"what-do-you-need-before-you-start","What do you need before you start?",[14,95,96,57,99,104],{},[17,97,98],{},"You need Node.js 20 or later, an official SDK and an API for the tools to call.",[42,100,103],{"href":101,"rel":102},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fdocs\u002Fsdk",[62],"MCP SDK page"," lists six Tier 1 SDKs: TypeScript, Python, C#, Go, Rust and Ruby. Java is Tier 2. Swift, PHP and Kotlin are Tier 3.",[14,106,107,108,110,111,114,115,117,118,121],{},"This guide uses the TypeScript SDK v2. It ships as separate packages: ",[25,109,27],{}," and ",[25,112,113],{},"@modelcontextprotocol\u002Fclient",", plus adapters such as ",[25,116,31],{},". The older v1 package, ",[25,119,120],{},"@modelcontextprotocol\u002Fsdk",", is now the maintenance line and implements the spec up to 2025-11-25.",[48,123,125],{"id":124},"which-transport-should-your-mcp-server-use","Which transport should your MCP server use?",[14,127,128,57,131,136],{},[17,129,130],{},"Use Streamable HTTP when people connect to your server by URL, and stdio when it runs on the user’s machine.",[42,132,135],{"href":133,"rel":134},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2026-07-28\u002Fbasic\u002Ftransports",[62],"transports page"," defines only these two.",[138,139,140,156],"table",{},[141,142,143],"thead",{},[144,145,146,150,153],"tr",{},[147,148,149],"th",{},"Transport",[147,151,152],{},"How it works",[147,154,155],{},"Use it for",[157,158,159,171,182],"tbody",{},[144,160,161,165,168],{},[162,163,164],"td",{},"stdio",[162,166,167],{},"The client starts your server as a subprocess and sends newline-delimited messages over its standard streams",[162,169,170],{},"Local tools, such as a server in Claude Desktop that reads files",[144,172,173,176,179],{},[162,174,175],{},"Streamable HTTP",[162,177,178],{},"Each message is an HTTP POST to one endpoint. Replies are a JSON object or an SSE stream for that request",[162,180,181],{},"A server over your SaaS API that many users reach by URL",[144,183,184,187,190],{},[162,185,186],{},"HTTP+SSE (2024-11-05)",[162,188,189],{},"Deprecated since 2025-03-26",[162,191,192],{},"Nothing new",[14,194,195,196,199,200,203,204,207,208,211],{},"Revision 2026-07-28 changed Streamable HTTP. It removed the GET stream endpoint and protocol-level sessions, so ",[25,197,198],{},"Mcp-Session-Id"," is gone. Every POST now carries an ",[25,201,202],{},"MCP-Protocol-Version"," header and an ",[25,205,206],{},"Mcp-Method"," header. Tool calls also carry ",[25,209,210],{},"Mcp-Name",". The server checks each header against the body.",[48,213,215],{"id":214},"step-1-how-do-you-set-up-the-project","Step 1: How do you set up the project?",[14,217,218,57,221,226],{},[17,219,220],{},"Create a Node.js project and install the server package, the Express adapter, Express and zod.",[42,222,225],{"href":223,"rel":224},"https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002Fserving\u002Fexpress.html",[62],"Express guide"," lists these packages.",[228,229,234],"pre",{"className":230,"code":231,"language":232,"meta":233,"style":233},"language-bash shiki shiki-themes github-light github-dark","mkdir invoices-mcp && cd invoices-mcp\nnpm init -y\nnpm pkg set type=module\nnpm install @modelcontextprotocol\u002Fserver @modelcontextprotocol\u002Fexpress @modelcontextprotocol\u002Fnode express zod tsx\nmkdir src\n","bash","",[25,235,236,260,272,286,312],{"__ignoreMap":233},[237,238,241,245,249,253,257],"span",{"class":239,"line":240},"line",1,[237,242,244],{"class":243},"sScJk","mkdir",[237,246,248],{"class":247},"sZZnC"," invoices-mcp",[237,250,252],{"class":251},"sVt8B"," && ",[237,254,256],{"class":255},"sj4cs","cd",[237,258,259],{"class":247}," invoices-mcp\n",[237,261,263,266,269],{"class":239,"line":262},2,[237,264,265],{"class":243},"npm",[237,267,268],{"class":247}," init",[237,270,271],{"class":255}," -y\n",[237,273,275,277,280,283],{"class":239,"line":274},3,[237,276,265],{"class":243},[237,278,279],{"class":247}," pkg",[237,281,282],{"class":247}," set",[237,284,285],{"class":247}," type=module\n",[237,287,289,291,294,297,300,303,306,309],{"class":239,"line":288},4,[237,290,265],{"class":243},[237,292,293],{"class":247}," install",[237,295,296],{"class":247}," @modelcontextprotocol\u002Fserver",[237,298,299],{"class":247}," @modelcontextprotocol\u002Fexpress",[237,301,302],{"class":247}," @modelcontextprotocol\u002Fnode",[237,304,305],{"class":247}," express",[237,307,308],{"class":247}," zod",[237,310,311],{"class":247}," tsx\n",[237,313,315,317],{"class":239,"line":314},5,[237,316,244],{"class":243},[237,318,319],{"class":247}," src\n",[14,321,322,323,110,326,329],{},"The example wraps a small invoices API. It has two endpoints, ",[25,324,325],{},"GET \u002Fv1\u002Finvoices",[25,327,328],{},"GET \u002Fv1\u002Finvoices\u002F:id",", and needs an API key. Swap in your own API.",[48,331,333],{"id":332},"step-2-how-do-you-register-tools-over-your-api","Step 2: How do you register tools over your API?",[14,335,336,339,340,343],{},[17,337,338],{},"Register each tool with a name, a description for the model, a zod input schema and a handler that calls your API."," Put this in ",[25,341,342],{},"src\u002Fserver.ts",":",[228,345,349],{"className":346,"code":347,"language":348,"meta":233,"style":233},"language-ts shiki shiki-themes github-light github-dark","import { createMcpExpressApp } from '@modelcontextprotocol\u002Fexpress';\nimport { toNodeHandler } from '@modelcontextprotocol\u002Fnode';\nimport { createMcpHandler, McpServer } from '@modelcontextprotocol\u002Fserver';\nimport * as z from 'zod\u002Fv4';\n\nconst API_BASE = process.env.API_BASE ?? 'http:\u002F\u002F127.0.0.1:4000';\nconst API_KEY = process.env.API_KEY; \u002F\u002F the server's own key, never the caller's token\n\nasync function callApi(path: string) {\n  const res = await fetch(`${API_BASE}${path}`, { headers: { Authorization: `Bearer ${API_KEY}` } });\n  if (!res.ok) throw new Error(`API returned HTTP ${res.status} for ${path}`);\n  return res.json();\n}\n\nconst handler = createMcpHandler(() => {\n  const server = new McpServer({ name: 'invoices', version: '1.0.0' });\n\n  server.registerTool(\n    'list_invoices',\n    {\n      description: 'List invoices. Use when the user asks which invoices are open or paid. Returns id, client, total and status.',\n      inputSchema: z.object({\n        status: z.enum(['open', 'paid']).optional().describe('Only invoices with this status'),\n      }),\n      annotations: { readOnlyHint: true },\n    },\n    async ({ status }) => {\n      const rows = await callApi(`\u002Fv1\u002Finvoices${status ? `?status=${status}` : ''}`);\n      return { content: [{ type: 'text', text: JSON.stringify(rows) }] };\n    },\n  );\n\n  server.registerTool(\n    'get_invoice',\n    {\n      description: 'Get one invoice by its id, such as inv_101.',\n      inputSchema: z.object({ id: z.string().describe('Invoice id, for example inv_101') }),\n      annotations: { readOnlyHint: true },\n    },\n    async ({ id }) => {\n      const row = await callApi(`\u002Fv1\u002Finvoices\u002F${encodeURIComponent(id)}`);\n      return { content: [{ type: 'text', text: JSON.stringify(row) }] };\n    },\n  );\n\n  return server;\n});\n","ts",[25,350,351,369,383,397,417,423,449,471,476,503,548,596,611,617,622,644,674,679,691,700,706,717,729,770,776,788,794,812,852,878,883,889,894,903,911,916,926,951,960,965,981,1014,1034,1039,1044,1049,1057],{"__ignoreMap":233},[237,352,353,357,360,363,366],{"class":239,"line":240},[237,354,356],{"class":355},"szBVR","import",[237,358,359],{"class":251}," { createMcpExpressApp } ",[237,361,362],{"class":355},"from",[237,364,365],{"class":247}," '@modelcontextprotocol\u002Fexpress'",[237,367,368],{"class":251},";\n",[237,370,371,373,376,378,381],{"class":239,"line":262},[237,372,356],{"class":355},[237,374,375],{"class":251}," { toNodeHandler } ",[237,377,362],{"class":355},[237,379,380],{"class":247}," '@modelcontextprotocol\u002Fnode'",[237,382,368],{"class":251},[237,384,385,387,390,392,395],{"class":239,"line":274},[237,386,356],{"class":355},[237,388,389],{"class":251}," { createMcpHandler, McpServer } ",[237,391,362],{"class":355},[237,393,394],{"class":247}," '@modelcontextprotocol\u002Fserver'",[237,396,368],{"class":251},[237,398,399,401,404,407,410,412,415],{"class":239,"line":288},[237,400,356],{"class":355},[237,402,403],{"class":255}," *",[237,405,406],{"class":355}," as",[237,408,409],{"class":251}," z ",[237,411,362],{"class":355},[237,413,414],{"class":247}," 'zod\u002Fv4'",[237,416,368],{"class":251},[237,418,419],{"class":239,"line":314},[237,420,422],{"emptyLinePlaceholder":421},true,"\n",[237,424,426,429,432,435,438,441,444,447],{"class":239,"line":425},6,[237,427,428],{"class":355},"const",[237,430,431],{"class":255}," API_BASE",[237,433,434],{"class":355}," =",[237,436,437],{"class":251}," process.env.",[237,439,440],{"class":255},"API_BASE",[237,442,443],{"class":355}," ??",[237,445,446],{"class":247}," 'http:\u002F\u002F127.0.0.1:4000'",[237,448,368],{"class":251},[237,450,452,454,457,459,461,464,467],{"class":239,"line":451},7,[237,453,428],{"class":355},[237,455,456],{"class":255}," API_KEY",[237,458,434],{"class":355},[237,460,437],{"class":251},[237,462,463],{"class":255},"API_KEY",[237,465,466],{"class":251},"; ",[237,468,470],{"class":469},"sJ8bj","\u002F\u002F the server's own key, never the caller's token\n",[237,472,474],{"class":239,"line":473},8,[237,475,422],{"emptyLinePlaceholder":421},[237,477,479,482,485,488,491,495,497,500],{"class":239,"line":478},9,[237,480,481],{"class":355},"async",[237,483,484],{"class":355}," function",[237,486,487],{"class":243}," callApi",[237,489,490],{"class":251},"(",[237,492,494],{"class":493},"s4XuR","path",[237,496,343],{"class":355},[237,498,499],{"class":255}," string",[237,501,502],{"class":251},") {\n",[237,504,506,509,512,514,517,520,522,525,527,530,532,535,538,541,543,545],{"class":239,"line":505},10,[237,507,508],{"class":355},"  const",[237,510,511],{"class":255}," res",[237,513,434],{"class":355},[237,515,516],{"class":355}," await",[237,518,519],{"class":243}," fetch",[237,521,490],{"class":251},[237,523,524],{"class":247},"`${",[237,526,440],{"class":255},[237,528,529],{"class":247},"}${",[237,531,494],{"class":251},[237,533,534],{"class":247},"}`",[237,536,537],{"class":251},", { headers: { Authorization: ",[237,539,540],{"class":247},"`Bearer ${",[237,542,463],{"class":255},[237,544,534],{"class":247},[237,546,547],{"class":251}," } });\n",[237,549,551,554,557,560,563,566,569,572,574,577,580,583,586,589,591,593],{"class":239,"line":550},11,[237,552,553],{"class":355},"  if",[237,555,556],{"class":251}," (",[237,558,559],{"class":355},"!",[237,561,562],{"class":251},"res.ok) ",[237,564,565],{"class":355},"throw",[237,567,568],{"class":355}," new",[237,570,571],{"class":243}," Error",[237,573,490],{"class":251},[237,575,576],{"class":247},"`API returned HTTP ${",[237,578,579],{"class":251},"res",[237,581,582],{"class":247},".",[237,584,585],{"class":251},"status",[237,587,588],{"class":247},"} for ${",[237,590,494],{"class":251},[237,592,534],{"class":247},[237,594,595],{"class":251},");\n",[237,597,599,602,605,608],{"class":239,"line":598},12,[237,600,601],{"class":355},"  return",[237,603,604],{"class":251}," res.",[237,606,607],{"class":243},"json",[237,609,610],{"class":251},"();\n",[237,612,614],{"class":239,"line":613},13,[237,615,616],{"class":251},"}\n",[237,618,620],{"class":239,"line":619},14,[237,621,422],{"emptyLinePlaceholder":421},[237,623,625,627,630,632,635,638,641],{"class":239,"line":624},15,[237,626,428],{"class":355},[237,628,629],{"class":255}," handler",[237,631,434],{"class":355},[237,633,634],{"class":243}," createMcpHandler",[237,636,637],{"class":251},"(() ",[237,639,640],{"class":355},"=>",[237,642,643],{"class":251}," {\n",[237,645,647,649,652,654,656,659,662,665,668,671],{"class":239,"line":646},16,[237,648,508],{"class":355},[237,650,651],{"class":255}," server",[237,653,434],{"class":355},[237,655,568],{"class":355},[237,657,658],{"class":243}," McpServer",[237,660,661],{"class":251},"({ name: ",[237,663,664],{"class":247},"'invoices'",[237,666,667],{"class":251},", version: ",[237,669,670],{"class":247},"'1.0.0'",[237,672,673],{"class":251}," });\n",[237,675,677],{"class":239,"line":676},17,[237,678,422],{"emptyLinePlaceholder":421},[237,680,682,685,688],{"class":239,"line":681},18,[237,683,684],{"class":251},"  server.",[237,686,687],{"class":243},"registerTool",[237,689,690],{"class":251},"(\n",[237,692,694,697],{"class":239,"line":693},19,[237,695,696],{"class":247},"    'list_invoices'",[237,698,699],{"class":251},",\n",[237,701,703],{"class":239,"line":702},20,[237,704,705],{"class":251},"    {\n",[237,707,709,712,715],{"class":239,"line":708},21,[237,710,711],{"class":251},"      description: ",[237,713,714],{"class":247},"'List invoices. Use when the user asks which invoices are open or paid. Returns id, client, total and status.'",[237,716,699],{"class":251},[237,718,720,723,726],{"class":239,"line":719},22,[237,721,722],{"class":251},"      inputSchema: z.",[237,724,725],{"class":243},"object",[237,727,728],{"class":251},"({\n",[237,730,732,735,738,741,744,747,750,753,756,759,762,764,767],{"class":239,"line":731},23,[237,733,734],{"class":251},"        status: z.",[237,736,737],{"class":243},"enum",[237,739,740],{"class":251},"([",[237,742,743],{"class":247},"'open'",[237,745,746],{"class":251},", ",[237,748,749],{"class":247},"'paid'",[237,751,752],{"class":251},"]).",[237,754,755],{"class":243},"optional",[237,757,758],{"class":251},"().",[237,760,761],{"class":243},"describe",[237,763,490],{"class":251},[237,765,766],{"class":247},"'Only invoices with this status'",[237,768,769],{"class":251},"),\n",[237,771,773],{"class":239,"line":772},24,[237,774,775],{"class":251},"      }),\n",[237,777,779,782,785],{"class":239,"line":778},25,[237,780,781],{"class":251},"      annotations: { readOnlyHint: ",[237,783,784],{"class":255},"true",[237,786,787],{"class":251}," },\n",[237,789,791],{"class":239,"line":790},26,[237,792,793],{"class":251},"    },\n",[237,795,797,800,803,805,808,810],{"class":239,"line":796},27,[237,798,799],{"class":355},"    async",[237,801,802],{"class":251}," ({ ",[237,804,585],{"class":493},[237,806,807],{"class":251}," }) ",[237,809,640],{"class":355},[237,811,643],{"class":251},[237,813,815,818,821,823,825,827,829,832,834,837,840,842,844,847,850],{"class":239,"line":814},28,[237,816,817],{"class":355},"      const",[237,819,820],{"class":255}," rows",[237,822,434],{"class":355},[237,824,516],{"class":355},[237,826,487],{"class":243},[237,828,490],{"class":251},[237,830,831],{"class":247},"`\u002Fv1\u002Finvoices${",[237,833,585],{"class":251},[237,835,836],{"class":355}," ?",[237,838,839],{"class":247}," `?status=${",[237,841,585],{"class":251},[237,843,534],{"class":247},[237,845,846],{"class":355}," :",[237,848,849],{"class":247}," ''}`",[237,851,595],{"class":251},[237,853,855,858,861,864,867,870,872,875],{"class":239,"line":854},29,[237,856,857],{"class":355},"      return",[237,859,860],{"class":251}," { content: [{ type: ",[237,862,863],{"class":247},"'text'",[237,865,866],{"class":251},", text: ",[237,868,869],{"class":255},"JSON",[237,871,582],{"class":251},[237,873,874],{"class":243},"stringify",[237,876,877],{"class":251},"(rows) }] };\n",[237,879,881],{"class":239,"line":880},30,[237,882,793],{"class":251},[237,884,886],{"class":239,"line":885},31,[237,887,888],{"class":251},"  );\n",[237,890,892],{"class":239,"line":891},32,[237,893,422],{"emptyLinePlaceholder":421},[237,895,897,899,901],{"class":239,"line":896},33,[237,898,684],{"class":251},[237,900,687],{"class":243},[237,902,690],{"class":251},[237,904,906,909],{"class":239,"line":905},34,[237,907,908],{"class":247},"    'get_invoice'",[237,910,699],{"class":251},[237,912,914],{"class":239,"line":913},35,[237,915,705],{"class":251},[237,917,919,921,924],{"class":239,"line":918},36,[237,920,711],{"class":251},[237,922,923],{"class":247},"'Get one invoice by its id, such as inv_101.'",[237,925,699],{"class":251},[237,927,929,931,933,936,939,941,943,945,948],{"class":239,"line":928},37,[237,930,722],{"class":251},[237,932,725],{"class":243},[237,934,935],{"class":251},"({ id: z.",[237,937,938],{"class":243},"string",[237,940,758],{"class":251},[237,942,761],{"class":243},[237,944,490],{"class":251},[237,946,947],{"class":247},"'Invoice id, for example inv_101'",[237,949,950],{"class":251},") }),\n",[237,952,954,956,958],{"class":239,"line":953},38,[237,955,781],{"class":251},[237,957,784],{"class":255},[237,959,787],{"class":251},[237,961,963],{"class":239,"line":962},39,[237,964,793],{"class":251},[237,966,968,970,972,975,977,979],{"class":239,"line":967},40,[237,969,799],{"class":355},[237,971,802],{"class":251},[237,973,974],{"class":493},"id",[237,976,807],{"class":251},[237,978,640],{"class":355},[237,980,643],{"class":251},[237,982,984,986,989,991,993,995,997,1000,1003,1005,1007,1010,1012],{"class":239,"line":983},41,[237,985,817],{"class":355},[237,987,988],{"class":255}," row",[237,990,434],{"class":355},[237,992,516],{"class":355},[237,994,487],{"class":243},[237,996,490],{"class":251},[237,998,999],{"class":247},"`\u002Fv1\u002Finvoices\u002F${",[237,1001,1002],{"class":243},"encodeURIComponent",[237,1004,490],{"class":247},[237,1006,974],{"class":251},[237,1008,1009],{"class":247},")",[237,1011,534],{"class":247},[237,1013,595],{"class":251},[237,1015,1017,1019,1021,1023,1025,1027,1029,1031],{"class":239,"line":1016},42,[237,1018,857],{"class":355},[237,1020,860],{"class":251},[237,1022,863],{"class":247},[237,1024,866],{"class":251},[237,1026,869],{"class":255},[237,1028,582],{"class":251},[237,1030,874],{"class":243},[237,1032,1033],{"class":251},"(row) }] };\n",[237,1035,1037],{"class":239,"line":1036},43,[237,1038,793],{"class":251},[237,1040,1042],{"class":239,"line":1041},44,[237,1043,888],{"class":251},[237,1045,1047],{"class":239,"line":1046},45,[237,1048,422],{"emptyLinePlaceholder":421},[237,1050,1052,1054],{"class":239,"line":1051},46,[237,1053,601],{"class":355},[237,1055,1056],{"class":251}," server;\n",[237,1058,1060],{"class":239,"line":1059},47,[237,1061,1062],{"class":251},"});\n",[14,1064,1065],{},"Three details matter here:",[1067,1068,1069,1084,1090],"ol",{},[69,1070,1071,57,1078,1083],{},[17,1072,1073,1074,1077],{},"The factory builds a new ",[25,1075,1076],{},"McpServer"," each time.",[42,1079,1082],{"href":1080,"rel":1081},"https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002Fserving\u002Fhttp.html",[62],"HTTP guide"," runs it once per request, so no state lives on the instance and the endpoint scales sideways.",[69,1085,1086,1089],{},[17,1087,1088],{},"The description is for the model."," It says when to use the tool and what comes back. The model picks tools by these words.",[69,1091,1092,1095,1096,1099],{},[17,1093,1094],{},"Errors become tool errors."," When the API returns 404, the SDK sends back the message with ",[25,1097,1098],{},"isError: true",", so the model can tell the user.",[14,1101,1102,1103,1106],{},"The SDK validates inputs against the schema before your handler runs. A bad argument comes back as an ",[25,1104,1105],{},"Input validation error"," result, and your API never sees it.",[48,1108,1110],{"id":1109},"step-3-how-do-you-serve-it-over-streamable-http","Step 3: How do you serve it over Streamable HTTP?",[14,1112,1113,1120,1121,343],{},[17,1114,1115,1116,1119],{},"Mount the handler on one ",[25,1117,1118],{},"\u002Fmcp"," route."," Add these lines to the end of ",[25,1122,342],{},[228,1124,1126],{"className":346,"code":1125,"language":348,"meta":233,"style":233},"const app = createMcpExpressApp();\nconst node = toNodeHandler(handler);\napp.all('\u002Fmcp', (req, res) => void node(req, res, req.body));\napp.listen(3000, '127.0.0.1');\n",[25,1127,1128,1142,1157,1193],{"__ignoreMap":233},[237,1129,1130,1132,1135,1137,1140],{"class":239,"line":240},[237,1131,428],{"class":355},[237,1133,1134],{"class":255}," app",[237,1136,434],{"class":355},[237,1138,1139],{"class":243}," createMcpExpressApp",[237,1141,610],{"class":251},[237,1143,1144,1146,1149,1151,1154],{"class":239,"line":262},[237,1145,428],{"class":355},[237,1147,1148],{"class":255}," node",[237,1150,434],{"class":355},[237,1152,1153],{"class":243}," toNodeHandler",[237,1155,1156],{"class":251},"(handler);\n",[237,1158,1159,1162,1165,1167,1170,1173,1176,1178,1180,1183,1185,1188,1190],{"class":239,"line":274},[237,1160,1161],{"class":251},"app.",[237,1163,1164],{"class":243},"all",[237,1166,490],{"class":251},[237,1168,1169],{"class":247},"'\u002Fmcp'",[237,1171,1172],{"class":251},", (",[237,1174,1175],{"class":493},"req",[237,1177,746],{"class":251},[237,1179,579],{"class":493},[237,1181,1182],{"class":251},") ",[237,1184,640],{"class":355},[237,1186,1187],{"class":355}," void",[237,1189,1148],{"class":243},[237,1191,1192],{"class":251},"(req, res, req.body));\n",[237,1194,1195,1197,1200,1202,1205,1207,1210],{"class":239,"line":288},[237,1196,1161],{"class":251},[237,1198,1199],{"class":243},"listen",[237,1201,490],{"class":251},[237,1203,1204],{"class":255},"3000",[237,1206,746],{"class":251},[237,1208,1209],{"class":247},"'127.0.0.1'",[237,1211,595],{"class":251},[14,1213,1214,1215,1218,1219,1222,1223,1226],{},"Start it with ",[25,1216,1217],{},"API_KEY=your-key npx tsx src\u002Fserver.ts",". It listens on 127.0.0.1 only, as the spec recommends for local servers. ",[25,1220,1221],{},"createMcpExpressApp()"," turns on Host and Origin checks by default, and a request from another origin gets HTTP 403. The spec requires the Origin check: servers ",[17,1224,1225],{},"MUST"," validate it to stop DNS rebinding attacks.",[14,1228,1229],{},"For a public deployment, name your hosts:",[228,1231,1233],{"className":346,"code":1232,"language":348,"meta":233,"style":233},"const publicApp = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });\n",[25,1234,1235],{"__ignoreMap":233},[237,1236,1237,1239,1242,1244,1246,1249,1252,1255,1258],{"class":239,"line":240},[237,1238,428],{"class":355},[237,1240,1241],{"class":255}," publicApp",[237,1243,434],{"class":355},[237,1245,1139],{"class":243},[237,1247,1248],{"class":251},"({ host: ",[237,1250,1251],{"class":247},"'0.0.0.0'",[237,1253,1254],{"class":251},", allowedHosts: [",[237,1256,1257],{"class":247},"'api.example.com'",[237,1259,1260],{"class":251},"] });\n",[14,1262,1263,1264,1267,1268,1271,1272,1275],{},"The same handler also has a web-standard ",[25,1265,1266],{},"handler.fetch(request)",", which takes a ",[25,1269,1270],{},"Request"," and returns a ",[25,1273,1274],{},"Response",". So the server can run on hosts without Express.",[48,1277,1279],{"id":1278},"step-4-how-do-you-test-an-mcp-server","Step 4: How do you test an MCP server?",[14,1281,1282,1285,1286,1289],{},[17,1283,1284],{},"Use the MCP Inspector CLI, once in each protocol era."," The modern era sends 2026-07-28 requests. The legacy era sends the 2025 ",[25,1287,1288],{},"initialize"," handshake.",[228,1291,1293],{"className":230,"code":1292,"language":232,"meta":233,"style":233},"npx @modelcontextprotocol\u002Finspector --cli http:\u002F\u002F127.0.0.1:3000\u002Fmcp \\\n  --transport http --protocol-era modern --method tools\u002Flist\n\nnpx @modelcontextprotocol\u002Finspector --cli http:\u002F\u002F127.0.0.1:3000\u002Fmcp \\\n  --transport http --protocol-era modern --method tools\u002Fcall \\\n  --tool-name get_invoice --tool-arg id=inv_101\n",[25,1294,1295,1312,1332,1336,1348,1365],{"__ignoreMap":233},[237,1296,1297,1300,1303,1306,1309],{"class":239,"line":240},[237,1298,1299],{"class":243},"npx",[237,1301,1302],{"class":247}," @modelcontextprotocol\u002Finspector",[237,1304,1305],{"class":255}," --cli",[237,1307,1308],{"class":247}," http:\u002F\u002F127.0.0.1:3000\u002Fmcp",[237,1310,1311],{"class":255}," \\\n",[237,1313,1314,1317,1320,1323,1326,1329],{"class":239,"line":262},[237,1315,1316],{"class":255},"  --transport",[237,1318,1319],{"class":247}," http",[237,1321,1322],{"class":255}," --protocol-era",[237,1324,1325],{"class":247}," modern",[237,1327,1328],{"class":255}," --method",[237,1330,1331],{"class":247}," tools\u002Flist\n",[237,1333,1334],{"class":239,"line":274},[237,1335,422],{"emptyLinePlaceholder":421},[237,1337,1338,1340,1342,1344,1346],{"class":239,"line":288},[237,1339,1299],{"class":243},[237,1341,1302],{"class":247},[237,1343,1305],{"class":255},[237,1345,1308],{"class":247},[237,1347,1311],{"class":255},[237,1349,1350,1352,1354,1356,1358,1360,1363],{"class":239,"line":314},[237,1351,1316],{"class":255},[237,1353,1319],{"class":247},[237,1355,1322],{"class":255},[237,1357,1325],{"class":247},[237,1359,1328],{"class":255},[237,1361,1362],{"class":247}," tools\u002Fcall",[237,1364,1311],{"class":255},[237,1366,1367,1370,1373,1376],{"class":239,"line":425},[237,1368,1369],{"class":255},"  --tool-name",[237,1371,1372],{"class":247}," get_invoice",[237,1374,1375],{"class":255}," --tool-arg",[237,1377,1378],{"class":247}," id=inv_101\n",[14,1380,1381,1382,1385,1386,1389],{},"The first command lists both tools with their JSON schemas. The second returns the invoice as text. Run both again with ",[25,1383,1384],{},"--protocol-era legacy",". ",[25,1387,1388],{},"createMcpHandler"," serves both eras from the same factory by default.",[14,1391,1392],{},"You can also send a raw 2026-07-28 request with curl:",[228,1394,1396],{"className":230,"code":1395,"language":232,"meta":233,"style":233},"curl -s -X POST http:\u002F\u002F127.0.0.1:3000\u002Fmcp \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -H 'Accept: application\u002Fjson, text\u002Fevent-stream' \\\n  -H 'MCP-Protocol-Version: 2026-07-28' \\\n  -H 'Mcp-Method: tools\u002Fcall' \\\n  -H 'Mcp-Name: list_invoices' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_invoices\",\"arguments\":{\"status\":\"open\"},\"_meta\":{\"io.modelcontextprotocol\u002FprotocolVersion\":\"2026-07-28\",\"io.modelcontextprotocol\u002FclientInfo\":{\"name\":\"curl\",\"version\":\"1.0.0\"},\"io.modelcontextprotocol\u002FclientCapabilities\":{}}}}'\n",[25,1397,1398,1416,1426,1435,1444,1453,1462],{"__ignoreMap":233},[237,1399,1400,1403,1406,1409,1412,1414],{"class":239,"line":240},[237,1401,1402],{"class":243},"curl",[237,1404,1405],{"class":255}," -s",[237,1407,1408],{"class":255}," -X",[237,1410,1411],{"class":247}," POST",[237,1413,1308],{"class":247},[237,1415,1311],{"class":255},[237,1417,1418,1421,1424],{"class":239,"line":262},[237,1419,1420],{"class":255},"  -H",[237,1422,1423],{"class":247}," 'Content-Type: application\u002Fjson'",[237,1425,1311],{"class":255},[237,1427,1428,1430,1433],{"class":239,"line":274},[237,1429,1420],{"class":255},[237,1431,1432],{"class":247}," 'Accept: application\u002Fjson, text\u002Fevent-stream'",[237,1434,1311],{"class":255},[237,1436,1437,1439,1442],{"class":239,"line":288},[237,1438,1420],{"class":255},[237,1440,1441],{"class":247}," 'MCP-Protocol-Version: 2026-07-28'",[237,1443,1311],{"class":255},[237,1445,1446,1448,1451],{"class":239,"line":314},[237,1447,1420],{"class":255},[237,1449,1450],{"class":247}," 'Mcp-Method: tools\u002Fcall'",[237,1452,1311],{"class":255},[237,1454,1455,1457,1460],{"class":239,"line":425},[237,1456,1420],{"class":255},[237,1458,1459],{"class":247}," 'Mcp-Name: list_invoices'",[237,1461,1311],{"class":255},[237,1463,1464,1467],{"class":239,"line":451},[237,1465,1466],{"class":255},"  -d",[237,1468,1469],{"class":247}," '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools\u002Fcall\",\"params\":{\"name\":\"list_invoices\",\"arguments\":{\"status\":\"open\"},\"_meta\":{\"io.modelcontextprotocol\u002FprotocolVersion\":\"2026-07-28\",\"io.modelcontextprotocol\u002FclientInfo\":{\"name\":\"curl\",\"version\":\"1.0.0\"},\"io.modelcontextprotocol\u002FclientCapabilities\":{}}}}'\n",[14,1471,1472,1473,1475,1476,556,1479,1482],{},"If ",[25,1474,210],{}," names a different tool than the body, the server answers HTTP 400 with error code ",[25,1477,1478],{},"-32020",[25,1480,1481],{},"HeaderMismatch","). That is the spec working as designed: a load balancer that routes on headers can’t be fooled by a different body.",[48,1484,1486],{"id":1485},"step-5-how-do-you-add-sign-in-to-an-mcp-server","Step 5: How do you add sign-in to an MCP server?",[14,1488,1489,57,1495,1500],{},[17,1490,1491,1492,1494],{},"Gate the ",[25,1493,1118],{}," route with a bearer-token check, and accept only tokens issued for your server.",[42,1496,1499],{"href":1497,"rel":1498},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2026-07-28\u002Fbasic\u002Fauthorization",[62],"authorization spec"," makes auth optional. When you use it, three rules decide most designs:",[66,1502,1503,1512,1521],{},[69,1504,1505,1508,1509,1511],{},[17,1506,1507],{},"stdio servers"," should not use the OAuth flow. They take credentials from the environment, like the ",[25,1510,463],{}," above.",[69,1513,1514,1517,1518,1520],{},[17,1515,1516],{},"HTTP servers"," act as an OAuth 2.1 resource server. They ",[17,1519,1225],{}," publish OAuth 2.0 Protected Resource Metadata (RFC 9728). Clients find your authorization server there.",[69,1522,1523,1526,1527,1529,1530,1533],{},[17,1524,1525],{},"Tokens are bound to your server."," Servers ",[17,1528,1225],{}," check that a token was issued for them, and ",[17,1531,1532],{},"MUST NOT"," accept or pass on any other tokens.",[14,1535,1536,1537,1542,1543,110,1546,1549,1550,1553,1554,1557],{},"The SDK’s ",[42,1538,1541],{"href":1539,"rel":1540},"https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002Fserving\u002Fauthorization.html",[62],"authorization guide"," wires it like this. It continues from the ",[25,1544,1545],{},"publicApp",[25,1547,1548],{},"node"," variables above. You supply two things: ",[25,1551,1552],{},"verifyAccessToken",", which checks the token with your authorization server, and ",[25,1555,1556],{},"oauthMetadata",", that server’s own RFC 8414 metadata document.",[228,1559,1561],{"className":346,"code":1560,"language":348,"meta":233,"style":233},"import type { OAuthTokenVerifier } from '@modelcontextprotocol\u002Fexpress';\nimport { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, requireBearerAuth } from '@modelcontextprotocol\u002Fexpress';\n\nconst mcpServerUrl = new URL('https:\u002F\u002Fapi.example.com\u002Fmcp');\nconst verifier: OAuthTokenVerifier = { verifyAccessToken };\n\nconst auth = requireBearerAuth({\n  verifier,\n  requiredScopes: ['mcp'],\n  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),\n  expectedResource: mcpServerUrl,\n});\n\npublicApp.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl }));\npublicApp.all('\u002Fmcp', auth, (req, res) => void node(req, res, req.body));\n",[25,1562,1563,1579,1592,1596,1617,1634,1638,1652,1657,1668,1679,1684,1688,1692,1708],{"__ignoreMap":233},[237,1564,1565,1567,1570,1573,1575,1577],{"class":239,"line":240},[237,1566,356],{"class":355},[237,1568,1569],{"class":355}," type",[237,1571,1572],{"class":251}," { OAuthTokenVerifier } ",[237,1574,362],{"class":355},[237,1576,365],{"class":247},[237,1578,368],{"class":251},[237,1580,1581,1583,1586,1588,1590],{"class":239,"line":262},[237,1582,356],{"class":355},[237,1584,1585],{"class":251}," { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, requireBearerAuth } ",[237,1587,362],{"class":355},[237,1589,365],{"class":247},[237,1591,368],{"class":251},[237,1593,1594],{"class":239,"line":274},[237,1595,422],{"emptyLinePlaceholder":421},[237,1597,1598,1600,1603,1605,1607,1610,1612,1615],{"class":239,"line":288},[237,1599,428],{"class":355},[237,1601,1602],{"class":255}," mcpServerUrl",[237,1604,434],{"class":355},[237,1606,568],{"class":355},[237,1608,1609],{"class":243}," URL",[237,1611,490],{"class":251},[237,1613,1614],{"class":247},"'https:\u002F\u002Fapi.example.com\u002Fmcp'",[237,1616,595],{"class":251},[237,1618,1619,1621,1624,1626,1629,1631],{"class":239,"line":314},[237,1620,428],{"class":355},[237,1622,1623],{"class":255}," verifier",[237,1625,343],{"class":355},[237,1627,1628],{"class":243}," OAuthTokenVerifier",[237,1630,434],{"class":355},[237,1632,1633],{"class":251}," { verifyAccessToken };\n",[237,1635,1636],{"class":239,"line":425},[237,1637,422],{"emptyLinePlaceholder":421},[237,1639,1640,1642,1645,1647,1650],{"class":239,"line":451},[237,1641,428],{"class":355},[237,1643,1644],{"class":255}," auth",[237,1646,434],{"class":355},[237,1648,1649],{"class":243}," requireBearerAuth",[237,1651,728],{"class":251},[237,1653,1654],{"class":239,"line":473},[237,1655,1656],{"class":251},"  verifier,\n",[237,1658,1659,1662,1665],{"class":239,"line":478},[237,1660,1661],{"class":251},"  requiredScopes: [",[237,1663,1664],{"class":247},"'mcp'",[237,1666,1667],{"class":251},"],\n",[237,1669,1670,1673,1676],{"class":239,"line":505},[237,1671,1672],{"class":251},"  resourceMetadataUrl: ",[237,1674,1675],{"class":243},"getOAuthProtectedResourceMetadataUrl",[237,1677,1678],{"class":251},"(mcpServerUrl),\n",[237,1680,1681],{"class":239,"line":550},[237,1682,1683],{"class":251},"  expectedResource: mcpServerUrl,\n",[237,1685,1686],{"class":239,"line":598},[237,1687,1062],{"class":251},[237,1689,1690],{"class":239,"line":613},[237,1691,422],{"emptyLinePlaceholder":421},[237,1693,1694,1697,1700,1702,1705],{"class":239,"line":619},[237,1695,1696],{"class":251},"publicApp.",[237,1698,1699],{"class":243},"use",[237,1701,490],{"class":251},[237,1703,1704],{"class":243},"mcpAuthMetadataRouter",[237,1706,1707],{"class":251},"({ oauthMetadata, resourceServerUrl: mcpServerUrl }));\n",[237,1709,1710,1712,1714,1716,1718,1721,1723,1725,1727,1729,1731,1733,1735],{"class":239,"line":624},[237,1711,1696],{"class":251},[237,1713,1164],{"class":243},[237,1715,490],{"class":251},[237,1717,1169],{"class":247},[237,1719,1720],{"class":251},", auth, (",[237,1722,1175],{"class":493},[237,1724,746],{"class":251},[237,1726,579],{"class":493},[237,1728,1182],{"class":251},[237,1730,640],{"class":355},[237,1732,1187],{"class":355},[237,1734,1148],{"class":243},[237,1736,1192],{"class":251},[14,1738,1739,1740,1742],{},"Your ",[25,1741,1552],{}," must do three things, or valid users get locked out:",[66,1744,1745,1761,1774],{},[69,1746,1747,1753,1754,1757,1758,1760],{},[17,1748,1749,1750,582],{},"Reject a bad token with ",[25,1751,1752],{},"OAuthError"," Throw ",[25,1755,1756],{},"new OAuthError(OAuthErrorCode.InvalidToken, 'reason')",", both from ",[25,1759,27],{},". Any other exception comes back as HTTP 500, not 401.",[69,1762,1763,1769,1770,1773],{},[17,1764,1765,1766],{},"Return ",[25,1767,1768],{},"expiresAt"," from the token’s ",[25,1771,1772],{},"exp"," claim. A token without it gets 401.",[69,1775,1776,1769,1781,1784,1785,1788],{},[17,1777,1765,1778],{},[25,1779,1780],{},"resource",[25,1782,1783],{},"aud"," claim. With ",[25,1786,1787],{},"expectedResource"," set, a token for any other server gets 401.",[14,1790,1791,1792,1795,1796,1799,1800,1804],{},"Without a valid token, the server answers 401 with a ",[25,1793,1794],{},"WWW-Authenticate"," header that points to the metadata. Inside a tool, ",[25,1797,1798],{},"ctx.http?.authInfo"," holds the caller’s client id and scopes. Use it to load that user’s own API credentials, not a shared admin key. App369’s ",[42,1801,1803],{"href":1802},"\u002Fblog\u002Fmcp-server-authentication-api-keys-vs-oauth-2026\u002F","auth guide"," compares API keys and OAuth for each client.",[48,1806,1808],{"id":1807},"how-do-you-connect-your-mcp-server-to-claude","How do you connect your MCP server to Claude?",[14,1810,1811,1821,1822,1825,1826,1831],{},[17,1812,1813,1814,1817,1818,582],{},"For a local stdio server, add it under ",[25,1815,1816],{},"mcpServers"," in Claude Desktop’s ",[25,1819,1820],{},"claude_desktop_config.json"," On macOS the file is at ",[25,1823,1824],{},"~\u002FLibrary\u002FApplication Support\u002FClaude\u002Fclaude_desktop_config.json",". The ",[42,1827,1830],{"href":1828,"rel":1829},"https:\u002F\u002Fmodelcontextprotocol.io\u002Fdocs\u002Fdevelop\u002Fbuild-server",[62],"official tutorial"," uses this shape:",[228,1833,1836],{"className":1834,"code":1835,"language":607,"meta":233,"style":233},"language-json shiki shiki-themes github-light github-dark","{\n  \"mcpServers\": {\n    \"weather\": {\n      \"command\": \"node\",\n      \"args\": [\"\u002FABSOLUTE\u002FPATH\u002FTO\u002FPARENT\u002FFOLDER\u002Fweather\u002Fbuild\u002Findex.js\"]\n    }\n  }\n}\n",[25,1837,1838,1843,1851,1858,1871,1885,1890,1895],{"__ignoreMap":233},[237,1839,1840],{"class":239,"line":240},[237,1841,1842],{"class":251},"{\n",[237,1844,1845,1848],{"class":239,"line":262},[237,1846,1847],{"class":255},"  \"mcpServers\"",[237,1849,1850],{"class":251},": {\n",[237,1852,1853,1856],{"class":239,"line":274},[237,1854,1855],{"class":255},"    \"weather\"",[237,1857,1850],{"class":251},[237,1859,1860,1863,1866,1869],{"class":239,"line":288},[237,1861,1862],{"class":255},"      \"command\"",[237,1864,1865],{"class":251},": ",[237,1867,1868],{"class":247},"\"node\"",[237,1870,699],{"class":251},[237,1872,1873,1876,1879,1882],{"class":239,"line":314},[237,1874,1875],{"class":255},"      \"args\"",[237,1877,1878],{"class":251},": [",[237,1880,1881],{"class":247},"\"\u002FABSOLUTE\u002FPATH\u002FTO\u002FPARENT\u002FFOLDER\u002Fweather\u002Fbuild\u002Findex.js\"",[237,1883,1884],{"class":251},"]\n",[237,1886,1887],{"class":239,"line":425},[237,1888,1889],{"class":251},"    }\n",[237,1891,1892],{"class":239,"line":451},[237,1893,1894],{"class":251},"  }\n",[237,1896,1897],{"class":239,"line":473},[237,1898,616],{"class":251},[14,1900,1901,1902,1905,1906,1909,1910,582],{},"A stdio server must never write to stdout, because that corrupts the JSON-RPC messages. Log with ",[25,1903,1904],{},"console.error()"," instead. In TypeScript, serve stdio with ",[25,1907,1908],{},"serveStdio(createServer)"," from ",[25,1911,1912],{},"@modelcontextprotocol\u002Fserver\u002Fstdio",[14,1914,1915,1916,1919],{},"A remote server is added by its URL as a custom connector. Which sign-in each client accepts differs, and the ",[42,1917,1918],{"href":44},"SaaS guide"," covers Claude and ChatGPT.",[14,1921,1922,1923,1925],{},"The Python SDK has the same model. This stdio server lists its tool in both protocol eras with ",[25,1924,39],{}," 2.3.0:",[228,1927,1931],{"className":1928,"code":1929,"language":1930,"meta":233,"style":233},"language-python shiki shiki-themes github-light github-dark","from mcp.server import MCPServer\n\nmcp = MCPServer(\"invoices\")\n\n@mcp.tool()\nasync def get_invoice(id: str) -> str:\n    \"\"\"Get one invoice by its id, such as inv_101.\"\"\"\n    return f\"invoice {id}\"\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"stdio\")\n","python",[25,1932,1933,1938,1942,1947,1951,1956,1961,1966,1971,1975,1980],{"__ignoreMap":233},[237,1934,1935],{"class":239,"line":240},[237,1936,1937],{},"from mcp.server import MCPServer\n",[237,1939,1940],{"class":239,"line":262},[237,1941,422],{"emptyLinePlaceholder":421},[237,1943,1944],{"class":239,"line":274},[237,1945,1946],{},"mcp = MCPServer(\"invoices\")\n",[237,1948,1949],{"class":239,"line":288},[237,1950,422],{"emptyLinePlaceholder":421},[237,1952,1953],{"class":239,"line":314},[237,1954,1955],{},"@mcp.tool()\n",[237,1957,1958],{"class":239,"line":425},[237,1959,1960],{},"async def get_invoice(id: str) -> str:\n",[237,1962,1963],{"class":239,"line":451},[237,1964,1965],{},"    \"\"\"Get one invoice by its id, such as inv_101.\"\"\"\n",[237,1967,1968],{"class":239,"line":473},[237,1969,1970],{},"    return f\"invoice {id}\"\n",[237,1972,1973],{"class":239,"line":478},[237,1974,422],{"emptyLinePlaceholder":421},[237,1976,1977],{"class":239,"line":505},[237,1978,1979],{},"if __name__ == \"__main__\":\n",[237,1981,1982],{"class":239,"line":550},[237,1983,1984],{},"    mcp.run(transport=\"stdio\")\n",[48,1986,1988],{"id":1987},"what-breaks-most-often-when-you-build-an-mcp-server","What breaks most often when you build an MCP server?",[14,1990,1991,1994],{},[17,1992,1993],{},"Most failures are stdout logging, a reused server instance, or a token passed through."," Each one maps to a spec rule:",[138,1996,1997,2010],{},[141,1998,1999],{},[144,2000,2001,2004,2007],{},[147,2002,2003],{},"Symptom",[147,2005,2006],{},"Cause",[147,2008,2009],{},"Fix",[157,2011,2012,2029,2055,2069,2084,2098,2118],{},[144,2013,2014,2017,2020],{},[162,2015,2016],{},"A stdio server connects, then drops",[162,2018,2019],{},"Something printed to stdout",[162,2021,2022,2023,2025,2026],{},"Log to stderr with ",[25,2024,1904],{}," or Python’s ",[25,2027,2028],{},"logging",[144,2030,2031,2034,2045],{},[162,2032,2033],{},"Newer clients can’t connect",[162,2035,2036,2037,2040,2041,2044],{},"Wired with ",[25,2038,2039],{},"server.connect(new StdioServerTransport())",", which the ",[42,2042,1830],{"href":1828,"rel":2043},[62]," says only serves 2025-era clients",[162,2046,2047,2048,2051,2052,2054],{},"Use ",[25,2049,2050],{},"serveStdio"," or ",[25,2053,1388],{},". Both negotiate the version",[144,2056,2057,2060,2066],{},[162,2058,2059],{},"Odd state shared between users",[162,2061,2062,2063,2065],{},"One ",[25,2064,1076],{}," reused across requests",[162,2067,2068],{},"Return a new instance from the factory every time",[144,2070,2071,2074,2081],{},[162,2072,2073],{},"HTTP 400, code -32020",[162,2075,2076,2051,2078,2080],{},[25,2077,210],{},[25,2079,206],{}," doesn’t match the body",[162,2082,2083],{},"Send the headers your client library sets, or fix the hand-built request",[144,2085,2086,2089,2092],{},[162,2087,2088],{},"HTTP 403 on a local server",[162,2090,2091],{},"The Origin or Host check refused the request",[162,2093,2094,2095],{},"Add your host to ",[25,2096,2097],{},"allowedHosts",[144,2099,2100,2103,2109],{},[162,2101,2102],{},"HTTP 500 instead of 401 for a bad token",[162,2104,2105,2106],{},"The verifier threw a plain ",[25,2107,2108],{},"Error",[162,2110,2111,2112,2114,2115],{},"Throw ",[25,2113,1752],{}," with ",[25,2116,2117],{},"OAuthErrorCode.InvalidToken",[144,2119,2120,2123,2126],{},[162,2121,2122],{},"Your API sees the AI client’s token",[162,2124,2125],{},"The token is forwarded upstream",[162,2127,2128],{},"Validate it, then call the API with that user’s own credentials",[14,2130,2131,2132,2136,2137,2141,2142,2147],{},"If a client gets HTTP 405 or 406, see ",[42,2133,2135],{"href":2134},"\u002Ffix\u002Fmcp-405-406-streamable-http\u002F","MCP server 405 and 406 errors",". When a tool call fails in a client and you can’t tell why, the ",[42,2138,2140],{"href":2139},"\u002Fblog\u002Fai-api-errors-openai-claude-gemini-mcp-fixes-2026\u002F","AI API errors guide"," lists the common MCP errors and fixes. For a full reference build with per-user scoping, gated writes and rate limits, see Simon’s open-source ",[42,2143,2146],{"href":2144,"rel":2145},"https:\u002F\u002Fgithub.com\u002FSzymonDziak\u002Fsaas-mcp-connector",[62],"saas-mcp-connector"," on GitHub.",[48,2149,2151],{"id":2150},"or-have-it-built","Or have it built",[14,2153,2154,2157],{},[17,2155,2156],{},"App369 builds this server over your existing API at a fixed price."," Simon Dziak is the one developer who scopes, builds and tests it.",[2159,2160],"cost-answer",{"level":2161,"topic":39},"3",[14,2163,2164,2165,582],{},"What isn’t included: new API endpoints, and any guarantee that a directory approves the listing, because that is up to each directory. The full scope is on the ",[42,2166,2168],{"href":2167},"\u002Ffixed-price\u002Fmcp-server-for-saas\u002F","MCP Connector page",[2170,2171,2172],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":233,"searchDepth":262,"depth":262,"links":2174},[2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185],{"id":50,"depth":262,"text":51},{"id":92,"depth":262,"text":93},{"id":124,"depth":262,"text":125},{"id":214,"depth":262,"text":215},{"id":332,"depth":262,"text":333},{"id":1109,"depth":262,"text":1110},{"id":1278,"depth":262,"text":1279},{"id":1485,"depth":262,"text":1486},{"id":1807,"depth":262,"text":1808},{"id":1987,"depth":262,"text":1988},{"id":2150,"depth":262,"text":2151},null,"AI & Technology","2026-11-08T09:00:00.000Z","Build an MCP server over your existing REST API with the TypeScript SDK v2: tools, Streamable HTTP, Inspector tests and OAuth, on the 2026-07-28 spec.","md",[2192,2195,2198,2201,2204,2207],{"question":2193,"answer":2194},"What do I need to build an MCP server?","You need an official MCP SDK and something for its tools to call, usually your own REST API. For the TypeScript SDK v2 that means Node.js 20 or later and the @modelcontextprotocol\u002Fserver package with zod. Python, C#, Go, Rust and Ruby also have Tier 1 official SDKs.",{"question":2196,"answer":2197},"Should my MCP server use stdio or Streamable HTTP?","Use Streamable HTTP for a server other people connect to by URL. Use stdio for a server that runs on the user's own machine as a subprocess. The older HTTP+SSE transport is deprecated, so new servers should not use it.",{"question":2199,"answer":2200},"What changed in the 2026-07-28 MCP spec?","Streamable HTTP lost protocol-level sessions and the GET stream endpoint. Each request now carries its protocol version in the body and in an MCP-Protocol-Version header, plus Mcp-Method and Mcp-Name headers. Servers no longer send their own requests; they return an InputRequiredResult instead.",{"question":2202,"answer":2203},"Will my server still work with older MCP clients?","Yes, if you serve it with the SDK v2 helpers. createMcpHandler and serveStdio answer both 2026-07-28 clients and clients that open with the 2025-era initialize handshake. A server wired by hand with server.connect(new StdioServerTransport()) only serves 2025-era clients.",{"question":2205,"answer":2206},"How do I test an MCP server without Claude?","Use the MCP Inspector CLI. Run npx @modelcontextprotocol\u002Finspector --cli with your server URL or start command, --protocol-era modern and --method tools\u002Flist. Run it again with --protocol-era legacy to check older clients.",{"question":2208,"answer":2209},"Does an MCP server need OAuth?","No. The spec makes authorization optional. A stdio server should take credentials from its environment. A remote server that holds user data should use OAuth 2.1 as the spec describes, and it must reject tokens that were not issued for it.",{"name":2211,"steps":2212},"Build an MCP server over an existing REST API",[2213,2216,2219,2222,2225],{"name":2214,"text":2215},"Set up the project","Create a Node.js 20+ project and install @modelcontextprotocol\u002Fserver, @modelcontextprotocol\u002Fexpress, @modelcontextprotocol\u002Fnode, express and zod.",{"name":2217,"text":2218},"Register tools over your API","Inside a createMcpHandler factory, create an McpServer and register each tool with a name, a description for the model, a zod input schema and a handler that calls your API.",{"name":2220,"text":2221},"Serve it over Streamable HTTP","Mount the handler on one \u002Fmcp route with createMcpExpressApp and toNodeHandler.",{"name":2223,"text":2224},"Test an MCP server","List and call the tools with the Inspector CLI in the modern and legacy protocol eras.",{"name":2226,"text":2227},"Add sign-in","Gate the \u002Fmcp route with requireBearerAuth, serve protected resource metadata, and check each token was issued for your server.","2026-11-08","Miami, FL",{"readingTimeStats":2231},{"text":2232,"minutes":2233,"time":2234,"words":2235},"10 min read",9.11,546600,1822,"\u002Fblog\u002Fhow-to-build-an-mcp-server-2026","build mcp server","10 min",[2240,2241,2242],"how-to-add-an-mcp-server-to-your-saas-2026","mcp-server-authentication-api-keys-vs-oauth-2026","ai-api-errors-openai-claude-gemini-mcp-fixes-2026",{"title":5,"description":2189},"How to Build an MCP Server (2026): Step by Step with Code",[2246,2248,2250,2253,2255,2257,2259,2261,2263,2265],{"title":2247,"url":60},"MCP specification, revision 2026-07-28",{"title":2249,"url":133},"MCP transports (stdio and Streamable HTTP)",{"title":2251,"url":2252},"MCP Streamable HTTP transport","https:\u002F\u002Fmodelcontextprotocol.io\u002Fspecification\u002F2026-07-28\u002Fbasic\u002Ftransports\u002Fstreamable-http",{"title":2254,"url":1497},"MCP authorization",{"title":2256,"url":1828},"Build an MCP server (official tutorial)",{"title":2258,"url":101},"MCP SDKs and tiers",{"title":2260,"url":1080},"TypeScript SDK v2: serving over HTTP",{"title":2262,"url":223},"TypeScript SDK v2: Express",{"title":2264,"url":1539},"TypeScript SDK v2: authorization",{"title":2266,"url":2267},"TypeScript SDK v2: legacy clients","https:\u002F\u002Fts.sdk.modelcontextprotocol.io\u002Fv2\u002Fserving\u002Flegacy-clients.html","blog\u002Fhow-to-build-an-mcp-server-2026","build mcp server, mcp server tutorial, model context protocol, mcp typescript sdk, streamable http, mcp inspector",[2271,2272,2273,2274,2275],"An MCP server registers tools with an official SDK and serves them over stdio or Streamable HTTP. The current spec revision is 2026-07-28.","With the TypeScript SDK v2 (@modelcontextprotocol\u002Fserver), a remote server with two tools over an existing REST API is 52 lines.","Revision 2026-07-28 removed protocol sessions and the GET stream from Streamable HTTP. Every request now carries its own version and headers.","Build a new server instance per request with a factory. The SDK calls it for each request and for version probes.","On a remote server, check every token was issued for your server, and never pass the caller's token on to your API.","https:\u002F\u002Fapp369.com\u002Fblog\u002Fhow-to-build-an-mcp-server-2026\u002F","rjVVWcjjTmwYSHYt_DSC3XG4IUm2CDXw5UrzxPSZtAA",[2279,2283,2287],{"path":2280,"title":2281,"description":2282,"category":2187},"\u002Fblog\u002Fhow-to-add-an-mcp-server-to-your-saas-2026","How to Add an MCP Server to Your SaaS: A Practical Guide (2026)","Add an MCP server to your SaaS so it works in Claude and ChatGPT, over your REST API: choosing tools, authentication, hosting, testing and directory listing.",{"path":2284,"title":2285,"description":2286,"category":2187},"\u002Fblog\u002Fmcp-server-authentication-api-keys-vs-oauth-2026","MCP Server Authentication for SaaS: API Keys vs OAuth (2026)","How should users sign in to your remote MCP server? What the MCP spec requires, when API keys are enough, and what Claude and ChatGPT expect today.",{"path":2288,"title":2289,"description":2290,"category":2187},"\u002Fblog\u002Fai-api-errors-openai-claude-gemini-mcp-fixes-2026","AI API Errors: 7 OpenAI, Claude, Gemini and MCP Fixes (2026)","AI feature broken by a 401, 429 or 529, or an MCP connector that won't connect? 7 OpenAI, Anthropic, Gemini and MCP errors, what each means and how to fix it.",[2292,2294],{"keyword":2281,"url":2293,"title":2281,"relevance":505},"https:\u002F\u002Fapp369.com\u002Fblog\u002Fhow-to-add-an-mcp-server-to-your-saas-2026\u002F",{"keyword":2285,"url":2295,"title":2285,"relevance":425},"https:\u002F\u002Fapp369.com\u002Fblog\u002Fmcp-server-authentication-api-keys-vs-oauth-2026\u002F",1794140707201]