This is the exhaustive reference for the Lua helpers available inside Snulbug policies. If you are writing a policy from scratch, start with the Lua Policy DSL guide, then return here for exact helper names, arguments, and decision fields.
Lua policies return a function:
return function(request, context, state)
return { action = "continue" }
endThe request table contains:
methodpathraw_pathquery_stringheaders, keyed by lowercase header nameclientschemebody, when body reading is enabledbody_bytes_latin1, when body reading is enabled
The context table is copied from scope["lua"] when present. Actions can merge new context into the downstream ASGI scope.
The state table is available only when a state store is configured. It exposes get, put, delete, incr, and cas.
The sandbox includes an mcp helper table for local-dev MCP gateway policies. Helpers parse the JSON-RPC request body without exposing a general Python JSON API.
local method = mcp.method(request)
local params = mcp.params(request)
local tool = mcp.tool_name(request)
local call = mcp.call(request)
local path = mcp.arg(call, "path")
if mcp.is_tool_call(request) then
local blocked = mcp.allow_tools(request, { "safe_read_file", "list_project_files" })
if blocked ~= nil then
return blocked
end
endAvailable helpers:
mcp.body(request): parsed JSON-RPC body table, ornilfor missing/malformed JSON.mcp.call(request): normalized JSON-RPC call table withmethod,params,args,tool,id,batch,invalid,error,is_tool_call,is_read,is_write,is_task_augmented,is_task_method,is_task_notification,is_task_request,is_completion_request,is_progress_notification,is_cancelled_notification,is_resource_subscription,is_resource_notification,is_server_to_client_request,is_sampling_request,is_elicitation_request,is_roots_request,task_id,task_status,task_operation,task_ttl_ms,related_task_id,progress_token,resource_uri, andresource_operationfields.mcp.arg(request_or_call, key): read one tool/prompt argument from a request or normalized call.mcp.arg_keys(request_or_call): sorted list of observed tool/prompt argument keys.mcp.method(request): JSON-RPC method string, ornil.mcp.params(request): JSON-RPC params table, or an empty table.mcp.is_method(request, method): true when the request method matches.mcp.is_tool_call(request): true fortools/call.mcp.is_task_augmented(request): true when a request includes official MCPparams.taskaugmentation.mcp.is_task_method(request): true for official MCPtasks/get,tasks/result,tasks/list, andtasks/cancel.mcp.is_task_notification(request): true fornotifications/tasks/status.mcp.is_task_request(request): true for task-augmented requests, task methods, or task status notifications.mcp.is_completion_request(request): true forcompletion/complete.mcp.is_progress_notification(request): true fornotifications/progress.mcp.is_cancelled_notification(request): true fornotifications/cancelled.mcp.is_resource_subscription(request): true forresources/subscribeorresources/unsubscribe.mcp.is_resource_notification(request): true fornotifications/resources/updatedornotifications/resources/list_changed.mcp.is_server_to_client_request(request): true for upstream requests aimed back at the MCP client, currentlysampling/createMessage,elicitation/create, orroots/list.mcp.is_sampling_request(request): true forsampling/createMessage.mcp.is_elicitation_request(request): true forelicitation/create.mcp.is_roots_request(request): true forroots/list.mcp.task_id(request): official MCP task ID fromparams.taskId, ornil.mcp.related_task_id(request): related task ID from_meta["io.modelcontextprotocol/related-task"], ornil.mcp.task_status(request): task status from task notifications or task status payloads, ornil.mcp.task_operation(request):get,result,list,cancel, orstatusfor MCP task methods/notifications.mcp.task_ttl_ms(request): requested task TTL fromparams.task.ttl, ornil.mcp.task_support(): schema-awareexecution.taskSupportfor the current tool when supplied in policy context.mcp.completion_ref_type(request): completion reference type, usuallyref/promptorref/resource.mcp.completion_ref_name(request): prompt name for prompt completions, ornil.mcp.completion_ref_uri(request): resource template URI for resource completions, ornil.mcp.completion_argument_name(request): argument name being completed.mcp.completion_argument_value(request): current partial argument value.mcp.completion_context_keys(request): sorted keys fromparams.context.arguments.mcp.progress_token(request): original request_meta.progressToken, or progress notificationparams.progressToken.mcp.progress_value(request): numericparams.progressfromnotifications/progress, ornil.mcp.progress_total(request): numericparams.totalfromnotifications/progress, ornil.mcp.progress_message(request): optional progress message string, ornil.mcp.cancelled_request_id(request):params.requestIdfromnotifications/cancelled, ornil.mcp.cancelled_reason(request): optional cancellation reason string, ornil.mcp.resource_uri(request): resource URI forresources/read, subscribe/unsubscribe, or update notifications.mcp.resource_operation(request):read,subscribe,unsubscribe,updated,list_changed, ornil.mcp.sampling_tools_requested(request): true when asampling/createMessagerequest includes a non-emptyparams.toolsarray.mcp.sampling_tool_names(request): sorted tool names from sampling-with-tools requests.mcp.sampling_tool_choice_mode(request):auto,required,none, ornil.mcp.elicitation_mode(request):form,url, ornil.mcp.elicitation_url(request): URL from URL-mode elicitation, ornil.mcp.tool_name(request):params.namefortools/call, ornil.mcp.tool_allowed(request, allowed): true when the request is not a tool call or the tool is allowed.mcp.allow_tools(request, allowed, options): returnsnilwhen allowed, otherwise arejectdecision.mcp.reject_tool(request_or_name, status, body, options): builds a standard tool rejection decision.
allowed can be an array, such as { "read_file" }, or a map, such as { read_file = true }.
options.reason and options.reason_code can override the default
mcp.tool_not_allowed reason metadata.
Official MCP Tasks are distinct from snulbug task leases. MCP Tasks are durable protocol request wrappers and task polling/result methods; snulbug leases are temporary capability grants enforced by the gateway. A policy can gate MCP Tasks without changing lease behavior:
return function(request)
return cap.mcp_task_method(request, { "tasks/get", "tasks/result" })
or (mcp.is_task_augmented(request) and intent.confirm_if("write", {
prompt = "Allow task-augmented write-like MCP tool call?"
}))
or decision.allow("mcp.task_policy_allowed", {
task_id = mcp.task_id(request),
task_operation = mcp.task_operation(request)
})
endMCP server-to-client requests are also distinct from normal client-originated tool calls. They let an upstream server ask the client/model/user for more work or more data. In HTTP proxy mode, snulbug blocks these on the response path by default before they reach the client. Lua policies can still identify the same methods in replay/simulation or nonstandard flows:
return function(request)
return cap.server_to_client_method(request, {})
or decision.allow("mcp.server_to_client_allowed")
endMCP progress and cancellation messages are mediated by the proxy runtime, but Lua policies can still inspect them when replaying, simulating, or handling nonstandard flows:
return function(request)
if mcp.is_progress_notification(request) then
return decision.allow("mcp.progress_observed", {
progress = mcp.progress_value(request),
total = mcp.progress_total(request)
})
end
if mcp.is_cancelled_notification(request) then
return decision.allow("mcp.cancel_observed", {
request_id = mcp.cancelled_request_id(request)
})
end
return decision.allow("mcp.allowed")
endResource subscription and change messages are likewise visible to Lua. The proxy runtime can track subscriptions, while policies can record or narrow the same protocol surface:
return function(request)
if mcp.is_resource_subscription(request) then
return decision.allow("mcp.resource_subscription", {
operation = mcp.resource_operation(request),
uri = mcp.resource_uri(request)
})
end
if mcp.is_resource_notification(request) then
return decision.allow("mcp.resource_change", {
operation = mcp.resource_operation(request),
uri = mcp.resource_uri(request)
})
end
return decision.allow("mcp.allowed")
endThe intent table exposes schema-aware MCP tool capability and risk metadata.
When the reverse proxy has seen a tools/list response, snulbug classifies the
called tool using its cached inputSchema. Without cached schema metadata,
helpers fall back to deterministic tool-name inference.
return function(request)
return intent.require_max_risk("medium")
or intent.confirm_if({ "filesystem.write", "network.egress" })
or decision.allow("mcp.intent_allowed", {
tool = intent.name(),
risk = intent.risk(),
categories = intent.categories()
})
endAvailable helpers:
intent.info(): return the current intent metadata table. Fields can includename,level,score,categories,signals,schema,source, andconfidence.intent.name(): current MCP tool name, ornilfor non-tool calls.intent.category(): first normalized category, ornil.intent.categories(): normalized category list. This includes classifier categories such ascommand,mutation,network,filesystem,secrets,read,open-schema, andschema-drift, plus policy-friendly aliases such asshell.exec,network.egress,filesystem.read,filesystem.write,git.read,git.write,secrets.access,read, andwrite.intent.risk(): risk level, usuallylow,medium, orhigh.intent.risk_score(): numeric risk score.intent.has_category(category_or_categories): true when any current category matches. Wildcards such asfilesystem.*are supported.intent.require_category(category_or_categories, options): returnnilwhen matched, otherwise reject withreason_code = "mcp.intent_category_denied".intent.require_max_risk(level, options): returnnilwhen the current risk is at or belowlevel, otherwise reject withreason_code = "mcp.intent_risk_denied".intent.block_if(category_or_categories, options): reject when the current intent matches one of the supplied categories. Defaultreason_codeismcp.intent_blocked.intent.confirm_if(category_or_categories, options): return a confirmation decision when the current intent matches one of the supplied categories. Defaultreason_codeismcp.intent_confirmation_required.
Intent decisions include sanitized context such as tool, risk, risk score,
categories, source, confidence, and any required or blocked category. The raw
schema document is not added to decision context unless the policy explicitly
copies fields from intent.info().schema.
The sandbox includes a decision table for building supported middleware
actions without repeating raw table shapes:
return function(request, context)
local call = mcp.call(request)
return decision.reject(403, "tool blocked", {
reason_code = "mcp.tool_not_allowed",
context = { tool = call.tool }
})
endAvailable builders:
decision.continue(options): continue to the upstream app.decision.allow(reason_code, context): continue with optional decision metadata.decision.set_context(context, options): merge context into the downstream ASGI scope.decision.respond(status, body, options): return a response directly.decision.reject(status, body, options): reject before reaching the upstream. Setoptions.confirm = trueto route the rejection through the same approval broker used bydecision.confirm.decision.challenge(options): build an auth challenge.decision.redirect(location, options): build a redirect.decision.rate_limit(key, limit, window, options): invoke configured bounded policy state.decision.confirm(prompt, options): ask the live decision console for approval. Ifoptions.capability_requestis present and confirmation is denied or unavailable, snulbug returns an MCP JSON-RPC error with structurederror.data.capability_request.
options can include reason, reason_code, context, and headers where
the underlying action supports them. Confirmation options such as prompt,
remember_key, and timeout_seconds are also available for decision.confirm
and confirmable decision.reject results.
The access table builds standardized auth and lease denials for Lua policies.
Use these when a policy is enforcing identity, scope, lease, or route checks and
you want audit logs, replay diffs, and reports to use consistent reason codes:
local denied = auth.require("tools/call:git.status")
or auth.require_tenant("tenant-a")
or auth.require_group("platform-dev")
or lease.require()
if denied then
return denied
endAvailable builders:
access.missing_scope(scope, options):decision.challengewithstatus = 403,reason_code = "oauth.missing_scope", anderror = "insufficient_scope".access.scope_denied(selector, options):decision.challengewithstatus = 403andreason_code = "oauth.scope_map_denied".access.wrong_subject(subject_or_subjects, options):decision.rejectwithreason_code = "oauth.subject_denied".access.wrong_tenant(tenant_or_tenants, options):decision.rejectwithreason_code = "oauth.tenant_denied".access.wrong_group(group_or_groups, options):decision.rejectwithreason_code = "oauth.group_denied".access.lease_required(options):decision.rejectwith the current lease reason code, orlease.requiredwhen no more specific lease reason exists.access.expired_lease(options):decision.rejectwithreason_code = "lease.expired".access.contract_required(options):decision.rejectwithreason_code = "share.contract_required"when a policy requires a bound share contract.access.contract_mismatch(details, options):decision.rejectwithreason_code = "share.contract_mismatch"when a policy requires a specific contract digest or signer key id.access.route_mismatch(details, options):decision.rejectwithreason_code = "access.route_mismatch"for tenant/upstream/facade route fences.
auth.require_scope, auth.require, auth.require_subject,
auth.require_tenant, auth.require_group, and lease.require use these
builders by default. options.context is merged into the standard context, and
options.body, options.reason, and options.reason_code can still override
the emitted decision when a policy needs a local code.
When snulbug mcp share run starts with --require-contract, the proxy binds
the approved share contract, exposes it at the snulbug well-known endpoints,
and passes safe contract metadata into Lua as context.share. The sandbox also
includes a share helper table for policies that want to enforce that runtime
binding:
return function(request, context)
return share.require_contract_bound()
or share.require_contract_key_id("local-review")
or decision.allow("share.contract_bound", {
contract_digest = share.contract_digest()
})
endAvailable helpers:
share.info(): return the sanitizedcontext.sharetable.share.bound(): true when a runtime contract is bound and has a digest.share.required(): true when the share was started with a required contract.share.signed(): true when the bound contract includes a snulbug signature.share.verified(): true when snulbug verified the runtime contract before binding it.share.runtime_status(): runtime binding status, such asbound.share.contract_digest(): stable binding digest used in audit metadata.share.binding_digest(): binding digest, falling back toshare.contract_digest().share.document_digest(): document digest for the approved contract JSON.share.key_id(): signer key id from the approved contract.share.require_contract_bound(options): returnnilwhen bound, otherwiseaccess.contract_required.share.require_contract_digest(digest_or_digests, options): returnnilwhen the runtime digest matches, otherwiseaccess.contract_mismatch.share.require_contract_key_id(key_or_keys, options): returnnilwhen the signer key id matches, otherwiseaccess.contract_mismatch.
These helpers do not expose the raw contract document, bearer tokens, lease tokens, or upstream credentials. They are intended for policies that need to fail closed unless a human-reviewed contract is the exact one currently running.
In facade/fabric mode, Lua policies receive a pre-routing preview as
context.upstream plus an upstream helper table. This lets a policy bind
OAuth identity to a specific upstream route or fabric member before the request
is forwarded:
local wrong_route = upstream.require_for_tenant({
["tenant-a"] = { "files" },
["tenant-b"] = { "git" }
})
if wrong_route then
return wrong_route
endAvailable helpers:
upstream.info(): return the sanitizedcontext.upstreamtable.upstream.matched(): true when the facade route matched the request.upstream.name(): selected upstream/fabric member name for a routed call.upstream.transport(): selected upstream transport, such ashttp,stdio, orholepunch.upstream.tool_prefix(): client-facing facade tool prefix.upstream.tool(): client-facing tool name, such asgit.status.upstream.upstream_tool(): upstream-local tool name after prefix removal.upstream.manifest_identity(): signed upstream manifest identity when configured.upstream.is(upstream_or_upstreams): true when the selected upstream matches a string or one entry in an array.upstream.require(upstream_or_upstreams, options): returnnilwhen the selected upstream is allowed, otherwiseaccess.route_mismatch.upstream.require_for_tenant(map, options): useauth.tenant()as a key inmap, then require the selected upstream to match that value.upstream.require_for_issuer(map, options): useauth.issuer()as a key.upstream.require_for_auth_profile(map, options): useauth.profile_id()as a key.
Identity maps can use strings or arrays as values:
upstream.require_for_issuer({
["https://tenant-a-idp.example.com"] = { "tenant-a-files", "tenant-a-git" },
["https://tenant-b-idp.example.com"] = "tenant-b-files",
["*"] = "public-readonly"
})These helpers expose route metadata needed for policy: upstream name, transport, tool prefix, selected tool, manifest identity/metadata, bridge peer metadata, and route revision/fingerprint. They do not expose upstream credentials.
The cap table provides small guard helpers that return nil when allowed or
a standard rejection decision when blocked. This makes policies read as a
short-circuit chain:
return function(request, context)
local call = mcp.call(request)
return cap.method(request, { "tools/call" })
or cap.tool(request, { "safe_read_file" })
or cap.arg_path(call, "path", { "README.md", "docs" })
or decision.allow("mcp.allowed", { tool = call.tool })
endAvailable guards:
cap.allowed(value, allowed): boolean membership check for array or map allowlists.cap.method(request_or_method, allowed, options): allow listed JSON-RPC methods.cap.mcp_task_method(request_or_method, allowed, options): allow listed official MCPtasks/*methods; non-task methods pass through.cap.completion_ref_type(request_or_type, allowed, options): allow listed completion reference types; non-completion requests pass through.cap.server_to_client_method(request_or_method, allowed, options): allow listed upstream-to-client methods; non-server-to-client methods pass through.cap.tool(request_or_name, allowed, options): allow listed MCP tools; non-tool calls pass through.cap.arg_string(request_or_call, key, options): require a non-empty string argument.cap.arg_path(request_or_call, key, allowed_paths, options): require a relative path argument under listed roots.cap.arg_host(request_or_call, key, allowed_hosts, options): require a URL/host argument matching listed hosts.cap.arg_command(request_or_call, key, allowed_commands, options): require a shell-like command argument whose first token is allowed.cap.path(path, allowed_paths, options): allow non-absolute, non-traversing relative paths under listed roots.cap.host(url_or_host, allowed_hosts, options): allow listed hosts, including*.example.comsuffix entries.cap.command(command, allowed_commands, options): allow shell-like command strings by first token.cap.request(request, options): build a confirmation-backed, MCP-native just-in-time capability request. It suggests a normal snulbug task lease usingallow_tools,allow_paths,allow_hosts,allow_commands,ttl,max_calls, andtask.
Every cap.* rejection and mcp.allow_tools supports confirmation options:
return cap.tool(request, { "files.read_file" }, {
confirm = true,
prompt = "Allow unlisted tool once?",
remember_key = "tool:" .. tostring(mcp.tool_name(request)),
timeout_seconds = 30,
reason_code = "mcp.policy.tool_rejected"
})Use cap.request when the right next step is a lease review rather than a
hard deny:
return cap.request(request, {
task = "Read project docs",
ttl = "10m",
max_calls = 2,
allow_paths = { "README.md", "docs" },
remember_key = "cap:" .. tostring(mcp.tool_name(request)),
reason_code = "mcp.docs_capability_requested"
})The workspace table provides higher-level local-dev filesystem guards for
MCP tools that accept project path arguments. The helpers inspect the current
MCP request, so policies can stay terse:
return function(request, context)
return workspace.require_under_project("path")
or workspace.block_secret_paths("path")
or workspace.block_generated_paths("path")
or workspace.readonly_only()
or decision.allow("mcp.workspace_allowed")
endAvailable guards:
workspace.require_under_project(arg_key_or_keys, options): require matching path arguments to be relative project paths with no absolute paths, home-dir paths, drive-letter paths, or parent traversal. By default this allows any relative project path; passoptions.allowed_paths,options.roots, oroptions.project_pathsto constrain paths to selected roots.workspace.block_secret_paths(arg_key_or_keys, options): block secret-looking paths such as.env,.env.*,.ssh/,.gnupg/,secrets/,.kube/config,*.pem,*.key,*.p12,*.pfx,*.crt, and*.cert.workspace.block_generated_paths(arg_key_or_keys, options): block generated or cache paths such as.git/,.snulbug/,.venv/,venv/,node_modules/,__pycache__/,.ruff_cache/,.pytest_cache/,.mypy_cache/,dist/,build/, andcoverage/. Setoptions.write_only = trueto apply this only to write-like tool names.workspace.readonly_only(options): allow read-oriented MCP methods and reject write-like tool calls such as names containingwrite,edit,create,delete,rename,patch,append,mkdir,rm,touch, orsave.
arg_key_or_keys can be a string, an array, a map, or nil. When it is nil,
the helpers inspect common path-like argument names such as path, paths,
file, directory, cwd, source, destination, target, oldpath, and
newpath.
Supporting helpers:
workspace.write_intent(): true when the current tool name looks write-like.workspace.path_values(arg_key_or_keys): return matching raw argument values.workspace.path_summary(arg_key_or_keys, options): return one sanitized summary table withargument,path,path_class, andwrite_intent.
Workspace guard rejections use stable reason codes such as
mcp.workspace_path_invalid, mcp.workspace_path_outside,
mcp.workspace_secret_blocked, mcp.workspace_generated_path_blocked, and
mcp.workspace_readonly_required. options.context, options.reason,
options.reason_code, and confirmation fields can override or extend the
standard decision.
When [mcp.auth] runs in OAuth protected-resource mode, Lua policies also get
a request-scoped auth helper table. The helper reads the sanitized auth
context produced by the proxy; it never exposes the raw bearer token.
return function(request, context)
local wrong_tenant = auth.require_tenant("tenant-a", {
reason_code = "oauth.tenant_required"
})
if wrong_tenant then
return wrong_tenant
end
local missing_group = auth.require_group({ "platform-dev", "mcp-admins" }, {
reason_code = "oauth.platform_group_required"
})
if missing_group then
return missing_group
end
local denied = auth.require("tools/call:git.status", {
reason_code = "oauth.git_status_scope_required"
})
if denied then
return denied
end
return decision.allow("mcp.allowed", {
subject = auth.subject(),
tenant = auth.tenant(),
groups = auth.groups(),
client_id = auth.client_id(),
})
endAvailable helpers:
auth.claims(): return the sanitizedcontext.authtable. When OAuth DPoP validation ran,context.auth.proof_of_possessionincludes safe metadata such asenabled,bound,jkt,jti,htu,htm, andalg; it never includes the raw proof JWT or access token.auth.subject(): return the JWT subject claim.auth.issuer(): return the JWT issuer claim.auth.profile_id(): return the matched[[mcp.auth.issuers]]profile id.auth.client_id(): returnazporclient_idwhen present.auth.email(): return the token email claim when present.auth.tenant(): returntidortenantwhen present.auth.groups(): return token groups as an array.auth.is_subject(subject_or_subjects): true when the token subject matches a string or one entry in an array.auth.in_tenant(tenant_or_tenants): true when the token tenant matches a string or one entry in an array.auth.has_group(group_or_groups): true when any required group is present.auth.scopes(): return the token scopes as an array.auth.has_scope(scope): true when the token includesscope.auth.can(selector): true when the token has a scope mapped to an MCP selector such astools/listortools/call:git.status.auth.require_subject(subject_or_subjects, options): returnnilwhen the subject matches, otherwise a 403 reject.auth.require_tenant(tenant_or_tenants, options): returnnilwhen the tenant matches, otherwise a 403 reject.auth.require_group(group_or_groups, options): returnnilwhen any group matches, otherwise a 403 reject.auth.require_scope(scope, options): returnnilwhen present, otherwiseaccess.missing_scope(scope, options).auth.require(selector, options): returnnilwhenauth.can(selector)is true, otherwiseaccess.scope_denied(selector, options).
Provider-aware helpers read normalized claim shapes from context.auth.provider
so policies do not need to know each provider's raw JWT/header layout:
- Keycloak:
auth.keycloak_realm_roles(): return realm roles fromrealm_access.roles.auth.keycloak_client_roles(client_id): return roles fromresource_access[client_id].roles.auth.keycloak_has_role(role, client_id): true when the role is present; omitclient_idto check realm roles and all client role sets.
- Cloudflare Access:
auth.cloudflare_email(): return the normalized Access email. When assertion validation is enabled, this comes from the signed JWT claim; otherwise it comes from the Access email header.auth.cloudflare_jwt_validated(): true when the Access assertion was cryptographically validated by the proxy.auth.cloudflare_subject(): return the validated Access JWT subject when available.auth.cloudflare_groups(): return normalized Access groups.auth.cloudflare_has_group(group_or_groups): true when any group matches.
- GitHub Actions OIDC:
auth.github_repository(),auth.github_workflow(),auth.github_workflow_ref(),auth.github_job_workflow_ref(),auth.github_ref(), andauth.github_event_name(): return normalized GitHub Actions claims.auth.github_matches(options): true when all supplied fields match. It acceptsrepository,repository_owner,workflow,workflow_ref,job_workflow_ref,ref,event_name,actor, andenvironment.
- Entra:
auth.entra_groups(): return Entra group IDs fromgroups.auth.entra_has_group(group_or_groups): true when any group matches.auth.entra_app_roles(): return app roles fromroles.auth.entra_has_app_role(role_or_roles): true when any app role matches.auth.entra_tenant_id()andauth.entra_app_id(): returntidandappid/azp/client_id.
local denied = nil
if not auth.keycloak_has_role("mcp-admin") then
denied = access.wrong_group("mcp-admin", {
reason_code = "oauth.keycloak_role_required"
})
end
if denied then
return denied
end
if not auth.github_matches({
repository = "acme/widget",
ref = "refs/heads/main",
event_name = "workflow_dispatch"
}) then
return access.wrong_subject("github-actions-main", {
reason_code = "oauth.github_actions_ref_denied"
})
endauth.can uses the same [mcp.auth.scope_map] selectors enforced by the
proxy, so Lua policy and pre-Lua OAuth enforcement stay aligned.
Use [mcp.auth.claim_policy] when tenant, subject, group, client ID, or custom
JWT claims can be mapped to allowed tool names declaratively; Lua still receives
context.auth.claim_policy for audit/context-aware follow-up decisions.
When [[mcp.auth.issuers]] profiles are configured, Lua receives the selected
profile as context.auth.profile_id.
Use the subject, tenant, and group helpers for identity fences inside a share:
for example, "only members of platform-dev in tenant-a may call this
write-capable tool." These helpers read sanitized claims only; raw bearer
tokens are never exposed to Lua.
When mcp.proxy.lease_file is configured, Lua policies also get a non-consuming
lease preview in context.lease and a lease helper table. The preview checks
the presented x-snulbug-lease token before Lua runs, but the lease is consumed
only later if the request reaches the upstream.
return function(request, context)
local denied = lease.require({
reason_code = "lease.active_task_lease_required"
})
if denied then
return denied
end
return decision.allow("mcp.allowed", {
lease_id = lease.id(),
lease_task = lease.task(),
lease_capabilities = lease.capabilities(),
})
endAvailable helpers:
lease.info(): return the sanitizedcontext.leasetable.lease.enabled(): true when a lease file is configured.lease.required(): true whenlease_required = true.lease.checked(): true when a presented lease token was checked.lease.allowed()/lease.active(): true when the presented lease covers the currenttools/call.lease.id(): return the matched lease id.lease.task(): return the matched lease task label.lease.capabilities(): return temporary capability labels attached to the lease, such asproject_readonlyordocs_review.lease.has_capability(name): true when the active lease includes that label.lease.reason_code(): return the lease denial reason, such aslease.missing,lease.path_not_allowed, orlease.subject_not_allowed.lease.require(options): returnnilwhen no lease is needed or the active lease covers the request, otherwise a standardizedaccess.lease_requiredoraccess.expired_leaserejection.
Declare the labels a policy supports before returning the request handler:
capabilities.declare({
{ id = "project_readonly", label = "Project readonly", default = true },
{ id = "docs_review", label = "Docs review" },
})The share console uses these declarations to render the invite capability menu and rejects undeclared labels submitted to the console API.
For auth-bound leases, lease.info() / context.lease also includes:
auth_bound: true when the matched lease has OAuth identity constraints.auth: sanitized OAuth fields used for the binding check, such assubject,issuer,tenant,client_id,groups, andprofile_id.
Auth-bound lease denials use reason codes such as lease.auth_missing,
lease.subject_not_allowed, lease.issuer_not_allowed,
lease.tenant_not_allowed, lease.client_id_not_allowed,
lease.group_not_allowed, and lease.auth_profile_not_allowed.
For public shares, the intended composition is: valid OAuth subject, required MCP scopes, active snulbug task lease, and Lua policy approval.