wolfpack-mcp 1.0.19 → 1.0.21

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/apiClient.js CHANGED
@@ -96,6 +96,34 @@ export class ApiClient {
96
96
  throw new Error(`Failed to PATCH ${path}: ${error}`);
97
97
  }
98
98
  }
99
+ async postMultipart(path, buffer, filename, mimeType) {
100
+ const url = `${config.apiUrl}${path}`;
101
+ const boundary = `----FormBoundary${Date.now()}`;
102
+ const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${mimeType}\r\n\r\n`;
103
+ const footer = `\r\n--${boundary}--\r\n`;
104
+ const body = Buffer.concat([Buffer.from(header), buffer, Buffer.from(footer)]);
105
+ try {
106
+ const response = await fetch(url, {
107
+ method: 'POST',
108
+ headers: {
109
+ Authorization: `Bearer ${config.apiKey}`,
110
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
111
+ },
112
+ body,
113
+ });
114
+ if (!response.ok) {
115
+ const errorText = await response.text();
116
+ throw new ApiError(response.status, `API error: ${response.statusText} - ${errorText}`);
117
+ }
118
+ return (await response.json());
119
+ }
120
+ catch (error) {
121
+ if (error instanceof ApiError) {
122
+ throw error;
123
+ }
124
+ throw new Error(`Failed to POST multipart ${path}: ${error}`);
125
+ }
126
+ }
99
127
  async delete(path) {
100
128
  const url = `${config.apiUrl}${path}`;
101
129
  try {
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ const PAGE_SIZE = 50;
6
6
  export class WolfpackClient {
7
7
  api;
8
8
  teamId = null;
9
+ teamSlug = null;
9
10
  constructor() {
10
11
  this.api = new ApiClient();
11
12
  }
@@ -17,6 +18,7 @@ export class WolfpackClient {
17
18
  try {
18
19
  const team = await this.api.get(`/team/${teamSlug}`);
19
20
  this.teamId = team.id;
21
+ this.teamSlug = team.slug;
20
22
  return team.id;
21
23
  }
22
24
  catch (error) {
@@ -26,9 +28,15 @@ export class WolfpackClient {
26
28
  getTeamId() {
27
29
  return this.teamId;
28
30
  }
31
+ getTeamSlug() {
32
+ return this.teamSlug;
33
+ }
29
34
  setTeamId(teamId) {
30
35
  this.teamId = teamId;
31
36
  }
37
+ setTeamSlug(slug) {
38
+ this.teamSlug = slug;
39
+ }
32
40
  /**
33
41
  * List all teams the authenticated user is a member of.
34
42
  */
@@ -375,6 +383,13 @@ export class WolfpackClient {
375
383
  async createIssueComment(issueId, data) {
376
384
  return this.api.post(`/issues/${issueId}/comments`, data);
377
385
  }
386
+ /**
387
+ * Upload an image and get back a URL that can be used in markdown content.
388
+ */
389
+ async uploadImage(teamSlug, base64Data, filename, mimeType) {
390
+ const buffer = Buffer.from(base64Data, 'base64');
391
+ return this.api.postMultipart(`/teams/${teamSlug}/upload-image`, buffer, filename, mimeType);
392
+ }
378
393
  close() {
379
394
  // No cleanup needed for API client
380
395
  }
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
@@ -240,6 +242,13 @@ const CreateIssueCommentSchema = z.object({
240
242
  .string()
241
243
  .describe('Comment content (markdown). Supports base64-encoded images which will be auto-uploaded.'),
242
244
  });
245
+ // Image upload schema
246
+ const UploadImageSchema = z.object({
247
+ file_path: z
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
+ });
243
252
  // Helper to detect if a work item description contains a plan
244
253
  function hasPlan(description) {
245
254
  if (!description)
@@ -783,6 +792,25 @@ class WolfpackMCPServer {
783
792
  required: ['issue_id', 'content'],
784
793
  },
785
794
  },
795
+ // Image tools
796
+ {
797
+ name: 'upload_image',
798
+ description: 'Upload an image file from disk and get back a permanent URL. Much faster than embedding base64 in content. ' +
799
+ 'Pass the absolute file path - the file is read directly from disk without going through the LLM. ' +
800
+ 'Returns a markdown image URL you can include in any content field. ' +
801
+ 'Requires mcp:images:upload permission.',
802
+ inputSchema: {
803
+ type: 'object',
804
+ properties: {
805
+ file_path: {
806
+ type: 'string',
807
+ description: 'Absolute path to an image file on disk (e.g., "/tmp/screenshot.png"). ' +
808
+ 'Supported formats: JPEG, PNG, GIF, WebP. Max 5MB.',
809
+ },
810
+ },
811
+ required: ['file_path'],
812
+ },
813
+ },
786
814
  ],
787
815
  }));
788
816
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -1198,6 +1226,48 @@ class WolfpackMCPServer {
1198
1226
  ],
1199
1227
  };
1200
1228
  }
1229
+ case 'upload_image': {
1230
+ const parsed = UploadImageSchema.parse(args);
1231
+ const teamSlug = this.client.getTeamSlug();
1232
+ if (!teamSlug) {
1233
+ throw new Error('No team selected. Use list_teams first.');
1234
+ }
1235
+ const filePath = resolve(parsed.file_path);
1236
+ // Validate file extension
1237
+ const ext = extname(filePath).toLowerCase();
1238
+ const allowedExtensions = {
1239
+ '.png': 'image/png',
1240
+ '.jpg': 'image/jpeg',
1241
+ '.jpeg': 'image/jpeg',
1242
+ '.gif': 'image/gif',
1243
+ '.webp': 'image/webp',
1244
+ };
1245
+ const mimeType = allowedExtensions[ext];
1246
+ if (!mimeType) {
1247
+ throw new Error(`Unsupported image format "${ext}". Supported: ${Object.keys(allowedExtensions).join(', ')}`);
1248
+ }
1249
+ // Check file exists and size
1250
+ const fileStat = await stat(filePath);
1251
+ const maxSize = 5 * 1024 * 1024; // 5MB
1252
+ if (fileStat.size > maxSize) {
1253
+ throw new Error(`File too large (${fileStat.size} bytes). Maximum size is 5MB.`);
1254
+ }
1255
+ // Validate magic bytes to confirm it's actually an image
1256
+ const buffer = await readFile(filePath);
1257
+ if (!this.isImageBuffer(buffer)) {
1258
+ throw new Error('File does not appear to be a valid image.');
1259
+ }
1260
+ const filename = basename(filePath);
1261
+ const result = await this.client.uploadImage(teamSlug, buffer.toString('base64'), filename, mimeType);
1262
+ return {
1263
+ content: [
1264
+ {
1265
+ type: 'text',
1266
+ text: `Image uploaded successfully.\n\nURL: ${result.url}\n\nUse this URL in markdown: ![${filename}](${result.url})`,
1267
+ },
1268
+ ],
1269
+ };
1270
+ }
1201
1271
  default:
1202
1272
  throw new Error(`Unknown tool: ${name}`);
1203
1273
  }
@@ -1211,6 +1281,31 @@ class WolfpackMCPServer {
1211
1281
  }
1212
1282
  });
1213
1283
  }
1284
+ isImageBuffer(buffer) {
1285
+ if (buffer.length < 4)
1286
+ return false;
1287
+ // PNG: 89 50 4E 47
1288
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
1289
+ return true;
1290
+ // JPEG: FF D8 FF
1291
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff)
1292
+ return true;
1293
+ // GIF: 47 49 46 38
1294
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38)
1295
+ return true;
1296
+ // WebP: 52 49 46 46 ... 57 45 42 50
1297
+ if (buffer.length >= 12 &&
1298
+ buffer[0] === 0x52 &&
1299
+ buffer[1] === 0x49 &&
1300
+ buffer[2] === 0x46 &&
1301
+ buffer[3] === 0x46 &&
1302
+ buffer[8] === 0x57 &&
1303
+ buffer[9] === 0x45 &&
1304
+ buffer[10] === 0x42 &&
1305
+ buffer[11] === 0x50)
1306
+ return true;
1307
+ return false;
1308
+ }
1214
1309
  async start() {
1215
1310
  // Start version check early (non-blocking)
1216
1311
  const updateCheckPromise = checkForUpdates();
@@ -1233,6 +1328,7 @@ class WolfpackMCPServer {
1233
1328
  if (teams.length === 1) {
1234
1329
  // Auto-select the only team
1235
1330
  this.client.setTeamId(teams[0].id);
1331
+ this.client.setTeamSlug(teams[0].slug);
1236
1332
  console.error(`Auto-selected team: ${teams[0].name} (${teams[0].slug})`);
1237
1333
  }
1238
1334
  else if (teams.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",