@frontmcp/guard

中文文档
1.12.2发布于 2026/11/15Apache-2.0TypeScriptESM + CJS构建来源已验证近 30 天下载 1.2万

Rate limiting, concurrency control, timeout, IP filtering, and traffic guard utilities for FrontMCP

原始使用文档
文档目录23 节
  1. Install
  2. Usage
  3. In a FrontMCP server
  4. On its own
  5. Rate limiting
  6. Concurrency control
  7. Timeout
  8. IP filtering
  9. Partition keys
  10. Guard manager
  11. Configuration reference
  12. GuardConfig
  13. RateLimitConfig
  14. ConcurrencyConfig
  15. TimeoutConfig
  16. IpFilterConfig
  17. Storage
  18. Errors
  19. API reference
  20. Docs
  21. Related packages
  22. Contributing
  23. License

@frontmcp/guard

Rate limiting, concurrency limits, execution timeouts and IP filtering for FrontMCP. @frontmcp/sdk uses it for the throttle option of @FrontMcp and the rateLimit, concurrency and timeout options of tools and agents, so in a FrontMCP server you set those options and don't import this package. Install it directly to use the limiters on their own. They keep their counters behind the StorageAdapter interface of @frontmcp/utils, so they work with memory, Redis, Vercel KV, Upstash and Cloudflare KV storage.

npm License

Install

npm install @frontmcp/guard @frontmcp/lazy-zod zod

@frontmcp/lazy-zod is a peer dependency (the configuration schemas use it), and it needs zod 4. Node.js 24 or later. A FrontMCP server doesn't need this install: @frontmcp/sdk depends on @frontmcp/guard and re-exports everything in it.

Usage

In a FrontMCP server

Set limits on a tool, and defaults for the whole server. The SDK passes these options to this package:

import { App, FrontMcp, Tool, ToolContext } from '@frontmcp/sdk';

@Tool({
  name: 'export_tickets',
  description: 'Export every ticket as CSV. At most 2 exports an hour per caller, one at a time.',
  inputSchema: {},
  rateLimit: { maxRequests: 2, windowMs: 60 * 60_000, partitionBy: 'userId' },
  concurrency: { maxConcurrent: 1, queueTimeoutMs: 5_000 },
  timeout: { executeMs: 30_000 },
})
class ExportTickets extends ToolContext {
  async execute() {
    return { csv: 'id,title' };
  }
}

@App({ id: 'help-desk', name: 'Help Desk', tools: [ExportTickets] })
class HelpDeskApp {}

@FrontMcp({
  info: { name: 'help-desk', version: '1.0.0' },
  apps: [HelpDeskApp],
  throttle: {
    enabled: true,
    defaultRateLimit: { maxRequests: 60, windowMs: 60_000, partitionBy: 'userId' },
    global: { maxRequests: 1_000, windowMs: 60_000, partitionBy: 'ip' },
    ipFilter: { denyList: ['192.0.2.0/24'] },
  },
})
export default class Server {}

Guard options covers what each option does in a server, and the error a caller gets back.

On its own

createGuardManager builds a GuardManager from one configuration object:

import { createGuardManager } from '@frontmcp/guard';

const guard = await createGuardManager({
  config: {
    enabled: true,
    defaultRateLimit: { maxRequests: 100, windowMs: 60_000, partitionBy: 'session' },
    defaultConcurrency: { maxConcurrent: 5 },
    ipFilter: { denyList: ['10.0.0.0/8'], defaultAction: 'allow' },
  },
});

const ipResult = guard.checkIpFilter('203.0.113.5');
if (ipResult && !ipResult.allowed) throw new Error('IP refused');

const rateLimit = await guard.checkRateLimit('my-tool', undefined, { sessionId: 'sess-123' });
if (!rateLimit.allowed) throw new Error(`Retry after ${rateLimit.retryAfterMs}ms`);

