wolfpack-mcp 1.0.104 → 1.0.105

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/README.md CHANGED
@@ -309,6 +309,49 @@ Update an existing journal entry.
309
309
  - `entry_id` (required): The entry UUID
310
310
  - `title`, `content` (optional)
311
311
 
312
+ ### Ideas (the Idea Factory)
313
+
314
+ Offered to a key holding `mcp:ideas:read` on a project where the `idea-factory` feature is
315
+ switched on. There is no archive tool: archiving is the Idea Factory's delete, and MCP does not
316
+ surface deletes.
317
+
318
+ #### `list_ideas`
319
+
320
+ List the live ideas in a project. Each carries a stage and a boost count, and the response says
321
+ whether your own boost for today in that project is already spent.
322
+
323
+ - `sort` (optional): `popular` (default), `updated` or `newest`
324
+ - `limit`, `offset` (optional): Pagination
325
+
326
+ #### `get_idea`
327
+
328
+ Get a single idea, with its tags and the reference numbers of any work items being built from it.
329
+
330
+ - `idea_id` (required): The idea refId (number)
331
+
332
+ #### `create_idea`
333
+
334
+ Raise a new idea. It starts at the `spark` stage with no boosts.
335
+
336
+ - `title` (required): Idea title
337
+ - `content` (required): Markdown content
338
+
339
+ #### `update_idea`
340
+
341
+ Update an idea, or move it to another stage.
342
+
343
+ - `idea_id` (required): The idea refId (number)
344
+ - `title`, `content` (optional)
345
+ - `stage` (optional): `spark`, `exploring`, `building`, `shipped` or `parked`
346
+
347
+ #### `boost_idea`
348
+
349
+ Vote for an idea by spending your boost. One boost per project per day, whichever idea you spend
350
+ it on — spending it again the same day is refused rather than counted. Takes `mcp:ideas:boost`,
351
+ which is separate from `mcp:ideas:update`: voting is participation, not editing.
352
+
353
+ - `idea_id` (required): The idea refId (number)
354
+
312
355
  ### Comments
313
356
 
314
357
  #### `list_work_item_comments`
