@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.
- package/README.md +16 -177
- package/dist/build/compiler.d.ts +4 -4
- package/dist/build/compiler.js +3 -3
- package/dist/domain/describe/{chunk-P6PP6VVV.js → chunk-CW7X5Q5T.js} +1 -1
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/public.d.ts +3 -0
- package/dist/domain/describe/public.js +1 -0
- package/dist/infrastructure/help.js +1 -3
- package/dist/kernel/mcp-apps.d.ts +3 -2
- package/dist/kernel/mcp-apps.js +7 -2
- package/dist/kernel/mcp-handler.js +20 -8
- package/dist/kernel/mcp.d.ts +18 -0
- package/dist/kernel/mcp.js +1 -0
- package/dist/usecase/adminDescriptors.js +1 -1
- package/dist/usecase/generate.js +20 -4
- package/dist/usecase/rest.js +2 -4
- package/docs/examples/README.md +2 -2
- package/docs/examples/app-shell/README.md +1 -1
- package/docs/examples/app-shell/accept.mjs +12 -2
- package/docs/examples/app-shell/package.json +3 -3
- package/docs/handbook/cloudflare/authentication.md +1 -1
- package/docs/handbook/cloudflare/deploy-and-operate.md +20 -8
- package/docs/handbook/cloudflare/media-r2.md +31 -0
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +25 -67
- package/docs/handbook/guides/agent-workflow-acceptance.md +32 -5
- package/docs/handbook/guides/mcp-apps.md +43 -1
- package/docs/{skills/performance/SKILL.md → handbook/guides/performance.md} +53 -32
- package/docs/handbook/guides/size-and-startup.md +2 -2
- package/docs/handbook/guides/typed-queries.md +12 -0
- package/docs/handbook/navigation.json +4 -0
- package/docs/handbook/reference/describe.md +16 -6
- package/docs/handbook/reference/surface.md +10 -0
- package/docs/handbook/releases/index.md +85 -2
- package/docs/{upgrade-0.1-to-0.2.md → handbook/releases/upgrade-0.1-to-0.2.md} +13 -13
- package/docs/handbook/start/agents.md +25 -4
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/package.json +2 -3
- package/skills/README.md +7 -69
- package/skills/mantle/SKILL.md +15 -81
- package/dist/domain/describe/target-hash.js +0 -1
- package/docs/skills/develop/SKILL.md +0 -100
- package/docs/skills/integrate/SKILL.md +0 -51
- package/docs/skills/media-gc/SKILL.md +0 -109
- package/docs/skills/provision/SKILL.md +0 -44
- 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{
|
|
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 @@
|
|
|
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,
|
|
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.
|
|
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?:
|
|
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>;
|
package/dist/kernel/mcp-apps.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
224
|
-
if (
|
|
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 =
|
|
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,
|
|
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);
|
package/dist/kernel/mcp.d.ts
CHANGED
|
@@ -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. */
|
package/dist/kernel/mcp.js
CHANGED
|
@@ -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"] ?? {})
|
|
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)
|
package/dist/usecase/generate.js
CHANGED
|
@@ -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") ? [`
|
|
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
|
}
|
package/dist/usecase/rest.js
CHANGED
|
@@ -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
|
|
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)` : `
|
|
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";');
|
package/docs/examples/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
19
|
-
"@aotter/mantle-ui": "0.2.1-alpha.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|