Skip to main content
Glama

1MCP Server

scopeAuthMiddleware.ts7.56 kB
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js'; import { SDKOAuthServerProvider } from '@src/auth/sdkOAuthServerProvider.js'; import { AgentConfigManager } from '@src/core/server/agentConfig.js'; import { TagExpression } from '@src/domains/preset/parsers/tagQueryParser.js'; import { TagQuery } from '@src/domains/preset/types/presetTypes.js'; import logger from '@src/logger/logger.js'; import { auditScopeOperation, hasRequiredScopes, scopesToTags } from '@src/utils/validation/scopeValidation.js'; import { NextFunction, Request, Response } from 'express'; /** * Authentication information structure */ export interface AuthInfo { token: string; clientId: string; grantedScopes: string[]; grantedTags: string[]; } /** * Creates a scope validation middleware that uses the SDK's bearer auth middleware * * This middleware: * 1. Uses SDK's requireBearerAuth to verify tokens (when auth enabled) * 2. Validates that requested tags are covered by granted scopes * 3. Provides authentication context to downstream handlers * * When scope validation is disabled, all tags are allowed. * When scope validation is enabled: * - If auth is also enabled, validates tokens and scopes * - If auth is disabled, allows all tags (useful for development/testing) */ export function createScopeAuthMiddleware(oauthProvider?: SDKOAuthServerProvider) { const serverConfig = AgentConfigManager.getInstance(); // If scope validation is disabled, return a pass-through middleware if (!serverConfig.isScopeValidationEnabled()) { return (_req: Request, res: Response, next: NextFunction): void => { const requestedTags = res.locals.tags || []; res.locals.validatedTags = requestedTags; next(); }; } // If scope validation is enabled but auth is disabled, allow all tags if (!serverConfig.isAuthEnabled()) { return (_req: Request, res: Response, next: NextFunction): void => { const requestedTags = res.locals.tags || []; res.locals.validatedTags = requestedTags; next(); }; } const provider = oauthProvider || new SDKOAuthServerProvider(); // Create the SDK's bearer auth middleware const bearerAuthMiddleware = requireBearerAuth({ verifier: provider, resourceMetadataUrl: `${AgentConfigManager.getInstance().getUrl()}/.well-known/oauth-protected-resource`, }); // Return a combined middleware that does both auth and scope validation return async (req: Request, res: Response, next: NextFunction): Promise<void> => { try { // First run the SDK's bearer auth middleware await new Promise<void>((resolve, reject) => { bearerAuthMiddleware(req, res, (err?: any) => { if (err) reject(err); else resolve(); }); }); // If we get here, auth succeeded and req.auth is populated const authInfo = req.auth!; const grantedScopes = authInfo.scopes || []; const grantedTags = scopesToTags(grantedScopes); // Get requested tags and tag expression from previous middleware (tagsExtractor) const requestedTags = res.locals.tags || []; const tagExpression = res.locals.tagExpression; const tagFilterMode = res.locals.tagFilterMode || 'none'; let allRequestedTags: string[] = []; // Determine all tags that need validation based on filter mode if (tagFilterMode === 'advanced' && tagExpression) { // For advanced expressions, extract all referenced tags allRequestedTags = extractTagsFromExpression(tagExpression); } else if (tagFilterMode === 'simple-or') { // For simple mode, use the parsed tags allRequestedTags = requestedTags; } // Validate that all requested tags are covered by granted scopes if (allRequestedTags.length > 0 && !hasRequiredScopes(grantedScopes, allRequestedTags)) { auditScopeOperation('insufficient_scopes', { clientId: authInfo.clientId, requestedScopes: allRequestedTags.map((tag: string) => `tag:${tag}`), grantedScopes, success: false, error: 'Insufficient scopes for requested tags', }); res.status(403).json({ error: 'insufficient_scope', error_description: `Insufficient scopes. Required: ${allRequestedTags.join(', ')}, Granted: ${grantedTags.join(', ')}`, }); return; } // Provide authentication context to downstream handlers via res.locals res.locals.auth = { token: req.headers.authorization?.slice(7) || '', // Remove 'Bearer ' prefix clientId: authInfo.clientId, grantedScopes, grantedTags, }; // Provide validated tags to downstream handlers // If no specific tags requested, use all granted tags res.locals.validatedTags = allRequestedTags.length > 0 ? allRequestedTags : grantedTags; auditScopeOperation('scope_validation_success', { clientId: authInfo.clientId, requestedScopes: allRequestedTags.map((tag: string) => `tag:${tag}`), grantedScopes, success: true, }); next(); } catch (error) { logger.error('Scope auth middleware error:', error); res.status(500).json({ error: 'server_error', error_description: 'Internal server error', }); } }; } /** * Extract all tag names from a tag expression (for scope validation) */ function extractTagsFromExpression(expression: TagExpression): string[] { const tags: string[] = []; function traverse(expr: TagExpression) { switch (expr.type) { case 'tag': if (expr.value && !tags.includes(expr.value)) { tags.push(expr.value); } break; case 'and': case 'or': case 'not': case 'group': if (expr.children) { expr.children.forEach(traverse); } break; } } traverse(expression); return tags; } /** * Utility function to get validated tags from response locals * * This should be used by downstream handlers instead of directly accessing res.locals.tags * to ensure they get scope-validated tags. */ export function getValidatedTags(res: Response): string[] { if (!res?.locals?.validatedTags) { return []; } // Ensure it's an array if (!Array.isArray(res.locals.validatedTags)) { return []; } return res.locals.validatedTags; } /** * Utility function to get tag expression from response locals */ export function getTagExpression(res: Response): TagExpression | undefined { return res?.locals?.tagExpression; } /** * Utility function to get tag filter mode from response locals */ export function getTagFilterMode(res: Response): 'simple-or' | 'advanced' | 'preset' | 'none' { return res?.locals?.tagFilterMode || 'none'; } /** * Utility function to get tag query from response locals */ export function getTagQuery(res: Response): TagQuery | undefined { return res?.locals?.tagQuery; } /** * Utility function to get preset name from response locals */ export function getPresetName(res: Response): string | undefined { return res?.locals?.presetName; } /** * Utility function to get authentication information from response locals */ export function getAuthInfo(res: Response): AuthInfo | undefined { if (!res?.locals?.auth) { return undefined; } return res.locals.auth; } // Default export for backward compatibility (creates a new provider instance) export default createScopeAuthMiddleware();

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