@dollhousemcp/mcp-server 1.7.3 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/config/ConfigWizard.d.ts +78 -0
  2. package/dist/config/ConfigWizard.d.ts.map +1 -0
  3. package/dist/config/ConfigWizard.js +370 -0
  4. package/dist/config/ConfigWizardCheck.d.ts +47 -0
  5. package/dist/config/ConfigWizardCheck.d.ts.map +1 -0
  6. package/dist/config/ConfigWizardCheck.js +208 -0
  7. package/dist/config/ConfigWizardDisplay.d.ts +64 -0
  8. package/dist/config/ConfigWizardDisplay.d.ts.map +1 -0
  9. package/dist/config/ConfigWizardDisplay.js +150 -0
  10. package/dist/config/WizardFirstResponse.d.ts +25 -0
  11. package/dist/config/WizardFirstResponse.d.ts.map +1 -0
  12. package/dist/config/WizardFirstResponse.js +118 -0
  13. package/dist/config/portfolioConfig.d.ts +40 -0
  14. package/dist/config/portfolioConfig.d.ts.map +1 -0
  15. package/dist/config/portfolioConfig.js +58 -0
  16. package/dist/config/wizardTemplates.d.ts +84 -0
  17. package/dist/config/wizardTemplates.d.ts.map +1 -0
  18. package/dist/config/wizardTemplates.js +195 -0
  19. package/dist/elements/BaseElement.d.ts +15 -0
  20. package/dist/elements/BaseElement.d.ts.map +1 -1
  21. package/dist/elements/BaseElement.js +38 -5
  22. package/dist/generated/version.d.ts +2 -2
  23. package/dist/generated/version.js +3 -3
  24. package/dist/handlers/PortfolioPullHandler.d.ts +69 -0
  25. package/dist/handlers/PortfolioPullHandler.d.ts.map +1 -0
  26. package/dist/handlers/PortfolioPullHandler.js +340 -0
  27. package/dist/scripts/scripts/run-config-wizard.js +57 -0
  28. package/dist/scripts/src/config/ConfigManager.js +799 -0
  29. package/dist/scripts/src/config/ConfigWizard.js +368 -0
  30. package/dist/scripts/src/errors/SecurityError.js +47 -0
  31. package/dist/scripts/src/security/constants.js +28 -0
  32. package/dist/scripts/src/security/contentValidator.js +415 -0
  33. package/dist/scripts/src/security/errors.js +32 -0
  34. package/dist/scripts/src/security/regexValidator.js +217 -0
  35. package/dist/scripts/src/security/secureYamlParser.js +272 -0
  36. package/dist/scripts/src/security/securityMonitor.js +111 -0
  37. package/dist/scripts/src/security/validators/unicodeValidator.js +315 -0
  38. package/dist/scripts/src/utils/logger.js +288 -0
  39. package/dist/sync/PortfolioDownloader.d.ts +27 -0
  40. package/dist/sync/PortfolioDownloader.d.ts.map +1 -0
  41. package/dist/sync/PortfolioDownloader.js +120 -0
  42. package/dist/sync/PortfolioSyncComparer.d.ts +50 -0
  43. package/dist/sync/PortfolioSyncComparer.d.ts.map +1 -0
  44. package/dist/sync/PortfolioSyncComparer.js +158 -0
  45. package/dist/tools/getWelcomeMessage.d.ts +41 -0
  46. package/dist/tools/getWelcomeMessage.d.ts.map +1 -0
  47. package/dist/tools/getWelcomeMessage.js +109 -0
  48. package/dist/utils/TemplateRenderer.d.ts +63 -0
  49. package/dist/utils/TemplateRenderer.d.ts.map +1 -0
  50. package/dist/utils/TemplateRenderer.js +154 -0
  51. package/package.json +1 -1
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ /**
3
+ * Secure YAML Parser for DollhouseMCP - For Markdown Files with YAML Frontmatter
4
+ *
5
+ * IMPORTANT: This parser is specifically designed for Markdown files with YAML frontmatter
6
+ * (the format used by personas, skills, templates, and other elements).
7
+ *
8
+ * USE THIS FOR:
9
+ * - Persona files (e.g., creative-writer.md)
10
+ * - Skill files (e.g., code-review.md)
11
+ * - Template files (e.g., meeting-notes.md)
12
+ * - Any Markdown file with YAML frontmatter between --- markers
13
+ *
14
+ * DO NOT USE THIS FOR:
15
+ * - Pure YAML configuration files (use js-yaml directly with FAILSAFE_SCHEMA)
16
+ * - JSON files
17
+ * - Plain text files without frontmatter
18
+ *
19
+ * FILE FORMAT EXPECTED:
20
+ * ```
21
+ * ---
22
+ * name: Element Name
23
+ * description: Element description
24
+ * version: 1.0.0
25
+ * ---
26
+ *
27
+ * # Markdown content here
28
+ * The actual content/instructions go here...
29
+ * ```
30
+ *
31
+ * Provides safe YAML parsing that prevents deserialization attacks
32
+ * by using a restricted schema and pre-validation.
33
+ *
34
+ * Security: SEC-003 - YAML parsing vulnerability protection
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.SecureYamlParser = void 0;
38
+ const yaml = require("js-yaml");
39
+ const gray_matter_1 = require("gray-matter");
40
+ const SecurityError_js_1 = require("../errors/SecurityError.js");
41
+ const contentValidator_js_1 = require("./contentValidator.js");
42
+ const securityMonitor_js_1 = require("./securityMonitor.js");
43
+ class SecureYamlParser {
44
+ /**
45
+ * Parse a Markdown file with YAML frontmatter (Securely)
46
+ *
47
+ * @param input - The full content of a Markdown file with YAML frontmatter
48
+ * @param options - Parsing options for security and validation
49
+ * @returns ParsedContent with separated YAML data and Markdown content
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * // For a persona file:
54
+ * const personaFile = `---
55
+ * name: Creative Writer
56
+ * description: A creative writing assistant
57
+ * ---
58
+ * You are a creative writer...`;
59
+ *
60
+ * const result = SecureYamlParser.parse(personaFile);
61
+ * // result.data = { name: 'Creative Writer', description: '...' }
62
+ * // result.content = 'You are a creative writer...'
63
+ * ```
64
+ */
65
+ static parse(input, options = {}) {
66
+ const opts = { ...this.DEFAULT_OPTIONS, ...options };
67
+ // 1. Size validation
68
+ if (input.length > (opts.maxContentSize || this.DEFAULT_OPTIONS.maxContentSize)) {
69
+ throw new SecurityError_js_1.SecurityError('Content exceeds maximum allowed size', 'medium');
70
+ }
71
+ // 2. Extract frontmatter boundaries
72
+ const frontmatterMatch = input.match(/^---\n([\s\S]*?)\n---/);
73
+ if (!frontmatterMatch) {
74
+ // No frontmatter, return empty data
75
+ return {
76
+ data: {},
77
+ content: input
78
+ };
79
+ }
80
+ const yamlContent = frontmatterMatch[1];
81
+ const markdownContent = input.substring(frontmatterMatch[0].length);
82
+ // 3. Validate YAML size
83
+ if (yamlContent.length > (opts.maxYamlSize || this.DEFAULT_OPTIONS.maxYamlSize)) {
84
+ throw new SecurityError_js_1.SecurityError('YAML frontmatter exceeds maximum allowed size', 'medium');
85
+ }
86
+ // 4. Pre-parse security validation
87
+ if (!contentValidator_js_1.ContentValidator.validateYamlContent(yamlContent)) {
88
+ securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
89
+ type: 'YAML_INJECTION_ATTEMPT',
90
+ severity: 'CRITICAL',
91
+ source: 'secure_yaml_parser',
92
+ details: 'Malicious YAML pattern detected during parsing'
93
+ });
94
+ throw new SecurityError_js_1.SecurityError('Malicious YAML content detected', 'critical');
95
+ }
96
+ // 5. Parse with safe schema
97
+ let data;
98
+ try {
99
+ data = yaml.load(yamlContent, {
100
+ schema: this.SAFE_SCHEMA,
101
+ json: false, // Don't allow JSON-specific types
102
+ onWarning: (warning) => {
103
+ securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
104
+ type: 'YAML_PARSING_WARNING',
105
+ severity: 'LOW',
106
+ source: 'secure_yaml_parser',
107
+ details: `YAML warning: ${warning.message}`
108
+ });
109
+ }
110
+ });
111
+ }
112
+ catch (error) {
113
+ throw new SecurityError_js_1.SecurityError(`YAML parsing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'high');
114
+ }
115
+ // 6. Ensure data is an object
116
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
117
+ throw new SecurityError_js_1.SecurityError('YAML must contain an object at root level', 'medium');
118
+ }
119
+ // 7. Validate allowed keys if specified
120
+ if (opts.allowedKeys) {
121
+ const invalidKeys = Object.keys(data).filter(key => !opts.allowedKeys.includes(key));
122
+ if (invalidKeys.length > 0) {
123
+ throw new SecurityError_js_1.SecurityError(`Invalid YAML keys detected: ${invalidKeys.join(', ')}`, 'medium');
124
+ }
125
+ }
126
+ // 8. Validate field types and content
127
+ for (const [key, value] of Object.entries(data)) {
128
+ // Check field-specific validators only if field validation is enabled
129
+ if (opts.validateFields && this.FIELD_VALIDATORS[key] && !this.FIELD_VALIDATORS[key](value)) {
130
+ throw new SecurityError_js_1.SecurityError(`Invalid value for field '${key}'`, 'medium');
131
+ }
132
+ // Validate string fields for injection patterns
133
+ if (typeof value === 'string' && opts.validateContent) {
134
+ const validation = contentValidator_js_1.ContentValidator.validateAndSanitize(value);
135
+ if (!validation.isValid && validation.severity === 'critical') {
136
+ throw new SecurityError_js_1.SecurityError(`Security threat detected in field '${key}'`, 'critical');
137
+ }
138
+ // Replace with sanitized content
139
+ data[key] = validation.sanitizedContent;
140
+ }
141
+ }
142
+ // 9. Validate markdown content if requested
143
+ let finalContent = markdownContent;
144
+ if (opts.validateContent) {
145
+ const contentValidation = contentValidator_js_1.ContentValidator.validateAndSanitize(markdownContent);
146
+ if (!contentValidation.isValid && contentValidation.severity === 'critical') {
147
+ throw new SecurityError_js_1.SecurityError('Security threat detected in content', 'critical');
148
+ }
149
+ finalContent = contentValidation.sanitizedContent || markdownContent;
150
+ }
151
+ securityMonitor_js_1.SecurityMonitor.logSecurityEvent({
152
+ type: 'YAML_PARSE_SUCCESS',
153
+ severity: 'LOW',
154
+ source: 'secure_yaml_parser',
155
+ details: `Successfully parsed YAML with ${Object.keys(data).length} fields`
156
+ });
157
+ return {
158
+ data,
159
+ content: finalContent
160
+ };
161
+ }
162
+ /**
163
+ * Create a secure gray-matter compatible parser
164
+ */
165
+ static createSecureMatterParser() {
166
+ return {
167
+ parse: (input) => {
168
+ const result = this.parse(input);
169
+ return {
170
+ data: result.data,
171
+ content: result.content,
172
+ excerpt: result.excerpt,
173
+ orig: input
174
+ };
175
+ },
176
+ stringify: (content, data) => {
177
+ // Validate data before stringifying
178
+ const validation = contentValidator_js_1.ContentValidator.validateMetadata(data);
179
+ if (!validation.isValid) {
180
+ throw new SecurityError_js_1.SecurityError('Cannot stringify content with security threats', 'high');
181
+ }
182
+ // Use safe YAML dump
183
+ const yamlStr = yaml.dump(data, {
184
+ schema: this.SAFE_SCHEMA,
185
+ skipInvalid: true,
186
+ noRefs: true,
187
+ noCompatMode: true
188
+ });
189
+ return `---\n${yamlStr}---\n${content}`;
190
+ }
191
+ };
192
+ }
193
+ /**
194
+ * Safe wrapper for gray-matter with security validations
195
+ */
196
+ static safeMatter(input, options) {
197
+ // First, use our secure parser
198
+ const secureParsed = this.parse(input);
199
+ // Then use gray-matter with custom engines
200
+ return (0, gray_matter_1.default)(input, {
201
+ ...options,
202
+ engines: {
203
+ yaml: {
204
+ parse: (str) => {
205
+ // Use our secure YAML parsing
206
+ const parsed = yaml.load(str, {
207
+ schema: this.SAFE_SCHEMA,
208
+ json: false
209
+ });
210
+ // Ensure it's an object
211
+ if (typeof parsed !== 'object' || parsed === null) {
212
+ return {};
213
+ }
214
+ return parsed;
215
+ },
216
+ stringify: (obj) => {
217
+ return yaml.dump(obj, {
218
+ schema: this.SAFE_SCHEMA,
219
+ skipInvalid: true,
220
+ noRefs: true
221
+ });
222
+ }
223
+ }
224
+ }
225
+ });
226
+ }
227
+ }
228
+ exports.SecureYamlParser = SecureYamlParser;
229
+ SecureYamlParser.DEFAULT_OPTIONS = {
230
+ maxYamlSize: 64 * 1024, // 64KB for YAML
231
+ maxContentSize: 1024 * 1024, // 1MB for content
232
+ validateContent: true,
233
+ validateFields: true // By default, apply field validators
234
+ };
235
+ // Allowed YAML types - using CORE_SCHEMA (safe subset with basic types like booleans and integers)
236
+ SecureYamlParser.SAFE_SCHEMA = yaml.CORE_SCHEMA;
237
+ // Additional validation for specific persona fields
238
+ SecureYamlParser.FIELD_VALIDATORS = {
239
+ name: (v) => typeof v === 'string' && v.length <= 100,
240
+ description: (v) => typeof v === 'string' && v.length <= 500,
241
+ author: (v) => typeof v === 'string' && v.length <= 100,
242
+ version: (v) => typeof v === 'string' && /^\d+\.\d+(\.\d+)?(-[a-zA-Z0-9.-]+)?$/.test(v),
243
+ category: (v) => typeof v === 'string' && v.length <= 50,
244
+ age_rating: (v) => ['all', '13+', '18+'].includes(v),
245
+ price: (v) => typeof v === 'string' && (v === 'free' || /^\$\d+\.\d{2}$/.test(v)),
246
+ ai_generated: (v) => typeof v === 'boolean' || v === 'true' || v === 'false',
247
+ generation_method: (v) => ['human', 'ChatGPT', 'Claude', 'hybrid'].includes(v),
248
+ created_date: (v) => {
249
+ if (typeof v !== 'string')
250
+ return false;
251
+ // More flexible date validation - accept common formats
252
+ // ISO8601, US format, European format, simple dates
253
+ const datePatterns = [
254
+ /^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
255
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, // ISO8601 with time
256
+ /^\d{1,2}\/\d{1,2}\/\d{4}$/, // MM/DD/YYYY or M/D/YYYY
257
+ /^\d{1,2}-\d{1,2}-\d{4}$/, // MM-DD-YYYY or M-D-YYYY
258
+ /^\d{1,2}\.\d{1,2}\.\d{4}$/, // DD.MM.YYYY (European)
259
+ /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},?\s+\d{4}$/i // Month DD, YYYY
260
+ ];
261
+ // Check if it matches common patterns first
262
+ const matchesPattern = datePatterns.some(pattern => pattern.test(v.trim()));
263
+ if (!matchesPattern) {
264
+ // Fall back to Date.parse for other formats, but be more lenient
265
+ const parsed = Date.parse(v);
266
+ return !isNaN(parsed) && parsed > 0; // Ensure it's a valid positive timestamp
267
+ }
268
+ return true;
269
+ },
270
+ triggers: (v) => Array.isArray(v) && v.every(t => typeof t === 'string' && t.length <= 50),
271
+ content_flags: (v) => Array.isArray(v) && v.every(f => typeof f === 'string' && f.length <= 50)
272
+ };
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ /**
3
+ * Security Monitor for DollhouseMCP
4
+ *
5
+ * Centralized security event logging and monitoring system
6
+ * for tracking and alerting on security-related events.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SecurityMonitor = void 0;
10
+ const logger_js_1 = require("../utils/logger.js");
11
+ class SecurityMonitor {
12
+ /**
13
+ * Logs a security event
14
+ */
15
+ static logSecurityEvent(event) {
16
+ const logEntry = {
17
+ ...event,
18
+ timestamp: new Date().toISOString(),
19
+ id: `SEC-${Date.now()}-${++this.eventCount}`,
20
+ };
21
+ // Store in memory (circular buffer)
22
+ this.events.push(logEntry);
23
+ if (this.events.length > this.MAX_EVENTS) {
24
+ this.events.shift();
25
+ }
26
+ // In MCP servers, we cannot write to stderr/stdout as it breaks the JSON-RPC protocol
27
+ // Security events are stored in memory and can be retrieved via API
28
+ // Only send critical alerts via the proper channel
29
+ if (event.severity === 'CRITICAL') {
30
+ this.sendSecurityAlert(logEntry);
31
+ }
32
+ }
33
+ /**
34
+ * Sends security alerts for critical events
35
+ */
36
+ static sendSecurityAlert(event) {
37
+ // In a production environment, this would integrate with:
38
+ // - Slack webhooks
39
+ // - Email alerts
40
+ // - PagerDuty
41
+ // - Security Information and Event Management (SIEM) systems
42
+ // Log critical security alerts with structured data
43
+ // DO NOT use console.error in MCP servers as it breaks the JSON-RPC protocol
44
+ logger_js_1.logger.error('🚨 CRITICAL SECURITY ALERT 🚨', {
45
+ type: event.type,
46
+ details: event.details,
47
+ timestamp: event.timestamp,
48
+ id: event.id
49
+ });
50
+ // If in production mode with proper config, send actual alerts
51
+ if (process.env.DOLLHOUSE_SECURITY_ALERTS === 'true') {
52
+ // TODO: Implement actual alert mechanisms
53
+ }
54
+ }
55
+ /**
56
+ * Gets recent security events for analysis
57
+ */
58
+ static getRecentEvents(count = 100) {
59
+ return this.events.slice(-count);
60
+ }
61
+ /**
62
+ * Gets events by severity
63
+ */
64
+ static getEventsBySeverity(severity) {
65
+ return this.events.filter(event => event.severity === severity);
66
+ }
67
+ /**
68
+ * Gets events by type
69
+ */
70
+ static getEventsByType(type) {
71
+ return this.events.filter(event => event.type === type);
72
+ }
73
+ /**
74
+ * Generates a security report
75
+ */
76
+ static generateSecurityReport() {
77
+ const eventsBySeverity = {
78
+ CRITICAL: 0,
79
+ HIGH: 0,
80
+ MEDIUM: 0,
81
+ LOW: 0,
82
+ };
83
+ const eventsByType = {};
84
+ for (const event of this.events) {
85
+ eventsBySeverity[event.severity]++;
86
+ eventsByType[event.type] = (eventsByType[event.type] || 0) + 1;
87
+ }
88
+ return {
89
+ totalEvents: this.events.length,
90
+ eventsBySeverity,
91
+ eventsByType,
92
+ recentCriticalEvents: this.getEventsBySeverity('CRITICAL').slice(-10),
93
+ };
94
+ }
95
+ /**
96
+ * Clears old events (for memory management)
97
+ */
98
+ static clearOldEvents(daysToKeep = 7) {
99
+ const cutoffDate = new Date();
100
+ cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
101
+ const cutoffTimestamp = cutoffDate.toISOString();
102
+ const index = this.events.findIndex(event => event.timestamp >= cutoffTimestamp);
103
+ if (index > 0) {
104
+ this.events.splice(0, index);
105
+ }
106
+ }
107
+ }
108
+ exports.SecurityMonitor = SecurityMonitor;
109
+ SecurityMonitor.eventCount = 0;
110
+ SecurityMonitor.events = [];
111
+ SecurityMonitor.MAX_EVENTS = 1000; // Keep last 1000 events in memory