Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion libs/guard/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
"projectType": "library",
"tags": ["scope:libs", "scope:publishable", "versioning:synchronized"],
"targets": {
"generate-types": {
"executor": "nx:run-commands",
"options": {
"command": "npx tsx scripts/generate-schema-types.mjs"
}
},
"build-cjs": {
"executor": "@nx/esbuild:esbuild",
"dependsOn": ["^build"],
"dependsOn": ["^build", "generate-types"],
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "libs/guard/dist",
Expand Down
7 changes: 7 additions & 0 deletions libs/guard/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ export {
ipFilterConfigSchema,
guardConfigSchema,
} from './schemas';
export type {
ConcurrencyConfigInput,
RateLimitConfigInput,
TimeoutConfigInput,
IpFilterConfigInput,
GuardConfigInput,
} from './schemas.generated';
111 changes: 111 additions & 0 deletions libs/guard/src/schemas/schemas.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// AUTO-GENERATED from Zod schemas — do not edit manually.
// Run: npx tsx scripts/generate-schema-types.mjs
// Source: scripts/generate-schema-types.mjs

import type { PartitionKey } from '../partition-key/types';

/**
* Input type for concurrency control configuration.
* All fields are optional for IDE autocomplete; required fields
* are validated at runtime by concurrencyConfigSchema.
*/
export interface ConcurrencyConfigInput {
/** Maximum number of concurrent executions allowed. */
maxConcurrent?: number;
/**
* Maximum time in ms to wait in queue (0 = no wait).
* @default 0
*/
queueTimeoutMs?: number;
/**
* Partition key strategy.
* @default "global"
*/
partitionBy?: PartitionKey;
}

/**
* Input type for rate limiting configuration.
* All fields are optional for IDE autocomplete; required fields
* are validated at runtime by rateLimitConfigSchema.
*/
export interface RateLimitConfigInput {
/** Maximum number of requests allowed within the window. */
maxRequests?: number;
/**
* Time window in milliseconds.
* @default 60000
*/
windowMs?: number;
/**
* Partition key strategy.
* @default "global"
*/
partitionBy?: PartitionKey;
}

/**
* Input type for timeout configuration.
* All fields are optional for IDE autocomplete; required fields
* are validated at runtime by timeoutConfigSchema.
*/
export interface TimeoutConfigInput {
/** Maximum execution time in milliseconds. */
executeMs?: number;
}

/**
* Input type for IP filtering configuration.
* All fields are optional for IDE autocomplete; required fields
* are validated at runtime by ipFilterConfigSchema.
*/
export interface IpFilterConfigInput {
/** IP addresses or CIDR ranges to always allow. */
allowList?: Array<string>;
/** IP addresses or CIDR ranges to always block. */
denyList?: Array<string>;
/**
* Default action when IP matches neither list.
* @default "allow"
*/
defaultAction?: 'allow' | 'deny';
/**
* Trust X-Forwarded-For header.
* @default false
*/
trustProxy?: boolean;
/**
* Max number of proxies to trust from X-Forwarded-For.
* @default 1
*/
trustedProxyDepth?: number;
}

/**
* Input type for guard system configuration.
* All fields are optional for IDE autocomplete; required fields
* are validated at runtime by guardConfigSchema.
*/
export interface GuardConfigInput {
/** Whether the guard system is enabled. */
enabled?: boolean;
/** Storage backend configuration. */
storage?: Record<string, unknown>;
/**
* Key prefix for all storage keys.
* @default "mcp:guard:"
*/
keyPrefix?: string;
/** Global rate limit applied to all requests. */
global?: RateLimitConfigInput;
/** Global concurrency limit. */
globalConcurrency?: ConcurrencyConfigInput;
/** Default rate limit for entities without explicit config. */
defaultRateLimit?: RateLimitConfigInput;
/** Default concurrency for entities without explicit config. */
defaultConcurrency?: ConcurrencyConfigInput;
/** Default timeout for entity execution. */
defaultTimeout?: TimeoutConfigInput;
/** IP filtering configuration. */
ipFilter?: IpFilterConfigInput;
}
62 changes: 41 additions & 21 deletions libs/guard/src/schemas/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,65 @@ export const partitionKeySchema = z.union([
// ============================================

export const rateLimitConfigSchema = z.object({
maxRequests: z.number().int().positive(),
windowMs: z.number().int().positive().optional().default(60_000),
partitionBy: partitionKeySchema.optional().default('global'),
maxRequests: z.number().int().positive().describe('Maximum number of requests allowed within the window.'),
windowMs: z.number().int().positive().optional().default(60_000).describe('Time window in milliseconds.'),
partitionBy: partitionKeySchema.optional().default('global').describe('Partition key strategy.'),
});

export const concurrencyConfigSchema = z.object({
maxConcurrent: z.number().int().positive(),
queueTimeoutMs: z.number().int().nonnegative().optional().default(0),
partitionBy: partitionKeySchema.optional().default('global'),
maxConcurrent: z.number().int().positive().describe('Maximum number of concurrent executions allowed.'),
queueTimeoutMs: z
.number()
.int()
.nonnegative()
.optional()
.default(0)
.describe('Maximum time in ms to wait in queue (0 = no wait).'),
partitionBy: partitionKeySchema.optional().default('global').describe('Partition key strategy.'),
});

export const timeoutConfigSchema = z.object({
executeMs: z.number().int().positive(),
executeMs: z.number().int().positive().describe('Maximum execution time in milliseconds.'),
});

// ============================================
// IP Filter Config Schema
// ============================================

export const ipFilterConfigSchema = z.object({
allowList: z.array(z.string()).optional(),
denyList: z.array(z.string()).optional(),
defaultAction: z.enum(['allow', 'deny']).optional().default('allow'),
trustProxy: z.boolean().optional().default(false),
trustedProxyDepth: z.number().int().positive().optional().default(1),
allowList: z.array(z.string()).optional().describe('IP addresses or CIDR ranges to always allow.'),
denyList: z.array(z.string()).optional().describe('IP addresses or CIDR ranges to always block.'),
defaultAction: z
.enum(['allow', 'deny'])
.optional()
.default('allow')
.describe('Default action when IP matches neither list.'),
trustProxy: z.boolean().optional().default(false).describe('Trust X-Forwarded-For header.'),
trustedProxyDepth: z
.number()
.int()
.positive()
.optional()
.default(1)
.describe('Max number of proxies to trust from X-Forwarded-For.'),
});

// ============================================
// Guard Config Schema (App-Level)
// ============================================

export const guardConfigSchema = z.object({
enabled: z.boolean(),
storage: z.looseObject({}).optional(),
keyPrefix: z.string().optional().default('mcp:guard:'),
global: rateLimitConfigSchema.optional(),
globalConcurrency: concurrencyConfigSchema.optional(),
defaultRateLimit: rateLimitConfigSchema.optional(),
defaultConcurrency: concurrencyConfigSchema.optional(),
defaultTimeout: timeoutConfigSchema.optional(),
ipFilter: ipFilterConfigSchema.optional(),
enabled: z.boolean().describe('Whether the guard system is enabled.'),
storage: z.looseObject({}).optional().describe('Storage backend configuration.'),
keyPrefix: z.string().optional().default('mcp:guard:').describe('Key prefix for all storage keys.'),
global: rateLimitConfigSchema.optional().describe('Global rate limit applied to all requests.'),
globalConcurrency: concurrencyConfigSchema.optional().describe('Global concurrency limit.'),
defaultRateLimit: rateLimitConfigSchema
.optional()
.describe('Default rate limit for entities without explicit config.'),
defaultConcurrency: concurrencyConfigSchema
.optional()
.describe('Default concurrency for entities without explicit config.'),
defaultTimeout: timeoutConfigSchema.optional().describe('Default timeout for entity execution.'),
ipFilter: ipFilterConfigSchema.optional().describe('IP filtering configuration.'),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import 'reflect-metadata';
import { z } from 'zod';
import { Tool, ToolContext } from '../../';

describe('Tool decorator guard config validation', () => {
it('should accept valid concurrency config', () => {
expect(() => {
@Tool({
name: 'valid-concurrency',
inputSchema: { query: z.string() },
concurrency: { maxConcurrent: 5 },
})
class ValidTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return ValidTool;
}).not.toThrow();
});

it('should accept valid rateLimit config', () => {
expect(() => {
@Tool({
name: 'valid-rate-limit',
inputSchema: { query: z.string() },
rateLimit: { maxRequests: 100, windowMs: 60_000 },
})
class ValidTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return ValidTool;
}).not.toThrow();
});

it('should accept valid timeout config', () => {
expect(() => {
@Tool({
name: 'valid-timeout',
inputSchema: { query: z.string() },
timeout: { executeMs: 30_000 },
})
class ValidTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return ValidTool;
}).not.toThrow();
});

it('should accept tool with all guard configs', () => {
expect(() => {
@Tool({
name: 'all-guards',
inputSchema: { query: z.string() },
concurrency: { maxConcurrent: 3, queueTimeoutMs: 5000 },
rateLimit: { maxRequests: 50, windowMs: 30_000 },
timeout: { executeMs: 10_000 },
})
class AllGuardsTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return AllGuardsTool;
}).not.toThrow();
});

it('should accept tool with no guard config', () => {
expect(() => {
@Tool({
name: 'no-guards',
inputSchema: { query: z.string() },
})
class SimpleTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return SimpleTool;
}).not.toThrow();
});

it('should reject concurrency config missing maxConcurrent at runtime', () => {
expect(() => {
@Tool({
name: 'bad-concurrency',
inputSchema: { query: z.string() },
concurrency: {},
})
class BadTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return BadTool;
}).toThrow();
});

it('should reject rateLimit config missing maxRequests at runtime', () => {
expect(() => {
@Tool({
name: 'bad-rate-limit',
inputSchema: { query: z.string() },
rateLimit: {},
})
class BadTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return BadTool;
}).toThrow();
});

it('should reject timeout config missing executeMs at runtime', () => {
expect(() => {
@Tool({
name: 'bad-timeout',
inputSchema: { query: z.string() },
timeout: {},
})
class BadTool extends ToolContext {
async execute(input: { query: string }) {
return { result: input.query };
}
}
return BadTool;
}).toThrow();
});
});
16 changes: 14 additions & 2 deletions libs/sdk/src/common/decorators/agent.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'reflect-metadata';
import { extendedAgentMetadata, FrontMcpAgentTokens } from '../tokens';
import { ToolInputType, ToolOutputType, AgentMetadata, frontMcpAgentMetadataSchema } from '../metadata';
import type { ConcurrencyConfigInput, RateLimitConfigInput, TimeoutConfigInput } from '@frontmcp/guard';
import z from 'zod';

// Forward reference - AgentContext will be defined in agent.interface.ts
Expand Down Expand Up @@ -303,9 +304,20 @@ type __AgentSingleOutputType = __PrimitiveOutputType | __StructuredOutputType;
type __OutputSchema = __AgentSingleOutputType | __AgentSingleOutputType[];

/**
* Agent metadata options with type constraints.
* Agent metadata options with permissive guard config types for IDE IntelliSense.
*
* Guard fields (concurrency, rateLimit, timeout) use auto-generated Input types
* where all fields are optional. Required fields are validated at runtime by Zod.
* @see schemas.generated.ts in @frontmcp/guard
*/
export type AgentMetadataOptions<I extends __Shape, O extends __OutputSchema> = AgentMetadata<I, O>;
export type AgentMetadataOptions<I extends __Shape, O extends __OutputSchema> = Omit<
AgentMetadata<I, O>,
'concurrency' | 'rateLimit' | 'timeout'
> & {
concurrency?: ConcurrencyConfigInput;
rateLimit?: RateLimitConfigInput;
timeout?: TimeoutConfigInput;
};

declare module '@frontmcp/sdk' {
// ---------- the decorator (overloads) ----------
Expand Down
Loading
Loading