Skip to main content
Glama

1MCP Server

scopeValidation.ts8.38 kB
import logger from '@src/logger/logger.js'; /** * Security constants for scope validation */ export const SCOPE_VALIDATION_CONFIG = { /** Maximum allowed length for a single scope */ MAX_SCOPE_LENGTH: 100, /** Maximum number of scopes per request */ MAX_SCOPES_COUNT: 50, /** Valid tag name pattern: alphanumeric, underscore, hyphen only */ TAG_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/, /** Valid tag scope pattern: tag: prefix followed by valid tag name */ TAG_SCOPE_PATTERN: /^tag:[a-zA-Z0-9_-]+$/, /** Minimum tag name length */ MIN_TAG_LENGTH: 1, /** Maximum tag name length */ MAX_TAG_LENGTH: 50, } as const; /** * Parsed scope structure */ export interface ParsedScope { type: 'tag'; value: string; } /** * Scope validation result */ export interface ScopeValidationResult { isValid: boolean; errors: string[]; validScopes: string[]; invalidScopes: string[]; } /** * Validates if a string is a valid tag name * @param tagName - The tag name to validate * @returns True if valid, false otherwise */ export function isValidTagName(tagName: string): boolean { if (!tagName || typeof tagName !== 'string') { return false; } // Check length constraints if ( tagName.length < SCOPE_VALIDATION_CONFIG.MIN_TAG_LENGTH || tagName.length > SCOPE_VALIDATION_CONFIG.MAX_TAG_LENGTH ) { return false; } // Check pattern return SCOPE_VALIDATION_CONFIG.TAG_NAME_PATTERN.test(tagName); } /** * Validates if a string is a valid tag scope * @param scope - The scope to validate * @returns True if valid, false otherwise */ export function isValidTagScope(scope: string): boolean { if (!scope || typeof scope !== 'string') { return false; } // Check length constraints if (scope.length > SCOPE_VALIDATION_CONFIG.MAX_SCOPE_LENGTH) { return false; } // Check pattern if (!SCOPE_VALIDATION_CONFIG.TAG_SCOPE_PATTERN.test(scope)) { return false; } // Extract and validate tag name const tagName = scope.slice(4); // Remove "tag:" prefix return isValidTagName(tagName); } /** * Safely extracts tag name from a tag scope * @param scope - The scope string (e.g., "tag:web") * @returns The tag name if valid, null otherwise */ export function extractTagFromScope(scope: string): string | null { if (!isValidTagScope(scope)) { logger.warn(`Invalid tag scope format: ${scope}`); return null; } return scope.slice(4); // Remove "tag:" prefix } /** * Parses a scope string into its components * @param scope - The scope string to parse * @returns Parsed scope object or null if invalid */ export function parseScope(scope: string): ParsedScope | null { if (!scope || typeof scope !== 'string') { return null; } // Currently only supporting tag scopes if (scope.startsWith('tag:')) { const tagName = extractTagFromScope(scope); if (!tagName) { return null; } return { type: 'tag', value: tagName }; } // Future: Add support for other scope types (capability:, resource:, etc.) return null; } /** * Validates an array of scopes for format and security * @param scopes - Array of scope strings to validate * @returns Validation result with details */ export function validateScopes(scopes: string[]): ScopeValidationResult { const result: ScopeValidationResult = { isValid: true, errors: [], validScopes: [], invalidScopes: [], }; // Check for null/undefined input if (!scopes || !Array.isArray(scopes)) { result.isValid = false; result.errors.push('Scopes must be an array'); return result; } // Check count limits if (scopes.length > SCOPE_VALIDATION_CONFIG.MAX_SCOPES_COUNT) { result.isValid = false; result.errors.push(`Too many scopes: ${scopes.length} > ${SCOPE_VALIDATION_CONFIG.MAX_SCOPES_COUNT}`); return result; } // Validate each scope const seenScopes = new Set<string>(); for (const scope of scopes) { // Check for duplicates if (seenScopes.has(scope)) { result.errors.push(`Duplicate scope: ${scope}`); result.invalidScopes.push(scope); continue; } seenScopes.add(scope); // Validate scope format if (isValidTagScope(scope)) { result.validScopes.push(scope); } else { result.errors.push(`Invalid scope format: ${scope}`); result.invalidScopes.push(scope); } } // Set overall validity result.isValid = result.errors.length === 0; return result; } /** * Validates that requested scopes are available (allowlist approach) * @param requestedScopes - Scopes requested by client * @param availableTags - Tags available on the server * @returns Validation result */ export function validateScopesAgainstAvailableTags( requestedScopes: string[], availableTags: string[], ): ScopeValidationResult { const result: ScopeValidationResult = { isValid: true, errors: [], validScopes: [], invalidScopes: [], }; // First validate scope formats const formatValidation = validateScopes(requestedScopes); if (!formatValidation.isValid) { return formatValidation; } // Create set of available tags for fast lookup const availableTagSet = new Set(availableTags); // Check each scope against available tags for (const scope of requestedScopes) { const tagName = extractTagFromScope(scope); if (!tagName) { result.errors.push(`Invalid scope format: ${scope}`); result.invalidScopes.push(scope); continue; } if (availableTagSet.has(tagName)) { result.validScopes.push(scope); } else { result.errors.push(`Scope not available: ${scope} (tag: ${tagName})`); result.invalidScopes.push(scope); } } result.isValid = result.errors.length === 0; return result; } /** * Checks if granted scopes satisfy the requirements for requested tags * @param grantedScopes - Scopes granted during OAuth flow * @param requestedTags - Tags requested for current operation * @returns True if all requested tags are covered by granted scopes */ export function hasRequiredScopes(grantedScopes: string[], requestedTags: string[]): boolean { // Fail-secure: deny if inputs are invalid if (!grantedScopes || !Array.isArray(grantedScopes) || !requestedTags || !Array.isArray(requestedTags)) { logger.warn('Invalid input to hasRequiredScopes', { grantedScopes, requestedTags }); return false; } // No tags requested means access is allowed if (requestedTags.length === 0) { return true; } // Extract tags from granted scopes const grantedTags = grantedScopes .map((scope) => extractTagFromScope(scope)) .filter((tag) => tag !== null) as string[]; // Check if all requested tags are in granted tags const hasAllTags = requestedTags.every((tag) => grantedTags.includes(tag)); if (!hasAllTags) { logger.warn('Insufficient scopes for requested tags', { grantedTags, requestedTags, missing: requestedTags.filter((tag) => !grantedTags.includes(tag)), }); } return hasAllTags; } /** * Converts tags to their corresponding scopes * @param tags - Array of tag names * @returns Array of tag scopes */ export function tagsToScopes(tags: string[]): string[] { if (!tags || !Array.isArray(tags)) { return []; } return tags.filter((tag) => isValidTagName(tag)).map((tag) => `tag:${tag}`); } /** * Converts scopes to their corresponding tags * @param scopes - Array of scope strings * @returns Array of tag names */ export function scopesToTags(scopes: string[]): string[] { if (!scopes || !Array.isArray(scopes)) { return []; } return scopes.map((scope) => extractTagFromScope(scope)).filter((tag) => tag !== null) as string[]; } /** * Security utility to audit scope operations * @param operation - The operation being performed * @param context - Additional context for logging */ export function auditScopeOperation( operation: string, context: { clientId?: string; requestedScopes?: string[]; grantedScopes?: string[]; success?: boolean; error?: string; }, ): void { logger.info(`Scope operation: ${operation}`, { operation, clientId: context.clientId, requestedScopes: context.requestedScopes, grantedScopes: context.grantedScopes, success: context.success, error: context.error, timestamp: new Date().toISOString(), }); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1mcp-app/agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server