@aotter/mantle 0.2.1-alpha.1 → 0.2.1-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +16 -177
  2. package/dist/build/compiler.d.ts +4 -4
  3. package/dist/build/compiler.js +3 -3
  4. package/dist/domain/describe/{chunk-P6PP6VVV.js → chunk-CW7X5Q5T.js} +1 -1
  5. package/dist/domain/describe/index.js +1 -1
  6. package/dist/domain/describe/public.d.ts +3 -0
  7. package/dist/domain/describe/public.js +1 -0
  8. package/dist/infrastructure/help.js +1 -3
  9. package/dist/kernel/mcp-apps.d.ts +3 -2
  10. package/dist/kernel/mcp-apps.js +7 -2
  11. package/dist/kernel/mcp-handler.js +20 -8
  12. package/dist/kernel/mcp.d.ts +18 -0
  13. package/dist/kernel/mcp.js +1 -0
  14. package/dist/usecase/adminDescriptors.js +1 -1
  15. package/dist/usecase/generate.js +20 -4
  16. package/dist/usecase/rest.js +2 -4
  17. package/docs/examples/README.md +2 -2
  18. package/docs/examples/app-shell/README.md +1 -1
  19. package/docs/examples/app-shell/accept.mjs +12 -2
  20. package/docs/examples/app-shell/package.json +3 -3
  21. package/docs/handbook/cloudflare/authentication.md +1 -1
  22. package/docs/handbook/cloudflare/deploy-and-operate.md +20 -8
  23. package/docs/handbook/cloudflare/media-r2.md +31 -0
  24. package/docs/handbook/examples/hub.md +1 -1
  25. package/docs/handbook/guides/agent-setup.md +25 -67
  26. package/docs/handbook/guides/agent-workflow-acceptance.md +32 -5
  27. package/docs/handbook/guides/mcp-apps.md +43 -1
  28. package/docs/{skills/performance/SKILL.md → handbook/guides/performance.md} +53 -32
  29. package/docs/handbook/guides/size-and-startup.md +2 -2
  30. package/docs/handbook/guides/typed-queries.md +12 -0
  31. package/docs/handbook/navigation.json +4 -0
  32. package/docs/handbook/reference/describe.md +16 -6
  33. package/docs/handbook/reference/surface.md +10 -0
  34. package/docs/handbook/releases/index.md +85 -2
  35. package/docs/{upgrade-0.1-to-0.2.md → handbook/releases/upgrade-0.1-to-0.2.md} +13 -13
  36. package/docs/handbook/start/agents.md +25 -4
  37. package/docs/handbook/start/quickstart-worker.md +1 -1
  38. package/package.json +2 -3
  39. package/skills/README.md +7 -69
  40. package/skills/mantle/SKILL.md +15 -81
  41. package/dist/domain/describe/target-hash.js +0 -1
  42. package/docs/skills/develop/SKILL.md +0 -100
  43. package/docs/skills/integrate/SKILL.md +0 -51
  44. package/docs/skills/media-gc/SKILL.md +0 -109
  45. package/docs/skills/provision/SKILL.md +0 -44
  46. package/docs/skills/update/SKILL.md +0 -42