const ticket = await guard.acquireSemaphore('my-tool', undefined, { sessionId: 'sess-123' });
if (!ticket) throw new Error('No free slot');
try {
  // ... do the work ...
} finally {
  await ticket.release();
}

await guard.destroy();

Without a storage configuration the manager keeps its counters in memory, one set per process, and warns about it through logger when you pass one.

Rate limiting

SlidingWindowRateLimiter uses the sliding window counter algorithm. It keeps two adjacent fixed-window counters and weights the previous window's count by how much of that window still falls within windowMs of now. That approximates a true sliding window with constant storage per key, without the bursts a single fixed window allows at its edges. Windows are aligned to the clock.

The class takes a storage adapter. createMemoryStorage and the other adapters come from @frontmcp/utils, which is installed with this package; add it to your own dependencies to import it.

import { SlidingWindowRateLimiter } from '@frontmcp/guard';
import { createMemoryStorage } from '@frontmcp/utils';

const storage = createMemoryStorage();
await storage.connect();
const limiter = new SlidingWindowRateLimiter(storage);

const result = await limiter.check(
  'user:42', // key
  100, // max requests
  60_000, // window in ms
);

if (!result.allowed) {
  console.log(`Rate limited. Retry after ${result.retryAfterMs}ms`);
}

An allowed check counts the request. RateLimitResult has:

  • allowed: whether the request can proceed
  • remaining: approximate requests left in this window
  • resetMs: milliseconds until the current window ends
  • retryAfterMs: when blocked, how long to wait

reset(key, windowMs) deletes a key's counters.

Concurrency control

DistributedSemaphore limits how many executions run at once for a key. The key has maxConcurrent slots, each stored as its own key. An execution claims a free slot with a write-if-absent that carries its ticket's time to live (TTL, 300 seconds by default), and renews the TTL every third of it until it calls release(). So the slot is held for as long as the execution runs, and the TTL only frees the slot of a process that died without releasing it. Releasing or renewing only touches a slot that still holds the same ticket, so the storage needs an atomic compare-and-delete and compare-and-expire: memory, Redis, Upstash or Vercel KV. With another adapter, acquire() throws GuardStorageUnsupportedError, and createGuardManager does too when globalConcurrency or defaultConcurrency is set.

When every slot is taken, a caller can wait for one, polling every 100 ms at first and backing off to once a second. If the storage supports publish/subscribe (memory, Redis, and Upstash with publish/subscribe turned on), a released slot wakes waiting callers at once.

import { DistributedSemaphore } from '@frontmcp/guard';
import { createMemoryStorage } from '@frontmcp/utils';

const storage = createMemoryStorage();
await storage.connect();
const semaphore = new DistributedSemaphore(storage, 300 /* ticket TTL in seconds */);

const ticket = await semaphore.acquire(
  'my-tool:global', // key
  5, // max concurrent
  10_000, // how long to wait for a slot, in ms (0 = don't wait)
  'my-tool', // name used in error messages
);

if (ticket) {
  try {
    // ... do the work ...
  } finally {
    await ticket.release();
  }
}

acquire returns null when every slot is taken and the wait is 0, and throws QueueTimeoutError when it waited and no slot freed up. getActiveCount(key) returns the number of taken slots, and forceReset(key) frees every slot.

Timeout

withTimeout runs an async function with a deadline. It passes the function an AbortSignal that is aborted when time runs out, and rejects with ExecutionTimeoutError. The function keeps running unless it listens to the signal.

import { withTimeout } from '@frontmcp/guard';

const response = await withTimeout(
  (signal) => fetch('https://api.example.com/data', { signal }),
  5_000, // timeout in ms
  'fetch-data', // name used in error messages
);

IP filtering

IpFilter checks a client IP against allow and deny lists of addresses and CIDR ranges, IPv4 and IPv6. An IPv4-mapped IPv6 address (::ffff:192.168.1.1, which a dual-stack socket reports for an IPv4 client) matches as the IPv4 address it maps. Matching uses bigint arithmetic, so it's exact across the whole IPv6 address space.

