mentat-mcp 1.0.0

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.
@@ -0,0 +1,362 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'yaml';
4
+ import { z } from 'zod';
5
+ // Skill definition schema
6
+ const SkillSchema = z.object({
7
+ skill: z.object({
8
+ id: z.string(),
9
+ name: z.string(),
10
+ description: z.string(),
11
+ category: z.string(),
12
+ pricing: z.union([z.literal('free'), z.number()]),
13
+ author: z.string().optional(),
14
+ version: z.string(),
15
+ inputs: z.record(z.object({
16
+ type: z.enum(['string', 'number', 'boolean', 'select', 'multiselect']),
17
+ required: z.boolean(),
18
+ default: z.any().optional(),
19
+ description: z.string(),
20
+ options: z.array(z.string()).optional(),
21
+ })).optional(),
22
+ context_needed: z.array(z.object({
23
+ description: z.string(),
24
+ pattern: z.string(),
25
+ max_files: z.number().optional(),
26
+ })).optional(),
27
+ execution: z.array(z.object({
28
+ step: z.string(),
29
+ }).passthrough()),
30
+ validation: z.array(z.object({
31
+ check: z.string(),
32
+ }).passthrough()).optional(),
33
+ success_message: z.string(),
34
+ estimated_time: z.number(),
35
+ }),
36
+ });
37
+ export class SkillEngine {
38
+ constructor(workspacePath) {
39
+ this.workspacePath = workspacePath;
40
+ this.backupPath = path.join(workspacePath, '.skill-backups');
41
+ }
42
+ /**
43
+ * Load and parse skill definition from YAML
44
+ */
45
+ async loadSkill(skillPath) {
46
+ const content = await fs.readFile(skillPath, 'utf-8');
47
+ const parsed = yaml.parse(content);
48
+ return SkillSchema.parse(parsed);
49
+ }
50
+ /**
51
+ * Execute a skill with given inputs
52
+ */
53
+ async executeSkill(skillDefinition, inputs, context) {
54
+ const { skill } = skillDefinition;
55
+ try {
56
+ // Step 1: Validate inputs
57
+ this.validateInputs(skill.inputs || {}, inputs);
58
+ // Step 2: Create backup of files we might modify
59
+ await this.createBackup(context.files || []);
60
+ // Step 3: Initialize execution state
61
+ const state = {
62
+ inputs,
63
+ context,
64
+ changes: [],
65
+ };
66
+ // Step 4: Run validation checks
67
+ if (skill.validation) {
68
+ for (const validation of skill.validation) {
69
+ await this.runValidation(validation, state);
70
+ }
71
+ }
72
+ // Step 5: Execute steps
73
+ for (const step of skill.execution) {
74
+ await this.executeStep(step, state);
75
+ }
76
+ // Step 6: Return success
77
+ return {
78
+ success: true,
79
+ message: skill.success_message,
80
+ changes: state.changes,
81
+ };
82
+ }
83
+ catch (error) {
84
+ // Rollback on error
85
+ await this.rollback();
86
+ return {
87
+ success: false,
88
+ message: 'Skill execution failed',
89
+ error: error instanceof Error ? error.message : 'Unknown error',
90
+ };
91
+ }
92
+ }
93
+ /**
94
+ * Validate inputs against skill definition
95
+ */
96
+ validateInputs(inputDef, inputs) {
97
+ for (const [key, def] of Object.entries(inputDef)) {
98
+ const value = inputs[key];
99
+ // Check required
100
+ if (def.required && value === undefined) {
101
+ throw new Error(`Missing required input: ${key}`);
102
+ }
103
+ // Type validation
104
+ if (value !== undefined) {
105
+ switch (def.type) {
106
+ case 'string':
107
+ if (typeof value !== 'string') {
108
+ throw new Error(`Input ${key} must be a string`);
109
+ }
110
+ break;
111
+ case 'number':
112
+ if (typeof value !== 'number') {
113
+ throw new Error(`Input ${key} must be a number`);
114
+ }
115
+ break;
116
+ case 'boolean':
117
+ if (typeof value !== 'boolean') {
118
+ throw new Error(`Input ${key} must be a boolean`);
119
+ }
120
+ break;
121
+ case 'select':
122
+ if (!def.options?.includes(value)) {
123
+ throw new Error(`Input ${key} must be one of: ${def.options?.join(', ')}`);
124
+ }
125
+ break;
126
+ case 'multiselect':
127
+ if (!Array.isArray(value)) {
128
+ throw new Error(`Input ${key} must be an array`);
129
+ }
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Run validation check
137
+ */
138
+ async runValidation(validation, state) {
139
+ const { check } = validation;
140
+ switch (check) {
141
+ case 'valid_javascript':
142
+ // Would need actual JS parser - simplified for MVP
143
+ break;
144
+ case 'no_syntax_errors':
145
+ // Would need linting - simplified for MVP
146
+ break;
147
+ case 'file_exists':
148
+ const filePath = this.resolvePath(validation.path, state);
149
+ try {
150
+ await fs.access(filePath);
151
+ }
152
+ catch {
153
+ throw new Error(`File not found: ${filePath}`);
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ /**
159
+ * Execute a single step
160
+ */
161
+ async executeStep(step, state) {
162
+ switch (step.step) {
163
+ case 'read_file':
164
+ await this.stepReadFile(step, state);
165
+ break;
166
+ case 'write_file':
167
+ await this.stepWriteFile(step, state);
168
+ break;
169
+ case 'update_file':
170
+ await this.stepUpdateFile(step, state);
171
+ break;
172
+ case 'delete_file':
173
+ await this.stepDeleteFile(step, state);
174
+ break;
175
+ case 'rename_file':
176
+ await this.stepRenameFile(step, state);
177
+ break;
178
+ case 'for_each':
179
+ await this.stepForEach(step, state);
180
+ break;
181
+ case 'analyze_code':
182
+ case 'generate_types':
183
+ case 'transform':
184
+ // These would require AI/LLM calls - simplified for MVP
185
+ // In production, these would call Claude API
186
+ state[step.save_as] = `[Generated content for ${step.step}]`;
187
+ break;
188
+ default:
189
+ console.warn(`Unknown step type: ${step.step}`);
190
+ }
191
+ }
192
+ /**
193
+ * Read file step
194
+ */
195
+ async stepReadFile(step, state) {
196
+ const filePath = this.resolvePath(step.path, state);
197
+ this.validateFilePath(filePath);
198
+ const content = await fs.readFile(filePath, 'utf-8');
199
+ if (step.save_as) {
200
+ state[step.save_as] = content;
201
+ }
202
+ }
203
+ /**
204
+ * Write file step
205
+ */
206
+ async stepWriteFile(step, state) {
207
+ const filePath = this.resolvePath(step.path, state);
208
+ this.validateFilePath(filePath);
209
+ const content = this.resolveValue(step.content, state);
210
+ await fs.writeFile(filePath, content, 'utf-8');
211
+ state.changes.push({
212
+ type: 'create',
213
+ path: filePath,
214
+ content,
215
+ });
216
+ }
217
+ /**
218
+ * Update file step
219
+ */
220
+ async stepUpdateFile(step, state) {
221
+ const filePath = this.resolvePath(step.path, state);
222
+ this.validateFilePath(filePath);
223
+ let content = await fs.readFile(filePath, 'utf-8');
224
+ const newContent = this.resolveValue(step.content, state);
225
+ switch (step.operation) {
226
+ case 'insert_after':
227
+ const target = step.target;
228
+ content = content.replace(target, `${target}\n${newContent}`);
229
+ break;
230
+ case 'replace':
231
+ content = newContent;
232
+ break;
233
+ case 'append':
234
+ content += `\n${newContent}`;
235
+ break;
236
+ }
237
+ await fs.writeFile(filePath, content, 'utf-8');
238
+ state.changes.push({
239
+ type: 'update',
240
+ path: filePath,
241
+ content,
242
+ });
243
+ }
244
+ /**
245
+ * Delete file step
246
+ */
247
+ async stepDeleteFile(step, state) {
248
+ const filePath = this.resolvePath(step.path, state);
249
+ this.validateFilePath(filePath);
250
+ await fs.unlink(filePath);
251
+ state.changes.push({
252
+ type: 'delete',
253
+ path: filePath,
254
+ });
255
+ }
256
+ /**
257
+ * Rename file step
258
+ */
259
+ async stepRenameFile(step, state) {
260
+ const fromPath = this.resolvePath(step.from, state);
261
+ const toPath = this.resolvePath(step.to, state);
262
+ this.validateFilePath(fromPath);
263
+ this.validateFilePath(toPath);
264
+ await fs.rename(fromPath, toPath);
265
+ state.changes.push({
266
+ type: 'delete',
267
+ path: fromPath,
268
+ });
269
+ state.changes.push({
270
+ type: 'create',
271
+ path: toPath,
272
+ });
273
+ }
274
+ /**
275
+ * For each loop step
276
+ */
277
+ async stepForEach(step, state) {
278
+ const items = this.resolveValue(step.items, state);
279
+ if (!Array.isArray(items)) {
280
+ throw new Error('for_each items must be an array');
281
+ }
282
+ for (const item of items) {
283
+ // Create sub-state with loop variable
284
+ const loopState = {
285
+ ...state,
286
+ [step.as]: item,
287
+ };
288
+ // Execute sub-steps
289
+ for (const subStep of step.do) {
290
+ await this.executeStep(subStep, loopState);
291
+ }
292
+ }
293
+ }
294
+ /**
295
+ * Resolve path with variable substitution
296
+ */
297
+ resolvePath(pathTemplate, state) {
298
+ const resolved = this.resolveValue(pathTemplate, state);
299
+ return path.resolve(this.workspacePath, resolved);
300
+ }
301
+ /**
302
+ * Resolve value with variable substitution
303
+ */
304
+ resolveValue(template, state) {
305
+ if (typeof template !== 'string') {
306
+ return template;
307
+ }
308
+ // Replace ${variable} patterns
309
+ return template.replace(/\$\{([^}]+)\}/g, (match, varPath) => {
310
+ const value = this.getNestedValue(state, varPath);
311
+ return value !== undefined ? value : match;
312
+ });
313
+ }
314
+ /**
315
+ * Get nested value from object (e.g., "context.files[0]")
316
+ */
317
+ getNestedValue(obj, path) {
318
+ const parts = path.split('.');
319
+ let current = obj;
320
+ for (const part of parts) {
321
+ if (current === undefined)
322
+ return undefined;
323
+ current = current[part];
324
+ }
325
+ return current;
326
+ }
327
+ /**
328
+ * Validate file path is within workspace
329
+ */
330
+ validateFilePath(filePath) {
331
+ const resolved = path.resolve(filePath);
332
+ const workspace = path.resolve(this.workspacePath);
333
+ if (!resolved.startsWith(workspace)) {
334
+ throw new Error('File path outside workspace not allowed');
335
+ }
336
+ }
337
+ /**
338
+ * Create backup of files
339
+ */
340
+ async createBackup(files) {
341
+ await fs.mkdir(this.backupPath, { recursive: true });
342
+ const timestamp = Date.now();
343
+ for (const file of files) {
344
+ try {
345
+ const content = await fs.readFile(file, 'utf-8');
346
+ const backupFile = path.join(this.backupPath, `${timestamp}-${path.basename(file)}`);
347
+ await fs.writeFile(backupFile, content, 'utf-8');
348
+ }
349
+ catch (error) {
350
+ // File might not exist yet, skip backup
351
+ }
352
+ }
353
+ }
354
+ /**
355
+ * Rollback changes on error
356
+ */
357
+ async rollback() {
358
+ // In production, this would restore from backup
359
+ // For MVP, just log the error
360
+ console.error('Rollback triggered - restore from backup directory');
361
+ }
362
+ }
package/dist/skills.js ADDED
@@ -0,0 +1,141 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'yaml';
4
+ import { glob } from 'glob';
5
+ const LIMITS = {
6
+ maxFiles: 10,
7
+ maxFileSize: 50000,
8
+ maxTotalChars: 400000,
9
+ };
10
+ export class SkillLibrary {
11
+ constructor(skillsPath, workspacePath) {
12
+ this.skillsPath = skillsPath;
13
+ this.workspacePath = workspacePath;
14
+ }
15
+ /**
16
+ * Load skill from YAML
17
+ */
18
+ async loadSkill(skillId) {
19
+ const skillPath = path.join(this.skillsPath, `${skillId}.yaml`);
20
+ const content = await fs.readFile(skillPath, 'utf-8');
21
+ const parsed = yaml.parse(content);
22
+ return {
23
+ id: parsed.skill.id,
24
+ name: parsed.skill.name,
25
+ description: parsed.skill.description,
26
+ instructions: parsed.skill.instructions || this.generateInstructions(parsed.skill),
27
+ context_patterns: parsed.skill.context_patterns,
28
+ examples: parsed.skill.examples,
29
+ };
30
+ }
31
+ /**
32
+ * Gather file context based on patterns or explicit files
33
+ */
34
+ async gatherContext(patterns = [], explicitFiles = []) {
35
+ const filePaths = new Set();
36
+ // Add explicit files
37
+ explicitFiles.forEach((f) => filePaths.add(path.resolve(this.workspacePath, f)));
38
+ // Add files matching patterns
39
+ for (const pattern of patterns) {
40
+ const matches = await glob(pattern, {
41
+ cwd: this.workspacePath,
42
+ absolute: true,
43
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
44
+ });
45
+ matches.slice(0, LIMITS.maxFiles).forEach((f) => filePaths.add(f));
46
+ }
47
+ // Rank and limit files
48
+ const rankedFiles = await this.rankFiles(Array.from(filePaths));
49
+ const limitedFiles = rankedFiles.slice(0, LIMITS.maxFiles);
50
+ // Read file contents
51
+ const files = [];
52
+ let totalChars = 0;
53
+ let truncated = false;
54
+ for (const filePath of limitedFiles) {
55
+ try {
56
+ const stats = await fs.stat(filePath);
57
+ if (stats.size > LIMITS.maxFileSize) {
58
+ truncated = true;
59
+ continue;
60
+ }
61
+ if (totalChars + stats.size > LIMITS.maxTotalChars) {
62
+ truncated = true;
63
+ break;
64
+ }
65
+ const content = await fs.readFile(filePath, 'utf-8');
66
+ const lines = content.split('\n').length;
67
+ files.push({
68
+ path: path.relative(this.workspacePath, filePath),
69
+ content,
70
+ lines,
71
+ });
72
+ totalChars += content.length;
73
+ }
74
+ catch (error) {
75
+ console.error(`Failed to read ${filePath}:`, error);
76
+ }
77
+ }
78
+ return { files, totalChars, truncated };
79
+ }
80
+ /**
81
+ * Rank files by relevance
82
+ */
83
+ async rankFiles(files) {
84
+ const scored = await Promise.all(files.map(async (f) => {
85
+ const stats = await fs.stat(f).catch(() => null);
86
+ const relativePath = path.relative(this.workspacePath, f);
87
+ let score = 0;
88
+ // Deprioritize test files
89
+ if (relativePath.includes('test') || relativePath.includes('spec')) {
90
+ score -= 100;
91
+ }
92
+ // Prefer root-level files
93
+ const depth = relativePath.split(path.sep).length;
94
+ score -= depth * 10;
95
+ // Slight preference for smaller files
96
+ if (stats) {
97
+ score -= stats.size / 10000;
98
+ }
99
+ return { path: f, score };
100
+ }));
101
+ return scored.sort((a, b) => b.score - a.score).map((s) => s.path);
102
+ }
103
+ /**
104
+ * Format skill + context for Claude to read
105
+ */
106
+ formatForClaude(skill, context) {
107
+ const parts = [];
108
+ parts.push(`# ${skill.name}`);
109
+ parts.push(`${skill.description}\n`);
110
+ parts.push(`## Instructions`);
111
+ parts.push(skill.instructions);
112
+ parts.push('');
113
+ if (skill.examples) {
114
+ parts.push(`## Examples`);
115
+ parts.push(skill.examples);
116
+ parts.push('');
117
+ }
118
+ if (context.files.length > 0) {
119
+ parts.push(`## Context (${context.files.length} file${context.files.length > 1 ? 's' : ''})`);
120
+ for (const file of context.files) {
121
+ parts.push(`\n### ${file.path} (${file.lines} lines)`);
122
+ parts.push('```');
123
+ parts.push(file.content);
124
+ parts.push('```');
125
+ }
126
+ if (context.truncated) {
127
+ parts.push(`\n⚠️ Some files were excluded due to size limits.`);
128
+ }
129
+ }
130
+ parts.push(`\n---`);
131
+ parts.push(`Use your Edit tool to make changes.`);
132
+ parts.push(`Use Read tool if you need additional files.`);
133
+ return parts.join('\n');
134
+ }
135
+ /**
136
+ * Backward compatibility for old YAML format
137
+ */
138
+ generateInstructions(skillDef) {
139
+ return skillDef.description || 'Complete the task as described.';
140
+ }
141
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "mentat-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for terminal AI tools (Claude Code, Cursor CLI, Codex CLI) - execute skills and hire AI workers",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mentat": "./dist/setup.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "setup": "node dist/setup.js"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "claude-code",
19
+ "cursor",
20
+ "codex",
21
+ "ai",
22
+ "agents",
23
+ "marketplace",
24
+ "mentat",
25
+ "dune"
26
+ ],
27
+ "author": "Victor Gardrinier",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/vgardrinier/a2a_marketplace.git",
32
+ "directory": "mcp-server"
33
+ },
34
+ "homepage": "https://github.com/vgardrinier/a2a_marketplace#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/vgardrinier/a2a_marketplace/issues"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^0.5.0",
40
+ "glob": "^10.3.10",
41
+ "yaml": "^2.3.4",
42
+ "zod": "^3.22.4"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.0.0",
46
+ "typescript": "^5.3.0"
47
+ }
48
+ }