@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.
759 lines • 324 kB
JavaScript
/**
* OperationSchema - Declarative operation definitions for MCP-AQL
*
* This module provides schema-driven operation definitions that enable:
* 1. Declarative operation configuration (no manual switch statements)
* 2. Auto-generated parameter validation
* 3. Type-safe dispatch to handler methods
* 4. Single source of truth for operation metadata
*
* ARCHITECTURE:
* Operations are defined with:
* - endpoint: CRUDE endpoint (CREATE, READ, UPDATE, DELETE, EXECUTE)
* - handler: Key in HandlerRegistry
* - method: Method name on the handler
* - params: Parameter definitions with types and mappings
* - description: Human-readable description
*
* The SchemaDispatcher uses these definitions to automatically route
* operations to handler methods without manual dispatch code.
*
* @see Issue #247 - Schema-driven operation definitions
*/
import { env } from '../../config/env.js';
// ============================================================================
// Collection Operations Schema (Proof of Concept)
// ============================================================================
/**
* Collection operations schema
*
* These are the first operations migrated to schema-driven dispatch.
* Each operation defines:
* - Where it lives (endpoint)
* - What handler to use
* - What method to call
* - What parameters it accepts
*/
export const COLLECTION_OPERATIONS = {
browse_collection: {
endpoint: 'READ',
handler: 'collectionHandler',
method: 'browseCollection',
category: 'Community Collection',
description: 'Browse the DollhouseMCP community collection by section and type',
optional: true,
params: {
section: { type: 'string', description: 'Collection section to browse' },
type: { type: 'string', description: 'Element type filter' },
},
returns: { name: 'CollectionBrowseResult', kind: 'object', description: 'Browseable collection sections and items' },
examples: ['{ operation: "browse_collection", params: { section: "personas" } }'],
},
search_collection: {
endpoint: 'READ',
handler: 'collectionHandler',
method: 'searchCollection',
category: 'Community Collection',
description: 'Search the community collection for elements by keywords',
optional: true,
params: {
query: { type: 'string', required: true, description: 'Search query' },
},
returns: { name: 'CollectionSearchResult', kind: 'object', description: 'Matching elements from collection' },
examples: ['{ operation: "search_collection", params: { query: "code review" } }'],
},
search_collection_enhanced: {
endpoint: 'READ',
handler: 'collectionHandler',
method: 'searchCollectionEnhanced',
category: 'Community Collection',
description: 'Advanced search with pagination, filtering, and sorting',
optional: true,
argBuilder: 'spread',
params: {
query: { type: 'string', required: true, description: 'Search query' },
},
returns: { name: 'EnhancedSearchResult', kind: 'object', description: 'Paginated search results with metadata' },
examples: ['{ operation: "search_collection_enhanced", params: { query: "assistant", limit: 10 } }'],
},
get_collection_content: {
endpoint: 'READ',
handler: 'collectionHandler',
method: 'getCollectionContent',
category: 'Community Collection',
description: 'Get detailed information about content from the collection',
optional: true,
params: {
path: { type: 'string', required: true, description: 'Content path in collection' },
},
returns: { name: 'CollectionContent', kind: 'object', description: 'Full content and metadata for collection item' },
examples: ['{ operation: "get_collection_content", params: { path: "personas/creative-writer.md" } }'],
},
get_collection_cache_health: {
endpoint: 'READ',
handler: 'collectionHandler',
method: 'getCollectionCacheHealth',
category: 'Community Collection',
description: 'Get health status and statistics for the collection cache',
optional: true,
params: {},
returns: { name: 'CacheHealthStatus', kind: 'object', description: 'Cache health metrics and diagnostics' },
examples: ['{ operation: "get_collection_cache_health" }'],
},
install_collection_content: {
endpoint: 'CREATE',
handler: 'collectionHandler',
method: 'installContent',
category: 'Community Collection',
description: 'Install an element from the collection to your local portfolio',
optional: true,
params: {
path: { type: 'string', required: true, description: 'Content path to install' },
},
returns: { name: 'InstallResult', kind: 'object', description: 'Installation result with element name, type, and local file path' },
examples: ['{ operation: "install_collection_content", params: { path: "personas/creative-writer.md" } }'],
},
submit_collection_content: {
endpoint: 'CREATE',
handler: 'collectionHandler',
method: 'submitContent',
category: 'Community Collection',
description: 'Submit a local element to the community collection via GitHub',
optional: true,
params: {
content: { type: 'string', required: true, description: 'Content to submit' },
},
returns: { name: 'SubmitResult', kind: 'object', description: 'Submission result with GitHub PR URL and status message' },
examples: ['{ operation: "submit_collection_content", params: { content: "personas/my-persona" } }'],
},
};
// ============================================================================
// Auth Operations Schema
// ============================================================================
export const AUTH_OPERATIONS = {
setup_github_auth: {
endpoint: 'CREATE',
handler: 'authHandler',
method: 'setupGitHubAuth',
category: 'GitHub Authentication',
description: 'Set up GitHub authentication using device flow',
optional: true,
params: {},
returns: { name: 'AuthSetupResult', kind: 'object', description: 'Device flow setup: { userCode, verificationUrl, expiresIn, message }' },
examples: ['{ operation: "setup_github_auth" }'],
},
check_github_auth: {
endpoint: 'READ',
handler: 'authHandler',
method: 'checkGitHubAuth',
category: 'GitHub Authentication',
description: 'Check current GitHub authentication status',
optional: true,
params: {},
returns: { name: 'AuthStatus', kind: 'object', description: 'Current authentication state and user info' },
examples: ['{ operation: "check_github_auth" }'],
},
clear_github_auth: {
endpoint: 'DELETE',
handler: 'authHandler',
method: 'clearGitHubAuth',
category: 'GitHub Authentication',
description: 'Remove GitHub authentication and disconnect',
optional: true,
params: {},
returns: { name: 'OperationResult', kind: 'union', description: 'Success or failure status' },
examples: ['{ operation: "clear_github_auth" }'],
},
configure_oauth: {
endpoint: 'CREATE',
handler: 'authHandler',
method: 'configureOAuth',
category: 'GitHub Authentication',
description: 'Configure GitHub OAuth client ID',
optional: true,
params: {
client_id: { type: 'string', description: 'OAuth client ID' },
},
returns: { name: 'OAuthConfig', kind: 'object', description: 'OAuth configuration status' },
examples: ['{ operation: "configure_oauth", params: { client_id: "your-client-id" } }'],
},
oauth_helper_status: {
endpoint: 'READ',
handler: 'authHandler',
method: 'getOAuthHelperStatus',
category: 'GitHub Authentication',
description: 'Get diagnostic information about OAuth helper process',
optional: true,
params: {
verbose: { type: 'boolean', description: 'Include verbose diagnostics' },
},
returns: { name: 'OAuthHelperStatus', kind: 'object', description: 'Helper process diagnostics' },
examples: ['{ operation: "oauth_helper_status", params: { verbose: true } }'],
},
};
// ============================================================================
// Enhanced Index Operations Schema
// ============================================================================
export const ENHANCED_INDEX_OPERATIONS = {
find_similar_elements: {
endpoint: 'READ',
handler: 'enhancedIndexHandler',
method: 'findSimilarElements',
category: 'Intelligence',
description: 'Find semantically similar elements using NLP scoring',
optional: true,
argBuilder: 'named',
params: {
element_name: { type: 'string', required: true, mapTo: 'elementName' },
element_type: { type: 'string', mapTo: 'elementType' },
limit: { type: 'number', default: 10 },
threshold: { type: 'number', default: 0.5 },
},
returns: { name: 'SimilarElements', kind: 'object', description: 'List of similar elements with similarity scores' },
examples: ['{ operation: "find_similar_elements", params: { element_name: "creative-writer", element_type: "persona", limit: 5 } }'],
},
get_element_relationships: {
endpoint: 'READ',
handler: 'enhancedIndexHandler',
method: 'getElementRelationships',
category: 'Intelligence',
description: 'Get all relationships for a specific element',
optional: true,
argBuilder: 'named',
params: {
element_name: { type: 'string', required: true, mapTo: 'elementName' },
element_type: { type: 'string', mapTo: 'elementType' },
relationship_types: { type: 'string[]', mapTo: 'relationshipTypes' },
},
returns: { name: 'ElementRelationships', kind: 'object', description: 'Relationship graph for the element' },
examples: ['{ operation: "get_element_relationships", params: { element_name: "code-reviewer", element_type: "skill" } }'],
},
search_by_verb: {
endpoint: 'READ',
handler: 'enhancedIndexHandler',
method: 'searchByVerb',
category: 'Intelligence',
description: 'Search for elements that handle a specific action verb',
optional: true,
argBuilder: 'named',
params: {
verb: { type: 'string', required: true },
limit: { type: 'number', default: 20 },
},
returns: { name: 'VerbSearchResult', kind: 'object', description: 'Elements matching the action verb' },
examples: ['{ operation: "search_by_verb", params: { verb: "analyze", limit: 10 } }'],
},
get_relationship_stats: {
endpoint: 'READ',
handler: 'enhancedIndexHandler',
method: 'getRelationshipStats',
category: 'Intelligence',
description: 'Get statistics about Enhanced Index relationships',
optional: true,
params: {},
returns: { name: 'RelationshipStats', kind: 'object', description: 'Aggregate statistics about element relationships' },
examples: ['{ operation: "get_relationship_stats" }'],
},
};
// ============================================================================
// Template Operations Schema
// ============================================================================
export const TEMPLATE_OPERATIONS = {
render: {
endpoint: 'READ',
handler: 'templateRenderer',
method: 'render',
category: 'Template Rendering',
description: 'Render a template with provided variables. For section-format templates (<template>, <style>, <script>), renders the <template> section by default — variable substitution only applies there. Use section: "style" or "script" to retrieve raw passthrough sections where }} is safe.',
params: {
// Issue #290: Use element_name for consistency
element_name: { type: 'string', required: true, mapTo: 'name', description: 'Template name' },
variables: { type: 'object', description: 'Variables to substitute in the <template> section (e.g. { "title": "My Page", "user": "Alice" })' },
section: { type: 'string', required: false, description: 'Extract a specific raw section without variable substitution: "style" or "script". Useful for CSS theme templates or JS-only templates. Only valid for section-format templates.' },
all_sections: { type: 'boolean', required: false, description: 'Return all sections together: { template: rendered, style: raw, script: raw }. Use when you need the rendered HTML alongside its CSS and JS.' },
},
returns: { name: 'RenderResult', kind: 'object', description: 'Rendered template content. content field has the result string; sections field populated when all_sections: true.' },
examples: [
'{ operation: "render", params: { element_name: "meeting-notes", variables: { date: "2024-01-15", attendees: ["Alice", "Bob"] } } }',
'{ operation: "render", params: { element_name: "dashboard-theme-dark", section: "style" } }',
'{ operation: "render", params: { element_name: "dashboard-page", variables: { title: "My Dashboard" }, all_sections: true } }',
],
},
};
// ============================================================================
// Introspection Operations Schema
// ============================================================================
export const INTROSPECTION_OPERATIONS = {
introspect: {
endpoint: 'READ',
handler: 'elementCRUD', // Uses IntrospectionResolver directly
method: '__introspect__', // Special marker for introspection
category: 'System Introspection',
description: 'Query available operations, types, and element format specs for discovery',
params: {
query: { type: 'string', required: true, description: "What to introspect: 'operations' (list/detail operations), 'types' (list/detail type definitions), 'format' (element creation format specs — required/optional fields, syntax, examples), 'categories' (category format rules + how to discover existing categories via query_elements). Default: 'operations'" },
name: { type: 'string', description: "Specific item name. For 'operations': operation name. For 'types': type name. For 'format': element type (e.g. 'template', 'persona'). Not used for 'categories'. Omit for overview." },
},
returns: { name: 'IntrospectionResult', kind: 'object', description: 'Operations, types, or format specification information' },
examples: [
'{ operation: "introspect", params: { query: "operations" } }',
'{ operation: "introspect", params: { query: "operations", name: "create_element" } }',
'{ operation: "introspect", params: { query: "types", name: "ElementType" } }',
'{ operation: "introspect", params: { query: "format" } }',
'{ operation: "introspect", params: { query: "format", name: "template" } }',
'{ operation: "introspect", params: { query: "categories" } }',
],
},
get_capabilities: {
endpoint: 'READ',
handler: 'elementCRUD', // Uses IntrospectionResolver directly
method: '__capabilities__', // Special marker for capabilities
category: 'System Introspection',
description: 'Get a high-level map of all server capabilities grouped by user intent. Returns brief descriptions of every available operation organized by category. Use introspect for full details on any specific operation.',
params: {
category: { type: 'string', description: 'Filter to a specific category name. Omit to see all categories.' },
},
returns: { name: 'CapabilitiesResult', kind: 'object', description: 'Categorized capability map with brief descriptions, sources, and status' },
examples: [
'{ operation: "get_capabilities" }',
'{ operation: "get_capabilities", params: { category: "Element Lifecycle" } }',
],
},
};
// ============================================================================
// Persona Operations Schema
// ============================================================================
export const PERSONA_OPERATIONS = {
import_persona: {
endpoint: 'CREATE',
handler: 'personaHandler',
method: 'importPersona',
category: 'Element Lifecycle',
description: 'Import a persona from a file path or JSON string',
optional: true,
params: {
source: { type: 'string', required: true, description: 'File path or JSON string' },
overwrite: { type: 'boolean', description: 'Overwrite existing persona' },
},
returns: { name: 'ImportResult', kind: 'object', description: 'Import status and persona info' },
examples: ['{ operation: "import_persona", params: { source: "/path/to/persona.md", overwrite: true } }'],
},
};
// ============================================================================
// Config Operations Schema
// ============================================================================
export const CONFIG_OPERATIONS = {
dollhouse_config: {
endpoint: 'READ',
handler: 'configHandler',
method: 'handleConfigOperation',
category: 'Configuration & Diagnostics',
description: 'Manage DollhouseMCP configuration settings',
optional: true,
argBuilder: 'named',
params: {
action: { type: 'string', required: true, description: 'get, set, reset, export, import, wizard' },
setting: { type: 'string' },
value: { type: 'string' },
section: { type: 'string' },
format: { type: 'string' },
data: { type: 'string' },
},
returns: { name: 'ConfigResult', kind: 'object', description: 'Configuration operation result' },
examples: [
'{ operation: "dollhouse_config", params: { action: "get", setting: "debug" } }',
'{ operation: "dollhouse_config", params: { action: "set", setting: "logLevel", value: "verbose" } }',
],
},
convert_skill_format: {
endpoint: 'READ',
handler: 'configHandler',
method: 'convertSkillFormat',
category: 'Configuration & Diagnostics',
description: 'Convert between current Agent Skill and Dollhouse Skill formats (both directions) with structured warnings and optional roundtrip state for lossless supported-field restoration',
optional: true,
argBuilder: 'named',
params: {
direction: { type: 'string', required: true, description: 'Conversion direction: agent_to_dollhouse or dollhouse_to_agent' },
agent_skill: { type: 'object', description: 'Agent Skill structure for agent_to_dollhouse: { "SKILL.md": "...", "scripts/": { ... }, "references/": { ... }, "assets/": { ... }, "agents/": { ... } }' },
dollhouse: { type: 'object', description: 'Structured Dollhouse skill artifact for dollhouse_to_agent: { metadata, instructions, content }' },
dollhouse_markdown: { type: 'string', description: 'Serialized Dollhouse markdown input for dollhouse_to_agent (alternative to dollhouse object)' },
roundtrip_state: { type: 'object', description: 'Optional state returned by agent_to_dollhouse for exact reverse conversion' },
prefer_roundtrip_state: { type: 'boolean', default: true, description: 'When true (default), use roundtrip_state if valid for lossless restoration' },
path_mode: { type: 'string', default: 'safe', description: 'Path handling mode: safe (default, allowlisted/validated paths, security findings sanitized) or lossless (preserve non-allowlisted paths for full-fidelity conversions while still reporting security findings). Input bounds: 2 MiB per text field, 16 MiB aggregate, 2000 file entries. Conversion metrics returned in report.metrics.' },
security_mode: { type: 'string', default: 'strict', description: 'Security handling mode for agent_to_dollhouse: strict (default, fail conversion on high/critical findings) or warn (surface findings in report.warnings and continue). Frontmatter is also checked for unsafe YAML patterns (including YAML-bomb/amplification patterns). Use warn only for trusted migration workflows.' },
},
returns: { name: 'SkillConversionResult', kind: 'object', description: 'Converted artifact plus machine-readable conversion report and warnings' },
examples: [
'{ operation: "convert_skill_format", params: { direction: "agent_to_dollhouse", agent_skill: { "SKILL.md": "---\\nname: my-skill\\ndescription: test\\n---\\n\\nUse this skill." } } }',
'{ operation: "convert_skill_format", params: { direction: "agent_to_dollhouse", security_mode: "warn", path_mode: "lossless", agent_skill: { "SKILL.md": "---\\nname: my-skill\\ndescription: test\\n---\\n\\nUse this skill." } } }',
'{ operation: "convert_skill_format", params: { direction: "dollhouse_to_agent", path_mode: "lossless", dollhouse_markdown: "---\\nname: my-skill\\ndescription: test\\ninstructions: Use this skill.\\n---\\n\\nReference content" } }',
],
},
get_build_info: {
endpoint: 'READ',
handler: 'buildInfoService',
method: '__buildInfo__', // Special marker for build info
category: 'Configuration & Diagnostics',
description: 'Get comprehensive build and runtime information',
optional: true,
params: {},
returns: { name: 'BuildInfo', kind: 'object', description: 'Build info: { version, buildDate, nodeVersion, platform, elementCounts }' },
examples: ['{ operation: "get_build_info" }'],
},
get_cache_budget_report: {
endpoint: 'READ',
handler: 'cacheMemoryBudget',
method: '__cacheBudget__',
category: 'Configuration & Diagnostics',
description: 'Get global cache memory budget report with per-cache diagnostics',
optional: true,
params: {},
returns: { name: 'BudgetReport', kind: 'object', description: 'Cache memory usage, per-cache stats, hit rates, and budget utilization' },
examples: ['{ operation: "get_cache_budget_report" }'],
},
};
// ============================================================================
// Portfolio Operations Schema (Issue #252)
// ============================================================================
/**
* Portfolio operations schema - demonstrates paramStyle conversion pattern.
*
* PATTERN: Automatic Parameter Style Conversion
* External APIs often use snake_case naming, while JavaScript handlers
* typically use camelCase. The `paramStyle: 'snakeToCamel'` setting
* enables automatic conversion without individual `mapTo` definitions.
*
* This pattern is important for MCP-AQL adapters wrapping external MCP
* servers that follow different naming conventions.
*
* @example
* // Input from MCP client: { dry_run: true, max_results: 10 }
* // Handler receives: { dryRun: true, maxResults: 10 }
*/
export const PORTFOLIO_OPERATIONS = {
portfolio_status: {
endpoint: 'READ',
handler: 'portfolioHandler',
method: 'portfolioStatus',
category: 'Portfolio Management',
description: 'Check GitHub portfolio repository status and element counts',
optional: true,
argBuilder: 'single',
params: {
username: { type: 'string', description: 'GitHub username to check' },
},
returns: { name: 'PortfolioStatus', kind: 'object', description: 'Portfolio sync status and element counts' },
examples: ['{ operation: "portfolio_status" }'],
},
init_portfolio: {
endpoint: 'CREATE',
handler: 'portfolioHandler',
method: 'initPortfolio',
category: 'Portfolio Management',
description: 'Initialize a new GitHub portfolio repository',
optional: true,
argBuilder: 'named',
paramStyle: 'snakeToCamel',
params: {
repository_name: { type: 'string', description: 'Name for the portfolio repository' },
private: { type: 'boolean', description: 'Whether the repository should be private' },
description: { type: 'string', description: 'Repository description' },
},
returns: { name: 'PortfolioInitResult', kind: 'object', description: 'Newly created portfolio repository details' },
examples: ['{ operation: "init_portfolio", params: { repository_name: "my-portfolio", private: true } }'],
},
portfolio_config: {
endpoint: 'READ',
handler: 'portfolioHandler',
method: 'portfolioConfig',
category: 'Portfolio Management',
description: 'Configure portfolio settings (auto-sync, visibility, etc.)',
optional: true,
argBuilder: 'named',
paramStyle: 'snakeToCamel',
params: {
auto_sync: { type: 'boolean', description: 'Enable automatic syncing' },
default_visibility: { type: 'string', description: 'Default visibility for new elements' },
auto_submit: { type: 'boolean', description: 'Automatically submit to collection' },
repository_name: { type: 'string', description: 'Portfolio repository name' },
},
returns: { name: 'PortfolioConfig', kind: 'object', description: 'Current portfolio configuration settings' },
examples: ['{ operation: "portfolio_config", params: { auto_sync: true } }'],
},
sync_portfolio: {
endpoint: 'CREATE',
handler: 'portfolioHandler',
method: 'syncPortfolio',
category: 'Portfolio Management',
description: 'Sync local portfolio with GitHub repository',
optional: true,
argBuilder: 'named',
paramStyle: 'snakeToCamel',
params: {
direction: { type: 'string', default: 'push', description: 'Sync direction: push or pull' },
mode: { type: 'string', description: 'Sync mode' },
force: { type: 'boolean', default: false, description: 'Force sync even with conflicts' },
dry_run: { type: 'boolean', default: false, description: 'Preview changes without applying' },
confirm_deletions: { type: 'boolean', description: 'Confirm before deleting elements' },
},
returns: { name: 'SyncResult', kind: 'object', description: 'Sync results with changed element list and conflict details' },
examples: ['{ operation: "sync_portfolio", params: { direction: "push", dry_run: true } }'],
},
/**
* Unified search operation (Issue #243)
*
* Consolidates all search functionality with a scope parameter.
* Uses the 'searchParams' normalizer to transform:
* - scope → sources array conversion
* - pagination (offset/limit → page/pageSize)
* - sort parameter normalization
* - filter parameter extraction
*
* @see SearchParamsNormalizer for transformation logic
*/
search: {
endpoint: 'READ',
handler: 'portfolioHandler',
method: 'searchAll',
category: 'Element Discovery',
description: 'Unified search across local, GitHub, and collection sources with flexible scope',
optional: true,
normalizer: 'searchParams',
argBuilder: 'named',
params: {
query: { type: 'string', required: true, description: 'Search query' },
scope: { type: 'unknown', description: 'Search scope: "local", "github", "collection", "all", or array of scopes' },
type: { type: 'string', description: 'Filter by element type' },
page: { type: 'number', description: 'Page number for pagination' },
limit: { type: 'number', description: 'Results per page' },
sort: { type: 'object', description: 'Sort options: { field, order }' },
filters: { type: 'object', description: 'Filter options: { tags, author, createdAfter, createdBefore }' },
options: { type: 'object', description: 'Search options: { fuzzyMatch, includeKeywords, includeTags }' },
fields: {
type: 'string | string[]',
description: 'Fields to include: array like ["element_name", "description"] OR preset: "minimal", "standard", "full"',
},
},
returns: { name: 'UnifiedSearchResult', kind: 'object', description: 'Paginated search results from specified scopes' },
examples: [
'{ operation: "search", params: { query: "creative" } }',
'{ operation: "search", params: { query: "helper", fields: "minimal" } }',
'{ operation: "search", params: { query: "assistant", fields: ["element_name", "description"] } }',
// Memory-specific: filter by tags to find categorized entries
'{ operation: "search", params: { query: "*", type: "memory", filters: { tags: ["important", "reference"] } } }',
],
},
// Legacy search operations (prefer unified "search" operation above)
search_portfolio: {
endpoint: 'READ',
handler: 'portfolioHandler',
method: 'searchPortfolio',
category: 'Element Discovery',
description: 'Search local portfolio by content name, keywords, or tags',
optional: true,
argBuilder: 'named',
paramStyle: 'snakeToCamel',
params: {
query: { type: 'string', required: true, description: 'Search query' },
type: { type: 'string', mapTo: 'elementType', description: 'Filter by element type' },
fuzzy_match: { type: 'boolean', description: 'Enable fuzzy matching' },
max_results: { type: 'number', description: 'Maximum number of results' },
include_keywords: { type: 'boolean', description: 'Search in keywords' },
include_tags: { type: 'boolean', description: 'Search in tags' },
include_triggers: { type: 'boolean', description: 'Search in triggers' },
include_descriptions: { type: 'boolean', description: 'Search in descriptions' },
fields: {
type: 'string | string[]',
description: 'Fields to include: array like ["element_name", "description"] OR preset: "minimal", "standard", "full"',
},
},
returns: { name: 'PortfolioSearchResult', kind: 'object', description: 'Matching elements from local portfolio' },
examples: [
'{ operation: "search_portfolio", params: { query: "creative", type: "persona" } }',
'{ operation: "search_portfolio", params: { query: "helper", fields: "minimal" } }',
],
},
search_all: {
endpoint: 'READ',
handler: 'portfolioHandler',
method: 'searchAll',
category: 'Element Discovery',
description: 'Unified search across local, GitHub, and collection sources',
optional: true,
argBuilder: 'named',
paramStyle: 'snakeToCamel',
params: {
query: { type: 'string', required: true, description: 'Search query' },
sources: { type: 'string[]', description: 'Sources to search: local, github, collection' },
type: { type: 'string', mapTo: 'elementType', description: 'Filter by element type' },
page: { type: 'number', description: 'Page number for pagination' },
page_size: { type: 'number', description: 'Results per page' },
sort_by: { type: 'string', description: 'Sort field' },
fields: {
type: 'string | string[]',
description: 'Fields to include: array like ["element_name", "description"] OR preset: "minimal", "standard", "full"',
},
},
returns: { name: 'UnifiedSearchResult', kind: 'object', description: 'Search results from all sources with source attribution' },
examples: [
'{ operation: "search_all", params: { query: "helper", sources: ["local", "collection"] } }',
'{ operation: "search_all", params: { query: "creative", fields: "standard" } }',
],
},
portfolio_element_manager: {
endpoint: 'CREATE',
handler: 'syncHandler',
method: 'handleSyncOperation',
category: 'Portfolio Management',
description: 'Manage individual elements between local and GitHub',
optional: true,
argBuilder: 'named',
params: {
operation: { type: 'string', required: true, description: 'list-remote, download, upload, compare' },
element_name: { type: 'string', description: 'Element name to operate on' },
element_type: { type: 'string', description: 'Element type' },
filter: { type: 'object', description: 'Filter options for list operations' },
options: { type: 'object', description: 'Operation options (force, dry_run, etc.)' },
},
returns: { name: 'ElementManagerResult', kind: 'object', description: 'Element management result: { action, element_name, success, message }' },
examples: ['{ operation: "portfolio_element_manager", params: { operation: "download", element_name: "MyPersona", element_type: "persona" } }'],
},
};
// ============================================================================
// ElementCRUD Operations Schema (Issue #251)
// ============================================================================
/**
* ElementCRUD operations schema - demonstrates input normalization pattern.
*
* PATTERN: Input Normalization
* The 'type' parameter can come from two sources:
* 1. input.elementType (top-level in OperationInput)
* 2. params.type (inside the params object)
*
* The schema uses `sources` to define this fallback chain, and
* `needsFullInput: true` tells SchemaDispatcher to provide full input access.
*
* This pattern is important for MCP-AQL adapters where external servers
* may accept the same parameter in multiple locations.
*/
export const ELEMENT_CRUD_OPERATIONS = {
create_element: {
endpoint: 'CREATE',
handler: 'elementCRUD',
method: 'createElement',
category: 'Element Lifecycle',
description: 'Create a new element of any type. Note: Gatekeeper may return a confirmation prompt instead of creating immediately — use confirm_operation to approve, then retry.',
needsFullInput: true,
argBuilder: 'namedWithType',
params: {
element_name: { type: 'string', required: true, mapTo: 'elementName', description: 'Element name' },
element_type: {
type: 'string',
required: true,
mapTo: 'elementType',
description: 'Element type (persona, skill, template, agent, memory, ensemble)',
sources: ['input.element_type', 'input.elementType', 'params.element_type'],
},
description: { type: 'string', required: true, description: 'Element description' },
// Issue #602 resolved: Both 'instructions' and 'content' are first-class fields with distinct semantic roles.
instructions: { type: 'string', description: 'Behavioral INSTRUCTIONS — written in active/command voice as directives the AI must follow. For personas: "You ARE a security expert. ALWAYS check for vulnerabilities." For skills: "When triggered, ANALYZE code systematically. CHECK security FIRST." For agents: semantic behavioral profile — how the agent should behave (distinct from systemPrompt which is the raw LLM system message). For templates: rendering directives. Optional for memories and ensembles.' },
content: { type: 'string', description: 'Reference material, knowledge, and context — informational text the element draws from. For skills: domain knowledge, examples, reference frameworks. For templates: the template body with variable placeholders (REQUIRED). For personas: background context and expertise areas. For agents: reference material for task execution. For memories: initial entry content.' },
// --- Agent V2 goal (required for execute_agent) ---
goal: { type: 'object', description: 'For agents: Goal configuration REQUIRED for execute_agent. Contains template (string with {param} placeholders), parameters (array of {name, type, required, description}), and optional successCriteria (or success_criteria, string array). Snake_case keys are auto-normalized.' },
// --- Common fields — all element types (Issue #722) ---
// Routed in SchemaDispatcher.buildArgs → namedWithType for any element_type.
tags: { type: 'string[]', description: 'Tags for categorization and search (all element types). Example: ["code-review", "security", "automation"]' },
triggers: { type: 'string[]', description: 'Action verbs that trigger this element (all element types). Example: ["review", "audit", "analyze"]' },
// --- Agent V2 fields — only meaningful for agents (Issue #722) ---
// Routed in SchemaDispatcher.buildArgs → namedWithType inside the isAgent guard.
// Pipeline: SchemaDispatcher → createElement → AgentManager.create → serializeElement.
// Note: snake_case variants (e.g. system_prompt, risk_tolerance, success_criteria) are
// automatically normalized to camelCase at both the dispatcher and manager layers (Issue #725).
activates: { type: 'object', description: 'For agents: Elements to activate when agent executes. Keys: skills, personas, memories, templates, ensembles. Values: string arrays of element names.' },
tools: { type: 'object', description: 'For agents: Tool access policy. Contains allowed (required string array) and optional denied (string array).' },
systemPrompt: { type: 'string', description: 'For agents: Custom system prompt injected into LLM context during execution. Also accepted as system_prompt.' },
autonomy: { type: 'object', description: 'For agents: Autonomy configuration. Keys: riskTolerance (or risk_tolerance), maxAutonomousSteps (or max_autonomous_steps), requiresApproval (or requires_approval, glob patterns), autoApprove (or auto_approve, glob patterns). Snake_case keys are auto-normalized.' },
resilience: { type: 'object', description: 'For agents: Resilience policy for failure recovery. Keys: onStepLimitReached (or on_step_limit_reached), onExecutionFailure (or on_execution_failure), maxRetries (or max_retries), maxContinuations (or max_continuations), retryBackoff (or retry_backoff), preserveState (or preserve_state). Snake_case keys are auto-normalized.' },
category: { type: 'string', description: 'Category label for skills, templates, and memories. Must start with a letter, followed by letters, digits, hyphens, or underscores (max 21 chars). Example: "code-analysis". Not supported on personas, agents, or ensembles.' },
// --- Security: element-level gatekeeper policy (all element types, Issue #726) ---
// Layer 2 of the 3-layer gatekeeper system. Layer 1 (route policies) provides automatic
// baseline security for ALL elements. Layer 2 lets you customize per-element access control.
// Policies are DYNAMIC — they take effect when the element is activated and revert when deactivated.
gatekeeper: { type: 'object', description: 'Per-element security policy that controls what operations are allowed when this element is active. Policies are dynamic — activate the element and the policy takes effect; deactivate it and the restrictions revert. This gives users composable security: a "no-web" persona blocks web access while active, a "read-only" skill auto-approves reads but blocks writes, an ensemble can lock down which agents may be executed. Structure: { allow?: string[], confirm?: string[], deny?: string[], scopeRestrictions?: { allowedTypes?: string[], blockedTypes?: string[] }, externalRestrictions?: { description: string, allowPatterns?: string[], confirmPatterns?: string[], denyPatterns?: string[] } }. Use allow/confirm/deny for MCP-AQL operation patterns like "read_*", "edit_*", "execute_agent", or "delete_element". Use externalRestrictions for external tool or hook patterns like "Read:*", "Edit:*", "Bash:git status*", or "Bash:rm *". Priority: deny > confirm > allow > route default. scopeRestrictions limits which element types the policy applies to (e.g. allowedTypes: ["skill", "template"] means the policy only governs operations on those types). If omitted entirely, the element inherits system defaults which already include confirmation for sensitive operations.' },
metadata: { type: 'object', description: 'Additional metadata. For ensembles: include elements array. For skills/templates/memories: category can also be passed here.' },
},
returns: { name: 'Element', kind: 'object', description: 'Created element with name, type, version, file path, and metadata' },
examples: [
// Issue #602: Dual-field examples showing both 'instructions' (behavioral) and 'content' (reference)
'{ operation: "create_element", element_type: "persona", params: { element_name: "MyPersona", description: "A helpful assistant", instructions: "You ARE a helpful assistant. ALWAYS provide clear, accurate responses." } }',
'{ operation: "create_element", element_type: "agent", params: { element_name: "CodeReviewer", description: "Automated code review agent", instructions: "You are methodical and thorough. ALWAYS check security first. Report issues by severity.", goal: { template: "Review {files} for {review_type} issues", parameters: [{ name: "files", type: "string", required: true }, { name: "review_type", type: "string", required: true }], successCriteria: ["Review completed", "Issues documented"] } } }',
'{ operation: "create_element", element_type: "skill", params: { element_name: "CodeReview", description: "Reviews code for quality", instructions: "ANALYZE code systematically. CHECK security patterns FIRST. Report findings by severity.", content: "# Code Review Reference\\n\\n## Common Patterns\\n- OWASP Top 10\\n- CWE/SANS Top 25" } }',
'{ operation: "create_element", element_type: "skill", params: { element_name: "read-only-review", description: "Review session with write restrictions", instructions: "When active, ALLOW browsing and analysis but REQUIRE confirmation for edits.", gatekeeper: { allow: ["read_*", "list_*", "search_*", "get_*"], confirm: ["create_*", "edit_*", "update_*"], deny: ["delete_*"], externalRestrictions: { description: "Review shell guardrails", allowPatterns: ["Read:*", "Glob:*", "Grep:*"], confirmPatterns: ["Edit:*", "Write:*", "Bash:git push*"], denyPatterns: ["Bash:rm *", "WebSearch:*"] } } } }',
'{ operation: "create_element", element_type: "template", params: { element_name: "BugReport", description: "Bug report template", content: "## Bug Report\\n\\n**Summary:** {{summary}}\\n**Steps:** {{steps}}\\n**Expected:** {{expected}}", instructions: "Render all sections. NEVER omit required fields." } }',
// Memory relationship patterns - naming conventions for element-linked memories:
// agent-{name}-context: Agent execution state and learned behaviors
'{ operation: "create_element", element_type: "memory", params: { element_name: "agent-code-reviewer-context", description: "Stores context and learned preferences for code-reviewer agent" } }',
// persona-{name}-preferences: Persona personalization data
'{ operation: "create_element", element_type: "memory", params: { element_name: "persona-teacher-preferences", description: "Learned preferences for teacher persona behavior" } }',
// session-{purpose}: Cross-cutting session data used by multiple elements
'{ operation: "create_element", element_type: "memory", params: { element_name: "session-context", description: "Shared context accessible to all active elements" } }',
// project-{name}-{purpose}: Project-scoped knowledge
'{ operation: "create_element", element_type: "memory", params: { element_name: "project-webapp-decisions", description: "Architecture decisions for the webapp project" } }',
// Issue #1767: Ensemble example with valid roles (primary, support, override, monitor, core)
'{ operation: "create_element", element_type: "ensemble", params: { element_name: "my-ensemble", description: "Combined element set", metadata: { elements: [{ element_name: "expert", element_type: "persona", role: "primary" }, { element_name: "analysis", element_type: "skill", role: "support" }] } } }',
],
},
list_elements: {
endpoint: 'READ',
handler: 'elementCRUD',
method: 'listElements',
category: 'Element Discovery',
description: 'List elements with pagination, filtering, sorting, and aggregation. Returns structured JSON: { items, pagination, sorting, element_type }. Default: page 1, pageSize 20, sorted by name ascending. TIP: If the user wants to browse, explore, or view their portfolio visually, prefer open_portfolio_browser instead — it opens a full web UI with search, filters, and detail views.',
needsFullInput: true,
argBuilder: 'typeWithParams', // (type, fullParams) for pagination support
params: {
element_type: {
type: 'string',
required: true,
mapTo: 'elementType',
description: 'Element type to list (persona, skill, template, agent, memory, ensemble)',
sources: ['input.element_type', 'input.elementType', 'params.element_type'],
},
page: { type: 'number', default: 1, description: 'Page number (1-indexed)' },
pageSize: { type: 'number', default: 20, description: 'Items per page (max 100)' },
sortBy: { type: 'string', default: 'name', description: "Sort field: 'name', 'created', 'modified', 'version'" },
sortOrder: { type: 'string', default: 'asc', description: "Sort direction: 'asc' or 'desc'" },
nameContains: { type: 'string', description: 'Filter: partial name match (case-insensitive)' },
tags: { type: 'string[]', description: 'Filter: must have ALL specified tags (AND logic)' },
tagsAny: { type: 'string[]', description: 'Filter: must have ANY specified tag (OR logic)' },
author: { type: 'string', description: 'Filter: by author username' },
status: { type: 'string', description: "Filter: 'active', 'inactive', or 'all'" },
category: { type: 'string', description: 'Filter: by category (case-insensitive)' },
aggregate: { type: 'object', description: "Aggregation: { count: true } returns count only (~50 tokens). { count: true, group_by: 'category' } returns grouped counts. Allowed group_by fields: author, category, status, tags, version." },
fields: {
type: 'string | string[]',
description: 'Fields to include: preset ("minimal", "standard", "full") or array of field names',
},
},
returns: { name: 'ElementList', kind: 'object', description: 'Structured JSON: { items: [{ name, description, type, version, tags }], pagination: { page, pageSize, totalItems, totalPages, hasNextPage, hasPrevPage }, sorting: { sortBy, sortOrder }, element_type }. When aggregate is used: { count, element_type, groups? }' },
examples: [
'{ operation: "list_elements", element_type: "persona" }',
// Response: { items: [{ name: "Default", description: "...", type: "persona", version: "1.0.0", tags: [...] }, ...], pagination: { page: 1, pageSize: 20, totalItems: 42, totalPages: 3, hasNextPage: true, hasPrevPage: false }, sorting: { sortBy: "name", sortOrder: "asc" }, element_type: "persona" }
'{ operation: "list_elements", element_type: "persona", params: { page: 2, pageSize: 10 } }',
'{ operation: "list_elements", element_type: "skill", params: { tags: ["typescript"], sortBy: "modified", sortOrder: "desc" } }',
'{ operation: "list_elements", element_type: "persona", params: { aggregate: { count: true } } }',
// Response: { count: 42, element_type: "persona" }
'{ operation: "list_elements", element_type: "persona", params: { aggregate: { count: true, group_by: "category" } } }',
// Response: { count: 42, element_type: "persona", groups: { "assistant": 15, "creative": 12, "technical": 15 } }
'{ operation: "list_elements", element_type: "persona", params: { fields: "minimal" } }',
// TIP: For visual browsing, use open_portfolio_browser instead:
// { operation: "open_portfolio_browser", params: { tab: "portfolio", type: "persona" } }
],
},
get_element: {
endpoint: 'READ',
handler: 'elementCRUD',
method: 'getElementDetails',
category: 'Element Lifecycle',
description: 'Get a specific element by name. TIP: If the user wants to browse or explore multiple elements rather tha