Skip to content

Authorise operations for a caller

mcp.New requires a policy. AllowRegistered() is the deliberate choice for a local stdio host serving one person's own commands; a service with more than one caller writes its own.

The two callbacks

policy := mcp.Policy{
    Authorize: func(ctx context.Context, req mcp.AccessRequest) error {
        principal, ok := identityFrom(ctx)
        if !ok {
            return mcp.NewFailure(mcp.FailureUnavailable, "Sign in to use this tool", nil)
        }

        switch req.Kind {
        case mcp.Discover:
            if !principal.MaySee(req.Operation) {
                return mcp.NewFailure(mcp.FailureUnavailable, "Operation not available", nil)
            }
        case mcp.Execute:
            if !principal.MayRun(req.Operation, req.Arguments) {
                return mcp.NewFailure(mcp.FailureUnavailable, "Operation not available", nil)
            }
        }

        return nil
    },
    ScopeKey: func(ctx context.Context) (string, error) {
        principal, ok := identityFrom(ctx)
        if !ok {
            return "", errors.New("no identity")
        }

        return principal.ID + "/" + principal.PolicyRevision, nil
    },
}

Authorize is asked twice for a call that was discovered first: once with Discover when the operation is searched or inspected, and again with Execute, carrying the validated canonical arguments, when it is invoked. The second answer does not depend on the first; a name a caller may inspect is not a name they may run, and a known name may be invoked without any search.

Deny with a FailureUnavailable. Unknown names and denied names produce the same safe answer, so a caller cannot probe the catalogue. Any other error, or a panic, fails the request rather than authorising it.

What ScopeKey is for

ScopeKey names the caller's effective authorisation: an opaque, credential-free string that changes when their permissions change. Search cursors, resource list cursors and confirmation continuations are bound to it, so a token issued to one principal cannot be replayed by another, and one issued under an old policy stops working when the policy moves. Include a policy revision in it when permissions can change under a live session.

Resource policy

mcp.NewResources takes its own ResourcePolicy with four callbacks: Discover for registered descriptions, Visible for whether a planned item is in this caller's selection, Read for body access, and ScopeKey as above. The gateway asks Visible and Read for every item before reading any body, and asks again before delivering, so an item revoked mid-read is removed. A hidden item produces no warning; a visible item denied a read produces one.

Rules the module enforces for you

  • Both callbacks are required and must be safe to call concurrently.
  • Policy runs before application code, on detached arguments, so a handler cannot see a value the policy did not.
  • Every search page and every resume rechecks policy. Nothing caches an authorised catalogue between callers.
  • Hints (readOnly, destructive and the rest) describe an operation; they never grant or withhold access. Presentation hints likewise.

The registry reference states the exact contracts.