The deny list is checked first: an IP on it is refused, whatever the allow list says. Then the allow list, then defaultAction. An allow list on its own refuses nothing: add defaultAction: 'deny' to refuse every other address. A missing or unparseable IP matches no rule and gets defaultAction, so check(undefined) refuses under defaultAction: 'deny'.

import { IpFilter } from '@frontmcp/guard';

const filter = new IpFilter({
  allowList: ['192.168.0.0/16'],
  denyList: ['192.168.1.100'],
  defaultAction: 'deny',
});

const result = filter.check('192.168.2.50');
// { allowed: true, reason: 'allowlisted', matchedRule: '192.168.0.0/16' }

const blocked = filter.check('192.168.1.100');
// { allowed: false, reason: 'denylisted', matchedRule: '192.168.1.100' }

isAllowListed(ip) tells you whether an IP is on the allow list, for example to skip rate limiting for trusted addresses.

Partition keys

A partition key decides which calls share a rate-limit count or a set of concurrency slots:

StrategyCalls that share a count
'global'All of them (the default)
'ip'Calls from the same client IP. Without an IP, calls by the same userId; without either, one bucket
'session'Calls in the same session
'userId'Calls by the same authenticated user, or in the same session when there's no user

You can also pass a function of the PartitionKeyContext, which has sessionId (always set) and optional clientIp and userId:

import type { RateLimitConfig } from '@frontmcp/guard';

const config: RateLimitConfig = {
  maxRequests: 100,
  windowMs: 60_000,
  partitionBy: (ctx) => (ctx.userId ? `user:${ctx.userId}` : `session:${ctx.sessionId}`),
};

Guard manager

GuardManager combines rate limiting, concurrency control and IP filtering behind one configuration. createGuardManager creates its storage and connects it.

The configuration has two levels:

  • Global, applied to every request: global (a rate limit) and globalConcurrency.
  • Default, applied to an entity that doesn't pass its own: defaultRateLimit, defaultConcurrency, defaultTimeout.

A configuration passed for one entity replaces the default; it doesn't add to it.

import { createGuardManager } from '@frontmcp/guard';

const guard = await createGuardManager({
  config: {
    enabled: true,
    storage: { type: 'redis', redis: { config: { host: 'localhost', port: 6379 } } },
    keyPrefix: 'myapp:guard:',
    global: { maxRequests: 1000, windowMs: 60_000, partitionBy: 'ip' },
    globalConcurrency: { maxConcurrent: 50, partitionBy: 'global' },
    defaultRateLimit: { maxRequests: 100, windowMs: 60_000, partitionBy: 'session' },
    defaultConcurrency: { maxConcurrent: 10, queueTimeoutMs: 5_000 },
    defaultTimeout: { executeMs: 30_000 },
    ipFilter: {
      denyList: ['10.0.0.0/8'],
      allowList: ['10.0.1.0/24'],
      defaultAction: 'allow',
    },
  },
  logger: console,
});

const context = { sessionId: 'sess-1', clientIp: '203.0.113.5' };
const globalRateLimit = await guard.checkGlobalRateLimit(context);
const entityRateLimit = await guard.checkRateLimit('my-tool', undefined, context);
const globalTicket = await guard.acquireGlobalSemaphore(context);
const ticket = await guard.acquireSemaphore('my-tool', undefined, context);

await ticket?.release();
await globalTicket?.release();
await guard.destroy();
MethodReturns
checkIpFilter(ip)An IpFilterResult, or undefined when there's no ipFilter
isIpAllowListed(ip)Whether the IP is on the allow list
checkRateLimit(name, config, context)A RateLimitResult. With no config and no defaultRateLimit, always allowed
checkGlobalRateLimit(context)A RateLimitResult against global
acquireSemaphore(name, config, context)A SemaphoreTicket, or null when no slot is free or there's no limit to apply
acquireGlobalSemaphore(context)A SemaphoreTicket against globalConcurrency, or null
destroy()Disconnects the storage

