@aotter/mantle 0.2.1-alpha.1 → 0.2.1-alpha.2
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/dist/build/compiler.d.ts +4 -4
- package/dist/build/compiler.js +2 -2
- 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/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/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/package.json +3 -3
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +7 -7
- package/docs/handbook/guides/agent-workflow-acceptance.md +1 -1
- package/docs/handbook/guides/mcp-apps.md +40 -0
- package/docs/handbook/reference/describe.md +16 -6
- package/docs/handbook/reference/surface.md +10 -0
- package/docs/handbook/releases/index.md +31 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/update/SKILL.md +1 -1
- package/package.json +2 -3
- package/skills/README.md +1 -1
- package/dist/domain/describe/target-hash.js +0 -1
|
@@ -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};
|
|
@@ -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 = {};
|
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.
|
|
42
|
+
[Reference service](https://github.com/aotter/mantle/tree/v0.2.1-alpha.2/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
43
|
Projects on 0.1.x move with [the upgrade guide](../upgrade-0.1-to-0.2.md).
|
|
@@ -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.2",
|
|
19
|
+
"@aotter/mantle-ui": "0.2.1-alpha.2",
|
|
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.2",
|
|
29
29
|
"react": "19.3.0",
|
|
30
30
|
"react-dom": "19.3.0"
|
|
31
31
|
},
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
Use the [Worker quickstart](../start/quickstart-worker.md) for a complete native manifest and entry, [typed queries](../guides/typed-queries.md) for embedding, and [service composition](../cloudflare/service-entry.md) for optional surfaces.
|
|
4
4
|
|
|
5
|
-
The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.1-alpha.
|
|
5
|
+
The runnable [reference service](https://github.com/aotter/mantle/tree/v0.2.1-alpha.2/docs/examples/reference-service) exercises the native package integration. See the [generated public website](../../examples/generated-web/README.md) for a native Worker example.
|
|
@@ -13,20 +13,20 @@ carries.
|
|
|
13
13
|
The Mantle plugin carries one OSS authoring skill, `mantle`, and local
|
|
14
14
|
acceptance references. It does not configure a hosted-service connection.
|
|
15
15
|
Install from the Git tag matching the exact SDK version you selected (currently
|
|
16
|
-
`v0.2.1-alpha.
|
|
16
|
+
`v0.2.1-alpha.2`). Pin the tag to keep instructions aligned with the application's
|
|
17
17
|
SDK. A skill install does not upgrade the application's SDK.
|
|
18
18
|
|
|
19
19
|
For Codex, pin the marketplace checkout:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
codex plugin marketplace add aotter/mantle --ref v0.2.1-alpha.
|
|
22
|
+
codex plugin marketplace add aotter/mantle --ref v0.2.1-alpha.2
|
|
23
23
|
codex plugin add mantle@mantle
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
For Claude Code, first clone the release into a directory you will keep:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
git clone --depth 1 --branch v0.2.1-alpha.
|
|
29
|
+
git clone --depth 1 --branch v0.2.1-alpha.2 https://github.com/aotter/mantle.git mantle-plugin
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Then, in Claude Code, use that checkout's absolute path:
|
|
@@ -44,7 +44,7 @@ npx skills add aotter/mantle@mantle
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Without a version it follows `main`, the latest stable release. To match the
|
|
47
|
-
application's SDK exactly, add its release tag: `npx skills add aotter/mantle#v0.2.1-alpha.
|
|
47
|
+
application's SDK exactly, add its release tag: `npx skills add aotter/mantle#v0.2.1-alpha.2@mantle`.
|
|
48
48
|
|
|
49
49
|
When upgrading the SDK, update the marketplace ref or local tagged checkout and
|
|
50
50
|
refresh/reinstall the plugin using the host's native workflow. Verify the loaded
|
|
@@ -57,14 +57,14 @@ project and copies no handbook.
|
|
|
57
57
|
## 2. Install the SDK, then read its own docs
|
|
58
58
|
|
|
59
59
|
Use the version the project already pins. For a new project, install this
|
|
60
|
-
handbook's version exactly. 0.2.1-alpha.
|
|
60
|
+
handbook's version exactly. 0.2.1-alpha.2 is a prerelease; stable 0.2.0 remains the
|
|
61
61
|
production line, and its own tagged handbook describes it.
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
pnpm add --save-exact @aotter/mantle@0.2.1-alpha.
|
|
64
|
+
pnpm add --save-exact @aotter/mantle@0.2.1-alpha.2
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
Add optional `@aotter/mantle-ui@0.2.1-alpha.
|
|
67
|
+
Add optional `@aotter/mantle-ui@0.2.1-alpha.2` and `@aotter/mantle-admin@0.2.1-alpha.2` only
|
|
68
68
|
when the selected surfaces need them. Every selected `@aotter/mantle*` package
|
|
69
69
|
stays at the same exact version. Read, from the application:
|
|
70
70
|
|
|
@@ -317,7 +317,7 @@ Use ordinary Markdown; no new receipt schema or framework is required:
|
|
|
317
317
|
- Findings ledger with P0–P3, owner, disposition, minimal repair and
|
|
318
318
|
regression commands/results.
|
|
319
319
|
- Coverage of each rubric item (evaluator-observed in this round,
|
|
320
|
-
evaluator-observed and reused from round
|
|
320
|
+
evaluator-observed and reused from round `<n>` at `<revision>` with the reason,
|
|
321
321
|
author regression only, or UNVERIFIED); the final CONVERGED / BLOCKED / UNVERIFIED verdict and rounds
|
|
322
322
|
used, which is layer 3 of the [layered report](#report-done-ness-in-layers).
|
|
323
323
|
Retain every failed report.
|
|
@@ -33,6 +33,46 @@ passed through as the resource's `_meta.ui`. `domain` is the dedicated sandbox
|
|
|
33
33
|
origin for hosts that serve each App from its own origin; use the form that
|
|
34
34
|
host documents.
|
|
35
35
|
|
|
36
|
+
### Tool metadata and result `_meta`
|
|
37
|
+
|
|
38
|
+
A hand-built kernel `Tool` may set `meta`, merged into its `_meta` in
|
|
39
|
+
`tools/list` with what App links produce: the top level shallowly, `ui` one key
|
|
40
|
+
deep. Use it for keys such as `ui.visibility`, `ui.resourceUri` with
|
|
41
|
+
`openai/outputTemplate`, or `openai/fileParams`. A value that contradicts an App
|
|
42
|
+
link (another `ui.resourceUri` than the `renders` resource, or a visibility other
|
|
43
|
+
than `["app"]` on an `appOnly` tool) fails when `mcp()` is constructed.
|
|
44
|
+
`meta: { ui: { visibility: ["app"] } }` hides any tool, a Procedure included,
|
|
45
|
+
from clients that declare no MCP Apps support, as `appOnly` does for Views. It
|
|
46
|
+
is a listing hint only: `tools/call` still runs every identity, scope, `check`
|
|
47
|
+
and `authorize` decision, so an App-only write is authorized like any other.
|
|
48
|
+
|
|
49
|
+
A `run` may return `withResultMeta(data, meta)` from `@aotter/mantle/mcp`. The
|
|
50
|
+
result's text and `structuredContent` are built from `data` exactly as before;
|
|
51
|
+
`meta` goes only into the result `_meta`, beside `net.aotter.mantle/tool`, which
|
|
52
|
+
it cannot replace. Hosts give result `_meta` to the App, not the model, so a
|
|
53
|
+
signed upload URL belongs there:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { withResultMeta, type Tool } from '@aotter/mantle/mcp';
|
|
57
|
+
|
|
58
|
+
declare function signUpload(caller: unknown): Promise<{ id: string; url: string; headers: Record<string, string> }>;
|
|
59
|
+
|
|
60
|
+
const uploadTarget: Tool<unknown> = {
|
|
61
|
+
name: 'upload_target', surface: 'public', description: 'Prepare an upload.', closed: true,
|
|
62
|
+
meta: { ui: { visibility: ['app'] } },
|
|
63
|
+
inputSchema: { type: 'object', additionalProperties: false }, validate: raw => raw as Record<string, unknown>,
|
|
64
|
+
run: async (_db, caller) => {
|
|
65
|
+
const upload = await signUpload(caller);
|
|
66
|
+
return withResultMeta({ id: upload.id }, { uploadUrl: upload.url, requiredHeaders: upload.headers });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`callTool` and `callToolLazy` return the wrapper itself, `{ data, _meta }`.
|
|
72
|
+
A caller that forwards a result to a model, such as an application's own agent,
|
|
73
|
+
must forward only `data` and drop `_meta`, or the signed URL leaks through that
|
|
74
|
+
path. A thrown error still produces the standard diagnostics result.
|
|
75
|
+
|
|
36
76
|
## Package and application ownership
|
|
37
77
|
|
|
38
78
|
Core (`@aotter/mantle`) owns tools, resources, endpoint links and server authorization,
|
|
@@ -5,10 +5,19 @@ An explicit target is required; use the generation target because hashes
|
|
|
5
5
|
include the dialect. The command writes no project files. `--diff <old.json>` prints a JSON
|
|
6
6
|
array of changes against a saved report.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
This command's output is the supported way to get a description outside the
|
|
9
|
+
CLI. Run it in Node.js or Bun, for example in a deploy step, from the
|
|
10
|
+
committed manifests, and give the saved JSON to whatever needs it. A Worker
|
|
11
|
+
cannot compile manifests that contain SQL, because the SQL parser's WASM needs
|
|
12
|
+
the whole isolate memory (ADR-0034); do not import compiler files from `dist`.
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
mantle describe cloudflare-d1 manifests/*.yaml > describe.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@aotter/mantle/describe` exports `matchTarget` and the `AppDescription`,
|
|
19
|
+
`DescribeTarget` and `TargetRequest` types; `AppDescription` is the contract.
|
|
20
|
+
It loads no compiler, SQL parser or Node API.
|
|
12
21
|
|
|
13
22
|
The schemaVersion 1 report contains:
|
|
14
23
|
|
|
@@ -55,8 +64,9 @@ imports no application, opens no database and makes no network call, so a
|
|
|
55
64
|
gateway can compute `props.mantle.target` and `targetHash` before forwarding.
|
|
56
65
|
|
|
57
66
|
```ts
|
|
58
|
-
import { matchTarget } from "@aotter/mantle/describe";
|
|
59
|
-
|
|
67
|
+
import { matchTarget, type AppDescription } from "@aotter/mantle/describe";
|
|
68
|
+
import saved from "./describe.json";
|
|
69
|
+
const describe = saved as AppDescription, url = new URL(request.url);
|
|
60
70
|
matchTarget(describe, { surface: "rest", method: request.method, path: url.pathname }); // rest:GET /api/views/orders
|
|
61
71
|
matchTarget(describe, { surface: "admin", method: request.method, path: url.pathname }); // admin:GET /entries/:id
|
|
62
72
|
matchTarget(describe, { surface: "mcp", tool: params.name, path: url.pathname }); // view:orders, admin:tool:<name>
|
|
@@ -215,6 +215,16 @@ by declared identity, scope and `check` requirements. Without `authorize`, the
|
|
|
215
215
|
complete endpoint catalogue is not a permission-filtered catalogue. Operation
|
|
216
216
|
validation, guards and credential checks still apply to calls.
|
|
217
217
|
|
|
218
|
+
`toolsPageSize` pages `tools/list`: each page holds at most that many tools and
|
|
219
|
+
carries an opaque `nextCursor` while more remain; pass it back as `cursor`. A
|
|
220
|
+
cursor is valid only for the catalogue version that issued it; an unknown or
|
|
221
|
+
stale cursor is answered with JSON-RPC error `-32602`, and the client lists
|
|
222
|
+
again from the first page. Without the option, one page lists every tool and
|
|
223
|
+
carries no `nextCursor`.
|
|
224
|
+
|
|
225
|
+
A hand-built tool's `meta` and a `run` returning `withResultMeta(data, meta)`
|
|
226
|
+
add listed and result `_meta`; see [MCP Apps](../guides/mcp-apps.md#tool-metadata-and-result-_meta).
|
|
227
|
+
|
|
218
228
|
The app resolves bearer tokens with `resolveCaller(request, auth, { audience:
|
|
219
229
|
new URL('/mcp', request.url).href })` (both surfaces use `<origin>/mcp`). The app
|
|
220
230
|
must also mount Better Auth's `/.well-known/oauth-protected-resource/mcp`
|
|
@@ -33,6 +33,37 @@ and interactive transactions can use `store.batch` for atomic multi-writes, and
|
|
|
33
33
|
`mantle bundle` under `wrangler dev` reports stale generated output in one line
|
|
34
34
|
instead of a failed build.
|
|
35
35
|
|
|
36
|
+
### Upgrading to 0.2.1-alpha.2
|
|
37
|
+
|
|
38
|
+
Pin the selected packages to `0.2.1-alpha.2` and regenerate. Breaking change:
|
|
39
|
+
|
|
40
|
+
- **`@aotter/mantle/describe`** keeps only `matchTarget` and the
|
|
41
|
+
`AppDescription`, `DescribeTarget` and `TargetRequest` types. `describeApp`,
|
|
42
|
+
`diffApp` and the `@aotter/mantle/target-hash` subpath are removed.
|
|
43
|
+
**`mantle describe` output is the supported way to get a description outside
|
|
44
|
+
the CLI:** run `mantle describe <target> manifests/*.yaml > describe.json` in
|
|
45
|
+
Node.js or Bun (for example in a deploy step) and give the JSON to
|
|
46
|
+
`matchTarget`. Do not import compiler files from `dist`; a Worker cannot
|
|
47
|
+
compile manifests that contain SQL ([describe](../reference/describe.md)).
|
|
48
|
+
Use `mantle describe --diff` instead of `diffApp`, and `_meta.targetHash`
|
|
49
|
+
instead of `targetHash()`.
|
|
50
|
+
|
|
51
|
+
Hand-built MCP tools gain three opt-in capabilities; without them the output
|
|
52
|
+
is unchanged ([MCP Apps](../guides/mcp-apps.md), [surface](../reference/surface.md)):
|
|
53
|
+
|
|
54
|
+
- `Tool.meta` merges into the tool's listed `_meta`. `ui.visibility: ["app"]`
|
|
55
|
+
can now hide a Procedure from clients without MCP Apps; it is a listing hint,
|
|
56
|
+
and calls are authorized as before.
|
|
57
|
+
- `withResultMeta(data, meta)` returns `meta` only in the result `_meta`, so a
|
|
58
|
+
value such as a signed upload URL stays out of `content`. `callTool` returns
|
|
59
|
+
`{ data, _meta }`; drop `_meta` before forwarding a result to a model.
|
|
60
|
+
- `toolsPageSize` pages `tools/list` with an opaque `nextCursor`.
|
|
61
|
+
|
|
62
|
+
Also: generation reports a SQL parser that cannot load in the current runtime
|
|
63
|
+
as such, instead of `SQL_SYNTAX`, and the handbook renders under Vue-based
|
|
64
|
+
site generators (bare `<placeholder>` text is now quoted, and `pnpm check`
|
|
65
|
+
fails on any that return).
|
|
66
|
+
|
|
36
67
|
### Upgrading to 0.2.1-alpha.1
|
|
37
68
|
|
|
38
69
|
Pin `@aotter/mantle`, and any selected `@aotter/mantle-ui` and
|
|
@@ -3,7 +3,7 @@ description: Generate a small native D1 service and mount an application-owned W
|
|
|
3
3
|
---
|
|
4
4
|
# Quickstart: a service on Workers
|
|
5
5
|
|
|
6
|
-
Create an application outside the SDK checkout. Install `@aotter/mantle@0.2.1-alpha.
|
|
6
|
+
Create an application outside the SDK checkout. Install `@aotter/mantle@0.2.1-alpha.2` exactly (a prerelease; stable 0.2.0 has its own tagged handbook), TypeScript, Wrangler and Cloudflare Worker types; `node_modules/@aotter/mantle/docs/examples/app-shell/package.json` lists the versions this release is tested with. Read that installed version's handbook; this page describes the native 0.2 contract.
|
|
7
7
|
|
|
8
8
|
Save `manifests/tickets.yaml`:
|
|
9
9
|
|
|
@@ -24,7 +24,7 @@ on a copy, never boot 0.2 against a 0.1.x database, and keep exports out of the
|
|
|
24
24
|
repository. The guide's appendix is historical; do not follow it. Between 0.2
|
|
25
25
|
versions, apply reviewed migrations rehearsed on a copy.
|
|
26
26
|
|
|
27
|
-
For the stable 0.2 line, pin every selected Mantle package to `0.2.0`; to try the 0.2.1 prerelease, pin all of them to `0.2.1-alpha.
|
|
27
|
+
For the stable 0.2 line, pin every selected Mantle package to `0.2.0`; to try the 0.2.1 prerelease, pin all of them to `0.2.1-alpha.2` and read its release notes. Do not upgrade an existing application by following a moving dist-tag.
|
|
28
28
|
|
|
29
29
|
Pin exact target SDK/peer versions and read its release notes and Admin build
|
|
30
30
|
instructions. Core-to-native replaces app imports/wiring; it is not
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aotter/mantle",
|
|
3
|
-
"version": "0.2.1-alpha.
|
|
3
|
+
"version": "0.2.1-alpha.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"engines": {
|
|
@@ -68,8 +68,7 @@
|
|
|
68
68
|
"./json-body": "./dist/kernel/json-body.js",
|
|
69
69
|
"./generate/auth": "./dist/infrastructure/generateAuth.js",
|
|
70
70
|
"./package.json": "./package.json",
|
|
71
|
-
"./describe": "./dist/domain/describe/
|
|
72
|
-
"./target-hash": "./dist/domain/describe/target-hash.js"
|
|
71
|
+
"./describe": "./dist/domain/describe/public.js"
|
|
73
72
|
},
|
|
74
73
|
"peerDependencies": {
|
|
75
74
|
"@modelcontextprotocol/server": "2.3.1",
|
package/skills/README.md
CHANGED
|
@@ -32,7 +32,7 @@ npx skills add aotter/mantle@mantle
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Without a version it follows `main`, the latest stable release; pin the SDK's
|
|
35
|
-
release tag with `npx skills add aotter/mantle#v0.2.1-alpha.
|
|
35
|
+
release tag with `npx skills add aotter/mantle#v0.2.1-alpha.2@mantle`.
|
|
36
36
|
The explicit skill name (`@mantle`) excludes repository maintainer skills. The manifests
|
|
37
37
|
are generated by `scripts/sync-plugin-manifests.mjs` and checked with `--check`.
|
|
38
38
|
When upgrading, update the marketplace ref or local tagged checkout and refresh
|