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.
package/src/index.ts ADDED
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from '@modelcontextprotocol/sdk/types.js';
8
+ import { SkillLibrary } from './skills.js';
9
+ import path from 'path';
10
+
11
+ const API_BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
12
+ const WORKSPACE_PATH = process.cwd();
13
+ const SKILLS_PATH = path.join(WORKSPACE_PATH, 'skills');
14
+
15
+ class AgentMarketplaceServer {
16
+ private server: Server;
17
+ private skillLibrary: SkillLibrary;
18
+ private apiKey: string | null = null;
19
+
20
+ constructor() {
21
+ this.server = new Server(
22
+ {
23
+ name: 'agent-marketplace',
24
+ version: '2.0.0',
25
+ },
26
+ {
27
+ capabilities: {
28
+ tools: {},
29
+ },
30
+ }
31
+ );
32
+
33
+ this.skillLibrary = new SkillLibrary(SKILLS_PATH, WORKSPACE_PATH);
34
+
35
+ this.setupHandlers();
36
+ }
37
+
38
+ private setupHandlers() {
39
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
40
+ tools: [
41
+ {
42
+ name: 'execute_skill',
43
+ description: 'Execute a skill locally (instant, free)',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ skillId: {
48
+ type: 'string',
49
+ description: 'Skill ID to execute (e.g., seo-meta-tags)',
50
+ },
51
+ inputs: {
52
+ type: 'object',
53
+ description: 'Input parameters for the skill',
54
+ },
55
+ targetFiles: {
56
+ type: 'array',
57
+ items: { type: 'string' },
58
+ description: 'Files to operate on',
59
+ },
60
+ },
61
+ required: ['skillId'],
62
+ },
63
+ },
64
+ {
65
+ name: 'hire_worker',
66
+ description: 'Hire a specialist worker for custom work (paid)',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ task: {
71
+ type: 'string',
72
+ description: 'Description of the task',
73
+ },
74
+ specialty: {
75
+ type: 'string',
76
+ description: 'Worker specialty (optional)',
77
+ },
78
+ budget: {
79
+ type: 'number',
80
+ description: 'Maximum budget in USD',
81
+ },
82
+ context: {
83
+ type: 'object',
84
+ description: 'Context files and metadata',
85
+ },
86
+ },
87
+ required: ['task'],
88
+ },
89
+ },
90
+ {
91
+ name: 'check_job',
92
+ description: 'Check status of a job',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ jobId: {
97
+ type: 'string',
98
+ description: 'Job ID to check',
99
+ },
100
+ },
101
+ required: ['jobId'],
102
+ },
103
+ },
104
+ {
105
+ name: 'approve_job',
106
+ description: 'Approve job and release payment',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ jobId: {
111
+ type: 'string',
112
+ description: 'Job ID to approve',
113
+ },
114
+ rating: {
115
+ type: 'number',
116
+ description: 'Rating from 1-5',
117
+ minimum: 1,
118
+ maximum: 5,
119
+ },
120
+ feedback: {
121
+ type: 'string',
122
+ description: 'Optional feedback',
123
+ },
124
+ },
125
+ required: ['jobId', 'rating'],
126
+ },
127
+ },
128
+ {
129
+ name: 'reject_job',
130
+ description: 'Reject job and request refund',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ jobId: {
135
+ type: 'string',
136
+ description: 'Job ID to reject',
137
+ },
138
+ reason: {
139
+ type: 'string',
140
+ description: 'Reason for rejection',
141
+ },
142
+ },
143
+ required: ['jobId', 'reason'],
144
+ },
145
+ },
146
+ {
147
+ name: 'check_wallet',
148
+ description: 'Check wallet balance',
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {},
152
+ },
153
+ },
154
+ ],
155
+ }));
156
+
157
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
158
+ const { name, arguments: args } = request.params;
159
+
160
+ try {
161
+ switch (name) {
162
+ case 'execute_skill':
163
+ return await this.executeSkill(args as any);
164
+ case 'hire_worker':
165
+ return await this.hireWorker(args as any);
166
+ case 'check_job':
167
+ return await this.checkJob(args as any);
168
+ case 'approve_job':
169
+ return await this.approveJob(args as any);
170
+ case 'reject_job':
171
+ return await this.rejectJob(args as any);
172
+ case 'check_wallet':
173
+ return await this.checkWallet();
174
+ default:
175
+ throw new Error(`Unknown tool: ${name}`);
176
+ }
177
+ } catch (error) {
178
+ return {
179
+ content: [
180
+ {
181
+ type: 'text',
182
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
183
+ },
184
+ ],
185
+ isError: true,
186
+ };
187
+ }
188
+ });
189
+ }
190
+
191
+ private async executeSkill(args: {
192
+ skillId: string;
193
+ inputs?: Record<string, any>;
194
+ targetFiles?: string[];
195
+ }) {
196
+ // Load skill definition
197
+ const skill = await this.skillLibrary.loadSkill(args.skillId);
198
+
199
+ // Gather context from files
200
+ const context = await this.skillLibrary.gatherContext(
201
+ skill.context_patterns || [],
202
+ args.targetFiles || []
203
+ );
204
+
205
+ // Format for Claude to read
206
+ const formattedPrompt = this.skillLibrary.formatForClaude(skill, context);
207
+
208
+ return {
209
+ content: [
210
+ {
211
+ type: 'text',
212
+ text: formattedPrompt,
213
+ },
214
+ ],
215
+ };
216
+ }
217
+
218
+ private async hireWorker(args: {
219
+ task: string;
220
+ specialty?: string;
221
+ budget?: number;
222
+ context?: Record<string, any>;
223
+ }) {
224
+ const response = await fetch(`${API_BASE_URL}/api/match`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ },
229
+ body: JSON.stringify({
230
+ task: args.task,
231
+ specialty: args.specialty,
232
+ budget: args.budget,
233
+ }),
234
+ });
235
+
236
+ if (!response.ok) {
237
+ throw new Error(`Match API failed: ${response.statusText}`);
238
+ }
239
+
240
+ const { match } = await response.json();
241
+
242
+ if (match.type === 'skill') {
243
+ return {
244
+ content: [
245
+ {
246
+ type: 'text',
247
+ text: `Found matching skill: ${match.skill.name}\n\nExecute with: execute_skill(skillId: "${match.skill.id}")`,
248
+ },
249
+ ],
250
+ };
251
+ }
252
+
253
+ if (match.type === 'worker') {
254
+ const { matches, recommendation } = match;
255
+
256
+ // Build transparent worker list with reasoning
257
+ let text = '💡 Recommended: Hire a Worker\n\n';
258
+ text += `Why: ${recommendation}\n\n`;
259
+ text += '─────────────────────────────────────\n';
260
+ text += `Top ${Math.min(matches.length, 5)} Matching Workers:\n\n`;
261
+
262
+ matches.slice(0, 5).forEach((m: any, index: number) => {
263
+ const confidenceIcon =
264
+ m.confidence === 'high' ? '🟢' : m.confidence === 'medium' ? '🟡' : '🔴';
265
+
266
+ text += `${index + 1}. ${m.worker.name} ${confidenceIcon}\n`;
267
+ text += ` Specialty: ${m.worker.specialty}\n`;
268
+ text += ` Rating: ${m.worker.reputationScore}/5 (${m.worker.completionCount} jobs)\n`;
269
+ text += ` Avg Time: ~${m.worker.avgCompletionTime} min\n`;
270
+ text += ` Est Cost: $${m.worker.pricing}\n`;
271
+ text += ` Match: ${Math.round(m.score)}% - ${m.reasons.join(', ')}\n`;
272
+ text += ` Confidence: ${m.confidence.toUpperCase()}\n`;
273
+ text += '\n';
274
+ });
275
+
276
+ text += '─────────────────────────────────────\n\n';
277
+ text += '📌 To hire a worker, note their number and:\n';
278
+ text += ' 1. Check your wallet: check_wallet()\n';
279
+ text += ' 2. Create job with preferred worker\n\n';
280
+
281
+ if (matches.length > 5) {
282
+ text += `💡 Showing top 5 of ${matches.length} matching workers\n\n`;
283
+ }
284
+
285
+ text += 'Or type: execute_skill(...) to try a pre-built skill instead';
286
+
287
+ return {
288
+ content: [{ type: 'text', text }],
289
+ };
290
+ }
291
+
292
+ return {
293
+ content: [
294
+ {
295
+ type: 'text',
296
+ text: match.message || 'No match found',
297
+ },
298
+ ],
299
+ };
300
+ }
301
+
302
+ private async checkJob(args: { jobId: string }) {
303
+ const response = await fetch(`${API_BASE_URL}/api/jobs/${args.jobId}`, {
304
+ headers: this.getAuthHeaders(),
305
+ });
306
+
307
+ if (!response.ok) {
308
+ throw new Error(`Job fetch failed: ${response.statusText}`);
309
+ }
310
+
311
+ const { job } = await response.json();
312
+
313
+ return {
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: `Job ${job.id}\n\nStatus: ${job.status}\nTask: ${job.task}\nBudget: $${job.budget}\nCreated: ${new Date(job.createdAt).toLocaleString()}\n${job.deliveredAt ? `\nDelivered: ${new Date(job.deliveredAt).toLocaleString()}` : ''}`,
318
+ },
319
+ ],
320
+ };
321
+ }
322
+
323
+ private async approveJob(args: {
324
+ jobId: string;
325
+ rating: number;
326
+ feedback?: string;
327
+ }) {
328
+ const response = await fetch(
329
+ `${API_BASE_URL}/api/jobs/${args.jobId}/approve`,
330
+ {
331
+ method: 'POST',
332
+ headers: {
333
+ ...this.getAuthHeaders(),
334
+ 'Content-Type': 'application/json',
335
+ },
336
+ body: JSON.stringify({
337
+ rating: args.rating,
338
+ feedback: args.feedback,
339
+ }),
340
+ }
341
+ );
342
+
343
+ if (!response.ok) {
344
+ throw new Error(`Approval failed: ${response.statusText}`);
345
+ }
346
+
347
+ const { job } = await response.json();
348
+
349
+ return {
350
+ content: [
351
+ {
352
+ type: 'text',
353
+ text: `✓ Job approved!\n\nPayment released to worker.\nRating: ${args.rating}/5`,
354
+ },
355
+ ],
356
+ };
357
+ }
358
+
359
+ private async rejectJob(args: { jobId: string; reason: string }) {
360
+ const response = await fetch(
361
+ `${API_BASE_URL}/api/jobs/${args.jobId}/reject`,
362
+ {
363
+ method: 'POST',
364
+ headers: {
365
+ ...this.getAuthHeaders(),
366
+ 'Content-Type': 'application/json',
367
+ },
368
+ body: JSON.stringify({
369
+ reason: args.reason,
370
+ }),
371
+ }
372
+ );
373
+
374
+ if (!response.ok) {
375
+ throw new Error(`Rejection failed: ${response.statusText}`);
376
+ }
377
+
378
+ return {
379
+ content: [
380
+ {
381
+ type: 'text',
382
+ text: `✓ Job rejected.\n\nFunds refunded to your wallet.`,
383
+ },
384
+ ],
385
+ };
386
+ }
387
+
388
+ private async checkWallet() {
389
+ const response = await fetch(`${API_BASE_URL}/api/wallet`, {
390
+ headers: this.getAuthHeaders(),
391
+ });
392
+
393
+ if (!response.ok) {
394
+ throw new Error(`Wallet fetch failed: ${response.statusText}`);
395
+ }
396
+
397
+ const { balance, needsTopUp } = await response.json();
398
+
399
+ return {
400
+ content: [
401
+ {
402
+ type: 'text',
403
+ text: `Wallet Balance: $${balance.toFixed(2)}\n${needsTopUp ? '\n⚠️ Low balance - consider topping up' : ''}`,
404
+ },
405
+ ],
406
+ };
407
+ }
408
+
409
+ private getAuthHeaders(): Record<string, string> {
410
+ if (this.apiKey) {
411
+ return {
412
+ Authorization: `Bearer ${this.apiKey}`,
413
+ };
414
+ }
415
+ return {};
416
+ }
417
+
418
+ async run() {
419
+ const transport = new StdioServerTransport();
420
+ await this.server.connect(transport);
421
+ console.error('Agent Marketplace MCP server running on stdio');
422
+ }
423
+ }
424
+
425
+ const server = new AgentMarketplaceServer();
426
+ server.run().catch(console.error);
package/src/setup.ts ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ import { exec } from 'child_process';
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+ import { promisify } from 'util';
6
+ import http from 'http';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ const API_URL = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
11
+ const SETUP_PORT = 3456;
12
+
13
+ interface ClaudeConfig {
14
+ mcpServers: {
15
+ [key: string]: {
16
+ command: string;
17
+ args: string[];
18
+ env?: Record<string, string>;
19
+ };
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Terminal-first setup for Agent Marketplace MCP
25
+ *
26
+ * Flow:
27
+ * 1. Start local server to receive auth token
28
+ * 2. Open browser to authenticate
29
+ * 3. Receive token from callback
30
+ * 4. Save token and configure Claude Code
31
+ * 5. Done!
32
+ */
33
+ async function setup() {
34
+ console.log('');
35
+ console.log('╔════════════════════════════════════════════════╗');
36
+ console.log('║ 🚀 Agent Marketplace Setup ║');
37
+ console.log('╚════════════════════════════════════════════════╝');
38
+ console.log('');
39
+
40
+ // Step 1: Start local callback server
41
+ console.log('📡 Starting local server to receive auth token...');
42
+ const token = await startCallbackServer();
43
+
44
+ // Step 2: Save token
45
+ console.log('💾 Saving authentication token...');
46
+ await saveToken(token);
47
+
48
+ // Step 3: Configure Claude Code
49
+ console.log('⚙️ Configuring Claude Code...');
50
+ await configureClaudeCode(token);
51
+
52
+ // Step 4: Success!
53
+ console.log('');
54
+ console.log('╔════════════════════════════════════════════════╗');
55
+ console.log('║ ✅ Setup Complete! ║');
56
+ console.log('╚════════════════════════════════════════════════╝');
57
+ console.log('');
58
+ console.log('Next steps:');
59
+ console.log('1. Restart Claude Code');
60
+ console.log('2. Open any project');
61
+ console.log('3. Try: @agentmarketplace execute_skill --skillId seo-meta-tags');
62
+ console.log('');
63
+ console.log('Available skills:');
64
+ console.log(' • seo-meta-tags - Add SEO meta tags');
65
+ console.log(' • typescript-convert - Convert JS to TypeScript');
66
+ console.log(' • add-loading-states - Add loading states');
67
+ console.log(' • add-error-boundaries - Add error boundaries');
68
+ console.log(' • fix-eslint - Fix ESLint errors');
69
+ console.log(' • optimize-images - Optimize images');
70
+ console.log('');
71
+ console.log('Need help? Visit: https://agentmarketplace.com/docs');
72
+ console.log('');
73
+ }
74
+
75
+ /**
76
+ * Start local server to receive auth token from web callback
77
+ */
78
+ function startCallbackServer(): Promise<string> {
79
+ return new Promise((resolve, reject) => {
80
+ const server = http.createServer((req, res) => {
81
+ // Parse URL
82
+ const url = new URL(req.url || '', `http://localhost:${SETUP_PORT}`);
83
+
84
+ if (url.pathname === '/callback') {
85
+ const token = url.searchParams.get('token');
86
+
87
+ if (!token) {
88
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
89
+ res.end('Missing token');
90
+ return;
91
+ }
92
+
93
+ // Send success response
94
+ res.writeHead(200, { 'Content-Type': 'text/html' });
95
+ res.end(`
96
+ <!DOCTYPE html>
97
+ <html>
98
+ <head>
99
+ <title>Setup Complete</title>
100
+ <style>
101
+ body {
102
+ font-family: system-ui, -apple-system, sans-serif;
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ height: 100vh;
107
+ margin: 0;
108
+ background: #f5f5f5;
109
+ }
110
+ .container {
111
+ text-align: center;
112
+ background: white;
113
+ padding: 48px;
114
+ border-radius: 12px;
115
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
116
+ }
117
+ h1 { color: #10b981; margin: 0 0 16px 0; }
118
+ p { color: #6b7280; margin: 0; }
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <div class="container">
123
+ <h1>✅ Authentication Complete!</h1>
124
+ <p>You can close this window and return to your terminal.</p>
125
+ </div>
126
+ </body>
127
+ </html>
128
+ `);
129
+
130
+ // Close server and resolve with token
131
+ server.close();
132
+ resolve(token);
133
+ }
134
+ });
135
+
136
+ server.listen(SETUP_PORT, () => {
137
+ console.log('✓ Local server started');
138
+ console.log('');
139
+ console.log('🌐 Opening browser to authenticate...');
140
+
141
+ // Open browser
142
+ const authUrl = `${API_URL}/setup/auth?callback=http://localhost:${SETUP_PORT}/callback`;
143
+ openBrowser(authUrl);
144
+ });
145
+
146
+ // Timeout after 5 minutes
147
+ setTimeout(() => {
148
+ server.close();
149
+ reject(new Error('Setup timed out after 5 minutes'));
150
+ }, 5 * 60 * 1000);
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Save auth token to config file
156
+ */
157
+ async function saveToken(token: string): Promise<void> {
158
+ const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.agentmarketplace');
159
+ const configPath = path.join(configDir, 'config.json');
160
+
161
+ await fs.mkdir(configDir, { recursive: true });
162
+ await fs.writeFile(
163
+ configPath,
164
+ JSON.stringify({ authToken: token, apiUrl: API_URL }, null, 2)
165
+ );
166
+
167
+ console.log(`✓ Saved to: ${configPath}`);
168
+ }
169
+
170
+ /**
171
+ * Configure Claude Code MCP settings
172
+ */
173
+ async function configureClaudeCode(token: string): Promise<void> {
174
+ const configPath = getClaudeConfigPath();
175
+
176
+ if (!configPath) {
177
+ console.log('⚠️ Could not find Claude Code config');
178
+ console.log(' You may need to manually add the MCP server');
179
+ return;
180
+ }
181
+
182
+ // Read existing config or create new
183
+ let config: ClaudeConfig;
184
+ try {
185
+ const content = await fs.readFile(configPath, 'utf-8');
186
+ config = JSON.parse(content);
187
+ } catch {
188
+ config = { mcpServers: {} };
189
+ }
190
+
191
+ // Add agentmarketplace MCP server
192
+ const mcpServerPath = path.join(__dirname, 'index.js');
193
+ config.mcpServers.agentmarketplace = {
194
+ command: 'node',
195
+ args: [mcpServerPath],
196
+ env: {
197
+ AUTH_TOKEN: token,
198
+ API_URL: API_URL,
199
+ },
200
+ };
201
+
202
+ // Write config
203
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
204
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
205
+
206
+ console.log(`✓ Configured Claude Code at: ${configPath}`);
207
+ }
208
+
209
+ /**
210
+ * Get Claude Code config path based on OS
211
+ */
212
+ function getClaudeConfigPath(): string | null {
213
+ const home = process.env.HOME || process.env.USERPROFILE || '';
214
+
215
+ if (process.platform === 'darwin') {
216
+ // macOS
217
+ return path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
218
+ } else if (process.platform === 'win32') {
219
+ // Windows
220
+ return path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
221
+ } else if (process.platform === 'linux') {
222
+ // Linux
223
+ return path.join(home, '.config', 'Claude', 'claude_desktop_config.json');
224
+ }
225
+
226
+ return null;
227
+ }
228
+
229
+ /**
230
+ * Open URL in default browser
231
+ */
232
+ function openBrowser(url: string) {
233
+ const command = process.platform === 'darwin'
234
+ ? 'open'
235
+ : process.platform === 'win32'
236
+ ? 'start'
237
+ : 'xdg-open';
238
+
239
+ exec(`${command} "${url}"`);
240
+ }
241
+
242
+ // Run setup
243
+ setup().catch((error) => {
244
+ console.error('');
245
+ console.error('❌ Setup failed:', error.message);
246
+ console.error('');
247
+ console.error('Need help? Visit: https://agentmarketplace.com/docs');
248
+ process.exit(1);
249
+ });