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 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
- image_data: z
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('MIME type of the image (e.g., "image/png"). If omitted, inferred from filename extension.'),
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. Use this instead of embedding base64 images in content - ' +
801
- 'it is much faster and more efficient. Returns a markdown image URL you can include in any content field. ' +
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
- image_data: {
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: 'Filename for the image (e.g., "screenshot.png")',
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
- mime_type: {
815
- type: 'string',
816
- description: 'MIME type of the image (e.g., "image/png"). If omitted, inferred from filename extension.',
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: ['image_data', 'filename'],
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.getTeamSlug();
1240
- if (!teamSlug) {
1241
- throw new Error('No team selected. Use list_teams first.');
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
- const mimeType = parsed.mime_type || this.inferMimeType(parsed.filename) || 'application/octet-stream';
1244
- const result = await this.client.uploadImage(teamSlug, parsed.image_data, parsed.filename, mimeType);
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: ![${parsed.filename}](${result.url})`,
1271
+ text: `Image uploaded successfully.\n\nURL: ${result.url}\n\nUse this URL in markdown: ![${filename}](${result.url})`,
1250
1272
  },
1251
1273
  ],
1252
1274
  };
@@ -1264,17 +1286,30 @@ class WolfpackMCPServer {
1264
1286
  }
1265
1287
  });
1266
1288
  }
1267
- inferMimeType(filename) {
1268
- const ext = filename.split('.').pop()?.toLowerCase();
1269
- const mimeTypes = {
1270
- png: 'image/png',
1271
- jpg: 'image/jpeg',
1272
- jpeg: 'image/jpeg',
1273
- gif: 'image/gif',
1274
- webp: 'image/webp',
1275
- svg: 'image/svg+xml',
1276
- };
1277
- return ext ? mimeTypes[ext] || null : null;
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",