wolfpack-mcp 1.0.20 → 1.0.22
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/dist/client.js +26 -0
- package/dist/index.js +70 -35
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -37,6 +37,32 @@ export class WolfpackClient {
|
|
|
37
37
|
setTeamSlug(slug) {
|
|
38
38
|
this.teamSlug = slug;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a team slug from a team ID, the cached slug, or by fetching teams.
|
|
42
|
+
*/
|
|
43
|
+
async resolveSlug(teamId) {
|
|
44
|
+
// If a specific team ID is provided, fetch teams to find its slug
|
|
45
|
+
if (teamId) {
|
|
46
|
+
const teams = await this.listTeams();
|
|
47
|
+
const team = teams.find((t) => t.id === teamId);
|
|
48
|
+
if (!team)
|
|
49
|
+
throw new Error(`Team with ID ${teamId} not found`);
|
|
50
|
+
return team.slug;
|
|
51
|
+
}
|
|
52
|
+
// Fall back to cached slug
|
|
53
|
+
if (this.teamSlug)
|
|
54
|
+
return this.teamSlug;
|
|
55
|
+
// Try to resolve from cached team ID
|
|
56
|
+
if (this.teamId) {
|
|
57
|
+
const teams = await this.listTeams();
|
|
58
|
+
const team = teams.find((t) => t.id === this.teamId);
|
|
59
|
+
if (!team)
|
|
60
|
+
throw new Error(`Team with ID ${this.teamId} not found`);
|
|
61
|
+
this.teamSlug = team.slug;
|
|
62
|
+
return team.slug;
|
|
63
|
+
}
|
|
64
|
+
throw new Error('No team selected. Use list_teams first.');
|
|
65
|
+
}
|
|
40
66
|
/**
|
|
41
67
|
* List all teams the authenticated user is a member of.
|
|
42
68
|
*/
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
|
+
import { readFile, stat } from 'fs/promises';
|
|
8
|
+
import { resolve, basename, extname } from 'path';
|
|
7
9
|
import { WolfpackClient } from './client.js';
|
|
8
10
|
import { validateConfig, config } from './config.js';
|
|
9
11
|
// Get current package version
|
|
@@ -242,14 +244,14 @@ const CreateIssueCommentSchema = z.object({
|
|
|
242
244
|
});
|
|
243
245
|
// Image upload schema
|
|
244
246
|
const UploadImageSchema = z.object({
|
|
245
|
-
|
|
246
|
-
.string()
|
|
247
|
-
.describe('Base64-encoded image data (without the data:image/...;base64, prefix)'),
|
|
248
|
-
filename: z.string().describe('Filename for the image (e.g., "screenshot.png")'),
|
|
249
|
-
mime_type: z
|
|
247
|
+
file_path: z
|
|
250
248
|
.string()
|
|
249
|
+
.describe('Absolute path to an image file on disk (e.g., "/tmp/screenshot.png"). ' +
|
|
250
|
+
'Supported formats: JPEG, PNG, GIF, WebP. Max 5MB.'),
|
|
251
|
+
team_id: z
|
|
252
|
+
.number()
|
|
251
253
|
.optional()
|
|
252
|
-
.describe('
|
|
254
|
+
.describe('Team ID (required for multi-team users, use list_teams to get IDs)'),
|
|
253
255
|
});
|
|
254
256
|
// Helper to detect if a work item description contains a plan
|
|
255
257
|
function hasPlan(description) {
|
|
@@ -797,26 +799,24 @@ class WolfpackMCPServer {
|
|
|
797
799
|
// Image tools
|
|
798
800
|
{
|
|
799
801
|
name: 'upload_image',
|
|
800
|
-
description: 'Upload an image and get back a permanent URL.
|
|
801
|
-
'
|
|
802
|
+
description: 'Upload an image file from disk and get back a permanent URL. Much faster than embedding base64 in content. ' +
|
|
803
|
+
'Pass the absolute file path - the file is read directly from disk without going through the LLM. ' +
|
|
804
|
+
'Returns a markdown image URL you can include in any content field. ' +
|
|
802
805
|
'Requires mcp:images:upload permission.',
|
|
803
806
|
inputSchema: {
|
|
804
807
|
type: 'object',
|
|
805
808
|
properties: {
|
|
806
|
-
|
|
807
|
-
type: 'string',
|
|
808
|
-
description: 'Base64-encoded image data (without the data:image/...;base64, prefix). Just the raw base64 string.',
|
|
809
|
-
},
|
|
810
|
-
filename: {
|
|
809
|
+
file_path: {
|
|
811
810
|
type: 'string',
|
|
812
|
-
description: '
|
|
811
|
+
description: 'Absolute path to an image file on disk (e.g., "/tmp/screenshot.png"). ' +
|
|
812
|
+
'Supported formats: JPEG, PNG, GIF, WebP. Max 5MB.',
|
|
813
813
|
},
|
|
814
|
-
|
|
815
|
-
type: '
|
|
816
|
-
description: '
|
|
814
|
+
team_id: {
|
|
815
|
+
type: 'number',
|
|
816
|
+
description: 'Team ID (required for multi-team users, use list_teams to get IDs)',
|
|
817
817
|
},
|
|
818
818
|
},
|
|
819
|
-
required: ['
|
|
819
|
+
required: ['file_path'],
|
|
820
820
|
},
|
|
821
821
|
},
|
|
822
822
|
],
|
|
@@ -1236,17 +1236,39 @@ class WolfpackMCPServer {
|
|
|
1236
1236
|
}
|
|
1237
1237
|
case 'upload_image': {
|
|
1238
1238
|
const parsed = UploadImageSchema.parse(args);
|
|
1239
|
-
const teamSlug = this.client.
|
|
1240
|
-
|
|
1241
|
-
|
|
1239
|
+
const teamSlug = await this.client.resolveSlug(parsed.team_id || this.client.getTeamId() || undefined);
|
|
1240
|
+
const filePath = resolve(parsed.file_path);
|
|
1241
|
+
// Validate file extension
|
|
1242
|
+
const ext = extname(filePath).toLowerCase();
|
|
1243
|
+
const allowedExtensions = {
|
|
1244
|
+
'.png': 'image/png',
|
|
1245
|
+
'.jpg': 'image/jpeg',
|
|
1246
|
+
'.jpeg': 'image/jpeg',
|
|
1247
|
+
'.gif': 'image/gif',
|
|
1248
|
+
'.webp': 'image/webp',
|
|
1249
|
+
};
|
|
1250
|
+
const mimeType = allowedExtensions[ext];
|
|
1251
|
+
if (!mimeType) {
|
|
1252
|
+
throw new Error(`Unsupported image format "${ext}". Supported: ${Object.keys(allowedExtensions).join(', ')}`);
|
|
1253
|
+
}
|
|
1254
|
+
// Check file exists and size
|
|
1255
|
+
const fileStat = await stat(filePath);
|
|
1256
|
+
const maxSize = 5 * 1024 * 1024; // 5MB
|
|
1257
|
+
if (fileStat.size > maxSize) {
|
|
1258
|
+
throw new Error(`File too large (${fileStat.size} bytes). Maximum size is 5MB.`);
|
|
1242
1259
|
}
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1260
|
+
// Validate magic bytes to confirm it's actually an image
|
|
1261
|
+
const buffer = await readFile(filePath);
|
|
1262
|
+
if (!this.isImageBuffer(buffer)) {
|
|
1263
|
+
throw new Error('File does not appear to be a valid image.');
|
|
1264
|
+
}
|
|
1265
|
+
const filename = basename(filePath);
|
|
1266
|
+
const result = await this.client.uploadImage(teamSlug, buffer.toString('base64'), filename, mimeType);
|
|
1245
1267
|
return {
|
|
1246
1268
|
content: [
|
|
1247
1269
|
{
|
|
1248
1270
|
type: 'text',
|
|
1249
|
-
text: `Image uploaded successfully.\n\nURL: ${result.url}\n\nUse this URL in markdown: `,
|
|
1250
1272
|
},
|
|
1251
1273
|
],
|
|
1252
1274
|
};
|
|
@@ -1264,17 +1286,30 @@ class WolfpackMCPServer {
|
|
|
1264
1286
|
}
|
|
1265
1287
|
});
|
|
1266
1288
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1289
|
+
isImageBuffer(buffer) {
|
|
1290
|
+
if (buffer.length < 4)
|
|
1291
|
+
return false;
|
|
1292
|
+
// PNG: 89 50 4E 47
|
|
1293
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
|
|
1294
|
+
return true;
|
|
1295
|
+
// JPEG: FF D8 FF
|
|
1296
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
|
|
1297
|
+
return true;
|
|
1298
|
+
// GIF: 47 49 46 38
|
|
1299
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38)
|
|
1300
|
+
return true;
|
|
1301
|
+
// WebP: 52 49 46 46 ... 57 45 42 50
|
|
1302
|
+
if (buffer.length >= 12 &&
|
|
1303
|
+
buffer[0] === 0x52 &&
|
|
1304
|
+
buffer[1] === 0x49 &&
|
|
1305
|
+
buffer[2] === 0x46 &&
|
|
1306
|
+
buffer[3] === 0x46 &&
|
|
1307
|
+
buffer[8] === 0x57 &&
|
|
1308
|
+
buffer[9] === 0x45 &&
|
|
1309
|
+
buffer[10] === 0x42 &&
|
|
1310
|
+
buffer[11] === 0x50)
|
|
1311
|
+
return true;
|
|
1312
|
+
return false;
|
|
1278
1313
|
}
|
|
1279
1314
|
async start() {
|
|
1280
1315
|
// Start version check early (non-blocking)
|