The manager doesn't apply defaultTimeout itself: read it from guard.config and pass it to withTimeout.

Configuration reference

GuardConfig

FieldTypeDefaultDescription
enabledbooleanrequiredWhether the guard is on. @frontmcp/sdk reads it; createGuardManager doesn't
storageStorageConfigmemoryStorage backend, from @frontmcp/utils
keyPrefixstring'mcp:guard:'Prefix for every storage key (a trailing : is dropped)
globalRateLimitConfignoneRate limit for every request
globalConcurrencyConcurrencyConfignoneConcurrency limit across every entity
defaultRateLimitRateLimitConfignoneRate limit for entities without their own
defaultConcurrencyConcurrencyConfignoneConcurrency limit for entities without their own
defaultTimeoutTimeoutConfignoneTimeout for entities without their own
ipFilterIpFilterConfignoneIP filtering

RateLimitConfig

FieldTypeDefaultDescription
maxRequestsnumberrequiredRequests allowed per window
windowMsnumber60000Window length in milliseconds
partitionByPartitionKey'global'Which calls share a count

ConcurrencyConfig

FieldTypeDefaultDescription
maxConcurrentnumberrequiredExecutions allowed at once
queueTimeoutMsnumber0How long to wait for a free slot (0 refuses at once)
partitionByPartitionKey'global'Which calls share the slots

TimeoutConfig

FieldTypeDefaultDescription
executeMsnumberrequiredMaximum execution time in milliseconds

IpFilterConfig

FieldTypeDefaultDescription
allowListstring[]noneIP addresses or CIDR ranges to allow
denyListstring[]noneIP addresses or CIDR ranges to refuse
defaultAction'allow' | 'deny''allow'What happens to an IP on neither list, or when the client IP isn't known
trustProxybooleanfalseRead by @frontmcp/sdk, not by IpFilter: true takes the client IP from X-Forwarded-For; false leaves it to FRONTMCP_TRUST_PROXY
trustedProxyDepthnumber1Read by @frontmcp/sdk when trustProxy is true: how many proxies in front of the server append to X-Forwarded-For

Storage

The guard keeps everything behind the StorageAdapter interface of @frontmcp/utils. createGuardManager builds one from storage with createStorage:

BackendUse it for
Memory (type: 'memory', the default)Development, tests and single-process servers. Each process counts on its own
Redis (type: 'redis')Several instances sharing limits. Publish/subscribe wakes callers waiting for a slot
Vercel KV (type: 'vercel-kv')Servers on Vercel
Upstash (type: 'upstash')Serverless functions: Redis over HTTP, with optional publish/subscribe
Cloudflare KV (type: 'cloudflare-kv')Cloudflare Workers, with the KV namespace passed in

storage is a StorageConfig, such as { type: 'redis', redis: { config: { host, port } } } or { type: 'redis', redis: { url } }. Without type it's 'auto', which picks a backend from environment variables.

storage.fallback decides what happens when the backend can't be reached. With 'error', the default in production (NODE_ENV=production), createGuardManager rejects with GuardStorageUnavailableError: rate limits fail closed. With 'memory', the default elsewhere, it starts with per-instance counters and logs a warning. The same setting applies if the backend stops answering later: each check then fails with GuardStorageUnavailableError, or, with 'memory', is served from per-instance counters until the backend answers again.

Keys are written as <keyPrefix><entity>:<partition>:<kind>:… with a single : after the prefix, for example mcp:guard:export_tickets:global:rl:…. Before 1.8.6 the default prefix produced mcp:guard::export_tickets:…. Counters written by an older instance aren't read by a newer one, so limits briefly split during a rolling deploy.

Errors

Every error extends GuardError, which has a machine-readable code and an HTTP statusCode. instanceof GuardError also recognizes an error from another copy of this package in the same process.

