# Object marks for server authors

The two tool marks an MCP server sends so Straza can limit a role to some of the server's objects, the listing tool's answer, the listing credential, what Straza sends on each call and what the server promises.


ReadthispagewhenyouwriteanMCPserverthatreachesmanyseparateobjectswithonecredential,suchaseveryrepositoryofanorganization,anditsoperatorswanttolimitaroletosomeofthem.Withoutmarks,Strazacanonlygivearoleatool,soarolethatmaycallyourwritetoolmaycallitoneveryobjectyourcredentialreaches.Withmarks,yourtoollistsayswhichargumentnamestheobjectandwhichtoolliststheobjects.Strazathencheckseachcallagainsttheobjectsthecaller'srolescoverbeforeyourserverseesit.Whentherolethatallowsacallcoversonlysomeobjects,Strazaalsotellsyourserverwhichobjectitchecked.YouneednoStrazalibrary:two`_meta`keysinyour`tools/list`answer,onelistingtool,andthechecksattheendofthispage.Themarkschangenothingbythemselves.Anoperatorreadsthemwith`strazactlappsobjects`andpinstheminyourserver'smanifestunder`straza.objects`,throughadraftthatapersonpublishes.FromthenonStrazadecideswiththatpinnedcopyandlistsyourobjectsinthebackground.Eachroleonyourserverthensayswhichobjectsitcovers:`covers:every`,or`covers:{objects:[...]}`withvaluesyourlistingreturned.[Addaserver](https://docs.straza.ai/guides/serve-mcp-apps/add-a-server/index.md#objects)istheoperator'ssideofthesamesteps.Theexamplesonthispagecomefromghapp,aGitHubAppserverthatreachestherepositoriesofanorganization,withtheobjecttype`repo`.Theenterprisedemostackrunsasmallerserverofthesameshape,[objects-demo](https://github.com/strazahq/straza/tree/main/test/objectsdemo),writteninGo.##Thetwomarks{#marks}Putthemarksineachtool's`_meta`,underStraza'sownprefix`ai.straza/`,astheMCPspecificationasksofextensions.Thisexample,trimmedfromghapp's`tools/list`answer,marksatoolthatworksononerepositoryandthelistingtool,whichcarriesbothmarks:```json{"tools":[{"name":"pr_create","inputSchema":{"type":"object","properties":{"repo":{"type":"string"},"title":{"type":"string"},"head":{"type":"string"},"base":{"type":"string"}},"required":["repo","title","head","base"]},"_meta":{"ai.straza/object":{"type":"repo","argument":"repo"}}},{"name":"repo_list","annotations":{"readOnlyHint":true},"inputSchema":{"type":"object","properties":{"cursor":{"type":"string"}}},"_meta":{"ai.straza/object":{"type":"repo","reach":"all"},"ai.straza/lists-objects":{"type":"repo","attributes":[{"name":"team","setBy":"org"}]}}}]}```Everyotherghapptool,itssearchesincluded,namestherepositoryinarequired`repo`argumentandcarriesthesamemarkas`pr_create`.`ai.straza/object`sayshowonetoolreachesobjects:|Field|Required|Value||---|---|---||`type`|yes|Theobjecttype,onelowercasewordofupto32letters,digitsorhyphensthatstartswithaletter,suchas`repo`.||`argument`|oneof`argument`and`reach`|Thenameoftheargumentthatcarriestheobject,foratoolwhosecallnamesoneobject.||`reach`|oneof`argument`and`reach`|`all`foratoolthatcanreacheveryobjectatonce,suchasasearchacrossthem.`none`foratoolthattouchesnoobject,suchasonethatsayswhothecalleris.`credential`foratoolwhosecalldoesnotnametheobject,sothecredentialthecallcarrieslimitswhatitreaches.|`ai.straza/lists-objects`goesontheonetoolthatliststheobjects:|Field|Required|Value||---|---|---||`type`|yes|Thesametypeaseveryothermarkoftheserver.||`attributes`|no|Upto16attributesyourlistingreturnsforeachobject,eachwitha`name`anda`setBy`.`setBy`is`org`whenonlytheorganization'sadminscanchangethevalueinyoursystem,and`object`whentheobject'sownadminscanchangeittoo.Amissing`setBy`readsas`object`.|Onceanoperatorpinsyourmarks,thereachofatooldecideswhichrolesmaycallit.Aserverthatworksononeobjectpercallneedsonly`argument`.Theotherreachesarefortoolsthatdonotnameoneobject:asearchacrosseveryobjectreaches`all`,atoolthatsayswhothecalleristouches`none`,andatoolwhosecallnevernamesitsobject,sothatthecredentialthecallcarriesistheonlylimit,reaches`credential`.|Reach|Whomaycallthetool||---|---||`argument`|Arolewhosecoverslinecoverstheobjecttheargumentnames.Strazalooksthevalueupbyteforbyteinitslist,soarolethatnamesobjectsonebyoneisrefusedavaluethelistdoesnothold.||`none`|Anyrolethatmaycallthetool,whateverobjectsitcovers.||`all`|Onlyaroleoneveryobject,becausenothingkeepsthecalltosomeofthem.||`credential`|Onlyaroleoneveryobjecttoday,becauseStrazadoesnotyetcheckwhatarole'sowncredentialreaches.||nomark|Treatedas`all`untilapersondecides,soatoolyouaddlaterneverslipspastalimit.|Thelistingtoollistseveryobjectbynature,soitsreachis`all`.Markit`all`,asghappdoes,orleaveits`ai.straza/object`markout.Strazarefusesanyotherreachforit,inyourmarksandinapinnedblockalike,and`strazactlappsobjects`namesthefault:`ghapp'smarksbreakamanifestrule:objects.tools.repo_listgivesthelistingtoolthereachnone.Thelistingtoolreturnseveryrepo,soitsreachisall.Writeall,orleaverepo_listoutofobjects.tools.`Adraftthatputsatoolofreach`all`or`credential`inaroleonsomeobjectsisrefusedbeforeanyonecanpublishit.##Rulesforavalidmark{#rules}Strazareadsyourmarksfromyourserver'slivetoollist.Whiletheserverruns,thecheckofadraftthatpinsthemcomparesthepinnedcopywiththatlist.Aserver'smarksarevalidwhenallofthesehold:-Each`ai.straza/object`markcarriesa`type`andexactlyoneof`argument`or`reach`,neverboth.`reach`is`all`,`none`or`credential`.-Everymarkoftheservernamesthesametype,becauseaserverhasoneobjecttype.-Exactlyonetoolcarries`ai.straza/lists-objects`,andthattoolsets`readOnlyHint:true`initsannotations.-Anargumentamarknamesisatop-levelpropertyofthetool's`inputSchema`whose`type`isthestring`"string"`,andtheschemalistsitin`required`.Itsnameisupto64letters,digits,`_`or`-`,anditstartswithaletteror`_`.-ToolnamesfollowtheMCPspecification:1to128ASCIIletters,digits,`_`,`-`or`.`.-Attributenamesareupto64letters,digits,`_`,`.`or`-`thatstartwithaletter,andeachappearsonce.Nestedpaths,arrays,numbers,optionalargumentsandanobjectnamedbytwoargumentstogether,suchasanownerandarepositoryname,areoutsidetheserules.Givesuchatoolonerequiredstringargumentthatnamestheobjectwhole,suchas`Strazahq/pay-api`,ormarkit`all`.Onarunningserverthatpinsnothing,`strazactlappsobjects`printstheblockyourmarksdeclareandasentenceforeachfaultitfinds.Wereoneofghapp'stoolstomarkthetype`project`,thesentencewouldbe"ghapp'stoolsmark2objecttypes:project,repo.Aserverhasoneobjecttype,soitsauthormustmarkeverytoolwiththesametype."Untilthemarksarefixed,`strazactlappsobjectsdraft`refusestoopenadraft.Thecheckofadraftthatpinstheblockrefusesapinnedargumentthatisnotarequiredstringofitstool,alistingtoolthatisnotmarkedread-only,andapinnedreachlooserthanyourownmark.Itcancompareonlywhileyourserverrunsandlistsitstools.Otherwisethecheckcarriesalinethatsaysnothingwascompared,whichdoesnotblockthepublish,andthegatestillrefuseswhatthepinnedblockdoesnotallow.##Thelisting{#listing}Strazacallsyourlistingtoolitself,withanordinary`tools/call`.Theargumentsare`{}`forthefirstpageand`{"cursor":"..."}`withyourlast`nextCursor`foreachpageafter,sodeclare`cursor`asanoptionalstringinthetool'sinputschema.Answereachpagein`structuredContent`.Strazareadsonlythatand`isError`,soanytextcontentisforpeople.Anexamplepageinghapp'sshape,whosecursorisopaquetoStraza:```json{"objects":[{"value":"Strazahq/pay-api","id":"88112070","title":"pay-api","attributes":{"team":"payments"}},{"value":"Strazahq/site","id":"90123310","title":"site","attributes":{"team":["web","docs"]}}],"nextCursor":"eyJpIjo1MTIzNDU2NywicCI6Mn0"}```|Field|Rule||---|---||`value`|Required.Theexactstringatoolargumentcarriesforthisobject:anon-emptystringofvalidUTF-8ofatmost512bytes.Strazacomparesitbyteforbyte,withnotrimming,casefoldingorURLdecoding.||`id`|Anon-emptystring,orawholenumberfrom0to2^53,thatneverchangesandisneverreused,soanobjectdeletedandmadeagainunderthesamevaluegetsanewid.Atmost512bytes,andStrazakeepsanumberasitsdecimalstring.Anitemwithnoidusesitsvalueastheid,soadeletedandrecreatedobjectthenlooksliketheoldone.||`title`|Thenamepeopleread,astringofatmost256bytes.||`attributes`|Atmost16perobject,andthatcountincludeseveryattributeyousend.Strazakeepsonlytheattributesthepinnedblocknamesandshowsthemwitheachobject.Eachkeptattributeisastringofatmost128bytesoralistofatmost16suchstrings,andStrazadoesnotreadtheothers.||`nextCursor`|Astring.Absentoremptyendsthelisting.Sendingbackthecursoryouwerecalledwithfailsthelisting,becauseitwouldneverend.|Alistingcountsonlywheneverypagearrived.Anitemthatrepeatsanearlieritemofthesamelistingexactlyisdropped,becauseaserverthatpagesbynumbershiftsitspageswhenanobjectiscreatedmid-listing.Onevaluewithtwoids,oneidwithtwovalues,orarepeatwithanothertitleorotherkeptattributesfailsthelisting.Alistingmayhold100,000objects,eachpagehas30seconds,andthewholelistinghas5minutes.Alistingthatbreaksaruleoralimit,answers`isError:true`,orrunsoutoftimefailsasawholeandchangesnothing.Strazakeepsthelastgoodlistandgoesondecidingwithit.Thereasonshowsontheserver'stabintheconsole,in`strazactlappsobjects`,andinthe`apps.objects.list`auditrecord,whichStrazawriteswhenalistingchangesthelistoritsoutcomeflips.Thereasonsthatpointatyourlistingtoolread,foratoolnamed`repo_list`:|Yourlistingtool|Theoperatorreads||---|---||lacks`readOnlyHint:true`|`repo_listisnotmarkedread-only,soStrazawillnotcallittolistrepos.Theserver'sauthormustsetreadOnlyHintonit.`||answerswithout`objects`in`structuredContent`|`repo_listansweredwithoutanobjectslistinitsstructuredcontent.Theserver'sauthormustreturn{objects,nextCursor}astheStrazaobjectmarksdescribe.`||returnsthecursoritwascalledwith|`repo_listansweredwiththecursoritwascalledwithasnextCursor,sothelistingwouldneverend.Theserver'sauthormustreturnanewnextCursor,ornoneonthelastpage.`||breaksalimit|Asentencethatnamesthelimit,suchas`repo_listreturnedmorethan100,000repos,sothelistingwasrefusedasawholeandthelastgoodliststays.`||answers`isError:true`|`repo_listansweredanerror:`,thefirstlineofyourerrortext,atmost160byteswithanythingshapedlikeacredentialmasked,then`.Checkghapp'slogsonitsActivitytab.`||takesmorethan30secondsforapage|`repo_listdidnotanswerwithin30secondsonpage2.`|Strazalistsyourserverevery10minutes.Italsolistswithinabout15secondsafterapersonpublishesapin,changesitstype,listingtoolorattributes,orchangesthelistingcredential.Achangeofthetype,thelistingtoolortheattributesstartsthelistoverempty,sountilthenextgoodlistingthereisnolastgoodlist,androlesthatnameobjectsarerefusedwhilerolesoneveryobjectkeepworking.Whenthesamepublishstartsorrestartsyourserver,thatfirstlistingcanfail.Whiletheserverisstillstarting,thereasonreads`ghappisnotrunning,soStrazacouldnotcallrepo_list.Startit,thenrefresh.`OnceitrunsbutStrazahasnotreaditstoollistyet,thereasonreads`Strazahasnotreadghapp'stoollistyet,soitcannotcheckrepo_listbeforeitlistsghapp'srepos.Ittriesagaininaboutaminute.Ifghappstaysdegraded,readwhywithstrazactlappsshowghapp.`Botharetriedagaininaboutaminute,andaserverthatfailedtostart,whichcangiveeitherreason,istriedagaineachminuteuntilitruns.Anyotherfailedlistingistriedagain10minuteslater,andsoisthelistingofaserveranoperatorstoppedorpaused.Arefusedcallthatnamesavaluethelistdoesnotholdasksforalistingsooner,andsodoesanoperator's`strazactlappsobjectsrefresh`,butneitherrunswithinaminuteofthelastlisting.Onereplicaofadeploymentlistsatatime,soexpectonefulllistingevery10minutesperdeploymentandatmostonemoreeachminute.##Thelistingcredential{#listing-credential}Ona`remote`serverwhosemanifestnamesacredential,Strazacallsyourlistingtoolwithalistingcredential:asecrettheoperatorstoresforthatonepurposewith`strazactlappssecretset<server>--listing`.Itarriveswhereeveryothercredentialdoes,intheheaderyourmanifest's`credential.inject`names.Strazasendsitonthelistingcallonly.Itneverfallsbacktoanothersecret,suchasyourserver'sown,sowithoutitthelistingfailswith"ghapphasnolistingcredential,soStrazacannotlistitsrepos.Addonewithstrazactlappssecretsetghapp--listing,thenrefreshwithstrazactlappsobjectsrefreshghapp."Aremoteserverthattakesnocredentialislistedwithoutone.A`command`or`oci`serverisoneprocesswithoneidentity,soStrazaliststhroughthatprocesswithnocredentialpercall.Acceptthelistingcredentialonthelistingtoolonly,andrefuseitoneveryothertool.Nopersonstandsbehindit,soalistingcredentialthatopenedyourwritetoolswouldbeawayaroundeveryrole.Offeroperatorsacredentialthatcanreadthelistanddonothingelse.Anagent'scallofyourlistingtoolthroughStrazacarriesthecredentialStrazausesforthatagent'sothercalls,neverthelistingcredential.Thelistingtool'sreachis`all`,soonlyaroleoneveryobjectmaymakethatcall.objects-demoshowsthesplit:itslistingtoolanswersonlytoitslistingtoken,itsrepositoryandsearchtoolsrefusethattoken,andits`whoami`toolonlysayswhichtokenacallcarried.##Whatreachesyourserveronacall{#calls}Onceyourmarksarepinned,Strazacheckseachcalltoyourserver'stoolsbeforeanypolicyruns,andacallitrefusesneverreachesyourserver.Foratoolofreach`argument`,itfirstrefusesargumentsthattwoJSONreaderscouldreaddifferently,suchasbytesthatarenotvalidUTF-8,akeythatappearstwice,orasecondkeythatdiffersfromyourargument'snameonlyincase.Italsorefusesacallwhoseargumentismissingorisnotastring.Strazaneverrewritestheobjectargument,soyourserverreceivesthevaluethecheckread,astheagentsentit.Whentherolethatletsacallthroughnamesobjectsonebyone,Strazaaddstheobject'sidfromitslisttothecall's`_meta`,asinthisexample:```json{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"pr_create","arguments":{"repo":"Strazahq/pay-api","title":"Fixthebuild","head":"fix-build","base":"main"},"_meta":{"ai.straza/object-id":"88112070"}}}```Theidisalwaysastring,thedecimalformwhenyourlistingsentanumber.Compareitwiththeidyoursystemholdsnowforthevaluethecallnames,andrefusethecallwhentheydiffer.AdifferentidmeanstheobjectwasdeletedandmadeagainunderthesamevalueafterStrazalistedit,andtherolecoveredtheoldone.Ifyourservermintsanarrowercredentialforeachcall,mintitfortheidStrazasent,notforanidyouresolvedfromthevalue,soarenamebetweenthecheckandthecallstillleavesthecredentialontheobjectStrazachecked.Acallthrougharoleoneveryobjectcarriesnoid,andyourserverresolvesthevalueitself.Strazasendsnoidonatoolwhosereachisnot`argument`,andnoneonthelistingcall.##Thethreepromises{#promises}EverymarkcarriesthreepromisesthatStrazacannotcheck.Whenyoumarkatoolyoumakethem,andanoperatorwhopinsyourmarksreliesonthem:1.Acalltouchesonlytheobjectitsargumentnames.2.Avaluenamesexactlyoneobjectatthetimeofthecall,withnorenameredirect,aliasorcasefolding.Matchthevaluebyteforbyte,asStrazadoes.3.YourserveracceptscallsonlyfromStraza'sgateway,orchecksacredentialthatonlythegatewaycanobtain.StrazachecksobjectsonlyinitsMCPgateway,anditsharnesshooksneverrunthischeck.Acallthatreachesyourserversomeotherwaycarrieswhateverargumentsand`_meta`itssenderchose,sothefirsttwopromisesholdonlywhilethethirddoes.##Whatyoucanrelyon{#rely}-Marksonlypre-filladraft.Strazadecideswiththecopyapersonpinnedunder`straza.objects`,neverwithyourlivemarks.-Whoeverpinsyourmarksmaymakeareachstricterandneverlooser.`all`isstricterthaneveryotherreach,andeveryreachisstricterthan`none`.-Achangedmarkisdriftthatapersonresolves.Strazacountsthe`ai.straza/*`keysinthetoolinventoryitwatches,soachangerecords`straza.apps.drift`asachangeddescriptionorschemadoes,and`strazactlappsobjects`tellstheoperatorwhenadraftfromyourmarkswouldchangethepinnedcopy.-Untilapersonpublishesanewpin,atoolisrefusedtorolesonsomeobjectswhenitslivemarkisnowstricterthanitspinnedreach,namesanothertypeornolongerparses,orwhenitsliveinputschemanolongerliststhepinnedargumentasarequiredtop-levelstring.Rolesoneveryobjectkeepcallingit.Removingamarkleavesthepinnedreachasitwas.-Atoolyouaddwithoutamarkreadsas`all`untilapersonnamesitsreach.-Strazareadsonlythenameandtheargumentsofanagent's`tools/call`,so`_meta`anagentsendsneverreachesyourserver,and`ai.straza/object-id`alwayscomesfromStraza.-Yourmarksarenotsecret.Thecombined`/mcp`endpointdropstool`_meta`,butaserver'sownendpoint,`/mcp/`followedbyitsname,servesyourtools'`_meta`asyousentit.-Anoperatorcanturneverylistingoffwith`apps.objects.enabled:false`.Atoolthatnamesanobjectthenanswersonlyrolesoneveryobject,soturningitoffneverwidensaccess.##Aserverthatalsoservesgit{#git}Aserverthatholdsagithost'scredential,asghappholdsaGitHubApp'skey,canalsoservegit,sothatanagentrunsplaingitandthecredentialneverreachestheagent'smachine.Givetheserveronetoolforafetchandoneforapush,eachmarkedwiththeargumentthatnamestherepository,andservegit'ssmartHTTPprotocolunderyourpublicaddressfollowedby`/git/`.Anoperatormapsthetwotoolsinthemanifest's`straza.git`block,andStrazarefusesthatblockunlesstheobjectsblockpinseachmappedtooltoanargument.Strazadecideseachgitoperationasacallofthemappedtoolwiththerepositoryinthatargument,thenissuesashortgitgrantthatyourserververifiesoneveryrequest,togetherwithaprooffromtheagentmachine'skey.[Thegitgrantspecification](https://github.com/strazahq/straza/blob/main/spec/grant/SPEC.md)statestheclaims,thebindingandtheorderofthechecks,anditsexamplesreplayinyourowntests.[GitthroughStraza](https://docs.straza.ai/guides/serve-mcp-apps/git-through-straza/index.md)istheoperator'sside.##Beforeyoutelloperatorsitisready{#checklist}RunthroughthislistagainstaStrazayoucontrol,suchasthestandaloneprofileonyourownmachine:1.Everytoolthatactsononeobjectcarries`ai.straza/object`withits`argument`,andeveryothertoolbutthelistingtoolcarriesa`reach`,sonotoolwaitsforadecision.2.Theargumenteachmarknamesisarequiredtop-levelstringofitstool'sinputschema.3.Onetoolcarries`ai.straza/lists-objects`and`readOnlyHint:true`,andallyourmarksnamethesametype.4.Ontherunningserver,`strazactlappsobjects<server>`printstheblockyourmarksdeclareandnofaultsentence.5.Onceyoupintheblock,andstorethelistingcredentialwhenyourservertakesone,Strazaliststheserverwithinabout15seconds.`strazactlappsobjects<server>`thenshowseveryobjectwiththeidandtitleyouexpect,andnofailurereason.Ifthatfirstlistingfailedbecausetheserverwasstillstarting,Strazalistsitagaininaboutaminute,withnorefresh.6.Anobjectdeletedandmadeagainunderthesamevaluegetsanewid,andeverylistingstayswithinthelimitsabove.7.Yourlistingtoolanswerstothelistingcredential,andeveryothertoolrefusesit.8.Atoolthatnamesanobjectrefusesacallwhose`ai.straza/object-id`differsfromtheobject'sidnow,andacceptsacallthatcarriesnoid.9.Valuesmatchbyteforbyte,asthesecondpromiseasks.10.YourserverisreachableonlyfromStraza'sgateway,orchecksacredentialonlythegatewayholds.



