[{"data":1,"prerenderedAt":539},["ShallowReactive",2],{"page-\u002Fguides\u002Fmcp-admin-ui":3},{"id":4,"title":5,"body":6,"description":531,"extension":532,"meta":533,"navigation":534,"path":535,"seo":536,"stem":537,"__hash__":538},"content\u002F4.guides\u002F17.mcp-admin-ui.md","MCP Admin UI",{"type":7,"value":8,"toc":520},"minimark",[9,28,47,52,105,109,131,135,146,184,294,301,317,321,328,409,414,424,428,431,435,450,469,473,492,508,516],[10,11,12,13,17,18,23,24,27],"p",{},"When the ",[14,15,16],"code",{},"mcp"," feature is enabled with OAuth (",[19,20,22],"a",{"href":21},".\u002Fmcp","see the MCP Server guide","), tileserver-rs ships a small web admin UI for managing the OAuth state directly. No CLI, no manual SQL — just two pages at ",[14,25,26],{},"\u002Fadmin\u002Fmcp\u002F*",".",[29,30,32],"callout",{"type":31},"warning",[10,33,34,38,39,42,43,46],{},[35,36,37],"strong",{},"Admin-bind only."," The admin UI and its underlying REST endpoints are mounted on the admin listener (",[14,40,41],{},"server.admin_bind","), not the public listener. They are never exposed alongside your public tile routes. Bind the admin listener to a private interface (loopback, VPN, or internal network) — never to ",[14,44,45],{},"0.0.0.0"," on a public host.",[48,49,51],"h2",{"id":50},"what-you-get","What you get",[53,54,55,71],"table",{},[56,57,58],"thead",{},[59,60,61,65,68],"tr",{},[62,63,64],"th",{},"Page",[62,66,67],{},"URL",[62,69,70],{},"Purpose",[72,73,74,90],"tbody",{},[59,75,76,82,87],{},[77,78,79],"td",{},[35,80,81],{},"Connected apps",[77,83,84],{},[14,85,86],{},"\u002Fadmin\u002Fmcp\u002Fconnected-apps",[77,88,89],{},"List every OAuth client registered via Dynamic Client Registration (DCR). Inspect scopes, count active sessions, revoke a client (cascade-deletes all its refresh tokens).",[59,91,92,97,102],{},[77,93,94],{},[35,95,96],{},"Devices",[77,98,99],{},[14,100,101],{},"\u002Fadmin\u002Fmcp\u002Fdevices",[77,103,104],{},"List every outstanding refresh token. Each row is one device or browser tab where a connected app is logged in. Revoke a single device session without removing the client.",[48,106,108],{"id":107},"when-to-use-this","When to use this",[110,111,112,119,122,128],"ul",{},[113,114,115,116,118],"li",{},"A user reports \"Claude desktop is hanging\" — check the ",[35,117,96],{}," page to see if their token is expired or stuck.",[113,120,121],{},"You're rotating credentials — bulk-revoke all clients before re-registering.",[113,123,124,125,127],{},"You want to audit which third-party apps have access — the ",[35,126,81],{}," page shows every registered client with its scopes, redirect URIs, and last-seen timestamp.",[113,129,130],{},"A user lost a laptop — revoke just that device session (and let the user re-authenticate from their other devices).",[48,132,134],{"id":133},"backend-sqlite-persistence","Backend: SQLite persistence",[10,136,137,138,141,142,145],{},"The admin UI surfaces state from the OAuth store. By default the store is\nin-memory (clients and tokens evaporate on restart). For a real deployment,\nbuild with the ",[14,139,140],{},"mcp-persistence"," feature and point ",[14,143,144],{},"store_path"," at a writable\nSQLite file:",[147,148,153],"pre",{"className":149,"code":150,"language":151,"meta":152,"style":152},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# Build with both MCP + persistence enabled\ncargo build --release --features mcp,mcp-persistence\n","bash","",[14,154,155,164],{"__ignoreMap":152},[156,157,160],"span",{"class":158,"line":159},"line",1,[156,161,163],{"class":162},"sHwdD","# Build with both MCP + persistence enabled\n",[156,165,167,171,175,178,181],{"class":158,"line":166},2,[156,168,170],{"class":169},"sBMFI","cargo",[156,172,174],{"class":173},"sfazB"," build",[156,176,177],{"class":173}," --release",[156,179,180],{"class":173}," --features",[156,182,183],{"class":173}," mcp,mcp-persistence\n",[147,185,189],{"className":186,"code":187,"language":188,"meta":152,"style":152},"language-toml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# config.toml\n[mcp.oauth]\nenabled = true\nissuer_url = \"https:\u002F\u002Fyour-server.example.com\"\nsigning_key_path = \"\u002Fvar\u002Flib\u002Ftileserver-rs\u002Foauth.pem\"\napproval_secret = \"${MCP_APPROVAL_SECRET}\"\n# Path to a writable SQLite file. Schema is created on first launch.\nstore_path = \"\u002Fvar\u002Flib\u002Ftileserver-rs\u002Foauth-store.sqlite\"\n","toml",[14,190,191,196,213,226,243,258,273,279],{"__ignoreMap":152},[156,192,193],{"class":158,"line":159},[156,194,195],{"class":162},"# config.toml\n",[156,197,198,202,204,207,210],{"class":158,"line":166},[156,199,201],{"class":200},"sMK4o","[",[156,203,16],{"class":169},[156,205,27],{"class":206},"sTEyZ",[156,208,209],{"class":169},"oauth",[156,211,212],{"class":200},"]\n",[156,214,216,219,222],{"class":158,"line":215},3,[156,217,218],{"class":206},"enabled ",[156,220,221],{"class":200},"=",[156,223,225],{"class":224},"sfNiH"," true\n",[156,227,229,232,234,237,240],{"class":158,"line":228},4,[156,230,231],{"class":206},"issuer_url ",[156,233,221],{"class":200},[156,235,236],{"class":200}," \"",[156,238,239],{"class":173},"https:\u002F\u002Fyour-server.example.com",[156,241,242],{"class":200},"\"\n",[156,244,246,249,251,253,256],{"class":158,"line":245},5,[156,247,248],{"class":206},"signing_key_path ",[156,250,221],{"class":200},[156,252,236],{"class":200},[156,254,255],{"class":173},"\u002Fvar\u002Flib\u002Ftileserver-rs\u002Foauth.pem",[156,257,242],{"class":200},[156,259,261,264,266,268,271],{"class":158,"line":260},6,[156,262,263],{"class":206},"approval_secret ",[156,265,221],{"class":200},[156,267,236],{"class":200},[156,269,270],{"class":173},"${MCP_APPROVAL_SECRET}",[156,272,242],{"class":200},[156,274,276],{"class":158,"line":275},7,[156,277,278],{"class":162},"# Path to a writable SQLite file. Schema is created on first launch.\n",[156,280,282,285,287,289,292],{"class":158,"line":281},8,[156,283,284],{"class":206},"store_path ",[156,286,221],{"class":200},[156,288,236],{"class":200},[156,290,291],{"class":173},"\u002Fvar\u002Flib\u002Ftileserver-rs\u002Foauth-store.sqlite",[156,293,242],{"class":200},[10,295,296,297,300],{},"Revoking a client in the admin UI also revokes its pending authorization codes and refresh tokens, and the ",[14,298,299],{},"\u002Fmcp"," endpoint refuses any access token issued to it.",[10,302,303,304,306,307,310,311,313,314,316],{},"Setting ",[14,305,144],{}," in a binary built ",[35,308,309],{},"without"," ",[14,312,140],{}," fails fast at startup — persistence you configured is never silently ignored. Without ",[14,315,144],{},", the admin UI still works against the in-memory store — but every restart resets the state, which limits its usefulness.",[48,318,320],{"id":319},"rest-api","REST API",[10,322,323,324,327],{},"The admin UI is a thin layer over four REST endpoints. They're admin-bind-only and have no authentication of their own — they trust the network boundary. You can hit them directly from ",[14,325,326],{},"curl"," or any HTTP client on the admin interface:",[53,329,330,343],{},[56,331,332],{},[59,333,334,337,340],{},[62,335,336],{},"Method",[62,338,339],{},"Path",[62,341,342],{},"Response",[72,344,345,360,378,392],{},[59,346,347,352,357],{},[77,348,349],{},[14,350,351],{},"GET",[77,353,354],{},[14,355,356],{},"\u002F__admin\u002Foauth\u002Fclients",[77,358,359],{},"JSON array of all registered clients with derived session counts",[59,361,362,367,372],{},[77,363,364],{},[14,365,366],{},"DELETE",[77,368,369],{},[14,370,371],{},"\u002F__admin\u002Foauth\u002Fclients\u002F{client_id}",[77,373,374,377],{},[14,375,376],{},"{ok, deleted, revoked_sessions}"," — cascade-deletes refresh tokens",[59,379,380,384,389],{},[77,381,382],{},[14,383,351],{},[77,385,386],{},[14,387,388],{},"\u002F__admin\u002Foauth\u002Fsessions",[77,390,391],{},"JSON array of all outstanding refresh tokens",[59,393,394,398,403],{},[77,395,396],{},[14,397,366],{},[77,399,400],{},[14,401,402],{},"\u002F__admin\u002Foauth\u002Fsessions\u002F{token_id}",[77,404,405,408],{},[14,406,407],{},"{ok, deleted, revoked_sessions: null}"," — single-session revoke",[410,411,413],"h3",{"id":412},"idempotent-deletes","Idempotent deletes",[10,415,416,417,419,420,423],{},"Both ",[14,418,366],{}," endpoints are idempotent. ",[14,421,422],{},"{deleted: false}"," is a normal success response (not an error) — it just means the resource was already gone.",[410,425,427],{"id":426},"auth-code-lifecycle","Auth-code lifecycle",[10,429,430],{},"The sessions endpoint reports refresh tokens only. Single-use authorization codes issued during the OAuth dance are stored in the same backend, but they're transient (5-minute TTL) and never surface in the UI or the REST API.",[48,432,434],{"id":433},"design","Design",[10,436,437,438,441,442,445,446,449],{},"The admin UI shares the client-wide \"Direction I\" design system (OKLch palette with a violet accent, table-density layout, sharp corners). The public map viewer at ",[14,439,440],{},"\u002F",", ",[14,443,444],{},"\u002Fstyles\u002F*",", and ",[14,447,448],{},"\u002Fdata\u002F*"," uses the same token set, honoring the light\u002Fdark toggle across both surfaces.",[10,451,452,453,456,457,460,461,464,465,468],{},"If you embed tileserver-rs inside a larger Nuxt app, you can disable the admin pages by removing the ",[14,454,455],{},"admin"," directory under ",[14,458,459],{},"apps\u002Fclient\u002Fapp\u002Fpages\u002F"," or by reverse-proxying ",[14,462,463],{},"\u002Fadmin\u002F*"," to ",[14,466,467],{},"404"," on the public host.",[48,470,472],{"id":471},"troubleshooting","Troubleshooting",[29,474,476],{"type":475},"info",[10,477,478,481,482,484,485,487,488,491],{},[35,479,480],{},"Pages 404 on the public listener."," That's intentional. ",[14,483,463],{}," is served only from the admin listener. Either bind the admin listener to the same port as the public one (development), or reverse-proxy ",[14,486,463],{}," and ",[14,489,490],{},"\u002F__admin\u002F*"," to the admin port (production).",[29,493,494],{"type":475},[10,495,496,499,500,503,504,507],{},[35,497,498],{},"Tokens disappear after restart."," You're running without persistence. Build with ",[14,501,502],{},"--features mcp,mcp-persistence"," and point ",[14,505,506],{},"[mcp.oauth].store_path"," at a SQLite file on a persistent volume.",[29,509,510],{"type":475},[10,511,512,515],{},[35,513,514],{},"Empty state on the Devices page."," No client has completed an authorization-code-to-refresh-token exchange yet. The first time a user signs into Claude.ai or a desktop client through the OAuth flow, a row will appear.",[517,518,519],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":152,"searchDepth":166,"depth":166,"links":521},[522,523,524,525,529,530],{"id":50,"depth":166,"text":51},{"id":107,"depth":166,"text":108},{"id":133,"depth":166,"text":134},{"id":319,"depth":166,"text":320,"children":526},[527,528],{"id":412,"depth":215,"text":413},{"id":426,"depth":215,"text":427},{"id":433,"depth":166,"text":434},{"id":471,"depth":166,"text":472},"Web admin pages for managing MCP OAuth clients and active device sessions — list, inspect, and revoke registered apps and refresh tokens.","md",{},true,"\u002Fguides\u002Fmcp-admin-ui",{"title":5,"description":531},"4.guides\u002F17.mcp-admin-ui","QVvgR_ClzfD0AVRyo9CjcGXvLNDf8G3i3XYMnpc8NM0",1794150613972]