package/dist/client.js CHANGED
@@ -489,6 +489,33 @@ export class WolfpackClient {
489
489
  async updateJournalEntry(entryId, data, teamSlug) {
490
490
  return this.api.patch(this.withTeamSlug(`/journal-entries/${encodeURIComponent(entryId)}`, teamSlug), data);
491
491
  }
492
+ // Idea methods (the Idea Factory)
493
+ async listIdeas(options) {
494
+ const params = new URLSearchParams();
495
+ if (options?.teamSlug)
496
+ params.append('teamSlug', options.teamSlug);
497
+ if (options?.sort)
498
+ params.append('sort', options.sort);
499
+ if (options?.limit !== undefined)
500
+ params.append('limit', options.limit.toString());
501
+ if (options?.offset !== undefined)
502
+ params.append('offset', options.offset.toString());
503
+ const query = params.toString();
504
+ return this.api.get(`/ideas${query ? `?${query}` : ''}`);
505
+ }
506
+ async getIdea(ideaId, teamSlug) {
507
+ return this.api.get(this.withTeamSlug(`/ideas/${encodeURIComponent(ideaId)}`, teamSlug));
508
+ }
509
+ async createIdea(data) {
510
+ const { teamSlug, ...rest } = data;
511
+ return this.api.post('/ideas', { ...rest, teamSlug });
512
+ }
513
+ async updateIdea(ideaId, data, teamSlug) {
514
+ return this.api.put(this.withTeamSlug(`/ideas/${encodeURIComponent(ideaId)}`, teamSlug), data);
515
+ }
516
+ async boostIdea(ideaId, teamSlug) {
517
+ return this.api.post(this.withTeamSlug(`/ideas/${encodeURIComponent(ideaId)}/boost`, teamSlug), {});
518
+ }
492
519
  // Comment methods
493
520
  async listWorkItemComments(workItemId, teamSlug) {
494
521
  return this.api.get(this.withTeamSlug(`/work-items/${workItemId}/comments`, teamSlug));
package/dist/index.js CHANGED
@@ -534,6 +534,37 @@ const UpdateJournalEntrySchema = z.object({
534
534
  .optional()
535
535
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
536
536
  });
537
+ // Idea schemas (the Idea Factory)
538
+ const PROJECT_SLUG_DESCRIPTION = 'Project slug (required for multi-project users, use list_projects to get slugs)';
539
+ const ListIdeasSchema = z.object({
540
+ sort: z
541
+ .string()
542
+ .optional()
543
+ .describe('Ordering: "popular" (default, most boosted first), "updated", or "newest"'),
544
+ limit: z.number().optional().describe('Maximum number of ideas to return'),
545
+ offset: z.number().optional().describe('Number of ideas to skip'),
546
+ project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
547
+ });
548
+ const GetIdeaSchema = z.object({
549
+ idea_id: refIdString().describe('The idea refId (number)'),
550
+ project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
551
+ });
552
+ const CreateIdeaSchema = z.object({
553
+ title: z.string().describe('Idea title'),
554
+ content: z.string().describe('Idea content (markdown)'),
555
+ project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
556
+ });
557
+ const UpdateIdeaSchema = z.object({
558
+ idea_id: refIdString().describe('The idea refId (number)'),
559
+ title: z.string().optional().describe('Updated title'),
560
+ content: z.string().optional().describe('Updated content (markdown)'),
561
+ stage: z.string().optional().describe('New stage: spark, exploring, building, shipped or parked'),
562
+ project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
563
+ });
564
+ const BoostIdeaSchema = z.object({
565
+ idea_id: refIdString().describe('The idea refId (number)'),
566
+ project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
567
+ });
537
568
  // Comment schemas
538
569
  const ListWorkItemCommentsSchema = z.object({
539
570
  work_item_id: refIdString().describe('The work item refId (number)'),
@@ -1512,6 +1543,71 @@ class WolfpackMCPServer {
1512
1543
  ],
1513
1544
  };
1514
1545
  }
1546
+ // Idea handlers (the Idea Factory)
1547
+ case 'list_ideas': {
1548
+ const parsed = ListIdeasSchema.parse(args);
1549
+ const result = await this.client.listIdeas({
1550
+ teamSlug: parsed.project_slug || this.client.getProjectSlug() || undefined,
1551
+ sort: parsed.sort,
1552
+ limit: parsed.limit,
1553
+ offset: parsed.offset,
1554
+ });
1555
+ const spent = result.boostSpentToday
1556
+ ? 'Your boost for today in this project is already spent.'
1557
+ : 'Your boost for today in this project is unspent.';
1558
+ let text = `${spent}\n\n${JSON.stringify(stripUuids(result.items), null, 2)}`;
1559
+ if (result.total > result.items.length) {
1560
+ text = `Note: Showing ${result.items.length} of ${result.total} ideas. Use limit/offset for pagination.\n\n${text}`;
1561
+ }
1562
+ return { content: [{ type: 'text', text }] };
1563
+ }
1564
+ case 'get_idea': {
1565
+ const parsed = GetIdeaSchema.parse(args);
1566
+ const idea = await this.client.getIdea(parsed.idea_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
1567
+ return {
1568
+ content: [{ type: 'text', text: JSON.stringify(stripUuids(idea), null, 2) }],
1569
+ };
1570
+ }
1571
+ case 'create_idea': {
1572
+ const parsed = CreateIdeaSchema.parse(args);
1573
+ const idea = await this.client.createIdea({
1574
+ title: parsed.title,
1575
+ content: parsed.content,
1576
+ teamSlug: parsed.project_slug || this.client.getProjectSlug() || undefined,
1577
+ });
1578
+ return {
1579
+ content: [
1580
+ {
1581
+ type: 'text',
1582
+ text: `Created idea #${idea.refId}: ${idea.title}\n\n${JSON.stringify(stripUuids(idea), null, 2)}`,
1583
+ },
1584
+ ],
1585
+ };
1586
+ }
1587
+ case 'update_idea': {
1588
+ const parsed = UpdateIdeaSchema.parse(args);
1589
+ const idea = await this.client.updateIdea(parsed.idea_id, { title: parsed.title, content: parsed.content, stage: parsed.stage }, parsed.project_slug || this.client.getProjectSlug() || undefined);
1590
+ return {
1591
+ content: [
1592
+ {
1593
+ type: 'text',
1594
+ text: `Updated idea #${idea.refId}: ${idea.title} (${idea.stage})\n\n${JSON.stringify(stripUuids(idea), null, 2)}`,
1595
+ },
1596
+ ],
1597
+ };
1598
+ }
1599
+ case 'boost_idea': {
1600
+ const parsed = BoostIdeaSchema.parse(args);
1601
+ const idea = await this.client.boostIdea(parsed.idea_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
1602
+ return {
1603
+ content: [
1604
+ {
1605
+ type: 'text',
1606
+ text: `Boosted idea #${idea.refId}: ${idea.title} now has ${idea.boostCount} boost(s). That was your boost for today in this project.`,
1607
+ },
1608
+ ],
1609
+ };
1610
+ }
1515
1611
  // Comment handlers
1516
1612
  case 'list_work_item_comments': {
1517
1613
  const parsed = ListWorkItemCommentsSchema.parse(args);
@@ -1146,6 +1146,124 @@ export const TOOL_CATALOGUE = [
1146
1146
  required: ['entry_id'],
1147
1147
  },
1148
1148
  },
1149
+ // Idea Factory tools (#2473). Gated on the `ideas` capability, which the backend
1150
+ // grants a key holding mcp:ideas:read once the `idea-factory` flag is on for it.
1151
+ // No archive tool: archiving is the Idea Factory's delete, and MCP does not
1152
+ // surface deletes (packages/mcp/CLAUDE.md).
1153
+ {
1154
+ name: 'list_ideas',
1155
+ permission: 'mcp:ideas:read',
1156
+ capability: 'ideas',
1157
+ description: 'List the live ideas in a project (the Idea Factory). Use when user asks about "ideas", ' +
1158
+ '"the idea factory", "what has been suggested", or "what are we thinking about". ' +
1159
+ 'Each idea carries a stage (spark, exploring, building, shipped, parked) and a boostCount — ' +
1160
+ 'the number of members who have voted for it. The response also says whether your own boost ' +
1161
+ 'for today in this project has already been spent.',
1162
+ inputSchema: {
1163
+ type: 'object',
1164
+ properties: {
1165
+ sort: {
1166
+ type: 'string',
1167
+ description: 'Ordering: "popular" (default, most boosted first), "updated", or "newest"',
1168
+ },
1169
+ limit: { type: 'number', description: 'Maximum number of ideas to return' },
1170
+ offset: { type: 'number', description: 'Number of ideas to skip' },
1171
+ project_slug: {
1172
+ type: 'string',
1173
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1174
+ },
1175
+ },
1176
+ },
1177
+ },
1178
+ {
1179
+ name: 'get_idea',
1180
+ permission: 'mcp:ideas:read',
1181
+ capability: 'ideas',
1182
+ description: 'Get a single idea by its refId, with its full content, stage, tags, boost count and the ' +
1183
+ 'refIds of any work items being built from it.',
1184
+ inputSchema: {
1185
+ type: 'object',
1186
+ properties: {
1187
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1188
+ project_slug: {
1189
+ type: 'string',
1190
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1191
+ },
1192
+ },
1193
+ required: ['idea_id'],
1194
+ },
1195
+ },
1196
+ {
1197
+ name: 'create_idea',
1198
+ permission: 'mcp:ideas:create',
1199
+ capability: 'ideas',
1200
+ description: 'Raise a new idea in the Idea Factory. Use when user wants to "suggest", "raise an idea", ' +
1201
+ '"add to the idea factory", or "capture a thought". A new idea starts at the "spark" stage ' +
1202
+ 'with no boosts. Requires mcp:ideas:create permission. ' +
1203
+ CONTENT_LINKING_HELP,
1204
+ inputSchema: {
1205
+ type: 'object',
1206
+ properties: {
1207
+ title: { type: 'string', description: 'Idea title' },
1208
+ content: { type: 'string', description: 'Idea content (markdown)' },
1209
+ // No tag_ids: the service takes tag UUIDs and the MCP surface strips UUIDs
1210
+ // from every response, so an agent never holds one to send back. Tag an
1211
+ // idea in the UI.
1212
+ project_slug: {
1213
+ type: 'string',
1214
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1215
+ },
1216
+ },
1217
+ required: ['title', 'content'],
1218
+ },
1219
+ },
1220
+ {
1221
+ name: 'update_idea',
1222
+ permission: 'mcp:ideas:update',
1223
+ capability: 'ideas',
1224
+ description: 'Update an idea, or move it to another stage. The stages are "spark" (just raised), ' +
1225
+ '"exploring", "building", "shipped" and "parked" — moving an idea along is how the project ' +
1226
+ 'sees what became of it. Requires mcp:ideas:update permission. ' +
1227
+ CONTENT_LINKING_HELP,
1228
+ inputSchema: {
1229
+ type: 'object',
1230
+ properties: {
1231
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1232
+ title: { type: 'string', description: 'Updated title' },
1233
+ content: { type: 'string', description: 'Updated content (markdown)' },
1234
+ stage: {
1235
+ type: 'string',
1236
+ description: 'New stage: spark, exploring, building, shipped or parked',
1237
+ },
1238
+ project_slug: {
1239
+ type: 'string',
1240
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1241
+ },
1242
+ },
1243
+ required: ['idea_id'],
1244
+ },
1245
+ },
1246
+ {
1247
+ name: 'boost_idea',
1248
+ permission: 'mcp:ideas:boost',
1249
+ capability: 'ideas',
1250
+ description: 'Vote for an idea by spending your boost. Use when user wants to "vote for", "boost", ' +
1251
+ '"back" or "+1" an idea. You get ONE boost per project per day, whichever idea you spend ' +
1252
+ 'it on, so it is a choice between ideas rather than a click — spending it again the same ' +
1253
+ 'day is refused rather than counted. Requires mcp:ideas:boost permission, which is separate ' +
1254
+ 'from mcp:ideas:update: voting is participation, not editing.',
1255
+ inputSchema: {
1256
+ type: 'object',
1257
+ properties: {
1258
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1259
+ project_slug: {
1260
+ type: 'string',
1261
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1262
+ },
1263
+ },
1264
+ required: ['idea_id'],
1265
+ },
1266
+ },
1149
1267
  // Comment tools
1150
1268
  {
1151
1269
  name: 'list_work_item_comments',
@@ -61,13 +61,16 @@ describe('the MCP tool catalogue', () => {
61
61
  .map((param) => `${tool.name}.${param}`));
62
62
  expect(unforwarded).toEqual([]);
63
63
  });
64
- it('offers the procedure tools over stdio only to a key with that capability', () => {
65
- const withProcedures = stdioTools(['procedures']).map((t) => t.name);
64
+ // Every capability in the catalogue, not just `procedures`: a family added with
65
+ // its `capability` left off is offered to every key, and nothing else notices.
66
+ it.each([...new Set(TOOL_CATALOGUE.map((t) => t.capability).filter(Boolean))])('offers the %s tools over stdio only to a key with that capability', (capability) => {
67
+ const gated = TOOL_CATALOGUE.filter((t) => t.capability === capability).map((t) => t.name);
68
+ const withIt = stdioTools([capability]).map((t) => t.name);
66
69
  const without = stdioTools([]).map((t) => t.name);
67
- expect(withProcedures).toContain('start_case');
68
- expect(without).not.toContain('start_case');
70
+ expect(withIt).toEqual(expect.arrayContaining(gated));
71
+ gated.forEach((name) => expect(without).not.toContain(name));
69
72
  // Gating is the only difference: nothing else drops out.
70
- expect(withProcedures.length - without.length).toBe(TOOL_CATALOGUE.filter((t) => t.capability === 'procedures').length);
73
+ expect(withIt.length - without.length).toBe(gated.length);
71
74
  });
72
75
  it('offers a tool over stdio only to a key holding its scope', () => {
73
76
  // #2428 — the list told an agent it could do things the 403 then refused.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.104",
3
+ "version": "1.0.105",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",