@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
857 lines • 118 kB
JavaScript
/**
* SchemaDispatcher - Generic dispatcher for schema-driven operations
*
* This module provides automatic dispatch from operation schemas to handler methods.
* It eliminates the need for manual switch statements in MCPAQLHandler by:
*
* 1. Looking up operation in schema
* 2. Resolving handler from registry
* 3. Building method arguments from params
* 4. Calling the handler method
* 5. Applying field selection/transformation to response (Issue #202)
*
* ARCHITECTURE:
* - SchemaDispatcher.dispatch(operation, params, registry) → Promise<unknown>
* - Uses argBuilder to determine how to pass parameters to handler
* - Validates required parameters before dispatch
* - Provides clear error messages for missing handlers/methods
*
* INPUT NORMALIZATION (Issue #251):
* - Parameters can have multiple sources via `sources` field
* - Resolution order: sources[0], sources[1], ..., params[key]
* - Supports dot notation: 'input.elementType', 'params.type'
* - Operations with `needsFullInput: true` have access to full OperationInput
*
* FIELD SELECTION (Issue #202):
* - When `fields` param is provided, filters response to requested fields only
* - Transforms field names for LLM consistency (name → element_name)
* - Supports preset field sets: 'minimal', 'standard', 'full'
*
* @see Issue #247 - Schema-driven operation definitions
* @see Issue #251 - ElementCRUD input normalization
* @see Issue #202 - GraphQL field selection for response token optimization
*/
import yaml from 'js-yaml';
import { SecureYamlParser } from '../../security/secureYamlParser.js';
import { getOperationSchema, isSchemaOperation, } from './OperationSchema.js';
import { IntrospectionResolver } from './IntrospectionResolver.js';
import { NormalizerRegistry } from './normalizers/index.js';
import { ElementType } from '../../portfolio/types.js';
// Note: Field selection is now applied at MCPAQLHandler level (Issue #202)
// ============================================================================
// Input Normalization (Issue #251)
// ============================================================================
/**
* Validate property path to prevent prototype pollution attacks.
* Only allows alphanumeric characters, underscores, and dots.
*/
const SAFE_PATH_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
const FORBIDDEN_PATHS = new Set(['__proto__', 'constructor', 'prototype']);
/**
* Resolve a value from a dot-notation path on an object.
* Example: getNestedValue({ input: { elementType: 'persona' } }, 'input.elementType') => 'persona'
*
* Security: Validates path format and blocks prototype pollution vectors.
*/
function getNestedValue(obj, path) {
// Validate path format to prevent injection attacks
if (!SAFE_PATH_PATTERN.test(path)) {
throw new Error(`Invalid property path format: ${path}`);
}
const parts = path.split('.');
// Check for prototype pollution attempts
for (const part of parts) {
if (FORBIDDEN_PATHS.has(part)) {
throw new Error(`Forbidden property path segment: ${part}`);
}
}
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
/**
* Resolve a parameter value from multiple sources.
*
* The resolution order is:
* 1. Check each source in `sources` array (if defined)
* 2. Fall back to the direct parameter value
* 3. Fall back to default value (if defined)
*
* @param key - Parameter name
* @param def - Parameter definition with possible sources
* @param context - Resolution context containing input, params, and raw params
* @returns Resolved value or undefined
*/
function resolveParamValue(key, def, context) {
// If sources are defined, check them in order
if (def.sources && def.sources.length > 0) {
for (const source of def.sources) {
// Build the resolution context as a flat object for getNestedValue
const resolveContext = {
params: context.params,
};
// Add input fields to the context if available
if (context.input) {
resolveContext.input = context.input;
}
const value = getNestedValue(resolveContext, source);
if (value !== undefined) {
return value;
}
}
}
// Fall back to direct param value
const directValue = context.params[key];
if (directValue !== undefined) {
return directValue;
}
// Fall back to default
return def.default;
}
// ============================================================================
// Parameter Style Conversion (Issue #252)
// ============================================================================
/**
* Convert a snake_case string to camelCase.
*
* @example
* snakeToCamel('dry_run') => 'dryRun'
* snakeToCamel('max_results') => 'maxResults'
* snakeToCamel('already_camel') => 'alreadyCamel'
*/
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
* Apply param style conversion to a key based on the operation schema.
* Individual `mapTo` overrides take precedence over style conversion.
*/
function applyParamStyle(key, def, paramStyle) {
// Explicit mapTo takes precedence
if (def.mapTo) {
return def.mapTo;
}
// Apply style conversion if specified
if (paramStyle === 'snakeToCamel') {
return snakeToCamel(key);
}
// Default: use original key
return key;
}
// ============================================================================
// Parameter Mapping
// ============================================================================
/**
* Map input parameters to handler arguments based on param schema.
* Supports multi-source parameter resolution and param style conversion.
*/
function mapParams(params, schema, input, paramStyle) {
const mapped = {};
const context = { input, params };
for (const [key, def] of Object.entries(schema)) {
const value = resolveParamValue(key, def, context);
const targetKey = applyParamStyle(key, def, paramStyle);
if (value !== undefined) {
mapped[targetKey] = value;
}
}
return mapped;
}
/**
* Validate required parameters are present.
*
* Uses multi-source resolution to check all possible sources for each
* required parameter. Provides detailed error messages showing:
* - Which parameter is missing
* - Which sources were checked (in order)
* - What was actually provided in params
*
* @param params - Parameters provided by the caller
* @param schema - Parameter schema defining requirements
* @param operation - Operation name for error context
* @param input - Optional full OperationInput for source resolution
* @throws Error with debugging context if required parameter is missing
*/
function validateRequiredParams(params, schema, operation, input) {
const context = { input, params };
for (const [key, def] of Object.entries(schema)) {
if (def.required) {
const value = resolveParamValue(key, def, context);
if (value === undefined) {
// Build detailed error message for debugging
const sourcesChecked = def.sources
? `Sources checked (in order): [${def.sources.join(' → ')}] → params.${key}`
: `Source: params.${key}`;
const providedParams = Object.keys(params).length > 0
? `Provided params: {${Object.keys(params).join(', ')}}`
: 'No params provided';
// Issue #290: Check both snake_case and camelCase for element_type
const elementTypeValue = input?.element_type || input?.elementType;
const hasElementType = elementTypeValue
? `input.element_type: '${elementTypeValue}'`
: 'input.element_type: undefined';
throw new Error(`Missing required parameter '${key}' for operation '${operation}'. ` +
`${sourcesChecked}. ${providedParams}. ${hasElementType}`);
}
}
}
}
// ============================================================================
// Type Validation (Issue #255)
// ============================================================================
/**
* Get a human-readable type description for error messages
*/
function getActualType(value) {
if (value === null)
return 'null';
if (Array.isArray(value))
return 'array';
return typeof value;
}
/**
* Validate a single parameter value against its type definition
*
* @param value - The parameter value to validate
* @param def - The parameter definition from schema
* @param key - The parameter name (for error messages)
* @param operation - The operation name (for error messages)
* @throws Error if type validation fails
*
* @see Issue #255 - Runtime type validation
*/
function validateParamType(value, def, key, operation) {
// Skip validation for undefined values (handled by required check)
if (value === undefined)
return;
const actualType = getActualType(value);
switch (def.type) {
case 'string':
if (typeof value !== 'string') {
throw new Error(`Parameter '${key}' for operation '${operation}' must be a string, got ${actualType}`);
}
break;
case 'number':
if (typeof value !== 'number' || Number.isNaN(value)) {
throw new Error(`Parameter '${key}' for operation '${operation}' must be a number, got ${actualType}`);
}
break;
case 'boolean':
if (typeof value !== 'boolean') {
throw new Error(`Parameter '${key}' for operation '${operation}' must be a boolean, got ${actualType}`);
}
break;
case 'object':
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new Error(`Parameter '${key}' for operation '${operation}' must be an object, got ${actualType}`);
}
break;
case 'array':
if (!Array.isArray(value)) {
throw new Error(`Parameter '${key}' for operation '${operation}' must be an array, got ${actualType}`);
}
break;
case 'string[]':
if (!Array.isArray(value)) {
throw new Error(`Parameter '${key}' for operation '${operation}' must be a string array, got ${actualType}`);
}
// Validate array elements are strings
for (let i = 0; i < value.length; i++) {
if (typeof value[i] !== 'string') {
throw new Error(`Parameter '${key}[${i}]' for operation '${operation}' must be a string, got ${getActualType(value[i])}`);
}
}
break;
case 'unknown':
// Allow any type - no validation needed
break;
}
}
/**
* Validate all parameter types against schema definitions
*
* @param params - The parameters to validate
* @param schema - The parameter schema
* @param operation - The operation name (for error messages)
* @throws Error if any type validation fails
*
* @see Issue #255 - Runtime type validation
*/
function validateParamTypes(params, schema, operation) {
for (const [key, def] of Object.entries(schema)) {
const value = params[key];
validateParamType(value, def, key, operation);
}
}
// ============================================================================
// Handler Resolution
// ============================================================================
/**
* Type guard to check if a value is a non-null object.
* Used for safe property access without type assertions.
*/
function isObject(value) {
return typeof value === 'object' && value !== null;
}
/**
* Type guard to check if an object has a callable method.
* Provides type-safe method lookup without unsafe assertions.
*
* @param obj - Object to check
* @param methodName - Name of method to look for
* @returns True if obj has a function property with the given name
*/
function hasMethod(obj, methodName) {
return isObject(obj) && typeof obj[methodName] === 'function';
}
/**
* Get handler from registry with proper error handling.
*
* Resolves a handler by key from the registry, providing clear error messages
* when handlers are missing. Differentiates between required and optional handlers.
*
* @param registry - Handler registry containing all configured handlers
* @param handlerKey - Key identifying which handler to retrieve
* @param operation - Operation name for error context
* @param optional - Whether the handler is optional (affects error messaging)
* @returns The resolved handler
* @throws Error if handler is not configured
*/
function getHandler(registry, handlerKey, operation, optional) {
const handler = registry[handlerKey];
if (!handler && !optional) {
throw new Error(`Handler '${handlerKey}' is required for operation '${operation}' but not configured. ` +
`Ensure the handler is registered in the HandlerRegistry.`);
}
if (!handler && optional) {
throw new Error(`${handlerKey.charAt(0).toUpperCase() + handlerKey.slice(1)} operations not available: ` +
`${handlerKey} not configured. This is an optional handler that may not be available in all configurations.`);
}
return handler;
}
/**
* Get method from handler with type-safe lookup.
*
* Uses type guards instead of unsafe type assertions to verify the handler
* has the expected method. Provides detailed error messages for debugging.
*
* @param handler - Handler object to get method from
* @param methodName - Name of the method to retrieve
* @param handlerKey - Handler key for error context
* @param operation - Operation name for error context
* @returns Bound method ready to call
* @throws Error if method is not found or is not callable
*/
function getMethod(handler, methodName, handlerKey, operation) {
// Use type guard instead of unsafe assertion
if (!hasMethod(handler, methodName)) {
const availableMethods = isObject(handler)
? Object.keys(handler).filter(k => typeof handler[k] === 'function')
: [];
throw new Error(`Method '${methodName}' not found on handler '${handlerKey}' for operation '${operation}'. ` +
`Available methods: [${availableMethods.join(', ') || 'none'}]`);
}
// Now TypeScript knows handler[methodName] is a function
const method = handler[methodName];
return method.bind(handler);
}
// ============================================================================
// Argument Building
// ============================================================================
/**
* Params that are handled at the dispatch level and should NOT be passed to handlers.
* These are cross-cutting concerns processed by SchemaDispatcher itself.
*
* @see Issue #202 - fields is used for response filtering, not by handlers
*/
const DISPATCH_ONLY_PARAMS = new Set(['fields']);
/**
* Filter out dispatch-only params from a params object.
* These params are handled at the dispatch level (e.g., field selection)
* and should not be passed to handlers.
*/
function filterDispatchOnlyParams(params) {
const filtered = {};
for (const [key, value] of Object.entries(params)) {
if (!DISPATCH_ONLY_PARAMS.has(key)) {
filtered[key] = value;
}
}
return filtered;
}
/**
* Build arguments for handler method based on argBuilder type
*
* - 'single': Pass params in schema order as positional args (default)
* - 'spread': Pass query + remaining params
* - 'named': Pass mapped params as named object
* - 'namedWithType': Like 'named' but ensures 'type' is included from resolved sources
* - 'typeWithParams': Pass resolved type + full params object
*
* Note: Params in DISPATCH_ONLY_PARAMS are filtered out before passing to handlers.
*/
function buildArgs(params, schema, mappedParams, input) {
const builder = schema.argBuilder ?? 'single';
switch (builder) {
case 'spread':
// For handlers that take (query, options)
return [params.query, filterDispatchOnlyParams(params)];
case 'named': {
// For handlers that take a named params object
// Filter out dispatch-only params
const filtered = filterDispatchOnlyParams(mappedParams);
return [filtered];
}
case 'namedWithType': {
// Like 'named' but ensures 'elementType' and 'elementName' are available
// This handles the ElementCRUD pattern where type comes from input.element_type OR params.element_type
// Issue #290: element_name maps to elementName, element_type maps to elementType
const result = { ...mappedParams };
// If 'elementType' wasn't in mappedParams but is available from input.element_type (or legacy elementType), add it
// Issue #290: Prefer snake_case element_type, fallback to camelCase elementType for backward compat
if (!result.elementType && (input?.element_type || input?.elementType)) {
result.elementType = input.element_type || input.elementType;
}
// Set 'type' from 'elementType' for handlers that expect it
if (!result.type && result.elementType) {
result.type = result.elementType;
}
// Set 'name' from 'elementName' for handlers that expect it (Issue #290)
if (!result.name && result.elementName) {
result.name = result.elementName;
}
// Issue #278: For ensembles, merge top-level elements into metadata
// LLMs often pass elements at params level, not inside metadata
// Issue #290: Prefer snake_case element_type, fallback to camelCase elementType
const resolvedType = result.elementType || result.type || input?.element_type || input?.elementType;
// Check for ensemble type (handles both plural constant and singular form)
const isEnsemble = resolvedType === ElementType.ENSEMBLE || resolvedType === 'ensemble';
if (isEnsemble) {
const elements = params.elements;
const currentMetadata = result.metadata;
if (elements && (!currentMetadata || !currentMetadata.elements)) {
result.metadata = { ...currentMetadata, elements };
}
}
// Issue #722: Common fields (tags, triggers) — LLMs pass at params level for any element type.
{
let currentMeta = result.metadata;
let updatedMeta = currentMeta;
if (params.tags !== undefined && Array.isArray(params.tags) && (!updatedMeta || updatedMeta.tags === undefined)) {
updatedMeta = { ...updatedMeta, tags: params.tags };
}
if (params.triggers !== undefined && Array.isArray(params.triggers) && (!updatedMeta || updatedMeta.triggers === undefined)) {
updatedMeta = { ...updatedMeta, triggers: params.triggers };
}
if (updatedMeta !== currentMeta) {
result.metadata = updatedMeta;
}
}
const isTemplate = resolvedType === ElementType.TEMPLATE || resolvedType === 'template';
if (isTemplate && Array.isArray(params.variables)) {
const currentMetadata = result.metadata;
if (currentMetadata?.variables === undefined) {
result.metadata = { ...currentMetadata, variables: params.variables };
}
}
// Agent V2 fields: goal, activates, tools, systemPrompt, autonomy, resilience
const isAgent = resolvedType === ElementType.AGENT || resolvedType === 'agent';
if (isAgent) {
const currentMetadata = result.metadata;
let updatedMetadata = currentMetadata;
if (params.goal !== undefined && (!updatedMetadata || updatedMetadata.goal === undefined)) {
updatedMetadata = { ...updatedMetadata, goal: params.goal };
}
if (params.activates !== undefined && (!updatedMetadata || updatedMetadata.activates === undefined)) {
updatedMetadata = { ...updatedMetadata, activates: params.activates };
}
if (params.tools !== undefined && (!updatedMetadata || updatedMetadata.tools === undefined)) {
updatedMetadata = { ...updatedMetadata, tools: params.tools };
}
// Issue #725: Accept both systemPrompt and system_prompt (LLMs commonly use snake_case)
const systemPromptValue = params.systemPrompt ?? params.system_prompt;
if (systemPromptValue !== undefined && (!updatedMetadata || updatedMetadata.systemPrompt === undefined)) {
updatedMetadata = { ...updatedMetadata, systemPrompt: systemPromptValue };
}
if (params.autonomy !== undefined && (!updatedMetadata || updatedMetadata.autonomy === undefined)) {
updatedMetadata = { ...updatedMetadata, autonomy: params.autonomy };
}
// Issue #722: resilience was missing from the agent V2 merge
if (params.resilience !== undefined && (!updatedMetadata || updatedMetadata.resilience === undefined)) {
updatedMetadata = { ...updatedMetadata, resilience: params.resilience };
}
if (updatedMetadata !== currentMetadata) {
result.metadata = updatedMetadata;
}
}
// Issue #666: LLMs often pass gatekeeper at top-level params, not inside metadata.
// Merge it into metadata for all element types.
if (params.gatekeeper && typeof params.gatekeeper === 'object') {
const currentMeta = result.metadata;
if (!currentMeta || !currentMeta.gatekeeper) {
console.debug(`[SchemaDispatcher] Merging top-level gatekeeper into metadata for element '${result.name ?? 'unknown'}'`);
result.metadata = { ...currentMeta, gatekeeper: params.gatekeeper };
}
}
// Filter out dispatch-only params before returning
return [filterDispatchOnlyParams(result)];
}
case 'typeWithParams': {
// Pass resolved type + full params object (minus dispatch-only params)
// Used for operations like list_elements that need (type, paginationParams)
// Issue #290: element_type maps to elementType, with backward compat for type
// Prefer snake_case element_type, fallback to camelCase elementType
const resolvedType = mappedParams.elementType ?? mappedParams.type ?? input?.element_type ?? input?.elementType;
return [resolvedType, filterDispatchOnlyParams(params)];
}
case 'single':
default: {
// Extract params in schema order, using resolved values
if (!schema.params || Object.keys(schema.params).length === 0) {
return [];
}
// For simple handlers, pass resolved params in schema order
// Skip dispatch-only params (like 'fields') that are handled by SchemaDispatcher
const args = [];
for (const key of Object.keys(schema.params)) {
// Skip dispatch-level params that shouldn't be passed to handlers
if (DISPATCH_ONLY_PARAMS.has(key)) {
continue;
}
// Use mapped value if available (handles source resolution)
const targetKey = schema.params[key].mapTo ?? key;
args.push(mappedParams[targetKey]);
}
return args;
}
}
}
// ============================================================================
// Special Operation Handlers
// ============================================================================
/**
* Handle introspection operation (uses IntrospectionResolver directly)
*/
async function handleIntrospection(params) {
return IntrospectionResolver.resolve(params);
}
/**
* Handle get_capabilities operation (uses IntrospectionResolver directly)
* @see Issue #1760 - get_capabilities operation
*/
async function handleCapabilities(params) {
return IntrospectionResolver.getCapabilities(params);
}
/**
* Handle build info operation (formats output)
*/
async function handleBuildInfo(registry) {
const service = registry.buildInfoService;
if (!service) {
throw new Error('BuildInfo operations not available: BuildInfoService not configured');
}
const info = await service.getBuildInfo();
return {
structuredContent: info,
content: [{
type: 'text',
text: service.formatBuildInfo(info),
}],
};
}
/**
* Handle cache budget report operation (formats output as markdown table)
*/
function handleCacheBudgetReport(registry) {
const budget = registry.cacheMemoryBudget;
if (!budget) {
throw new Error('Cache budget not available: CacheMemoryBudget not configured');
}
const report = budget.getReport();
const lines = [
'# Cache Memory Budget Report',
'',
`**Budget:** ${report.budgetMB} MB`,
`**Used:** ${report.totalMemoryMB} MB (${report.utilizationPercent}%)`,
`**Registered Caches:** ${report.caches.length}`,
'',
];
if (report.caches.length > 0) {
lines.push('| Cache | Entries | Memory (MB) | Hit Rate | Last Activity |');
lines.push('|-------|---------|-------------|----------|---------------|');
for (const c of report.caches) {
const activity = c.lastActivityMs > 0
? `${((Date.now() - c.lastActivityMs) / 1000).toFixed(0)}s ago`
: 'never';
lines.push(`| ${c.name} | ${c.entries} | ${c.memoryMB} | ${(c.hitRate * 100).toFixed(1)}% | ${activity} |`);
}
}
else {
lines.push('_No caches registered._');
}
return {
content: [{
type: 'text',
text: lines.join('\n'),
}],
};
}
/**
* Handle element export operation.
* Returns an ExportPackage that can be used for import operations.
*/
async function handleExportElement(mappedParams, registry) {
const handler = registry.elementCRUD;
if (!handler) {
throw new Error('ElementCRUD operations not available: handler not configured');
}
// Get the exportable data from the element
// Issue #290: Use mapped names (elementName, elementType) from schema mapTo
const name = mappedParams.elementName;
const type = mappedParams.elementType;
const format = mappedParams.format || 'json';
// Use the element query service to get element details
const elementDetails = await handler.getElementDetails(name, type);
if (!elementDetails) {
throw new Error(`Element not found: ${type}/${name}`);
}
// Build export package (matches MCPAQLHandler.handleExportElement format)
const exportPackage = {
exportVersion: '1.0',
exportedAt: new Date().toISOString(),
elementType: type,
elementName: name,
format,
data: '',
};
// Serialize to requested format
if (format === 'yaml') {
exportPackage.data = yaml.dump(elementDetails, {
indent: 2,
lineWidth: 120,
noRefs: true,
});
}
else {
exportPackage.data = JSON.stringify(elementDetails, null, 2);
}
return exportPackage;
}
/**
* Handle element import operation.
* Parses export package and delegates to ElementCRUD.
*
* Supports two export package formats:
* 1. MCPAQLHandler format: { exportVersion, elementType, elementName, format, data }
* 2. Legacy format: { version, element: { type, name, ... } }
*/
async function handleImportElement(mappedParams, registry) {
const handler = registry.elementCRUD;
if (!handler) {
throw new Error('ElementCRUD operations not available: handler not configured');
}
const data = mappedParams.data;
const overwrite = mappedParams.overwrite;
// Parse the export package (accept string or already-parsed object)
let exportPackage;
if (typeof data === 'string') {
try {
exportPackage = JSON.parse(data);
}
catch {
throw new Error('Invalid export package: not valid JSON');
}
}
else if (typeof data === 'object' && data !== null) {
exportPackage = data;
}
else {
throw new Error('Invalid export package: data must be a string or object');
}
// Determine element details based on package format
let elementType;
let elementName;
let elementData;
if (exportPackage.elementType && exportPackage.data) {
// MCPAQLHandler format: { exportVersion, elementType, elementName, format, data }
elementType = exportPackage.elementType;
elementName = exportPackage.elementName;
const format = exportPackage.format;
// Parse the nested data field based on format
const nestedData = exportPackage.data;
if (typeof nestedData === 'string') {
try {
if (format === 'yaml') {
// Use SecureYamlParser.parseRawYaml for safe YAML parsing (CORE_SCHEMA, size limits)
elementData = SecureYamlParser.parseRawYaml(nestedData);
}
else {
elementData = JSON.parse(nestedData);
}
}
catch {
throw new Error(`Invalid export package: data field is not valid ${format || 'JSON'}`);
}
}
else if (typeof nestedData === 'object' && nestedData !== null) {
elementData = nestedData;
}
else {
throw new Error('Invalid export package: data field must be string or object');
}
}
else if (exportPackage.element) {
// Legacy format: { version, element: { type, name, ... } }
const element = exportPackage.element;
elementType = element.type;
elementName = element.name;
elementData = element;
}
else {
throw new Error('Invalid export package: missing element data');
}
// Check if element already exists when overwrite is false
if (!overwrite) {
try {
const existing = await handler.getElementDetails(elementName, elementType);
if (existing) {
throw new Error(`Element '${elementName}' already exists. Use overwrite: true to replace.`);
}
}
catch (e) {
// Element doesn't exist, which is what we want
if (!(e instanceof Error) || !e.message.includes('not found')) {
throw e;
}
}
}
// Create the element
return handler.createElement({
name: elementData.name || elementName,
type: elementType,
description: elementData.description || '',
content: elementData.content,
instructions: elementData.instructions,
metadata: elementData.metadata,
});
}
// ============================================================================
// Main Dispatcher
// ============================================================================
/**
* SchemaDispatcher - Dispatch operations using schema definitions
*
* This class provides the core dispatch logic for schema-driven operations.
* It replaces the manual dispatch methods in MCPAQLHandler for operations
* that are defined in the schema.
*/
export class SchemaDispatcher {
/**
* Check if an operation can be handled by schema dispatch
*/
static canDispatch(operation) {
return isSchemaOperation(operation);
}
/**
* Dispatch an operation using its schema definition
*
* @param operation - Operation name (e.g., 'browse_collection')
* @param params - Operation parameters
* @param registry - Handler registry with all configured handlers
* @param input - Optional full OperationInput for source resolution
* @returns Promise resolving to operation result
* @throws Error if operation not found, handler missing, or params invalid
*/
static async dispatch(operation, params, registry, input) {
// Get schema definition
const schema = getOperationSchema(operation);
if (!schema) {
throw new Error(`No schema definition found for operation '${operation}'`);
}
// Apply normalizer if schema specifies one (Issue #243)
// Normalizers transform raw input params before validation and dispatch
if (schema.normalizer) {
const normalizer = NormalizerRegistry.get(schema.normalizer);
if (!normalizer) {
throw new Error(`Normalizer '${schema.normalizer}' not found for operation '${operation}'. ` +
`Registered normalizers: [${NormalizerRegistry.list().join(', ') || 'none'}]`);
}
// Build normalizer context for debugging and future extensibility
const normalizerContext = {
operation,
endpoint: schema.endpoint,
handler: schema.handler,
method: schema.method,
elementType: input?.elementType,
};
// Normalize params - this may transform, validate, or enrich parameters
const result = normalizer.normalize(params, normalizerContext);
if (!result.success) {
throw new Error(result.error);
}
// Use normalized params for the rest of dispatch
params = result.params;
}
// Determine if we need full input context for source resolution
const needsInput = schema.needsFullInput && input;
// Handle special operations
if (schema.method === '__introspect__') {
return handleIntrospection(params);
}
if (schema.method === '__buildInfo__') {
return handleBuildInfo(registry);
}
if (schema.method === '__cacheBudget__') {
return handleCacheBudgetReport(registry);
}
if (schema.method === '__capabilities__') {
return handleCapabilities(params);
}
// Map params according to schema (with input context and param style)
// For operations with normalizers, the normalized params ARE the mapped params
// (the normalizer already transforms input to handler-ready format)
const mappedParams = schema.normalizer
? params // Normalizer already produced the correct output format
: schema.params
? mapParams(params, schema.params, needsInput ? input : undefined, schema.paramStyle)
: {};
// Validate params: required params first, then type validation (Issue #255)
// Skip validation for normalized operations (normalizer handles validation)
if (schema.params && !schema.normalizer) {
validateRequiredParams(params, schema.params, operation, needsInput ? input : undefined);
validateParamTypes(params, schema.params, operation);
}
// Handle special ElementCRUD operations
if (schema.method === '__export__') {
return handleExportElement(mappedParams, registry);
}
if (schema.method === '__import__') {
return handleImportElement(mappedParams, registry);
}
// Get handler from registry
const handler = getHandler(registry, schema.handler, operation, schema.optional ?? false);
// Get method from handler
const method = getMethod(handler, schema.method, schema.handler, operation);
// Build arguments based on argBuilder type (with input context if needed)
const args = buildArgs(params, schema, mappedParams, needsInput ? input : undefined);
// Call the handler method
// Note: Field selection is applied at MCPAQLHandler level (Issue #202)
return method(...args);
}
}
// ============================================================================
// Exports
// ============================================================================
export { isSchemaOperation, getOperationSchema };
// Test exports for security boundary verification
export const __test__ = {
getNestedValue,
SAFE_PATH_PATTERN,
FORBIDDEN_PATHS,
};
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2NoZW1hRGlzcGF0Y2hlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9oYW5kbGVycy9tY3AtYXFsL1NjaGVtYURpc3BhdGNoZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBZ0NHO0FBRUgsT0FBTyxJQUFJLE1BQU0sU0FBUyxDQUFDO0FBQzNCLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLG9DQUFvQyxDQUFDO0FBQ3RFLE9BQU8sRUFDTCxrQkFBa0IsRUFDbEIsaUJBQWlCLEdBS2xCLE1BQU0sc0JBQXNCLENBQUM7QUFDOUIsT0FBTyxFQUFFLHFCQUFxQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDbkUsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFJNUQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDBCQUEwQixDQUFDO0FBQ3ZELDJFQUEyRTtBQUUzRSwrRUFBK0U7QUFDL0UsbUNBQW1DO0FBQ25DLCtFQUErRTtBQUUvRTs7O0dBR0c7QUFDSCxNQUFNLGlCQUFpQixHQUFHLDZCQUE2QixDQUFDO0FBQ3hELE1BQU0sZUFBZSxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsV0FBVyxFQUFFLGFBQWEsRUFBRSxXQUFXLENBQUMsQ0FBQyxDQUFDO0FBRTNFOzs7OztHQUtHO0FBQ0gsU0FBUyxjQUFjLENBQUMsR0FBNEIsRUFBRSxJQUFZO0lBQ2hFLG9EQUFvRDtJQUNwRCxJQUFJLENBQUMsaUJBQWlCLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDbEMsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQ0FBaUMsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUMzRCxDQUFDO0lBRUQsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUU5Qix5Q0FBeUM7SUFDekMsS0FBSyxN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