@frontmcp/guard package. This library is SDK-agnostic and works with any StorageAdapter backend.
When using
@frontmcp/sdk, guard features are integrated automatically via the throttle config and tool/agent decorators. You only need to import from @frontmcp/guard directly if building custom integrations.Configuration Types
GuardConfig
Top-level configuration for the guard system. Passed to @FrontMcp({ throttle: ... }) or createGuardManager().
RateLimitConfig
Configuration for sliding window rate limiting.
ConcurrencyConfig
Configuration for distributed semaphore concurrency control.
TimeoutConfig
Configuration for execution timeout.
IpFilterConfig
Configuration for IP-based access control.
PartitionKey
Determines how limits are bucketed across requests.
PartitionKeyContext:
Classes
GuardManager
Orchestrates all guard features. Created via createGuardManager() or automatically by the SDK when throttle is configured.
checkRateLimit()
Check per-entity rate limit.
Returns
{ allowed: true, remaining: Infinity, resetMs: 0 } if no config applies.
checkGlobalRateLimit()
Check global (server-wide) rate limit.
config.global configuration. Returns allowed result if no global config.
acquireSemaphore()
Acquire a concurrency slot for an entity.
Returns
SemaphoreTicket on success, null if no config applies. May throw QueueTimeoutError if queue timeout expires.
acquireGlobalSemaphore()
Acquire a global concurrency slot.
config.globalConcurrency configuration.
checkIpFilter()
Check if a client IP is allowed.
undefined only when no IP filter is configured. A missing or unparseable clientIp matches no rule and gets the configured defaultAction, so with defaultAction: 'deny' it is rejected.
isIpAllowListed()
Check if a client IP is explicitly on the allow list.
true only if an IP filter is configured, clientIp is provided, and the IP matches the allow list.
destroy()
Disconnect storage backend and clean up resources.
SlidingWindowRateLimiter
Implements sliding window rate limiting with O(1) storage per key.
check()
Check and consume a rate limit token.
estimatedCount < maxRequests, the request is allowed and the current window counter is atomically incremented.
RateLimitResult:
reset()
Reset rate limit counters for a key.
DistributedSemaphore
Implements a distributed counting semaphore with optional queuing.
ticketTtlSeconds: 300 (5 minutes).
acquire()
Acquire a semaphore slot.
- Returns
SemaphoreTicketif a slot is acquired. - Returns
nullifqueueTimeoutMs <= 0and no slot is available. - Throws
QueueTimeoutErrorif queued and timeout expires.
SemaphoreTicket:
getActiveCount()
Get current number of active tickets for a key.
forceReset()
Force-clear all tickets and reset the counter for a key.
IpFilter
IP-based access control with CIDR support for IPv4 and IPv6.
check()
Check if a client IP is allowed.
- Deny list (takes precedence)
- Allow list
- Default action — also for a missing or unparseable IP
::ffff:203.0.113.7, ::ffff:cb00:7107) is matched as the IPv4 address it maps, and a rule written in mapped form (::ffff:10.0.0.0/104) as the IPv4 rule (10.0.0.0/8). A %zone suffix is ignored.
IpFilterResult:
isAllowListed()
Check if an IP is explicitly on the allow list.
Functions
createGuardManager()
Factory function to create a fully initialized GuardManager.
CreateGuardManagerArgs:
Behavior:
- Creates storage backend from
config.storage(or in-memory if not set) - Connects the storage backend. A configured backend that cannot be created or reached rejects with
GuardStorageUnavailableError— rate limits fail closed — unlessconfig.storage.fallbackis'memory'. The returned manager applies the same policy if the backend stops answering later: limit checks and slot acquisitions reject withGuardStorageUnavailableError, or, withfallback: 'memory', run on per-instance counters and retry the configured backend every 30 seconds (GuardManagerOptions.retryPrimaryAfterMs). Releasing a concurrency slot never throws.createStoragedefaultsfallbackto'error'in production and'memory'otherwise. - Creates namespaced storage with
config.keyPrefix, minus any trailing:(the namespace adds its own separator) - Returns initialized
GuardManager
config.storage is a StorageConfig from @frontmcp/utils: type selects the backend and its options go under the key of that name — { type: 'redis', redis: { config: { host, port, password?, tls? } } }, { type: 'redis', redis: { url } }, { type: 'vercel-kv', vercelKv: { url, token } } or { type: 'upstash', upstash: { url, token } }. A block without type is auto-detected from the environment.
Keys are
mcp:guard:<entity>:<partition>:<kind>:…. Before 1.8.6 the default prefix wrote mcp:guard::<entity>:…; the two versions do not read each other’s counters, so limits briefly split during a rolling deploy.withTimeout()
Wraps an async function with an execution deadline.
Throws
ExecutionTimeoutError if the deadline is exceeded, and aborts the signal passed to fn so the work can stop. Uses AbortController + Promise.race internally.
resolvePartitionKey()
Resolve a partition key strategy to a concrete string value.
buildStorageKey()
Build a namespaced storage key from components.
Error Classes
All guard errors extendGuardError, which has code (string) and statusCode (number) properties.
ExecutionTimeoutError
Thrown when execution exceeds the configured timeout.
ConcurrencyLimitError
Thrown when no concurrency slot is available and queueTimeoutMs is 0.
QueueTimeoutError
Thrown when a queued request exceeds its wait time.
IpBlockedError
For your own code to throw when a client IP matches a deny list. The SDK’s built-in throttle.ipFilter does not throw it; it answers the HTTP request with 403 from each flow’s checkIpFilter stage.
IpNotAllowedError
For your own code to throw when a client IP is not on an allow list and defaultAction is 'deny'. Not thrown by the built-in filter.
GuardStorageUnavailableError
Thrown when the configured storage backend cannot be created or reached: by createGuardManager() at SDK startup, and by GuardManager limit checks (checkRateLimit, checkGlobalRateLimit, acquireSemaphore, acquireGlobalSemaphore) if the backend goes away while the server runs. The message names throttle.storage and says to set throttle.storage.fallback: 'memory' to use per-instance counters instead. That message is for the server log: the SDK answers the client with a generic “Service temporarily unavailable” (HTTP 503 from the throttle.global check, or an isError tools/call result with _meta.code: 'GUARD_STORAGE_UNAVAILABLE' from a per-tool limit).