@@ -1 +1 @@
1
- function resolveLocalizedText(value,preferred,canonical2){if(value==null)return null;if(typeof value=="string")return value;if(Object.prototype.hasOwnProperty.call(value,preferred))return value[preferred];if(canonical2&&Object.prototype.hasOwnProperty.call(value,canonical2))return value[canonical2];let firstKey=Object.keys(value)[0];return firstKey!==void 0?value[firstKey]:null}var INPUT_RULE_KEYWORDS=["dependentRequired","anyOf","if","then","else"];function lookupLocalRef(root,ref){if(typeof ref!="string"||!ref.startsWith("#/"))return;let value=root;try{for(let key of decodeURIComponent(ref.slice(2)).split("/").map(key2=>key2.replace(/~1/g,"/").replace(/~0/g,"~")))value=value&&typeof value=="object"&&Object.hasOwn(value,key)?value[key]:void 0}catch{return}return value&&typeof value=="object"?value:void 0}function rootRefChain(input){let chain=[];for(let schema=input;schema!==void 0;schema=lookupLocalRef(input,schema.$ref)){if(chain.includes(schema))return;if(chain.push(schema),schema.$ref===void 0)return chain}}function mcpToolNameSegment(name){return name.toLowerCase().replace(/-/g,"_")}function toolTitle(name){let words=name.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/[-_.\s]+/g," ").trim().toLowerCase();return words.charAt(0).toUpperCase()+words.slice(1)}var viewAnnotations={readOnlyHint:!0,destructiveHint:!1};function procedureAnnotations(p){let readOnlyHint=p.mcp?.readOnlyHint??!1;return{readOnlyHint,destructiveHint:!readOnlyHint&&("ref"in p.handler||overwrites(p.handler.sql.stmts)),...p.mcp}}var overwrites=node=>!!node&&typeof node=="object"&&("DeleteStmt"in node||"UpdateStmt"in node||node.onConflictClause?.action==="ONCONFLICT_UPDATE"||Object.values(node).some(overwrites));function mcpTools(plan,surface,locale="en"){let text=t=>resolveLocalizedText(t,locale,"en")??void 0,tools=new Map,add=tool=>{let prior=tools.get(tool.name);if(prior&&(prior.kind!==tool.kind||prior.source!==tool.source))throw new TypeError(`MCP tool '${tool.name}' comes from both '${prior.source}' and '${tool.source}'.`);tools.set(tool.name,tool)};for(let t of Object.values(plan.triggers)){if(t.source.kind!=="mcp"||t.source.surface!==surface)continue;let p=plan.procedures[t.procedure],title=text(p.title);add({name:mcpToolNameSegment(t.procedure),kind:"procedure",source:t.procedure,title:title??toolTitle(t.procedure),description:text(p.description)??title??t.procedure,inputSchema:p.input,...p.output.type==="object"?{outputSchema:p.output}:{},annotations:procedureAnnotations(p),requires:p.requires})}for(let[name,v]of Object.entries(plan.views)){if(v.surface!==surface)continue;let chain=(v.input?rootRefChain(v.input)??[v.input]:[]).reverse(),props=Object.assign({},...chain.map(s=>s.properties??{})),required=[...new Set(chain.flatMap(s=>s.required??[]))];if("limit"in props||"cursor"in props)throw new TypeError(`View '${name}' declares an input named limit or cursor; the MCP tool reserves both for paging.`);let rules=chain.map(s=>Object.fromEntries(INPUT_RULE_KEYWORDS.flatMap(k=>s[k]===void 0?[]:[[k,s[k]]]))).filter(r=>Object.keys(r).length),defs=v.input?referencedDefs(v.input,props):{},title=text(v.title);add({name:mcpToolNameSegment(name),kind:"view",source:name,title:title??toolTitle(name),description:text(v.description)??title??name,inputSchema:{type:"object",properties:{...props,limit:{type:"integer",minimum:1,maximum:500},cursor:{type:"string"}},...required.length?{required}:{},...rules.length>1?{allOf:rules}:rules[0],...Object.keys(defs).length?{$defs:defs}:{}},annotations:viewAnnotations,requires:v.requires})}return[...tools.values()]}function referencedDefs(root,node,found={}){if(!node||typeof node!="object")return found;let ref=node.$ref,[head,segment]=typeof ref=="string"&&ref.startsWith("#/")?decoded(ref.slice(2)).split("/"):[],key=head==="$defs"?segment?.replace(/~1/g,"/").replace(/~0/g,"~"):void 0;key!==void 0&&!Object.hasOwn(found,key)&&root.$defs&&Object.hasOwn(root.$defs,key)&&(found[key]=root.$defs[key],referencedDefs(root,found[key],found));for(let child of Object.values(node))referencedDefs(root,child,found);return found}var decoded=pointer=>{try{return decodeURIComponent(pointer)}catch{return""}};function canonical(v){return Array.isArray(v)?`[${v.map(canonical).join(",")}]`:v&&typeof v=="object"?`{${Object.keys(v).sort().filter(k=>v[k]!==void 0).map(k=>`${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`:JSON.stringify(v)??"null"}var publishingTransitions=[["publish",["draft"],"published"],["unpublish",["published","archived"],"draft"],["archive",["draft","published"],"archived"]];var publishingInput={type:"object",properties:{id:{type:"string",minLength:1},expectedVersion:{type:"integer",minimum:1}},required:["id","expectedVersion"],additionalProperties:!1};function mediaDefinitions(){let define=(name,description,inputSchema,action,readOnlyHint=!1)=>({name,description,inputSchema,action,annotations:{readOnlyHint,destructiveHint:action==="delete",openWorldHint:!0}}),string={type:"string"},texts={alt:{type:"string",maxLength:1e3},caption:{type:"string",maxLength:1e3}},input=(properties,required=[])=>({type:"object",properties,required,additionalProperties:!1});return[define("get_media_upload_policy","Read declared image purpose MIME slots and byte caps before preparing variants or requesting an upload. Optimization runs locally, never in the host.",input({}),"policy",!0),define("create_media_upload","Create short-lived PUT capabilities for image variants. Read site media purpose policy first; optimize images locally. Obtain attachment bytes yourself, then PUT each variant with its requiredHeaders and call commit_media_upload. Never store upload URLs as published images or ask the user to run terminal commands.",input({filename:{type:"string",maxLength:255},purpose:string,variants:{type:"array",minItems:1,items:input({mimeType:string,byteSize:{type:"integer",minimum:1},role:{type:"string",enum:["primary","alternate","fallback"]}},["mimeType","byteSize","role"])},...texts},["filename","purpose","variants"]),"create"),define("commit_media_upload","Verify all uploaded variants and commit one permanent media asset. Store the returned asset id in fields referencing media_assets.id, never a temporary upload URL. Unknown or expired upload groups require a new create_media_upload.",input({uploadGroupId:string,...texts},["uploadGroupId"]),"commit"),define("list_media_assets","List committed image assets, newest first, with their permanent variants and alt/caption. Search matches id, alt or caption; reuse only a returned cursor.",input({limit:{type:"integer",minimum:1,maximum:500},cursor:string,search:string}),"list",!0),define("get_media_asset","Read one committed media asset and its permanent variant URLs.",input({id:string},["id"]),"get",!0),define("update_media_asset","Update committed asset alt or caption; omitted fields remain unchanged and empty strings clear text.",input({id:string,...texts},["id"]),"update"),define("delete_media_asset","Delete an asset's objects and library record. Check application references first. A partial failure can be retried using the same id.",input({id:string},["id"]),"delete")]}var ADMIN_API_BASE="/admin/api",pathTemplate=path=>path.replace(/\{([^}]+)\}/g,":$1"),routePath=template=>template.split("/").map(segment=>segment.startsWith(":")?`{${segment.slice(1)}}`:segment).join("/");function routeSource(template,base="",suffix=!1){let keys=[],path=template.split("/").map(segment=>{let rest=suffix?/^\{([a-zA-Z]\w*)\*\}$/.exec(segment):null,one=/^\{([^}]+)\}$/.exec(segment);return rest||one?(keys.push((rest??one)[1]),rest?"(.*)":"([^/]+)"):encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("/");return{source:`^${base}${path}/?$`,keys}}function canonicalPath(pathname){try{return pathname.split("/").map(segment=>encodeURIComponent(decodeURIComponent(segment))).join("/")}catch{return}}var isParameter=segment=>/^\{[^}]+\}$/.test(segment);function routeOrder(a,b){let x=a.split("/"),y=b.split("/"),count=segments=>segments.filter(isParameter).length;if(count(x)!==count(y))return count(x)-count(y);for(let i=0;i<Math.min(x.length,y.length);i++)if(isParameter(x[i])!==isParameter(y[i]))return isParameter(x[i])?1:-1;return a<b?-1:a>b?1:0}function inspectSql(value){let schemas=new Set,readSchemas=new Set,writes=[],readOnly=!0,walk=v=>{if(!v||typeof v!="object")return;let n=v;n.RangeVar?.mantle==="table"&&(schemas.add(n.RangeVar.relname.toLowerCase()),readSchemas.add(n.RangeVar.relname.toLowerCase()));for(let kind of["InsertStmt","UpdateStmt","DeleteStmt"])n[kind]&&(readOnly=!1,n[kind].relation?.relname&&writes.push({schema:n[kind].relation.relname.toLowerCase(),operation:kind==="InsertStmt"?"create":kind==="UpdateStmt"?"update":"delete"}),n[kind].relation?.relname&&(schemas.add(n[kind].relation.relname.toLowerCase()),kind!=="InsertStmt"&&readSchemas.add(n[kind].relation.relname.toLowerCase())));Object.values(n).forEach(walk)};return walk(value),{schemas:[...schemas].sort(),readSchemas:[...readSchemas].sort(),readOnly,writes}}function mergeRestrict(policies,conjunctive){return Object.fromEntries([...new Set(policies.flatMap(p=>Object.keys(p)))].sort().map(schema=>{let rows=policies.map(p=>p[schema]??{terms:[]}),terms=[...new Map(rows.flatMap(p=>p.terms).map(t=>[canonical(t),t])).values()].filter(t=>conjunctive||rows.every(p=>p.terms.some(v=>canonical(v)===canonical(t)))),owner=rows.find(p=>p.owner)?.owner;return[schema,{...owner&&(conjunctive||rows.every(p=>p.owner===owner))?{owner}:{},terms}]}))}function inspectBehavior(plan,ir,opaque,requires,restrict,writes){let inspected=inspectSql(ir);writes&&(inspected.writes=[...writes],inspected.schemas.push(...writes.map(w=>w.schema)),inspected.readSchemas.push(...writes.filter(w=>w.operation!=="create").map(w=>w.schema)));let dependencies=[],policies=[restrict??{}],procedures=new Set,hooks=new Set,visit=name=>{if(procedures.has(name))return;procedures.add(name);let p=plan.procedures[name];if(!p)throw new TypeError(`Unknown dependency '${name}'.`);dependencies.push({procedure:name,declaration:p}),policies.push(p.restrict??{}),opaque||="ref"in p.handler;let g=inspectSql("sql"in p.handler?p.handler.sql.stmts:void 0);inspected.schemas.push(...g.schemas),inspected.readSchemas.push(...g.readSchemas),inspected.writes.push(...g.writes),inspected.readOnly&&=g.readOnly,p.requires?.guard&&visit(p.requires.guard.procedure)};for(requires?.guard&&visit(requires.guard.procedure);;){let before=hooks.size;for(let[id,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0)){if(t.source.kind!=="lifecycle"||hooks.has(id))continue;let source=t.source;!opaque&&!inspected.writes.some(w=>w.schema===source.schema.toLowerCase()&&source.on.some(on=>on.endsWith(`_${w.operation}`)))||(hooks.add(id),dependencies.push({trigger:id,source}),visit(t.procedure))}if(hooks.size===before)break}let related=names=>{let all=new Set(opaque?Object.keys(plan.schemas):names);for(let name of all){let parent=plan.schemas[name]?.translates?.parent.toLowerCase();parent&&all.add(parent)}return[...all].sort()};return{...inspected,schemas:related(inspected.schemas),readSchemas:related(inspected.readSchemas),opaque,restrict:mergeRestrict(policies,!0),dependencies}}var objectInput={type:"object",properties:{}},adminRoutes=["GET /me","GET /site","GET /collections","GET /views-manifest","GET /operations","GET /webmcp","POST /webmcp/:tool","GET /bootstrap","GET /views/:name/export","GET /views/:name","GET /view-batch","GET /developer/views/:name","POST /operations/:name","GET /developer-console","GET /collections/:name/statistics","GET /entries","GET /entries/export","GET /entries/:id","POST /entries","PATCH /entries/:id","POST /entries/:id/publish","POST /entries/:id/unpublish","POST /entries/:id/archive","DELETE /entries/:id","GET /site-settings","PATCH /site-settings","POST /media/uploads","POST /media/uploads/:groupId/commit","GET /media","GET /media/:id","PATCH /media/:id","DELETE /media/:id","GET /admin","GET /members","PATCH /admin/:id/role","POST /admin/invitations","DELETE /admin/invitations/:id"];function targetDefinitions(plan){let out=[],tools=[...mcpTools(plan,"public"),...mcpTools(plan,"admin")];for(let[kind,entries]of[["view",plan.views],["procedure",plan.procedures]])for(let[name,p]of Object.entries(entries).sort(([a],[b])=>a<b?-1:a>b?1:0)){let view=kind==="view"?plan.views[name]:void 0,proc=kind==="procedure"?plan.procedures[name]:void 0,ir=view?.stmts??(proc&&"sql"in proc.handler?proc.handler.sql.stmts:void 0),inspected=inspectBehavior(plan,ir,!!proc&&"ref"in proc.handler,p.requires,p.restrict),tool=tools.find(t=>t.kind===kind&&t.source===name),title=resolveLocalizedText(p.title,"en","en")??name,triggers=Object.entries(plan.triggers).filter(([,t])=>kind==="procedure"&&t.procedure===name).sort(([a],[b])=>a<b?-1:a>b?1:0).map(([id,t])=>({id,source:t.source})),surfaces=[...new Set([...view?[view.surface,"admin",...view.surface==="public"?["rest"]:[]]:[],...triggers.map(t=>t.source.kind==="mcp"?t.source.surface:t.source.kind==="http"?"rest":t.source.kind)])].sort();surfaces.length||surfaces.push("internal"),out.push({id:`${kind}:${name}`,name:tool?.name??mcpToolNameSegment(name),kind,surface:view?.surface??(surfaces.includes("public")||surfaces.includes("rest")?"public":surfaces.includes("admin")?"admin":"internal"),surfaces,triggers,title,description:resolveLocalizedText(p.description,"en","en")??title,inputSchema:tool?.inputSchema??view?.input??proc?.input??objectInput,annotations:tool?.annotations??{},requires:p.requires,restrict:inspected.restrict,schemas:inspected.schemas,readSchemas:inspected.readSchemas,readOnly:!inspected.opaque&&inspected.readOnly,opaque:inspected.opaque,trustedOnly:!!proc?.trustedOnly,variants:[],behavior:{declaration:p,dependencies:inspected.dependencies}})}let addRoute=(id,dependencies,inputSchema=objectInput)=>{let admin=id.startsWith("admin:");out.push({id,name:id.replace(/[^a-zA-Z0-9_-]/g,"_"),title:id,description:id,kind:admin?"admin":"rest",surface:admin?"admin":"public",surfaces:[admin?"admin":"rest"],triggers:dependencies.flatMap(d=>d.triggers),inputSchema,annotations:{},requires:admin?{auth:{all:["ctx.admin"]}}:dependencies[0]?.requires,restrict:mergeRestrict(dependencies.map(d=>d.restrict),!1),schemas:[...new Set(dependencies.flatMap(d=>d.schemas))].sort(),readSchemas:[...new Set(dependencies.flatMap(d=>d.readSchemas))].sort(),readOnly:dependencies.length>0&&dependencies.every(d=>d.readOnly)&&(!admin||id.startsWith("admin:GET ")||id==="admin:POST /operations/:name"||id==="admin:POST /webmcp/:tool"),opaque:dependencies.some(d=>d.opaque),trustedOnly:dependencies.some(d=>d.trustedOnly),variants:dependencies,behavior:dependencies.map(d=>({id:d.id,behavior:d.behavior,surfaces:d.surfaces,triggers:d.triggers,input:d.inputSchema,requires:d.requires,restrict:d.restrict,trustedOnly:d.trustedOnly}))})},declarations=[...out],adminOperations=new Set(mcpTools(plan,"admin").filter(t=>t.kind==="procedure").map(t=>`procedure:${t.source}`));for(let d of declarations)d.kind==="view"&&d.surfaces.includes("public")&&addRoute(`rest:GET /api/views/${d.id.slice(5)}`,[d],d.inputSchema);for(let[,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0))if(t.source.kind==="http"){let d=declarations.find(d2=>d2.id===`procedure:${t.procedure}`);addRoute(`rest:${t.source.method} ${pathTemplate(t.source.path)}`,[d],d.inputSchema)}let generated=[];for(let[schema,s]of Object.entries(plan.schemas).sort(([a],[b])=>a<b?-1:a>b?1:0))if(s.publishing)for(let[verb,from,to]of publishingTransitions){addRoute(`procedure:${s.name}.${verb}`,[],publishingInput);let d=out.at(-1),inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,[{schema,operation:verb==="publish"?"publish":"update"}]);Object.assign(d,{kind:"procedure",name:mcpToolNameSegment(`${verb}_${s.name}`),surface:"admin",surfaces:["admin"],requires:{auth:{all:["ctx.admin"]}},schemas:[...new Set([schema,...inspected.schemas])].sort(),readSchemas:[...new Set([schema,...inspected.readSchemas])].sort(),opaque:inspected.opaque,behavior:{schema,verb,from,to,dependencies:inspected.dependencies}}),generated.push(d)}for(let tool of mediaDefinitions()){addRoute(`admin:tool:${tool.name}`,[],tool.inputSchema);let d=out.at(-1);Object.assign(d,{name:tool.name,readOnly:tool.annotations.readOnlyHint,opaque:!tool.annotations.readOnlyHint,annotations:tool.annotations,behavior:{tool,schemas:plan.schemas}}),generated.push(d)}for(let route of adminRoutes){let deps=route.includes("/views/:")||route==="GET /view-batch"?declarations.filter(d2=>d2.kind==="view"&&(route.includes("/developer/")||plan.views[d2.id.slice(5)]?.surface==="admin")):route.includes("/operations/:")?declarations.filter(d2=>adminOperations.has(d2.id)):route==="POST /webmcp/:tool"?declarations.filter(d2=>mcpTools(plan,"admin").some(t=>`${t.kind}:${t.source}`===d2.id)).concat(generated):[],generic=route.includes("/entries")||route.includes("/statistics")||route==="GET /bootstrap";addRoute(`admin:${route}`,deps,deps.length&&route!=="GET /view-batch"?{...objectInput,anyOf:deps.map(d2=>({type:"object",properties:{[route.includes("/webmcp/")?"tool":"name"]:{type:"string",const:route.includes("/webmcp/")?d2.name:d2.id.slice(d2.kind==="view"?5:10)}}}))}:objectInput);let d=out.at(-1);if(generic){let operation=route==="POST /entries"?"create":route==="DELETE /entries/:id"?"delete":route.startsWith("GET ")?void 0:route.endsWith("/publish")?"publish":"update",inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,operation?Object.keys(plan.schemas).map(schema=>({schema,operation})):[]);d.schemas=Object.keys(plan.schemas).sort(),d.readSchemas=d.schemas,d.opaque=inspected.opaque,d.behavior={schemas:plan.schemas,dependencies:inspected.dependencies}}}return[...new Map(out.map(d=>[d.id,d])).values()].sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0)}async function hash(value){let digest=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(value)));return[...new Uint8Array(digest)].map(b=>b.toString(16).padStart(2,"0")).join("")}function hashDefinition(plan,d){return hash({id:d.id,surfaces:d.surfaces,triggers:d.triggers,behavior:d.behavior,input:d.inputSchema,requires:d.requires,restrict:d.restrict,dialect:plan.dialect,schemas:Object.fromEntries(d.schemas.map(name=>[name,plan.schemas[name]])),userFields:plan.access?.userFields,readOnly:d.readOnly,opaque:d.opaque,trustedOnly:d.trustedOnly})}async function targetHash(plan,targetId){let target=targetDefinitions(plan).find(d=>d.id===targetId);if(!target)throw new TypeError(`Unknown target '${targetId}'.`);return hashDefinition(plan,target)}export{canonical,ADMIN_API_BASE,routePath,routeSource,canonicalPath,routeOrder,adminRoutes,targetDefinitions,hash,hashDefinition,targetHash};
1
+ function canonical(v){return Array.isArray(v)?`[${v.map(canonical).join(",")}]`:v&&typeof v=="object"?`{${Object.keys(v).sort().filter(k=>v[k]!==void 0).map(k=>`${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`:JSON.stringify(v)??"null"}function resolveLocalizedText(value,preferred,canonical2){if(value==null)return null;if(typeof value=="string")return value;if(Object.prototype.hasOwnProperty.call(value,preferred))return value[preferred];if(canonical2&&Object.prototype.hasOwnProperty.call(value,canonical2))return value[canonical2];let firstKey=Object.keys(value)[0];return firstKey!==void 0?value[firstKey]:null}var INPUT_RULE_KEYWORDS=["dependentRequired","anyOf","if","then","else"];function lookupLocalRef(root,ref){if(typeof ref!="string"||!ref.startsWith("#/"))return;let value=root;try{for(let key of decodeURIComponent(ref.slice(2)).split("/").map(key2=>key2.replace(/~1/g,"/").replace(/~0/g,"~")))value=value&&typeof value=="object"&&Object.hasOwn(value,key)?value[key]:void 0}catch{return}return value&&typeof value=="object"?value:void 0}function rootRefChain(input){let chain=[];for(let schema=input;schema!==void 0;schema=lookupLocalRef(input,schema.$ref)){if(chain.includes(schema))return;if(chain.push(schema),schema.$ref===void 0)return chain}}function mcpToolNameSegment(name){return name.toLowerCase().replace(/-/g,"_")}function toolTitle(name){let words=name.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/[-_.\s]+/g," ").trim().toLowerCase();return words.charAt(0).toUpperCase()+words.slice(1)}var viewAnnotations={readOnlyHint:!0,destructiveHint:!1};function procedureAnnotations(p){let readOnlyHint=p.mcp?.readOnlyHint??!1;return{readOnlyHint,destructiveHint:!readOnlyHint&&("ref"in p.handler||overwrites(p.handler.sql.stmts)),...p.mcp}}var overwrites=node=>!!node&&typeof node=="object"&&("DeleteStmt"in node||"UpdateStmt"in node||node.onConflictClause?.action==="ONCONFLICT_UPDATE"||Object.values(node).some(overwrites));function mcpTools(plan,surface,locale="en"){let text=t=>resolveLocalizedText(t,locale,"en")??void 0,tools=new Map,add=tool=>{let prior=tools.get(tool.name);if(prior&&(prior.kind!==tool.kind||prior.source!==tool.source))throw new TypeError(`MCP tool '${tool.name}' comes from both '${prior.source}' and '${tool.source}'.`);tools.set(tool.name,tool)};for(let t of Object.values(plan.triggers)){if(t.source.kind!=="mcp"||t.source.surface!==surface)continue;let p=plan.procedures[t.procedure],title=text(p.title);add({name:mcpToolNameSegment(t.procedure),kind:"procedure",source:t.procedure,title:title??toolTitle(t.procedure),description:text(p.description)??title??t.procedure,inputSchema:p.input,...p.output.type==="object"?{outputSchema:p.output}:{},annotations:procedureAnnotations(p),requires:p.requires})}for(let[name,v]of Object.entries(plan.views)){if(v.surface!==surface)continue;let chain=(v.input?rootRefChain(v.input)??[v.input]:[]).reverse(),props=Object.assign({},...chain.map(s=>s.properties??{})),required=[...new Set(chain.flatMap(s=>s.required??[]))];if("limit"in props||"cursor"in props)throw new TypeError(`View '${name}' declares an input named limit or cursor; the MCP tool reserves both for paging.`);let rules=chain.map(s=>Object.fromEntries(INPUT_RULE_KEYWORDS.flatMap(k=>s[k]===void 0?[]:[[k,s[k]]]))).filter(r=>Object.keys(r).length),defs=v.input?referencedDefs(v.input,props):{},title=text(v.title);add({name:mcpToolNameSegment(name),kind:"view",source:name,title:title??toolTitle(name),description:text(v.description)??title??name,inputSchema:{type:"object",properties:{...props,limit:{type:"integer",minimum:1,maximum:500},cursor:{type:"string"}},...required.length?{required}:{},...rules.length>1?{allOf:rules}:rules[0],...Object.keys(defs).length?{$defs:defs}:{}},annotations:viewAnnotations,requires:v.requires})}return[...tools.values()]}function referencedDefs(root,node,found={}){if(!node||typeof node!="object")return found;let ref=node.$ref,[head,segment]=typeof ref=="string"&&ref.startsWith("#/")?decoded(ref.slice(2)).split("/"):[],key=head==="$defs"?segment?.replace(/~1/g,"/").replace(/~0/g,"~"):void 0;key!==void 0&&!Object.hasOwn(found,key)&&root.$defs&&Object.hasOwn(root.$defs,key)&&(found[key]=root.$defs[key],referencedDefs(root,found[key],found));for(let child of Object.values(node))referencedDefs(root,child,found);return found}var decoded=pointer=>{try{return decodeURIComponent(pointer)}catch{return""}};var publishingTransitions=[["publish",["draft"],"published"],["unpublish",["published","archived"],"draft"],["archive",["draft","published"],"archived"]];var publishingInput={type:"object",properties:{id:{type:"string",minLength:1},expectedVersion:{type:"integer",minimum:1}},required:["id","expectedVersion"],additionalProperties:!1};function mediaDefinitions(){let define=(name,description,inputSchema,action,readOnlyHint=!1)=>({name,description,inputSchema,action,annotations:{readOnlyHint,destructiveHint:action==="delete",openWorldHint:!0}}),string={type:"string"},texts={alt:{type:"string",maxLength:1e3},caption:{type:"string",maxLength:1e3}},input=(properties,required=[])=>({type:"object",properties,required,additionalProperties:!1});return[define("get_media_upload_policy","Read declared image purpose MIME slots and byte caps before preparing variants or requesting an upload. Optimization runs locally, never in the host.",input({}),"policy",!0),define("create_media_upload","Create short-lived PUT capabilities for image variants. Read site media purpose policy first; optimize images locally. Obtain attachment bytes yourself, then PUT each variant with its requiredHeaders and call commit_media_upload. Never store upload URLs as published images or ask the user to run terminal commands.",input({filename:{type:"string",maxLength:255},purpose:string,variants:{type:"array",minItems:1,items:input({mimeType:string,byteSize:{type:"integer",minimum:1},role:{type:"string",enum:["primary","alternate","fallback"]}},["mimeType","byteSize","role"])},...texts},["filename","purpose","variants"]),"create"),define("commit_media_upload","Verify all uploaded variants and commit one permanent media asset. Store the returned asset id in fields referencing media_assets.id, never a temporary upload URL. Unknown or expired upload groups require a new create_media_upload.",input({uploadGroupId:string,...texts},["uploadGroupId"]),"commit"),define("list_media_assets","List committed image assets, newest first, with their permanent variants and alt/caption. Search matches id, alt or caption; reuse only a returned cursor.",input({limit:{type:"integer",minimum:1,maximum:500},cursor:string,search:string}),"list",!0),define("get_media_asset","Read one committed media asset and its permanent variant URLs.",input({id:string},["id"]),"get",!0),define("update_media_asset","Update committed asset alt or caption; omitted fields remain unchanged and empty strings clear text.",input({id:string,...texts},["id"]),"update"),define("delete_media_asset","Delete an asset's objects and library record. Check application references first. A partial failure can be retried using the same id.",input({id:string},["id"]),"delete")]}var ADMIN_API_BASE="/admin/api",pathTemplate=path=>path.replace(/\{([^}]+)\}/g,":$1"),routePath=template=>template.split("/").map(segment=>segment.startsWith(":")?`{${segment.slice(1)}}`:segment).join("/");function routeSource(template,base="",suffix=!1){let keys=[],path=template.split("/").map(segment=>{let rest=suffix?/^\{([a-zA-Z]\w*)\*\}$/.exec(segment):null,one=/^\{([^}]+)\}$/.exec(segment);return rest||one?(keys.push((rest??one)[1]),rest?"(.*)":"([^/]+)"):encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("/");return{source:`^${base}${path}/?$`,keys}}function canonicalPath(pathname){try{return pathname.split("/").map(segment=>encodeURIComponent(decodeURIComponent(segment))).join("/")}catch{return}}var isParameter=segment=>/^\{[^}]+\}$/.test(segment);function routeOrder(a,b){let x=a.split("/"),y=b.split("/"),count=segments=>segments.filter(isParameter).length;if(count(x)!==count(y))return count(x)-count(y);for(let i=0;i<Math.min(x.length,y.length);i++)if(isParameter(x[i])!==isParameter(y[i]))return isParameter(x[i])?1:-1;return a<b?-1:a>b?1:0}function inspectSql(value){let schemas=new Set,readSchemas=new Set,writes=[],readOnly=!0,walk=v=>{if(!v||typeof v!="object")return;let n=v;n.RangeVar?.mantle==="table"&&(schemas.add(n.RangeVar.relname.toLowerCase()),readSchemas.add(n.RangeVar.relname.toLowerCase()));for(let kind of["InsertStmt","UpdateStmt","DeleteStmt"])n[kind]&&(readOnly=!1,n[kind].relation?.relname&&writes.push({schema:n[kind].relation.relname.toLowerCase(),operation:kind==="InsertStmt"?"create":kind==="UpdateStmt"?"update":"delete"}),n[kind].relation?.relname&&(schemas.add(n[kind].relation.relname.toLowerCase()),kind!=="InsertStmt"&&readSchemas.add(n[kind].relation.relname.toLowerCase())));Object.values(n).forEach(walk)};return walk(value),{schemas:[...schemas].sort(),readSchemas:[...readSchemas].sort(),readOnly,writes}}function mergeRestrict(policies,conjunctive){return Object.fromEntries([...new Set(policies.flatMap(p=>Object.keys(p)))].sort().map(schema=>{let rows=policies.map(p=>p[schema]??{terms:[]}),terms=[...new Map(rows.flatMap(p=>p.terms).map(t=>[canonical(t),t])).values()].filter(t=>conjunctive||rows.every(p=>p.terms.some(v=>canonical(v)===canonical(t)))),owner=rows.find(p=>p.owner)?.owner;return[schema,{...owner&&(conjunctive||rows.every(p=>p.owner===owner))?{owner}:{},terms}]}))}function inspectBehavior(plan,ir,opaque,requires,restrict,writes){let inspected=inspectSql(ir);writes&&(inspected.writes=[...writes],inspected.schemas.push(...writes.map(w=>w.schema)),inspected.readSchemas.push(...writes.filter(w=>w.operation!=="create").map(w=>w.schema)));let dependencies=[],policies=[restrict??{}],procedures=new Set,hooks=new Set,visit=name=>{if(procedures.has(name))return;procedures.add(name);let p=plan.procedures[name];if(!p)throw new TypeError(`Unknown dependency '${name}'.`);dependencies.push({procedure:name,declaration:p}),policies.push(p.restrict??{}),opaque||="ref"in p.handler;let g=inspectSql("sql"in p.handler?p.handler.sql.stmts:void 0);inspected.schemas.push(...g.schemas),inspected.readSchemas.push(...g.readSchemas),inspected.writes.push(...g.writes),inspected.readOnly&&=g.readOnly,p.requires?.guard&&visit(p.requires.guard.procedure)};for(requires?.guard&&visit(requires.guard.procedure);;){let before=hooks.size;for(let[id,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0)){if(t.source.kind!=="lifecycle"||hooks.has(id))continue;let source=t.source;!opaque&&!inspected.writes.some(w=>w.schema===source.schema.toLowerCase()&&source.on.some(on=>on.endsWith(`_${w.operation}`)))||(hooks.add(id),dependencies.push({trigger:id,source}),visit(t.procedure))}if(hooks.size===before)break}let related=names=>{let all=new Set(opaque?Object.keys(plan.schemas):names);for(let name of all){let parent=plan.schemas[name]?.translates?.parent.toLowerCase();parent&&all.add(parent)}return[...all].sort()};return{...inspected,schemas:related(inspected.schemas),readSchemas:related(inspected.readSchemas),opaque,restrict:mergeRestrict(policies,!0),dependencies}}var objectInput={type:"object",properties:{}},adminRoutes=["GET /me","GET /site","GET /collections","GET /views-manifest","GET /operations","GET /webmcp","POST /webmcp/:tool","GET /bootstrap","GET /views/:name/export","GET /views/:name","GET /view-batch","GET /developer/views/:name","POST /operations/:name","GET /developer-console","GET /collections/:name/statistics","GET /entries","GET /entries/export","GET /entries/:id","POST /entries","PATCH /entries/:id","POST /entries/:id/publish","POST /entries/:id/unpublish","POST /entries/:id/archive","DELETE /entries/:id","GET /site-settings","PATCH /site-settings","POST /media/uploads","POST /media/uploads/:groupId/commit","GET /media","GET /media/:id","PATCH /media/:id","DELETE /media/:id","GET /admin","GET /members","PATCH /admin/:id/role","POST /admin/invitations","DELETE /admin/invitations/:id"];function targetDefinitions(plan){let out=[],tools=[...mcpTools(plan,"public"),...mcpTools(plan,"admin")];for(let[kind,entries]of[["view",plan.views],["procedure",plan.procedures]])for(let[name,p]of Object.entries(entries).sort(([a],[b])=>a<b?-1:a>b?1:0)){let view=kind==="view"?plan.views[name]:void 0,proc=kind==="procedure"?plan.procedures[name]:void 0,ir=view?.stmts??(proc&&"sql"in proc.handler?proc.handler.sql.stmts:void 0),inspected=inspectBehavior(plan,ir,!!proc&&"ref"in proc.handler,p.requires,p.restrict),tool=tools.find(t=>t.kind===kind&&t.source===name),title=resolveLocalizedText(p.title,"en","en")??name,triggers=Object.entries(plan.triggers).filter(([,t])=>kind==="procedure"&&t.procedure===name).sort(([a],[b])=>a<b?-1:a>b?1:0).map(([id,t])=>({id,source:t.source})),surfaces=[...new Set([...view?[view.surface,"admin",...view.surface==="public"?["rest"]:[]]:[],...triggers.map(t=>t.source.kind==="mcp"?t.source.surface:t.source.kind==="http"?"rest":t.source.kind)])].sort();surfaces.length||surfaces.push("internal"),out.push({id:`${kind}:${name}`,name:tool?.name??mcpToolNameSegment(name),kind,surface:view?.surface??(surfaces.includes("public")||surfaces.includes("rest")?"public":surfaces.includes("admin")?"admin":"internal"),surfaces,triggers,title,description:resolveLocalizedText(p.description,"en","en")??title,inputSchema:tool?.inputSchema??view?.input??proc?.input??objectInput,annotations:tool?.annotations??{},requires:p.requires,restrict:inspected.restrict,schemas:inspected.schemas,readSchemas:inspected.readSchemas,readOnly:!inspected.opaque&&inspected.readOnly,opaque:inspected.opaque,trustedOnly:!!proc?.trustedOnly,variants:[],behavior:{declaration:p,dependencies:inspected.dependencies}})}let addRoute=(id,dependencies,inputSchema=objectInput)=>{let admin=id.startsWith("admin:");out.push({id,name:id.replace(/[^a-zA-Z0-9_-]/g,"_"),title:id,description:id,kind:admin?"admin":"rest",surface:admin?"admin":"public",surfaces:[admin?"admin":"rest"],triggers:dependencies.flatMap(d=>d.triggers),inputSchema,annotations:{},requires:admin?{auth:{all:["ctx.admin"]}}:dependencies[0]?.requires,restrict:mergeRestrict(dependencies.map(d=>d.restrict),!1),schemas:[...new Set(dependencies.flatMap(d=>d.schemas))].sort(),readSchemas:[...new Set(dependencies.flatMap(d=>d.readSchemas))].sort(),readOnly:dependencies.length>0&&dependencies.every(d=>d.readOnly)&&(!admin||id.startsWith("admin:GET ")||id==="admin:POST /operations/:name"||id==="admin:POST /webmcp/:tool"),opaque:dependencies.some(d=>d.opaque),trustedOnly:dependencies.some(d=>d.trustedOnly),variants:dependencies,behavior:dependencies.map(d=>({id:d.id,behavior:d.behavior,surfaces:d.surfaces,triggers:d.triggers,input:d.inputSchema,requires:d.requires,restrict:d.restrict,trustedOnly:d.trustedOnly}))})},declarations=[...out],adminOperations=new Set(mcpTools(plan,"admin").filter(t=>t.kind==="procedure").map(t=>`procedure:${t.source}`));for(let d of declarations)d.kind==="view"&&d.surfaces.includes("public")&&addRoute(`rest:GET /api/views/${d.id.slice(5)}`,[d],d.inputSchema);for(let[,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0))if(t.source.kind==="http"){let d=declarations.find(d2=>d2.id===`procedure:${t.procedure}`);addRoute(`rest:${t.source.method} ${pathTemplate(t.source.path)}`,[d],d.inputSchema)}let generated=[];for(let[schema,s]of Object.entries(plan.schemas).sort(([a],[b])=>a<b?-1:a>b?1:0))if(s.publishing)for(let[verb,from,to]of publishingTransitions){addRoute(`procedure:${s.name}.${verb}`,[],publishingInput);let d=out.at(-1),inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,[{schema,operation:verb==="publish"?"publish":"update"}]);Object.assign(d,{kind:"procedure",name:mcpToolNameSegment(`${verb}_${s.name}`),surface:"admin",surfaces:["admin"],requires:{auth:{all:["ctx.admin"]}},schemas:[...new Set([schema,...inspected.schemas])].sort(),readSchemas:[...new Set([schema,...inspected.readSchemas])].sort(),opaque:inspected.opaque,behavior:{schema,verb,from,to,dependencies:inspected.dependencies}}),generated.push(d)}for(let tool of mediaDefinitions()){addRoute(`admin:tool:${tool.name}`,[],tool.inputSchema);let d=out.at(-1);Object.assign(d,{name:tool.name,readOnly:tool.annotations.readOnlyHint,opaque:!tool.annotations.readOnlyHint,annotations:tool.annotations,behavior:{tool,schemas:plan.schemas}}),generated.push(d)}for(let route2 of adminRoutes){let deps=route2.includes("/views/:")||route2==="GET /view-batch"?declarations.filter(d2=>d2.kind==="view"&&(route2.includes("/developer/")||plan.views[d2.id.slice(5)]?.surface==="admin")):route2.includes("/operations/:")?declarations.filter(d2=>adminOperations.has(d2.id)):route2==="POST /webmcp/:tool"?declarations.filter(d2=>mcpTools(plan,"admin").some(t=>`${t.kind}:${t.source}`===d2.id)).concat(generated):[],generic=route2.includes("/entries")||route2.includes("/statistics")||route2==="GET /bootstrap";addRoute(`admin:${route2}`,deps,deps.length&&route2!=="GET /view-batch"?{...objectInput,anyOf:deps.map(d2=>({type:"object",properties:{[route2.includes("/webmcp/")?"tool":"name"]:{type:"string",const:route2.includes("/webmcp/")?d2.name:d2.id.slice(d2.kind==="view"?5:10)}}}))}:objectInput);let d=out.at(-1);if(generic){let operation=route2==="POST /entries"?"create":route2==="DELETE /entries/:id"?"delete":route2.startsWith("GET ")?void 0:route2.endsWith("/publish")?"publish":"update",inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,operation?Object.keys(plan.schemas).map(schema=>({schema,operation})):[]);d.schemas=Object.keys(plan.schemas).sort(),d.readSchemas=d.schemas,d.opaque=inspected.opaque,d.behavior={schemas:plan.schemas,dependencies:inspected.dependencies}}}return[...new Map(out.map(d=>[d.id,d])).values()].sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0)}var tables=new WeakMap,route=/^(rest|admin):([A-Z]+) (\/.*)$/,normalized=new Set(["DELETE","GET","HEAD","OPTIONS","POST","PUT"]);function compile(describe){let table={rest:[],admin:[],tools:[]};for(let{name,_meta:meta}of describe.targets){let[,surface,method,template]=route.exec(meta.id)??[];if(surface==="rest")table.rest.push({method,pattern:new RegExp(routeSource(routePath(template)).source),view:meta.variants[0]?.id.startsWith("view:")===!0,meta});else if(surface==="admin")table.admin.push({method,pattern:new RegExp(routeSource(routePath(template),ADMIN_API_BASE).source),meta});else{let s=meta.surfaces,endpoints=meta.id.startsWith("admin:tool:")?["admin"]:meta.id.startsWith("view:")?s.includes("public")?["public"]:s.includes("internal")?[]:["admin"]:meta.id.startsWith("procedure:")?s.filter(x=>x==="public"||x==="admin"):[];endpoints.length&&table.tools.push({name,endpoints,meta})}}let order=id=>adminRoutes.indexOf(id.slice(6))>>>0,path=id=>routePath(id.slice(id.indexOf(" ")+1));return table.rest.sort((a,b)=>routeOrder(path(a.meta.id),path(b.meta.id))),table.admin.sort((a,b)=>order(a.meta.id)-order(b.meta.id)),table}function matchTarget(describe,request){let table=tables.get(describe);if(table||tables.set(describe,table=compile(describe)),request.surface==="mcp"){let path2=request.path?.replace(/\/+$/,""),endpoint=path2===void 0?void 0:path2==="/mcp"?"public":path2==="/mcp/admin"?"admin":null,found=table.tools.filter(t=>t.name===request.tool&&(endpoint===void 0||t.endpoints.includes(endpoint)));return endpoint!==null&&found.length&&found.every(t=>t.meta.id===found[0].meta.id)?found[0].meta:null}let method=normalized.has(request.method.toUpperCase())?request.method.toUpperCase():request.method,path=canonicalPath(request.path);return path===void 0?null:request.surface==="admin"?request.path!==ADMIN_API_BASE&&!request.path.startsWith(ADMIN_API_BASE+"/")?null:table.admin.find(r=>r.method===method&&r.pattern.test(path))?.meta??null:table.rest.find(r=>r.pattern.test(path)&&(r.method===method||method==="HEAD"&&r.method==="GET"&&r.view))?.meta??null}export{canonical,targetDefinitions,matchTarget};
@@ -1 +1 @@
1
- import{ADMIN_API_BASE,adminRoutes,canonical,canonicalPath,hashDefinition,routeOrder,routePath,routeSource,targetDefinitions,targetHash}from"./chunk-P6PP6VVV.js";var tables=new WeakMap,route=/^(rest|admin):([A-Z]+) (\/.*)$/,normalized=new Set(["DELETE","GET","HEAD","OPTIONS","POST","PUT"]);function compile(describe){let table={rest:[],admin:[],tools:[]};for(let{name,_meta:meta}of describe.targets){let[,surface,method,template]=route.exec(meta.id)??[];if(surface==="rest")table.rest.push({method,pattern:new RegExp(routeSource(routePath(template)).source),view:meta.variants[0]?.id.startsWith("view:")===!0,meta});else if(surface==="admin")table.admin.push({method,pattern:new RegExp(routeSource(routePath(template),ADMIN_API_BASE).source),meta});else{let s=meta.surfaces,endpoints=meta.id.startsWith("admin:tool:")?["admin"]:meta.id.startsWith("view:")?s.includes("public")?["public"]:s.includes("internal")?[]:["admin"]:meta.id.startsWith("procedure:")?s.filter(x=>x==="public"||x==="admin"):[];endpoints.length&&table.tools.push({name,endpoints,meta})}}let order=id=>adminRoutes.indexOf(id.slice(6))>>>0,path=id=>routePath(id.slice(id.indexOf(" ")+1));return table.rest.sort((a,b)=>routeOrder(path(a.meta.id),path(b.meta.id))),table.admin.sort((a,b)=>order(a.meta.id)-order(b.meta.id)),table}function matchTarget(describe,request){let table=tables.get(describe);if(table||tables.set(describe,table=compile(describe)),request.surface==="mcp"){let path2=request.path?.replace(/\/+$/,""),endpoint=path2===void 0?void 0:path2==="/mcp"?"public":path2==="/mcp/admin"?"admin":null,found=table.tools.filter(t=>t.name===request.tool&&(endpoint===void 0||t.endpoints.includes(endpoint)));return endpoint!==null&&found.length&&found.every(t=>t.meta.id===found[0].meta.id)?found[0].meta:null}let method=normalized.has(request.method.toUpperCase())?request.method.toUpperCase():request.method,path=canonicalPath(request.path);return path===void 0?null:request.surface==="admin"?request.path!==ADMIN_API_BASE&&!request.path.startsWith(ADMIN_API_BASE+"/")?null:table.admin.find(r=>r.method===method&&r.pattern.test(path))?.meta??null:table.rest.find(r=>r.pattern.test(path)&&(r.method===method||method==="HEAD"&&r.method==="GET"&&r.view))?.meta??null}function requirements(d,surface){let all=d.requires?.auth?.all??[];return{all:surface==="admin"&&!all.includes("ctx.admin")?["ctx.admin",...all]:all,...d.requires?.guard?{guard:d.requires.guard.procedure}:{}}}async function describeApp(plan){let targets=await Promise.all(targetDefinitions(plan).map(async d=>{let scope=Object.fromEntries(d.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}));return{name:d.name,title:d.title,description:d.description,inputSchema:d.inputSchema,annotations:{...d.annotations,readOnlyHint:d.readOnly},_meta:{surfaces:d.surfaces,triggers:d.triggers,id:d.id,targetHash:await hashDefinition(plan,d),readOnly:d.readOnly,scope,restrict:d.restrict,opaque:d.opaque,unscopedReads:d.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),trustedOnly:d.trustedOnly,variants:d.variants.map(v=>({id:v.id,restrict:v.restrict,requires:requirements(v,d.surface),inputSchema:v.inputSchema,readOnly:v.readOnly,trustedOnly:v.trustedOnly,opaque:v.opaque,unscopedReads:v.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),scope:Object.fromEntries(v.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}))})),requires:requirements(d,d.surface)}}}));return JSON.parse(canonical({schemaVersion:1,access:plan.access?.userFields??{},targets}))}var securityFlags=["readOnly","trustedOnly","opaque","unscopedReads"],flagDirection=(field,value)=>(field==="readOnly"||field==="trustedOnly"?!value:value)?"widened":"narrowed",inclusion=(before,after)=>before.some(t=>!after.includes(t))?"widened":after.some(t=>!before.includes(t))?"narrowed":"changed",exposureDirection=(before,after)=>after.some(t=>!before.includes(t)&&t!==canonical("internal"))?"widened":before.some(t=>!after.includes(t)&&t!==canonical("internal"))?"narrowed":"changed",combine=directions=>directions.includes("widened")?"widened":directions.length&&directions.every(d=>d==="narrowed")?"narrowed":"changed",terms=p=>[...p.owner?[canonical({owner:p.owner})]:[],...p.terms.map(canonical)];function policyDirection(before,after,scope){let directions=[];for(let schema of new Set([...Object.keys(before),...Object.keys(after)])){let x=before[schema],y=after[schema];canonical(x)!==canonical(y)&&directions.push(scope&&(!x||!y)?x?"narrowed":"widened":inclusion(x?terms(x):[],y?terms(y):[]))}return combine(directions)}function requiresDirection(x,y){let directions=[];return canonical(x.all)!==canonical(y.all)&&directions.push(inclusion(x.all.map(canonical),y.all.map(canonical))),x.guard!==y.guard&&directions.push(y.guard?x.guard?"changed":"narrowed":"widened"),combine(directions)}function diffApp(a,b){if(a.schemaVersion!==1||b.schemaVersion!==1)throw new TypeError("Unsupported describe schemaVersion.");let changes=[],old=new Map(a.targets.map(t=>[t._meta.id,t])),next=new Map(b.targets.map(t=>[t._meta.id,t]));for(let id of[...new Set([...old.keys(),...next.keys()])].sort()){let before=old.get(id),after=next.get(id);if(!before||!after){changes.push({id,field:"target",direction:before?"narrowed":"widened"});continue}let x=before._meta,y=after._meta,start=changes.length,record=(field,left,right,direction)=>{canonical(left)!==canonical(right)&&changes.push({id,field,direction,before:left,after:right})};record("scope",x.scope,y.scope,policyDirection(x.scope,y.scope,!0)),record("restrict",x.restrict,y.restrict,policyDirection(x.restrict,y.restrict,!1)),record("requires",x.requires,y.requires,requiresDirection(x.requires,y.requires));for(let field of["surfaces","triggers"])record(field,x[field],y[field],exposureDirection(x[field].map(canonical),y[field].map(canonical)));for(let field of securityFlags)record(field,x[field],y[field],flagDirection(field,y[field]));record("inputSchema",before.inputSchema,after.inputSchema,"changed");let directions=[],variants=new Map(x.variants.map(v=>[v.id,v])),added=new Map(y.variants.map(v=>[v.id,v]));for(let name of new Set([...variants.keys(),...added.keys()])){let left=variants.get(name),right=added.get(name);if(canonical(left)!==canonical(right))if(!left||!right)directions.push(left?"narrowed":"widened");else{canonical(left.scope)!==canonical(right.scope)&&directions.push(policyDirection(left.scope,right.scope,!0)),canonical(left.restrict)!==canonical(right.restrict)&&directions.push(policyDirection(left.restrict,right.restrict,!1)),canonical(left.requires)!==canonical(right.requires)&&directions.push(requiresDirection(left.requires,right.requires)),canonical(left.inputSchema)!==canonical(right.inputSchema)&&directions.push("changed");for(let field of securityFlags)left[field]!==right[field]&&directions.push(flagDirection(field,right[field]))}}record("variants",x.variants,y.variants,combine(directions)),changes.length===start&&x.targetHash!==y.targetHash&&changes.push({id,field:"behavior",direction:"changed"})}return changes}export{describeApp,diffApp,matchTarget,targetHash};
1
+ import{canonical,matchTarget,targetDefinitions}from"./chunk-CW7X5Q5T.js";async function hash(value){let digest=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(value)));return[...new Uint8Array(digest)].map(b=>b.toString(16).padStart(2,"0")).join("")}function hashDefinition(plan,d){return hash({id:d.id,surfaces:d.surfaces,triggers:d.triggers,behavior:d.behavior,input:d.inputSchema,requires:d.requires,restrict:d.restrict,dialect:plan.dialect,schemas:Object.fromEntries(d.schemas.map(name=>[name,plan.schemas[name]])),userFields:plan.access?.userFields,readOnly:d.readOnly,opaque:d.opaque,trustedOnly:d.trustedOnly})}async function targetHash(plan,targetId){let target=targetDefinitions(plan).find(d=>d.id===targetId);if(!target)throw new TypeError(`Unknown target '${targetId}'.`);return hashDefinition(plan,target)}function requirements(d,surface){let all=d.requires?.auth?.all??[];return{all:surface==="admin"&&!all.includes("ctx.admin")?["ctx.admin",...all]:all,...d.requires?.guard?{guard:d.requires.guard.procedure}:{}}}async function describeApp(plan){let targets=await Promise.all(targetDefinitions(plan).map(async d=>{let scope=Object.fromEntries(d.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}));return{name:d.name,title:d.title,description:d.description,inputSchema:d.inputSchema,annotations:{...d.annotations,readOnlyHint:d.readOnly},_meta:{surfaces:d.surfaces,triggers:d.triggers,id:d.id,targetHash:await hashDefinition(plan,d),readOnly:d.readOnly,scope,restrict:d.restrict,opaque:d.opaque,unscopedReads:d.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),trustedOnly:d.trustedOnly,variants:d.variants.map(v=>({id:v.id,restrict:v.restrict,requires:requirements(v,d.surface),inputSchema:v.inputSchema,readOnly:v.readOnly,trustedOnly:v.trustedOnly,opaque:v.opaque,unscopedReads:v.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),scope:Object.fromEntries(v.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}))})),requires:requirements(d,d.surface)}}}));return JSON.parse(canonical({schemaVersion:1,access:plan.access?.userFields??{},targets}))}var securityFlags=["readOnly","trustedOnly","opaque","unscopedReads"],flagDirection=(field,value)=>(field==="readOnly"||field==="trustedOnly"?!value:value)?"widened":"narrowed",inclusion=(before,after)=>before.some(t=>!after.includes(t))?"widened":after.some(t=>!before.includes(t))?"narrowed":"changed",exposureDirection=(before,after)=>after.some(t=>!before.includes(t)&&t!==canonical("internal"))?"widened":before.some(t=>!after.includes(t)&&t!==canonical("internal"))?"narrowed":"changed",combine=directions=>directions.includes("widened")?"widened":directions.length&&directions.every(d=>d==="narrowed")?"narrowed":"changed",terms=p=>[...p.owner?[canonical({owner:p.owner})]:[],...p.terms.map(canonical)];function policyDirection(before,after,scope){let directions=[];for(let schema of new Set([...Object.keys(before),...Object.keys(after)])){let x=before[schema],y=after[schema];canonical(x)!==canonical(y)&&directions.push(scope&&(!x||!y)?x?"narrowed":"widened":inclusion(x?terms(x):[],y?terms(y):[]))}return combine(directions)}function requiresDirection(x,y){let directions=[];return canonical(x.all)!==canonical(y.all)&&directions.push(inclusion(x.all.map(canonical),y.all.map(canonical))),x.guard!==y.guard&&directions.push(y.guard?x.guard?"changed":"narrowed":"widened"),combine(directions)}function diffApp(a,b){if(a.schemaVersion!==1||b.schemaVersion!==1)throw new TypeError("Unsupported describe schemaVersion.");let changes=[],old=new Map(a.targets.map(t=>[t._meta.id,t])),next=new Map(b.targets.map(t=>[t._meta.id,t]));for(let id of[...new Set([...old.keys(),...next.keys()])].sort()){let before=old.get(id),after=next.get(id);if(!before||!after){changes.push({id,field:"target",direction:before?"narrowed":"widened"});continue}let x=before._meta,y=after._meta,start=changes.length,record=(field,left,right,direction)=>{canonical(left)!==canonical(right)&&changes.push({id,field,direction,before:left,after:right})};record("scope",x.scope,y.scope,policyDirection(x.scope,y.scope,!0)),record("restrict",x.restrict,y.restrict,policyDirection(x.restrict,y.restrict,!1)),record("requires",x.requires,y.requires,requiresDirection(x.requires,y.requires));for(let field of["surfaces","triggers"])record(field,x[field],y[field],exposureDirection(x[field].map(canonical),y[field].map(canonical)));for(let field of securityFlags)record(field,x[field],y[field],flagDirection(field,y[field]));record("inputSchema",before.inputSchema,after.inputSchema,"changed");let directions=[],variants=new Map(x.variants.map(v=>[v.id,v])),added=new Map(y.variants.map(v=>[v.id,v]));for(let name of new Set([...variants.keys(),...added.keys()])){let left=variants.get(name),right=added.get(name);if(canonical(left)!==canonical(right))if(!left||!right)directions.push(left?"narrowed":"widened");else{canonical(left.scope)!==canonical(right.scope)&&directions.push(policyDirection(left.scope,right.scope,!0)),canonical(left.restrict)!==canonical(right.restrict)&&directions.push(policyDirection(left.restrict,right.restrict,!1)),canonical(left.requires)!==canonical(right.requires)&&directions.push(requiresDirection(left.requires,right.requires)),canonical(left.inputSchema)!==canonical(right.inputSchema)&&directions.push("changed");for(let field of securityFlags)left[field]!==right[field]&&directions.push(flagDirection(field,right[field]))}}record("variants",x.variants,y.variants,combine(directions)),changes.length===start&&x.targetHash!==y.targetHash&&changes.push({id,field:"behavior",direction:"changed"})}return changes}export{describeApp,diffApp,matchTarget,targetHash};
@@ -0,0 +1,3 @@
1
+ /** `@aotter/mantle/describe`: match a request to its target in saved `mantle describe` JSON. No compiler, parser or Node API. */
2
+ export { matchTarget, type TargetRequest } from "./match.js";
3
+ export type { AppDescription, DescribeTarget } from "./index.js";
@@ -0,0 +1 @@
1
+ import{matchTarget}from"./chunk-CW7X5Q5T.js";export{matchTarget};
@@ -15,11 +15,9 @@ export function topics(root = docsRoot()) {
15
15
  const unlisted = readdirSync(resolve(root, "handbook"), { recursive: true }).filter(path => path.endsWith(".md")).sort()
16
16
  .map(path => resolve(root, "handbook", path)).filter(file => !listed.has(file))
17
17
  .map(file => ({ name: relative(resolve(root, "handbook"), file).replaceAll("\\", "/").slice(0, -3), title: readFileSync(file, "utf8").match(/^# (.+)$/m)?.[1] ?? basename(file), file, group: "More pages" }));
18
- const skills = (existsSync(resolve(root, "skills")) ? readdirSync(resolve(root, "skills")) : []).filter(name => existsSync(resolve(root, "skills", name, "SKILL.md")))
19
- .map(name => ({ name: `skills/${name}`, title: `Package skill: ${name}`, file: resolve(root, "skills", name, "SKILL.md"), group: "Package skills" }));
20
18
  const examples = resolve(root, "examples");
21
19
  const shipped = readdirSync(examples, { withFileTypes: true }).filter(entry => entry.isDirectory() && existsSync(resolve(examples, entry.name, "README.md")));
22
- return [...pages, ...unlisted, ...skills, { name: "examples", title: "Examples and how to combine them", file: resolve(examples, "README.md"), group: "Examples" },
20
+ return [...pages, ...unlisted, { name: "examples", title: "Examples and how to combine them", file: resolve(examples, "README.md"), group: "Examples" },
23
21
  ...shipped.map(entry => ({ name: `examples/${entry.name}`, title: `Example: ${entry.name}`, file: resolve(examples, entry.name, "README.md"), group: "Examples" }))];
24
22
  }
25
23
  function find(all, name) {
@@ -40,7 +40,7 @@ export interface McpAppResource {
40
40
  readonly domain?: string;
41
41
  /** Tool names whose results this resource renders. */
42
42
  readonly renders?: readonly string[];
43
- /** Views only this App may call (`visibility: ["app"]`); a client without MCP Apps does not see them. A Procedure cannot be hidden from the model. */
43
+ /** Views only this App may call (`visibility: ["app"]`); a client without MCP Apps does not see them. To hide a Procedure, give its tool `meta: { ui: { visibility: ["app"] } }`. */
44
44
  readonly appOnly?: readonly string[];
45
45
  }
46
46
  export interface McpApps {
@@ -51,7 +51,7 @@ export type McpAppsSource = McpApps | ((context: Pick<McpAppRender, "env" | "req
51
51
  /** Flatten contributors; `mcp(...)` reads static resources getters once at construction. */
52
52
  export declare const resourcesOf: (apps: McpApps | readonly McpApps[]) => McpApps;
53
53
  /** Static contributors are checked at construction; resolved factories are checked per request. A request-bound tool table defers tool-link checks until its first request. */
54
- export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: ReadonlySet<string>, views?: ReadonlySet<string>): {
54
+ export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: ReadonlyMap<string, Readonly<Record<string, any>> | undefined>, views?: ReadonlySet<string>): {
55
55
  resources: McpAppResource[];
56
56
  renders: Map<string, string>;
57
57
  appOnly: Set<string>;
@@ -60,6 +60,7 @@ export declare function linkApps(apps: McpApps | readonly McpApps[], tools?: Rea
60
60
  export declare function linkSurface(apps: McpApps | readonly McpApps[], selected: readonly {
61
61
  name: string;
62
62
  page?: unknown;
63
+ meta?: Readonly<Record<string, unknown>>;
63
64
  }[], previous?: {
64
65
  key: string;
65
66
  links: ReturnType<typeof linkApps>;
@@ -27,9 +27,14 @@ export function linkApps(apps, tools, views) {
27
27
  appOnly.add(tool);
28
28
  }
29
29
  }
30
+ for (const [tool, uri] of renders) {
31
+ const meta = tools?.get(tool) ?? {}, ui = meta.ui ?? {};
32
+ if ([ui.resourceUri, meta["ui/resourceUri"], meta["openai/outputTemplate"]].some(value => value && value !== uri) || appOnly.has(tool) && ui.visibility && `${ui.visibility}` !== "app")
33
+ refuse(`Tool '${tool}' meta contradicts its MCP App links.`);
34
+ }
30
35
  return { resources, renders, appOnly };
31
36
  }
32
37
  export function linkSurface(apps, selected, previous) {
33
- const key = JSON.stringify(selected.map(tool => [tool.name, !!tool.page]).sort(([a], [b]) => a.localeCompare(b)));
34
- return previous?.key === key ? previous : { key, links: linkApps(apps, new Set(selected.map(tool => tool.name)), new Set(selected.filter(tool => tool.page).map(tool => tool.name))) };
38
+ const key = JSON.stringify(selected.map(tool => [tool.name, !!tool.page, tool.meta]).sort(([a], [b]) => a.localeCompare(b)));
39
+ return previous?.key === key ? previous : { key, links: linkApps(apps, new Map(selected.map(tool => [tool.name, tool.meta])), new Set(selected.filter(tool => tool.page).map(tool => tool.name))) };
35
40
  }
@@ -24,6 +24,8 @@ const MCP_INPUT_ELEMENTS = 10_000;
24
24
  const union = (scopes, extra) => [...new Set([...scopes, ...extra])].join(" ");
25
25
  export function handler(tools, options, apps = {}, reads = []) {
26
26
  const allowed = options.allowedOrigins ?? [];
27
+ if (options.toolsPageSize !== undefined && !(Number.isInteger(options.toolsPageSize) && options.toolsPageSize > 0))
28
+ throw new TypeError("mcp: toolsPageSize must be a positive integer.");
27
29
  return draining(async (request, env, ctx) => {
28
30
  const access = options.gateway ? (await import("./host-access.js")).accessRequest(ctx, options) : undefined;
29
31
  let resolved;
@@ -212,17 +214,25 @@ export function handler(tools, options, apps = {}, reads = []) {
212
214
  server.server.registerCapabilities({ tools: { listChanged: false } });
213
215
  if (host)
214
216
  server.server.onclose = () => access?.finish(isRecord(parsedBody) && parsedBody.method === "initialize" ? undefined : new ContractError("INPUT_VALIDATION_FAILED", "MCP request refused.", "mcp", { status: 400 }));
215
- const appMeta = links ? registerApps(server, links, parsedBody, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
217
+ const enabled = clientUiSupport(parsedBody) !== "unsupported";
218
+ const appMeta = links ? registerApps(server, links, enabled, new Set(selected.map(tool => tool.name)), { env, request }) : undefined;
216
219
  const registered = new Set(), set = server.server.setRequestHandler.bind(server.server);
220
+ const size = options.toolsPageSize;
217
221
  server.server.setRequestHandler = ((method, handler) => set(method, method === "tools/call" && typeof handler === "function" ? (request, context) => {
218
222
  if (!registered.has(request.params.name))
219
223
  throw new ProtocolError(ProtocolErrorCode.InvalidParams, `No tool named ${JSON.stringify(request.params.name)} exists in the current catalogue (catalogue version ${serverInfo.version}).`);
220
224
  return handler(request, context);
225
+ } : method === "tools/list" && size && typeof handler === "function" ? async (request, context) => {
226
+ const { tools } = await handler(request, context), cursor = request.params?.cursor, start = cursor === undefined ? 0 : Number(cursor.split(":")[1]);
227
+ if (cursor !== undefined && !(start > 0 && start % size === 0 && start < tools.length && cursor === `${version}:${start}`))
228
+ throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tools/list cursor; list again from the first page (catalogue version ${serverInfo.version}).`);
229
+ return { tools: tools.slice(start, start + size), ...(start + size < tools.length ? { nextCursor: `${version}:${start + size}` } : {}) };
221
230
  } : handler));
222
231
  for (const tool of selected) {
223
- const meta = appMeta?.(tool.name);
224
- if (meta === false)
232
+ const linked = appMeta?.(tool.name), own = tool.meta, visibility = own?.ui?.visibility;
233
+ if (linked === false || !enabled && Array.isArray(visibility) && !visibility.includes("model"))
225
234
  continue;
235
+ const meta = linked && own ? { ...linked, ...own, ui: { ...linked.ui, ...own.ui } } : linked ?? own;
226
236
  registered.add(tool.name);
227
237
  const about = (contract) => { const text = [tool.version && `tool version ${tool.version}`, contract && `input schema ${JSON.stringify(tool.page ? paging(tool.inputSchema) : tool.inputSchema)}`].filter(Boolean).join("; "); return text ? [{ type: "text", text }] : []; };
228
238
  server.registerTool(tool.name, { ...(meta ? { _meta: meta } : {}), ...(tool.title ? { title: text(tool.title, locale) } : {}), description: text(tool.description, locale) ?? text(tool.title, locale) ?? tool.name, inputSchema: schema(tool.page ? paging(tool.inputSchema) : tool.inputSchema), ...(tool.outputSchema ? { outputSchema: schema(tool.outputSchema) } : {}), ...(tool.annotations ? { annotations: tool.annotations } : {}) }, async (raw) => {
@@ -236,8 +246,11 @@ export function handler(tools, options, apps = {}, reads = []) {
236
246
  tool.check?.(caller);
237
247
  if (tool.run && (!host || !tool.readOnly))
238
248
  checkOrigin(request, caller);
239
- let db;
240
- const run = (input) => callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
249
+ let db, resultMeta;
250
+ const run = async (input) => {
251
+ const out = await callTool(tool, input, () => db ??= options.db(env), caller, options.handlers, access?.dryRun ? () => ({ dryRun: true, event: access.finish() }) : undefined);
252
+ return isRecord(out) && Symbol.for("net.aotter.mantle/result") in out ? (resultMeta = out._meta, out.data) : out;
253
+ };
241
254
  let data = await run(raw);
242
255
  for (let tries = 0; tool.page && isRecord(data) && Array.isArray(data.rows); tries++) {
243
256
  const rows = data.rows, total = JSON.stringify(data).length;
@@ -261,7 +274,7 @@ export function handler(tools, options, apps = {}, reads = []) {
261
274
  if (host)
262
275
  access?.finish();
263
276
  const text = JSON.stringify(data), notice = tool.page && text.length > MCP_PAGE_BUDGET;
264
- return { ...(meta ? { _meta: { "net.aotter.mantle/tool": tool.name } } : {}), content: [{ type: "text", text }, ...about(false), ...(notice ? [{ type: "text", text: `This page is larger than the ${MCP_PAGE_BUDGET}-character page size budget (a single row over it, or a View without an order, is returned whole), so a host may truncate it. Continue with nextCursor, if any.` }] : [])], ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}) };
277
+ return { ...(meta || resultMeta ? { _meta: { ...resultMeta, "net.aotter.mantle/tool": tool.name } } : {}), content: [{ type: "text", text }, ...about(false), ...(notice ? [{ type: "text", text: `This page is larger than the ${MCP_PAGE_BUDGET}-character page size budget (a single row over it, or a View without an order, is returned whole), so a host may truncate it. Continue with nextCursor, if any.` }] : [])], ...(data && typeof data === "object" && !Array.isArray(data) ? { structuredContent: data } : {}) };
265
278
  }
266
279
  catch (error) {
267
280
  if (host)
@@ -322,8 +335,7 @@ function appMeta(r, csp = r.csp) {
322
335
  const ui = { ...(csp ? { csp } : {}), ...(r.permissions ? { permissions: r.permissions } : {}), ...(r.domain !== undefined ? { domain: r.domain } : {}), ...(r.prefersBorder !== undefined ? { prefersBorder: r.prefersBorder } : {}) };
323
336
  return Object.keys(ui).length > 0 ? { ui } : undefined;
324
337
  }
325
- function registerApps(server, links, message, tools, request) {
326
- const enabled = clientUiSupport(message) !== "unsupported";
338
+ function registerApps(server, links, enabled, tools, request) {
327
339
  if (enabled)
328
340
  for (const resource of links.resources) {
329
341
  const meta = appMeta(resource);
@@ -37,6 +37,8 @@ export interface Options<Env, Database> extends GatewayOptions {
37
37
  allowedOrigins?: readonly string[];
38
38
  onObservation?: (event: McpObservation) => void | Promise<void>;
39
39
  onRefusal?: (event: CallerRefusal) => void | Promise<void>;
40
+ /** Tools per `tools/list` page: the result carries `nextCursor` while more remain. Unset, one page lists every tool. */
41
+ toolsPageSize?: number;
40
42
  }
41
43
  export interface CallerRefusal {
42
44
  readonly at: number;
@@ -98,7 +100,23 @@ export interface Tool<Database, Handlers = never> {
98
100
  readonly path?: string;
99
101
  readonly page?: (db: Database, caller: Caller, input: never, paging: Paging) => Promise<unknown>;
100
102
  readonly run?: (db: Database, caller: Caller, input: never, handlers: Handlers) => Promise<unknown>;
103
+ /**
104
+ * Merged into this tool's `_meta` in `tools/list` (top level shallow, `ui` one level deep) with what its App links produce;
105
+ * a contradicting value fails at `mcp(...)`. `ui.visibility: ["app"]` hides the tool from clients without MCP Apps.
106
+ * It is a listing hint and grants or removes no execution authority.
107
+ */
108
+ readonly meta?: Readonly<Record<string, unknown>>;
101
109
  }
110
+ /** What `withResultMeta` returns: `data` is the tool result as usual, `_meta` goes only to the `tools/call` result `_meta`. */
111
+ export interface ResultWithMeta<T> {
112
+ readonly data: T;
113
+ readonly _meta: Readonly<Record<string, unknown>>;
114
+ }
115
+ /**
116
+ * Returned from a tool's `run`: MCP sends `data` exactly as an unwrapped return, and `meta` only in the result `_meta`
117
+ * (beside `net.aotter.mantle/tool`, which it cannot replace). `callTool` returns the wrapper itself; drop `_meta` before forwarding to a model.
118
+ */
119
+ export declare const withResultMeta: <T>(data: T, meta: Readonly<Record<string, unknown>>) => ResultWithMeta<T>;
102
120
  /** Request-bound tool tables keep native environment bindings outside the cached App catalogue. */
103
121
  export type Tools<Env, Database, Handlers = never> = readonly Tool<Database, Handlers>[] | ((request: Request, env: Env) => readonly Tool<Database, Handlers>[] | Promise<readonly Tool<Database, Handlers>[]>);
104
122
  /** The selected MCP routes; all request work loads only when an MCP request arrives. */
@@ -1,6 +1,7 @@
1
1
  import { linkApps, linkSurface, resourcesOf } from "./mcp-apps.js";
2
2
  export const callTool = async (...args) => (await import("./mcp-call.js")).callTool(...args);
3
3
  export const callToolLazy = async (...args) => (await import("./mcp-call.js")).callToolLazy(...args);
4
+ export const withResultMeta = (data, meta) => ({ [Symbol.for("net.aotter.mantle/result")]: true, data, _meta: meta });
4
5
  export function mcp(tools, options, reads = []) {
5
6
  let serve;
6
7
  const linked = {};
@@ -7,7 +7,7 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
7
7
  const schemaList = Object.values(plan.schemas);
8
8
  const placed = [...schemaList.map(s => ["schemas/" + s.name, s.uiSchema, s]), ...Object.entries(plan.views).map(([name, v]) => ["views/" + name, v.uiSchema, null])];
9
9
  for (const [path, ui, s] of placed) {
10
- const nav = (ui?.["nav"] ?? {}), group = nav["group"];
10
+ const nav = (ui?.["nav"] ?? {});
11
11
  if (s && !isPrimaryCollection(s, schemaList))
12
12
  for (const key of ["group", "order", "hidden"])
13
13
  if (nav[key] !== undefined)
@@ -76,6 +76,9 @@ export function generate(input) {
76
76
  return fields;
77
77
  };
78
78
  const values = new Set();
79
+ const pool = new Map();
80
+ const pooled = (text) => { let id = pool.get(text); if (!id)
81
+ pool.set(text, id = `\u0000_p${pool.size}\u0000`); return id; };
79
82
  const step = (s, path, fields, row = false) => {
80
83
  const recipes = recipesOf(s, fields, message => refuse(path, message)), probe = row && s.probe ? recipesOf(s.probe, fields, message => refuse(path, message)) : [];
81
84
  for (const r of [...recipes, ...probe]) {
@@ -86,7 +89,7 @@ export function generate(input) {
86
89
  }
87
90
  const locks = Object.fromEntries((plan.schemas[s.schema ?? ""]?.locks ?? []).filter(l => l.on.includes(s.verb)).map(l => [l.name, l.message]));
88
91
  const lock = row && s.probe ? `, lock: ${q({ sql: s.probe.sql, binds: probe, schema: plan.schemas[s.schema].name, verb: s.verb, locks })}` : "";
89
- return `{ sql: ${q(s.sql)}, binds: ${q(recipes)}${row ? ", row: true" : s.onNoRows ? ', row: "some"' : ""}${lock} }`;
92
+ return `{ sql: ${pooled(q(s.sql))}, binds: ${pooled(q(recipes))}${row ? ", row: true" : s.onNoRows ? ', row: "some"' : ""}${lock} }`;
90
93
  };
91
94
  const declarations = [], views = [], procedures = [], refProcedures = [];
92
95
  const entries = {};
@@ -133,7 +136,7 @@ export function generate(input) {
133
136
  const fieldNames = inputFields(procedure.input, path + "/input");
134
137
  const steps = (statements) => `[${statements.map(s => step(s, path, fieldNames, s.kind === "row")).join(", ")}] as const`;
135
138
  const same = steps(modes.caller) === steps(modes.trusted);
136
- declarations.push(`export type Procedure${index}Input = ${queryInputType(procedure.input, [...modes.caller, ...modes.trusted])};`, `const ${fn}Caller = ${steps(modes.caller)};`, ...(same ? [] : [`const ${fn}Trusted = ${steps(modes.trusted)};`]), `async function ${fn}(db: Database, caller: Caller, raw: Procedure${index}Input): Promise<Output> {`, ...(modes.caller.some(s => s.kind !== "read") ? [` if ("authenticatedAs" in caller && caller.authenticatedAs === "snapshot") (await import("@aotter/mantle/host-caller")).checkFresh(caller, false, ${q(path)});`] : []), ...auth(procedure.requires, path), ...(user.used ? [' USER.modeOf(caller);'] : []), ...(procedure.trustedOnly ? [` deny(caller, USER.modeOf(caller) === "trusted", ${q(path)});`] : []), ` return { results: await DRIVER.procedure(db, bound(D, ${user.used ? "USER.scopedAll(" : ""}${same ? "" : `${user.used ? 'USER.modeOf(caller) === "trusted"' : 'caller.kind === "system"'} ? ${fn}Trusted : `}${fn}Caller${user.used ? ", caller)" : ""}, caller, raw), ${q(path)}) };`, "}");
139
+ declarations.push(`export type Procedure${index}Input = ${queryInputType(procedure.input, [...modes.caller, ...modes.trusted])};`, `const ${fn}Caller = ${steps(modes.caller)};`, ...(same ? [] : [`const ${fn}Trusted = ${steps(modes.trusted)};`]), `async function ${fn}(db: Database, caller: Caller, raw: Procedure${index}Input): Promise<Output> {`, ...(modes.caller.some(s => s.kind !== "read") ? [` await _fresh(caller, ${q(path)});`] : []), ...auth(procedure.requires, path), ...(user.used ? [' USER.modeOf(caller);'] : []), ...(procedure.trustedOnly ? [` deny(caller, USER.modeOf(caller) === "trusted", ${q(path)});`] : []), ` return { results: await DRIVER.procedure(db, bound(D, ${user.used ? "USER.scopedAll(" : ""}${same ? "" : `${user.used ? 'USER.modeOf(caller) === "trusted"' : 'caller.kind === "system"'} ? ${fn}Trusted : `}${fn}Caller${user.used ? ", caller)" : ""}, caller, raw), ${q(path)}) };`, "}");
137
140
  procedures.push(`[${q(name)}]: ${fn}`);
138
141
  declarations.push(`export { ${fn} as ${exportName("procedure", name)} };`);
139
142
  entries[`procedures/${encodeURIComponent(name)}.ts`] = entry("procedure", name, `Procedure${index}Input as Input`, `export type { Output } from "${base}contracts";\n`);
@@ -145,18 +148,31 @@ export function generate(input) {
145
148
  if (diagnostics.length)
146
149
  return { ok: false, diagnostics };
147
150
  const storeProcedures = hasHandlers ? "storeProcedures" : "procedures";
148
- const code = [...(Object.keys(plan.access?.userFields ?? {}).length ? [`export const userFields = ${q(plan.access.userFields)} as const;`] : []), ...declarations, `export const views = { ${views.join(", ")} };`, `export const procedures = { ${procedures.join(", ")} };`, ...(stores ? [
151
+ const code = [...(declarations.some(d => d.includes(" await _fresh(caller, ")) ? ['const _fresh = (caller: Caller, path: string) => "authenticatedAs" in caller && caller.authenticatedAs === "snapshot" ? import("@aotter/mantle/host-caller").then(m => m.checkFresh(caller, false, path)) : undefined;'] : []), ...(Object.keys(plan.access?.userFields ?? {}).length ? [`export const userFields = ${q(plan.access.userFields)} as const;`] : []), ...declarations, `export const views = { ${views.join(", ")} };`, `export const procedures = { ${procedures.join(", ")} };`, ...(stores ? [
149
152
  `export type Tables = ${stores.tables};`, `const RULES = ${stores.rules} as const;`,
150
153
  ...(hasHandlers ? [`const storeProcedures = { ${[...procedures, ...refProcedures].join(", ")} };`] : []),
151
154
  `const initStore = () => createStore<Database, Tables, typeof DRIVER, { views: typeof views; procedures: typeof ${storeProcedures} }${hasHandlers ? ", { handlers: Handlers }" : ""}>(${user.used ? "{...DIALECT.dialect, ...USER_VALUES}" : "DIALECT.dialect"}, DRIVER, RULES, ${stores.facts}, { views, procedures: ${storeProcedures} });`,
152
155
  `let makeStore: ReturnType<typeof initStore>;`,
153
156
  `export function store${hasHandlers ? "<const H extends Handlers>" : ""}(db: Database, caller: Caller${hasHandlers ? ", options: { handlers: H & Record<Exclude<keyof H, keyof Handlers>, never> }" : ", _options?: { handlers?: Handlers }"}): ReturnType<typeof makeStore> { return (makeStore ??= initStore())(db, caller${hasHandlers ? ", options" : ""}); }`,
154
157
  ] : [])].join("\n");
158
+ function unpool(text) {
159
+ const shared = [];
160
+ for (const [value, token] of pool) {
161
+ if (text.split(token).length > 2) {
162
+ const id = token.slice(1, -1);
163
+ shared.push(`const ${id} = ${value} as const;`);
164
+ text = text.replaceAll(token, id);
165
+ }
166
+ else
167
+ text = text.replace(token, () => value);
168
+ }
169
+ return [...shared, text];
170
+ }
155
171
  const dialect = `${base}dialect/${isSqlite ? "sqlite" : "postgres"}`;
156
172
  const contracts = [...(target.endsWith("postgres") ? ["type Postgres as Database", ...(views.length ? ["type Unsafe"] : [])] : []), "type Caller", ...(hasHandlers ? ["ContractError"] : []), ...(/^ deny\(/m.test(code) ? ["deny", ...(/\bisAdmin\(/.test(code) ? ["isAdmin"] : []), ...(/\bisUser\(/.test(code) ? ["isUser"] : [])] : []), ...(procedures.length ? ["type Output"] : [])];
157
173
  const dbType = isSqlite ? databaseImport(target) : "";
158
174
  return { ok: true, refProcedures: refNames, files: generatedFiles({ ...surfaces, ...entries, "generated.ts": ["// Generated native application SQL. No runtime plan or interpreter.", user.code, dbType, `import { ${contracts.join(", ")} } from "${base}contracts";`,
159
175
  ...(values.size || stores ? [`import * as DIALECT from "${dialect}";`] : []), `import * as DRIVER from "${base}driver/${driver}";`, ...((names => names.length ? [`import { ${names.join(", ")} } from "${base}store";`] : [])([...(stores ? ["createStore"] : []), ...(hasHandlers && target !== "cloudflare-d1" ? ["handlerTransaction"] : [])])),
160
176
  `import { ${procedures.length ? "binds, bound" : "binds"} } from "${base}binds";`,
161
- `const D = { ${[...values].sort().map(v => `${v}: DIALECT.${v}`).concat(user.values ? [user.values] : []).join(", ")} };`, code].filter(Boolean).join("\n") + "\n" }) };
177
+ `const D = { ${[...values].sort().map(v => `${v}: DIALECT.${v}`).concat(user.values ? [user.values] : []).join(", ")} };`, ...unpool(code)].filter(Boolean).join("\n") + "\n" }) };
162
178
  }
@@ -22,7 +22,7 @@ export function emitRest(plan, lowered, target, description) {
22
22
  for (const [name, view] of Object.entries(plan.views)) {
23
23
  if (view.surface !== "public")
24
24
  continue;
25
- const id = views.length, encoded = encodeURIComponent(name);
25
+ const id = views.length;
26
26
  const wire = outputConversions(plan, name, target);
27
27
  imports.push(...viewImports(name, id).map(line => wire.some(([, type]) => type === "int") ? line.replaceAll("./paged/views/", "./paged/wire/views/") : line));
28
28
  hasWire ||= wire.length > 0;
@@ -33,14 +33,12 @@ export function emitRest(plan, lowered, target, description) {
33
33
  if (trigger.source.kind !== "http")
34
34
  continue;
35
35
  const { method, path } = trigger.source, name = trigger.procedure, procedure = plan.procedures[name], id = procedures.length, ref = "ref" in procedure.handler;
36
- if (!ref)
37
- imports.push(`import procedure${id} from "./procedures/${encodeURIComponent(name)}.js";`);
38
36
  imports.push(`import { validate as validate${id} } from "./validation/procedures/${encodeURIComponent(name)}.js";`);
39
37
  const params = Object.fromEntries([...path.matchAll(/\{([^}]+)\}/g)].flatMap(([, key]) => {
40
38
  const type = [procedure.input?.properties?.[key]?.type].flat().find(t => t && t !== "null");
41
39
  return type === "integer" || type === "number" ? [[key, "number"]] : type === "boolean" ? [[key, "boolean"]] : [];
42
40
  }));
43
- procedures.push(`{ ${route(method, path)}, id: ${q(`rest:${method} ${template(path)}`)}, target: ${q({ kind: "procedure", name })}${description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? ", readOnly: true" : ""}${guard(procedure.requires, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), procedure.trustedOnly)}${Object.keys(params).length ? `, params: ${q(params)}` : ""}, validate: validate${id}${convert(conversions(procedure.input?.properties, lowered.procedures[name]?.caller?.flatMap(s => s.binds) ?? [], target))}, run: ${ref ? `(db, caller, input, handlers) => store(db, caller, { handlers }).procedures[${q(name)}](input)` : `procedure${id}`} }`);
41
+ procedures.push(`{ ${route(method, path)}, id: ${q(`rest:${method} ${template(path)}`)}, target: ${q({ kind: "procedure", name })}${description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? ", readOnly: true" : ""}${guard(procedure.requires, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), procedure.trustedOnly)}${Object.keys(params).length ? `, params: ${q(params)}` : ""}, validate: validate${id}${convert(conversions(procedure.input?.properties, lowered.procedures[name]?.caller?.flatMap(s => s.binds) ?? [], target))}, run: ${ref ? `(db, caller, input, handlers) => store(db, caller, { handlers }).procedures[${q(name)}](input)` : `async (db, caller, input) => (await import("./procedures/${encodeURIComponent(name)}.js")).default(db, caller, input)`} }`);
44
42
  }
45
43
  if (hasWire)
46
44
  imports.push('import { wireRows } from "@aotter/mantle/contracts";');
@@ -39,5 +39,5 @@ shell's allow-all `src/signup.ts`, unless sign-up should be invite-only. Purchas
39
39
 
40
40
  Every example uses two roles: `admin` (the admin layer) and `user`.
41
41
 
42
- [Reference service](https://github.com/aotter/mantle/tree/v0.2.1-alpha.1/docs/examples/reference-service) is the release smoke test for packed packages, not a starting point. Its source lives in the tagged repository; it is not included in the npm docs.
43
- Projects on 0.1.x move with [the upgrade guide](../upgrade-0.1-to-0.2.md).
42
+ [Reference service](https://github.com/aotter/mantle/tree/v0.2.1-alpha.3/docs/examples/reference-service) is the release smoke test for packed packages, not a starting point. Its source lives in the tagged repository; it is not included in the npm docs.
43
+ Projects on 0.1.x move with [the upgrade guide](../handbook/releases/upgrade-0.1-to-0.2.md).
@@ -112,7 +112,7 @@ its layered report ("Report done-ness in layers"): smoke and `pnpm test` are lay
112
112
 
113
113
  ### Independent acceptance
114
114
 
115
- `pnpm accept` (optionally `pnpm accept --fixtures fixtures.mjs --user member@example.test`; with npm, `npm run accept -- --fixtures …`) starts the app on a fresh local D1,
115
+ `pnpm accept` (optionally `pnpm accept --fixtures fixtures.mjs --user member@example.test`, and `--open` to open the signed-in admin's Developer UI in your browser; with npm, `npm run accept -- --fixtures …`) starts the app on a fresh local D1,
116
116
  signs the admin in, runs your fixtures module, signs the user in and prints one local URL per role:
117
117
 
118
118
  ```text
@@ -1,5 +1,5 @@
1
1
  // Independent acceptance setup (mantle help agent-workflow-acceptance): hands each role to a fresh evaluator without a credential.
2
- // node accept.mjs [--fixtures fixtures.mjs] [--user member@example.test]
2
+ // node accept.mjs [--fixtures fixtures.mjs] [--user member@example.test] [--open]
3
3
  // Starts the app on a fresh local D1 (schema.sql, then seed.sql), signs the admin in with email OTP, runs the optional
4
4
  // fixtures module (default export async ({ call, mcp, signIn, sql, sessions, user }) => {...}: realistic records through
5
5
  // the app's own REST/MCP as real users, e.g. other members' orders and more than one page), signs the user in, then
@@ -7,8 +7,11 @@
7
7
  // (a visitor gets the app's own origin):
8
8
  // MCP at <proxy>/mcp (user) or <proxy>/mcp/admin (admin), Admin at <proxy>/admin, pages and REST at <proxy>/.
9
9
  // The proxy refuses /api/auth/* so the evaluator cannot sign out, sign in or mint credentials. Ctrl-C stops everything.
10
+ // --open also opens the signed-in admin's Developer UI (<admin proxy>/admin/dev) in the default browser, so a person
11
+ // sees the app's wiring without a one-time code; the app keeps rebuilding on every change under src/ and generated/.
10
12
  // node accept.mjs mcp <endpoint> [tool [json-arguments]]
11
13
  // A minimal MCP client for an evaluator without one: lists the tools, or calls one and prints the raw result.
14
+ import { spawn } from "node:child_process";
12
15
  import { createServer } from "node:http";
13
16
  import { Readable } from "node:stream";
14
17
  import { parseArgs } from "node:util";
@@ -19,7 +22,7 @@ if (process.argv[2] === "mcp") await client(process.argv[3], process.argv[4], pr
19
22
  else await setup();
20
23
 
21
24
  async function setup() {
22
- const { values } = parseArgs({ args: process.argv.slice(2).filter(arg => arg !== "--"), options: { fixtures: { type: "string" }, user: { type: "string", default: "member@example.test" } } });
25
+ const { values } = parseArgs({ args: process.argv.slice(2).filter(arg => arg !== "--"), options: { fixtures: { type: "string" }, user: { type: "string", default: "member@example.test" }, open: { type: "boolean" } } });
23
26
  const { startApp } = await import("./smoke-kit.mjs");
24
27
  const app = await startApp();
25
28
  const stop = async () => { await app.stop(); process.exit(0); };
@@ -33,6 +36,7 @@ async function setup() {
33
36
  const lines = [`visitor (anonymous): ${app.origin}`];
34
37
  for (const [role, cookie] of Object.entries(sessions)) {
35
38
  const url = await proxy(app.origin, cookie);
39
+ if (role === "admin" && values.open) browse(`${url}/admin/dev`);
36
40
  lines.push(`${role} (${role === "admin" ? app.owner : values.user}): ${url} MCP ${url}/mcp${role === "admin" ? "/admin" : ""}${role === "admin" ? ` Admin ${url}/admin` : ""}`);
37
41
  }
38
42
  console.log(lines.join("\n") + "\nReady. Ctrl-C stops the app and deletes its data.");
@@ -43,6 +47,12 @@ async function setup() {
43
47
  }
44
48
  }
45
49
 
50
+ /** Opens a URL in the default browser; without one (CI, a container) the printed URL is all there is. */
51
+ function browse(url) {
52
+ const [command, ...args] = process.platform === "darwin" ? ["open"] : process.platform === "win32" ? ["cmd", "/c", "start", ""] : ["xdg-open"];
53
+ spawn(command, [...args, url], { stdio: "ignore", detached: true }).on("error", () => console.log(`Open ${url} in a browser.`)).unref();
54
+ }
55
+
46
56
  /** A local origin that forwards to the app with one role's session; sliding Set-Cookie updates stay inside the proxy. */
47
57
  function proxy(origin, cookie) {
48
58
  const jar = new Map(cookie.split("; ").map(pair => [pair.slice(0, pair.indexOf("=")), pair]));
@@ -15,8 +15,8 @@
15
15
  "accept": "node accept.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@aotter/mantle": "0.2.1-alpha.1",
19
- "@aotter/mantle-ui": "0.2.1-alpha.1",
18
+ "@aotter/mantle": "0.2.1-alpha.3",
19
+ "@aotter/mantle-ui": "0.2.1-alpha.3",
20
20
  "@better-auth/cimd": "1.7.7",
21
21
  "@better-auth/core": "1.7.7",
22
22
  "@better-auth/mcp": "1.7.7",
@@ -25,7 +25,7 @@
25
25
  "@modelcontextprotocol/ext-apps": "2.0.0",
26
26
  "@modelcontextprotocol/server": "2.3.1",
27
27
  "better-auth": "1.7.7",
28
- "@aotter/mantle-admin": "0.2.1-alpha.1",
28
+ "@aotter/mantle-admin": "0.2.1-alpha.3",
29
29
  "react": "19.3.0",
30
30
  "react-dom": "19.3.0"
31
31
  },
@@ -91,7 +91,7 @@ Invalid presented bearer credentials fail rather than becoming anonymous. Fresh
91
91
 
92
92
  ## Build: keep Better Auth out of startup
93
93
 
94
- Use the [Workers build instructions](../../../README.md#workers-build).
94
+ Generation configures `mantle bundle` as the Workers build ([outputs](../start/project-and-cli.md#outputs)).
95
95
  The metafile startup guard rejects static Better Auth imports; keep auth behind
96
96
  `authLoader(() => import(...))`.
97
97
 
@@ -5,6 +5,22 @@ description: Install schema explicitly, migrate before deploying, and operate a
5
5
 
6
6
  Run the same generation command with `--check`, typecheck the application and test its selected routes before deploying. Keep generated TypeScript, SQL and Wrangler edits committed; keep bundle `dist/` untracked. The app owns the database, auth secrets, trusted origin and assets.
7
7
 
8
+ Deploy only when the user asks; creating a project locally is not authorization. Confirm the exact account and reuse the
9
+ resources it already has before creating any. For ChatGPT Sites, follow [Mantle on ChatGPT Sites](chatgpt-sites.md).
10
+
11
+ ## Production checklist
12
+
13
+ 1. Migrations are reviewed, rehearsed on a copy and applied before traffic reaches the new code
14
+ ([install and change schemas](#install-and-change-schemas)).
15
+ 2. Secrets live in the host's secret store, never in source, chat or logs: the auth secret, the trusted HTTPS
16
+ `PUBLIC_ORIGIN`, the `ADMIN_EMAIL` bootstrap rule and the production email sender ([authentication](authentication.md)).
17
+ 3. Admin assets are served Worker-first ([Workers assets and secrets](service-entry.md#workers-assets-and-secrets)) and
18
+ both [public-fetch flags](#public-fetch-routing) stay in every environment.
19
+ 4. After deploying, verify on the deployed host: admin sign-in gets role `admin`, a member is denied Admin, public and
20
+ protected reads, a persisted write, a stale OCC write refused, and MCP `initialize` and `tools/list` with a real OAuth
21
+ bearer.
22
+ 5. Keep the previous build and the migration rollback until the release is verified.
23
+
8
24
  ## Public fetch routing
9
25
 
10
26
  Workers use `compatibility_flags: ["nodejs_compat", "global_fetch_strictly_public"]`.
@@ -28,13 +44,7 @@ checks do not verify production public-hostname routing.
28
44
 
29
45
  ## Install and change schemas
30
46
 
31
- The runtime never creates, alters or checks tables. Boot and requests run no DDL, introspection or schema checks.
32
-
33
- For the first install, apply generated `schema.sql`, including selected auth/site DDL, to an empty database. Apply selected seed SQL explicitly.
34
-
35
- For every later schema change, the application's coding agent diffs the regenerated `schema.sql`, writes a migration with the host's native tool (`wrangler d1 migrations` or the app's PostgreSQL migration tool), rehearses it on a copy, and applies it before deploying the new code.
36
-
37
- Destructive changes and type changes need explicit data migration. Mantle drops nothing. Initial `schema.sql` is not an upgrade script.
47
+ The rules are in [schema installation and migrations](../concepts/runtime-and-adapters.md#schema-installation-and-migrations): the runtime runs no DDL, the initial `schema.sql` is only for an empty database, and every later change is a reviewed migration rehearsed on a copy and applied before the new code deploys.
38
48
 
39
49
  For D1, create a migration with `pnpm exec wrangler d1 migrations create <database> <name>`, review its SQL, then rehearse `pnpm exec wrangler d1 migrations apply <database> --local` against copied data. Apply the reviewed migrations to the target before `wrangler deploy`. Initial DDL may be placed in the first migration for a new database.
40
50
 
@@ -79,4 +89,6 @@ Auth configuration and production email delivery are app-owned; follow [authenti
79
89
 
80
90
  Scheduling is app-owned. Wire generated system-only TTL sweeps into `scheduled()` and configure Wrangler crons explicitly. Bound each invocation and retain a cursor if work remains; see [TTL maintenance](../guides/native-execution.md#ttl-maintenance). Expired rows remain stored and hold unique keys until removed.
81
91
 
82
- After deployment, verify public and protected reads, member/admin sign-in, a persisted write, stale OCC and selected MCP/Admin/web routes. An `OUTCOME_UNKNOWN` write may have committed; reconcile before deciding what to do next and never retry automatically.
92
+ An `OUTCOME_UNKNOWN` write may have committed; reconcile before deciding what to do next and never retry automatically. For MCP Apps, verify a newly invoked render tool on the deployed host and keep old
93
+ conversations working with the [retained-page recipe](../guides/mcp-apps.md#keep-old-conversations-working-after-an-upgrade).
94
+ Deploying an application never publishes or bumps the SDK.