UNPKG

@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

174 lines • 9.22 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { loadConfig, validateConfig } from './utils/config.js'; import { HANDLERS, TOOLS } from './tools/index.js'; import { log, logInfo } from './utils/logger.js'; import { formatDomain } from './utils/http-utility.js'; import { maskTenantName } from './utils/terminal.js'; import { getAvailableTools } from './utils/tools.js'; import { packageVersion } from './utils/package.js'; import { createRateLimiter } from './utils/rate-limiter.js'; /** * Initializes and starts the Auth0 MCP server to provide AI assistants * with secure, controlled access to Auth0 Management API capabilities. * * This server acts as a secure bridge between AI models and Auth0 APIs, * enforcing proper authentication, authorization, and validation at every step. * The server validates credentials before any operations and continuously * monitors token validity during operation to prevent security issues. * * Security architecture: * - Initial user-friendly validation occurs in `run.ts` with detailed CLI feedback * - Startup validation here provides a secondary checkpoint * - Continuous validation during tool calls ensures credentials remain valid * - Token expiration checking prevents use of expired credentials * * This multi-layered approach balances security requirements with developer * experience by providing appropriate feedback at each stage. * * Key responsibilities include: * - Securing access to Auth0 Management API * - Validating user credentials and token expiration * - Automatically refreshing invalid configurations when possible * - Exposing selected tools based on user permissions and preferences * - Handling MCP protocol requests through configured transports * * @param {ServerOptions} [options] - Optional configuration for tool filtering and read-only mode * @returns {Promise<Server>} The initialized MCP server instance * @throws {Error} If configuration validation fails or server setup encounters errors */ export async function startServer(options) { try { log('Initializing Auth0 MCP server...'); // Log node version log(`Node.js version: ${process.version}`); log(`Process ID: ${process.pid}`); log(`Platform: ${process.platform} (${process.arch})`); // Load configuration let config = await loadConfig(); if (!config || !(await validateConfig(config))) { log('Failed to load valid Auth0 configuration'); throw new Error('Invalid Auth0 configuration'); } log(`Successfully loaded configuration for tenant: ${maskTenantName(config.tenantName)}`); // Get available tools based on options if provided const availableTools = getAvailableTools(TOOLS, options?.tools, options?.readOnly); const rateLimiter = createRateLimiter(5, 60_000); const RATE_LIMITED_TOOLS = new Set(['auth0_save_credentials_to_file']); // Create server instance const server = new Server({ name: 'auth0', version: packageVersion }, { capabilities: { tools: {}, logging: {} } }); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => { log('Received list tools request'); // Sanitize tools by removing _meta fields // See: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/264 const sanitizedTools = availableTools.map(({ _meta, ...rest }) => rest); return { tools: sanitizedTools }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; log(`Received tool call: ${toolName}`); try { // Check available (filtered) tools, not all handlers const tool = availableTools.find((t) => t.name === toolName); if (!tool || !HANDLERS[toolName]) { throw new Error(`Unknown or restricted tool: ${toolName}`); } // Reject any argument not declared in the tool's inputSchema. // MCP clients only surface schema-declared parameters for human approval, // so forwarding undeclared parameters to the Auth0 Management API would let // a prompt-injection attacker smuggle security-critical fields (e.g. // custom_login_page, addons, client_authentication_methods) past the // human-in-the-loop confirmation. Enforcing the schema as an allowlist // closes that LLM-to-tool trust boundary gap. const declaredParameters = tool.inputSchema?.properties; if (declaredParameters) { const allowedKeys = new Set(Object.keys(declaredParameters)); const undeclaredKeys = Object.keys(request.params.arguments || {}).filter((key) => !allowedKeys.has(key)); if (undeclaredKeys.length > 0) { throw new Error(`Rejected undeclared parameters: ${undeclaredKeys.join(', ')}`); } } const isDryRun = request.params.arguments?.dry_run === true; if (RATE_LIMITED_TOOLS.has(toolName) && !isDryRun) { const { allowed, retryAfterMs } = rateLimiter(toolName); if (!allowed) { const retryAfterSec = Math.ceil(retryAfterMs / 1000); throw new Error(`Rate limit exceeded for ${toolName}: maximum 5 calls per 60 seconds. ` + `Try again in ${retryAfterSec} second(s).`); } } // Check if config is still valid, reload if needed if (!config || !(await validateConfig(config))) { log('Config is invalid, attempting to reload'); config = await loadConfig(); if (!config || !(await validateConfig(config))) { throw new Error('Auth0 configuration is invalid or missing. Please check auth0-cli login status.'); } log('Successfully reloaded configuration'); } // Add auth token to request const requestWithToken = { token: config.token, parameters: request.params.arguments || {}, }; if (!config.domain) { throw new Error('Error: AUTH0_DOMAIN environment variable is not set'); } const domain = formatDomain(config.domain); // Execute handler log(`Executing handler for tool: ${toolName}`); const result = await HANDLERS[toolName](requestWithToken, { domain: domain }); log(`Handler execution completed for: ${toolName}`); return { content: result.content, isError: result.isError || false, }; } catch (error) { log(`Error handling tool call: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // Connect to transport log('Creating stdio transport...'); const transport = new StdioServerTransport(); // Connection with timeout log('Connecting server to transport...'); try { await Promise.race([ server.connect(transport), new Promise((_, reject) => setTimeout(() => reject(new Error('Connection timeout')), 5000)), ]); // Log server start information const enabledToolsCount = availableTools.length; const totalToolsCount = TOOLS.length; const logMsg = `Auth0 MCP Server version ${packageVersion} running on stdio with ${enabledToolsCount}/${totalToolsCount} tools available`; logInfo(logMsg); log(logMsg); return server; } catch (connectError) { log(`Transport connection error: ${connectError instanceof Error ? connectError.message : String(connectError)}`); if (connectError instanceof Error && connectError.message === 'Connection timeout') { log('Connection to transport timed out. This might indicate an issue with the stdio transport.'); } throw connectError; } } catch (error) { log('Error starting server:', error); throw error; } } //# sourceMappingURL=server.js.map