@auth0/auth0-mcp-server
Version:
Auth0 Model Context Protocol (MCP) Server (Beta) — A secure and extendable implementation of an MCP server that provides AI assistants with controlled access to the Auth0 Management API through natural language. This project is in beta and not intended fo
196 lines • 9.03 kB
JavaScript
import { HANDLERS, TOOLS } from '../tools/index.js';
import { ServerMode } from './types.js';
import { log } from './logger.js';
import { Glob } from './glob.js';
/**
* Filters the provided tools collection based on specified glob patterns and readOnly flag.
* This function processes the input patterns against available tools to determine
* which tools should be returned. It handles special cases like wildcard patterns,
* empty pattern arrays, and pattern matching errors. When readOnly is true,
* it only returns tools that have _meta.readOnly set to true or tools that follow read-only patterns.
*
* IMPORTANT: The readOnly flag takes priority over pattern matching for security reasons.
* Even if patterns match non-read-only tools, when readOnly=true is specified,
* only read-only tools will be returned.
*
* @param allTools - Complete collection of available tools to be filtered
* @param patterns - Optional glob patterns to filter tools by (e.g., 'auth0*', 'jwt-*')
* If omitted or empty, all tools will be returned
* A single '*' pattern will return all tools
* @param readOnly - Optional flag to only return read-only tools
* When true, only returns tools marked as readOnly
* Takes priority over pattern matching for security
* @returns Array of Tool objects that match the specified criteria
* Returns all tools if no patterns provided or on error
*
* @example
* // Return all tools that start with "auth"
* const authTools = getAvailableTools(tools, ['auth*']);
*
* @example
* // Return all read-only tools (regardless of pattern matching)
* const readOnlyTools = getAvailableTools(tools, ['*'], true);
*
* @example
* // Return only read-only tools that match the pattern
* // Note: --read-only takes priority, so even if the pattern matches non-read-only tools,
* // only the read-only ones will be returned
* const readOnlyAuthTools = getAvailableTools(tools, ['auth0_*_application'], true);
*/
export function getAvailableTools(allTools, patterns, readOnly) {
// Start with all tools
let filteredTools = allTools;
// Apply pattern filtering if patterns are provided
if (patterns?.length) {
filteredTools = filterToolsByPatterns(filteredTools, patterns);
}
// Apply read-only filtering if requested
// IMPORTANT: This is applied AFTER pattern filtering, ensuring that
// --read-only takes priority over --tools for security
// Even if non-read-only tools match the pattern, they will be filtered out here
if (readOnly) {
filteredTools = filterToolsByReadOnly(filteredTools);
}
return filteredTools;
}
function filterToolsByPatterns(tools, patterns) {
try {
// Special case for global wildcard
if (patterns.length === 1 && patterns[0] === '*') {
return tools; // Keep all tools, no pattern filtering needed
}
// Compile glob patterns once for performance
const globs = patterns.map((pattern) => new Glob(pattern));
// Track matching tools and matches per pattern
const enabledToolNames = new Set();
const matchesByPattern = new Map();
// For each tool, check if it matches any pattern
for (const tool of tools) {
for (const glob of globs) {
if (glob.matches(tool.name)) {
enabledToolNames.add(tool.name);
// Count matches per pattern for logging
const patternString = glob.toString();
matchesByPattern.set(patternString, (matchesByPattern.get(patternString) || 0) + 1);
// Once we find a match, no need to check other patterns
break;
}
}
}
// Log match counts for wildcard patterns for debugging
for (const [pattern, count] of matchesByPattern.entries()) {
if (pattern.includes('*')) {
log(`Glob pattern '${pattern}' matched ${count} tools`);
}
}
// Create the filtered tool list based on patterns
const filteredTools = tools.filter((tool) => enabledToolNames.has(tool.name));
log(`Selected ${filteredTools.length} available tools based on patterns`);
return filteredTools;
}
catch (error) {
// Log error and fail closed
log(`Error determining available tools: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
function filterToolsByReadOnly(tools) {
const readOnlyTools = tools.filter((tool) => tool._meta?.readOnly === true);
log(`Filtered to ${readOnlyTools.length} read-only tools`);
return readOnlyTools;
}
/**
* Validates tool patterns against available tools to ensure each pattern matches at least one tool.
* This function verifies that each provided pattern (including glob patterns) corresponds to
* at least one available tool, throwing specific errors for different validation scenarios.
*
* @param patterns - Array of tool name patterns to validate
* Can include glob patterns with wildcards (e.g., 'auth0*')
* Empty array or undefined will skip validation
* @param availableTools - Collection of Tool objects to validate patterns against
*
* @throws {Error} If availableTools is not a valid array or is empty
* @throws {Error} If any pattern doesn't match at least one tool name, with different
* error messages for exact matches vs. wildcard patterns
*
* @example
* // Validate specific tool names
* validatePatterns(['auth0-jwt', 'auth0-management'], tools);
*
* @example
* // Validate with glob patterns
* validatePatterns(['auth0*', 'jwt-*'], tools);
*
* @see {@link Glob} for the pattern matching implementation
* @see {@link getAvailableTools} for filtering tools using these patterns
*/
export function validatePatterns(patterns, availableTools) {
// Skip validation if patterns array is empty
if (!patterns || patterns.length === 0) {
return;
}
// Input validation
if (!availableTools || !Array.isArray(availableTools)) {
throw new Error('Invalid tools array provided for validation');
}
if (availableTools.length === 0) {
throw new Error('No tools available for pattern validation');
}
// Extract tool names for faster matching
const toolNames = availableTools.map((tool) => tool.name);
// Validate each pattern
for (const pattern of patterns) {
const glob = new Glob(pattern);
const matchesAnyTool = toolNames.some((name) => glob.matches(name));
if (!matchesAnyTool) {
const errorPrefix = pattern.includes('*') ? `No tools match the pattern` : `Invalid tool`;
throw new Error(`${errorPrefix}: ${pattern}. Accepted tools are: ${toolNames.join(', ')}`);
}
}
}
/**
* Returns all tools, or a filtered subset based on mode and scope options.
* In StreamableHttp mode, local-only tools (e.g. auth0_save_credentials_to_file) are excluded.
* When withScope is true, tools without _meta.requiredScopes are excluded.
*
* @param options.mode - Server mode; StreamableHttp excludes local-only tools
* @param options.withScope - When true, only returns tools that declare requiredScopes in _meta.
*
*/
export function getTools(options) {
let tools = TOOLS;
if (options?.mode === ServerMode.StreamableHttp) {
tools = tools.filter((t) => !t._meta?.localOnly);
}
if (options?.withScope) {
tools = tools.filter((t) => !!t._meta?.requiredScopes && t._meta.requiredScopes.length > 0);
}
return tools;
}
/**
* Returns all handlers, or a mode/headers-filtered subset.
* In StreamableHttp mode, local-only tool handlers are excluded and each handler is wrapped
* to auto-inject the mode into HandlerConfig so handlers can adapt their responses.
* Headers provided here are merged into every handler invocation (per-call config.headers
* take precedence over these defaults).
*/
export function getHandlers(options) {
const mode = options?.mode;
const baseHeaders = options?.headers;
if (mode === ServerMode.StreamableHttp) {
const localOnlyNames = new Set(TOOLS.filter((t) => t._meta?.localOnly).map((t) => t.name));
const filtered = Object.fromEntries(Object.entries(HANDLERS).filter(([name]) => !localOnlyNames.has(name)));
return Object.fromEntries(Object.entries(filtered).map(([name, handler]) => [
name,
(request, config) => handler(request, { ...config, mode, headers: { ...baseHeaders, ...config.headers } }),
]));
}
if (baseHeaders) {
return Object.fromEntries(Object.entries(HANDLERS).map(([name, handler]) => [
name,
(request, config) => handler(request, { ...config, headers: { ...baseHeaders, ...config.headers } }),
]));
}
return HANDLERS;
}
//# sourceMappingURL=tools.js.map