Error classCodeStatusThrown by
ExecutionTimeoutErrorEXECUTION_TIMEOUT408withTimeout, when time runs out
QueueTimeoutErrorQUEUE_TIMEOUT429DistributedSemaphore.acquire, when it waited and no slot freed up
GuardStorageUnavailableErrorGUARD_STORAGE_UNAVAILABLE503createGuardManager and the manager's checks, when the storage can't be reached
ConcurrencyLimitErrorCONCURRENCY_LIMIT429Your code (and @frontmcp/sdk), when acquireSemaphore returns null
IpBlockedErrorIP_BLOCKED403Your code, for an IP on the deny list
IpNotAllowedErrorIP_NOT_ALLOWED403Your code, for an IP not on the allow list
import { ExecutionTimeoutError, withTimeout } from '@frontmcp/guard';

try {
  await withTimeout(() => new Promise((resolve) => setTimeout(resolve, 10_000)), 5_000, 'slow-op');
} catch (err) {
  if (err instanceof ExecutionTimeoutError) {
    console.log(err.code); // 'EXECUTION_TIMEOUT'
    console.log(err.statusCode); // 408
    console.log(err.timeoutMs); // 5000
  }
}

API reference

Everything is exported from the package root.

KindExports
ClassesGuardManager, SlidingWindowRateLimiter, DistributedSemaphore, IpFilter
FunctionscreateGuardManager, withTimeout, resolvePartitionKey(partitionBy, context), buildStorageKey(entity, partition, suffix?)
ErrorsGuardError, ExecutionTimeoutError, ConcurrencyLimitError, QueueTimeoutError, IpBlockedError, IpNotAllowedError, GuardStorageUnavailableError
Zod schemasguardConfigSchema, rateLimitConfigSchema, concurrencyConfigSchema, timeoutConfigSchema, ipFilterConfigSchema, partitionKeySchema
TypesGuardConfig, GuardLogger, GuardManagerOptions, CreateGuardManagerArgs, RateLimitConfig, RateLimitResult, ConcurrencyConfig, SemaphoreTicket, TimeoutConfig, IpFilterConfig, IpFilterResult
Partition typesPartitionKey, PartitionKeyStrategy, CustomPartitionKeyFn, PartitionKeyContext
Schema inputsGuardConfigInput, RateLimitConfigInput, ConcurrencyConfigInput, TimeoutConfigInput, IpFilterConfigInput

Docs

  • @frontmcp/sdk: the framework that applies these limits to tools and agents
  • @frontmcp/utils: the storage adapters the guard keeps its counters in

Contributing

Source: libs/guard. Issues and pull requests are welcome; see CONTRIBUTING.md.

License

Apache-2.0. See LICENSE.

版本与发布记录

@frontmcp/guard 共发布 98 个版本,首次发布于 2026/03/17;过去 12 个月发布了 98 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.12.2latest2026/11/151 天前
1.12.12026/11/132 天前
1.12.02026/11/114 天前
1.12.0-rc.1rc2026/11/114 天前
1.11.32026/11/106 天前
1.11.22026/11/096 天前

其他发布通道:beta → 1.1.2-beta.1,rc → 1.12.0-rc.1。

关于 @frontmcp/guard 的常见问题

@frontmcp/guard 是什么?

Rate limiting, concurrency control, timeout, IP filtering, and traffic guard utilities for FrontMCP

如何安装 @frontmcp/guard?

在项目目录运行 npm install @frontmcp/guard,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@frontmcp/lazy-zod(1.12.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@frontmcp/guard 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@frontmcp/guard 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@frontmcp/guard 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@frontmcp/guard 可以商用吗?

@frontmcp/guard 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@frontmcp/guard 的下载量有多少?

根据 npm 官方下载统计,@frontmcp/guard 在 2026/10/16 – 2026/11/14 这 30 天内被下载 11,947 次,日均约 398 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@frontmcp/guard 还在维护吗?

最新版本 1.12.2 发布于 2026/11/15(1 天前);至今共发布 98 个版本,其中过去 12 个月发布了 98 个;首次发布于 2026/03/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。