AnnouncementsIntroducing Arkoren

    Menu

    Capability Access

    Filter tool availability by caller capability while enforcing resource authorization in shared application operations.

    What's on this page
    1. Offer tools by a shared capability rule without granting access to every resource.
    2. Authorize selected records inside shared application service methods.
    3. Add availability to tool copies and understand per-request checks and private failures.
    On this page

    A plan may unlock shipment tracking without granting access to every shipment. Availability decides whether a caller is offered an operation. Resource authorization decides whether that caller may access the selected record. Use a tool availability policy for the first decision and a shared application service for the second.

    Start with typed authentication already storing your application user in context. Admission to /mcp is not permission to use every tool or read every record. Reuse your existing Gate rules, not a separate MCP permission model.

    The examples import standard context, arkoren.dev/core/mcp, arkoren.dev/core/http/auth, and arkoren.dev/core/http/auth/gate.

    Capability Rules

    Assume these are your application’s identity and tracking request/result types:

    type User struct {
    	ID    string
    	Paid  bool
    	Admin bool
    }
    
    type ReadShipment struct {
    	ID string `json:"id"`
    }
    
    type Shipment struct {
    	ID    string `json:"id"`
    	State string `json:"state"`
    }

    An application-wide rule can permit tracking for paid accounts and administrators:

    func canTrack(ctx context.Context, user User) (bool, error) {
    	return user.Paid || user.Admin, nil
    }

    Adapt that shared rule to the context-only callback MCP needs:

    func trackingAvailable(ctx context.Context) (bool, error) {
    	user, ok := auth.FromContext[User](ctx)
    	if !ok {
    		return false, nil
    	}
    	return canTrack(ctx, user)
    }

    Missing identity denies availability here. The lookup must use the same concrete User type as admission; User and *User are different slots. If a real plan lookup fails, return its error rather than turning an outage into an ordinary denial. Policies must be observational and safe for concurrent use: listing tools must not change business data. Pass context into backend lookups.

    Resource Checks

    The policy above receives no arguments, so it cannot authorize input.ID. Place that check in the shared operation before loading the shipment. This sample service accepts your existing loader and resource-rule factory:

    type ShipmentService struct {
    	Load    func(context.Context, ReadShipment) (Shipment, error)
    	CanRead func(string) func(context.Context, User) (bool, error)
    }

    Construct it with non-nil application collaborators. CanRead(id) must enforce your tenant, ownership, or other resource rules; an ID alone is not evidence of ownership. Its shared method authorizes before loading:

    func (service *ShipmentService) Read(ctx context.Context, input ReadShipment) (Shipment, error) {
    	if err := gate.Authorize(ctx, service.CanRead(input.ID)); err != nil {
    		return Shipment{}, err
    	}
    	return service.Load(ctx, input)
    }

    Gate rejects missing identity, denies a false rule, and propagates a rule error before Load runs. Use this same method from HTTP and MCP; a check only in an MCP wrapper leaves other callers able to bypass it. If plan entitlement is a business requirement on every transport, enforce the shared capability rule in that service path too. Discovery filtering is not a substitute.

    With a configured shipments *ShipmentService, publish the operation:

    read := mcp.NewTool("shipments.read", shipments.Read, func(action *mcp.ToolBuilder[ReadShipment, Shipment]) {
    	action.Description("Read the state of a shipment the caller may access.")
    	action.ReadOnly(true)
    	action.Available(trackingAvailable)
    })

    Availability controls discovery and execution; shipments.Read still owns the selected-record decision. Add ID validation using the input-schema pattern. ReadOnly remains a hint, not a guard.

    Omitting Available permits every admitted caller. Explicit Available(nil) instead records a sticky declaration error, even if a later callback provides a valid policy.

    Guarded Copies

    Package factories such as MCPTools(prefix) can return definitions with their own policies. Select only the tools your endpoint needs, then add application availability without mutating the owner’s definition. With tool as one selected mcp.Tool:

    guarded := tool.WithAvailability(trackingAvailable)

    The copy requires the original policy and then the added policy. Denial short-circuits; neither policy runs during construction. The original tool stays unchanged, and nil policy is invalid. Captured state remains shared.

    This is still a capability gate, not a resource policy. For reusable libraries and arks, keep application user types, plan names, and permission rules application-owned. Pass a shared authorized operation or a domain authorization callback into your own integration rather than teaching the library your authentication model.

    Request Evaluation

    The adapter evaluates availability for every managed tool during tools/list. It evaluates availability again on every call, before argument decoding and validation. Guessing a hidden name cannot bypass this check, and a prior list does not grant a durable permission. Register one static catalog; do not cache per-user servers to implement discovery filtering.

    Discovery returns the complete filtered catalog without pagination or a next cursor. A nonempty cursor is rejected. Native SDK tools remain visible without managed availability; see Native Extensions.

    Tool.Available(ctx) can check policy directly without decoding or invoking the handler. It validates the declaration and cancellation first, checks cancellation again after successful policy evaluation, and caches nothing. Core also checks cancellation after input parsing, before handler execution.

    Evaluation Failures

    A false, nil policy result is an ordinary denial. Direct Tool.Call returns mcp.ErrUnavailable; the endpoint returns a failed tool result with message tool unavailable. A policy error preserves its cause under mcp.ErrAvailability in core, but becomes tool failed on the wire, even if it wraps mcp.ToolError or mcp.ToolFailure.

    A policy error or panic during listing fails the entire request with tool listing failed, not a partial catalog. Core propagates policy and handler panics; the adapter recovers them. Original errors and panic values stay in protected local logs. Safe Failures explains the other redaction boundaries.

    Test that denied callers never reach decoding or the handler, and that a permitted capability still cannot read another tenant’s resource. Direct core tests cover policy/service behavior; SDK HTTP tests cover discovery, identity propagation, and safe responses.

    Shared LLM Policy

    If your application also runs an LLM agent, reuse the typed handler, input schema, and context policy in a separate llm.NewTool declaration. These are shared application inputs, not converters between tool types. MCP lists availability per request; LLM discovery is fixed once per run and execution has its own checks. See Offering Capabilities for that boundary.

    For exact declaration and policy contracts, see the core MCP API and MCP server API.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive