wolfpack-mcp 1.0.116 → 1.0.117

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
@@ -196,9 +196,9 @@ export class WolfpackClient {
196
196
  throw error;
197
197
  }
198
198
  }
199
- async updateWorkProgress(workItemId, description, teamSlug) {
199
+ async updateWorkProgress(workItemId, body, teamSlug) {
200
200
  try {
201
- return await this.api.put(this.withTeamSlug(`/work-items/${workItemId}/progress`, teamSlug), { description });
201
+ return await this.api.put(this.withTeamSlug(`/work-items/${workItemId}/progress`, teamSlug), body);
202
202
  }
203
203
  catch (error) {
204
204
  if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
@@ -389,6 +389,20 @@ export class WolfpackClient {
389
389
  async updateIssue(issueId, data, teamSlug) {
390
390
  return this.api.patch(this.withTeamSlug(`/issues/${issueId}`, teamSlug), data);
391
391
  }
392
+ /**
393
+ * An entity's change timeline (#2684). `project_slug` is only needed to resolve a refId,
394
+ * so `withTeamSlug` supplies the configured project when the caller names none.
395
+ */
396
+ async getTimeline(entityType, entityId, options) {
397
+ const params = new URLSearchParams();
398
+ if (options?.since)
399
+ params.append('since', options.since);
400
+ if (options?.limit !== undefined)
401
+ params.append('limit', options.limit.toString());
402
+ const query = params.toString();
403
+ const path = `/timeline/${encodeURIComponent(entityType)}/${encodeURIComponent(entityId)}`;
404
+ return this.api.get(this.withTeamSlug(`${path}${query ? `?${query}` : ''}`, options?.teamSlug));
405
+ }
392
406
  // Wiki Page methods
393
407
  async listWikiPages(options) {
394
408
  const params = new URLSearchParams();
@@ -420,9 +434,16 @@ export class WolfpackClient {
420
434
  }
421
435
  return this.fetchAllPages('/wiki-pages', params);
422
436
  }
423
- async getWikiPage(pageId) {
437
+ async getWikiPage(pageId, view) {
438
+ const params = new URLSearchParams();
439
+ if (view?.mode)
440
+ params.append('mode', view.mode);
441
+ // Repeated rather than joined: the route takes an array, and a heading can contain
442
+ // whatever punctuation a separator would have claimed.
443
+ view?.section?.forEach((heading) => params.append('section', heading));
444
+ const query = params.toString();
424
445
  try {
425
- return await this.api.get(`/wiki-pages/${encodeURIComponent(pageId)}`);
446
+ return await this.api.get(`/wiki-pages/${encodeURIComponent(pageId)}${query ? `?${query}` : ''}`);
426
447
  }
427
448
  catch (error) {
428
449
  if (error && typeof error === 'object' && 'status' in error && error.status === 404) {
package/dist/index.js CHANGED
@@ -110,13 +110,29 @@ const GetWorkItemSchema = z.object({
110
110
  work_item_id: refIdString().describe('The refId (number) of the work item'),
111
111
  project_slug: z.string().optional().describe('Project slug (required when looking up by refId)'),
112
112
  });
113
+ /**
114
+ * One targeted edit. Loose on purpose: which fields a given `type` needs is the backend's rule,
115
+ * said once in `contentEditService`, and a second copy here would be the thing that drifts
116
+ * (#2679). The refusals it raises are written to be read by whoever sent the edit.
117
+ */
118
+ const ContentEditSchema = z.object({
119
+ type: z.string().describe('replace, replace_section, append_to_section, …'),
120
+ old: z.string().optional().describe('replace: the text to find, which must occur exactly once'),
121
+ new: z.string().optional().describe('replace: what to put in its place'),
122
+ section: z.array(z.string()).optional().describe('Section operations: the heading path'),
123
+ content: z.string().optional().describe('Section operations: the markdown to put there'),
124
+ });
113
125
  const UpdateWorkProgressSchema = z.object({
114
126
  work_item_id: refIdString().describe('The ID of the work item'),
115
- description: z.string().describe('Updated description/notes for the work item'),
127
+ description: z.string().optional().describe('Updated description/notes for the work item'),
116
128
  project_slug: z
117
129
  .string()
118
130
  .optional()
119
131
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
132
+ edits: z
133
+ .array(ContentEditSchema)
134
+ .optional()
135
+ .describe('Targeted edits to apply to `description` instead of sending it in full'),
120
136
  });
121
137
  /** The risk vocabulary, as the backend's `VALID_RISKS` holds it. */
122
138
  const VALID_RISKS = ['High', 'Medium', 'Low'];
@@ -200,6 +216,10 @@ const UpdateWorkItemSchema = z.object({
200
216
  .string()
201
217
  .optional()
202
218
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
219
+ edits: z
220
+ .array(ContentEditSchema)
221
+ .optional()
222
+ .describe('Targeted edits to apply to `description` instead of sending it in full'),
203
223
  });
204
224
  const PullWorkItemSchema = z.object({
205
225
  work_item_id: refIdString().describe('The ID of the work item to pull from backlog'),
@@ -335,6 +355,10 @@ const UpdateRadarItemSchema = z.object({
335
355
  .string()
336
356
  .optional()
337
357
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
358
+ edits: z
359
+ .array(ContentEditSchema)
360
+ .optional()
361
+ .describe('Targeted edits to apply to `description` instead of sending it in full'),
338
362
  });
339
363
  // Issue schemas
340
364
  const ListIssuesSchema = z.object({
@@ -453,6 +477,10 @@ const UpdateIssueSchema = z.object({
453
477
  .string()
454
478
  .optional()
455
479
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
480
+ edits: z
481
+ .array(ContentEditSchema)
482
+ .optional()
483
+ .describe('Targeted edits to apply to `description` instead of sending it in full'),
456
484
  });
457
485
  // Search schema
458
486
  const SearchSchema = z.object({
@@ -487,6 +515,24 @@ const ListWikiPagesSchema = z.object({
487
515
  });
488
516
  const GetWikiPageSchema = z.object({
489
517
  page_id: z.string().describe('The page slug'),
518
+ mode: z
519
+ .enum(['full', 'outline'])
520
+ .optional()
521
+ .describe('"outline" returns the heading tree and no content'),
522
+ section: z
523
+ .array(z.string())
524
+ .optional()
525
+ .describe('Read one section by heading path, outermost first'),
526
+ });
527
+ // Timeline schema (#2684). `entity_type` is left a plain string: the backend refuses an
528
+ // unknown one with a message naming the types that have a timeline, which tells the caller
529
+ // more than a parser error would.
530
+ const GetTimelineSchema = z.object({
531
+ entity_type: z.string().describe('The kind of content whose history you want'),
532
+ entity_id: z.string().describe('The page slug, or the refId for everything else'),
533
+ project_slug: z.string().optional().describe('Project slug, needed to resolve a refId'),
534
+ since: z.coerce.string().optional().describe('Only changes at or after this moment (ISO 8601)'),
535
+ limit: z.number().optional().describe('Maximum number of changes to return'),
490
536
  });
491
537
  const CreateWikiPageSchema = z.object({
492
538
  project_slug: z
@@ -507,13 +553,18 @@ const CreateWikiPageSchema = z.object({
507
553
  const UpdateWikiPageSchema = z.object({
508
554
  page_id: z.string().describe('The page slug'),
509
555
  title: z.string().optional().describe('Updated title'),
510
- content: z.string().optional().describe('Updated content (markdown)'),
511
- // Optional here, required by the server: a missing version is refused by the backend with
512
- // a message saying how to get one, which is more use than a parser error (#2681).
556
+ content: z.string().optional().describe('Updated content (markdown) — replaces the whole page'),
557
+ edits: z
558
+ .array(ContentEditSchema)
559
+ .optional()
560
+ .describe('Targeted edits to apply instead of sending the whole page'),
561
+ // Optional here, required by the server for everything but a `replace` edit: a missing
562
+ // version is refused by the backend with a message saying how to get one, which is more use
563
+ // than a parser error (#2681).
513
564
  expected_version: z
514
565
  .string()
515
566
  .optional()
516
- .describe('The `version` get_wiki_page returned for this page'),
567
+ .describe('The `version` from the read this write is based on'),
517
568
  });
518
569
  // Journal Entry schemas
519
570
  const ListJournalEntriesSchema = z.object({
@@ -550,6 +601,10 @@ const UpdateJournalEntrySchema = z.object({
550
601
  .string()
551
602
  .optional()
552
603
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
604
+ edits: z
605
+ .array(ContentEditSchema)
606
+ .optional()
607
+ .describe('Targeted edits to apply to `content` instead of sending it in full'),
553
608
  });
554
609
  // Idea schemas (the Idea Factory)
555
610
  const PROJECT_SLUG_DESCRIPTION = 'Project slug (required for multi-project users, use list_projects to get slugs)';
@@ -577,6 +632,10 @@ const UpdateIdeaSchema = z.object({
577
632
  content: z.string().optional().describe('Updated content (markdown)'),
578
633
  stage: z.string().optional().describe('New stage: spark, exploring, building, shipped or parked'),
579
634
  project_slug: z.string().optional().describe(PROJECT_SLUG_DESCRIPTION),
635
+ edits: z
636
+ .array(ContentEditSchema)
637
+ .optional()
638
+ .describe('Targeted edits to apply to `content` instead of sending it in full'),
580
639
  });
581
640
  const BoostIdeaSchema = z.object({
582
641
  idea_id: refIdString().describe('The idea refId (number)'),
@@ -932,6 +991,21 @@ class WolfpackMCPServer {
932
991
  content: [{ type: 'text', text }],
933
992
  };
934
993
  }
994
+ // Timeline handler (#2684)
995
+ case 'get_timeline': {
996
+ const parsed = GetTimelineSchema.parse(args);
997
+ const timeline = await this.client.getTimeline(parsed.entity_type, parsed.entity_id, {
998
+ teamSlug: parsed.project_slug || this.client.getProjectSlug() || undefined,
999
+ since: parsed.since,
1000
+ limit: parsed.limit,
1001
+ });
1002
+ const text = timeline.changes.length === 0
1003
+ ? `No changes recorded${parsed.since ? ` since ${parsed.since}` : ''} for ${parsed.entity_type} ${parsed.entity_id}.`
1004
+ : JSON.stringify(stripUuids(timeline), null, 2);
1005
+ return {
1006
+ content: [{ type: 'text', text }],
1007
+ };
1008
+ }
935
1009
  // Work Item handlers
936
1010
  case 'list_work_items': {
937
1011
  const parsed = ListWorkItemsSchema.parse(args);
@@ -989,7 +1063,7 @@ class WolfpackMCPServer {
989
1063
  case 'update_work_progress': {
990
1064
  const parsed = UpdateWorkProgressSchema.parse(args);
991
1065
  const teamSlug = parsed.project_slug || this.client.getProjectSlug() || undefined;
992
- const workItem = await this.client.updateWorkProgress(parsed.work_item_id, parsed.description, teamSlug);
1066
+ const workItem = await this.client.updateWorkProgress(parsed.work_item_id, { description: parsed.description, edits: parsed.edits }, teamSlug);
993
1067
  if (workItem) {
994
1068
  // Writing a description is not a claim that work has started: it is also how a
995
1069
  // plan is added before pickup and how the record is tidied after handover, so
@@ -1052,6 +1126,8 @@ class WolfpackMCPServer {
1052
1126
  fields.tags = parsed.tags;
1053
1127
  if (parsed.closing_comment !== undefined)
1054
1128
  fields.closingComment = parsed.closing_comment;
1129
+ if (parsed.edits !== undefined)
1130
+ fields.edits = parsed.edits;
1055
1131
  const workItem = await this.client.updateWorkItem(parsed.work_item_id, fields, teamSlug);
1056
1132
  if (workItem) {
1057
1133
  // Build a summary of what changed
@@ -1306,6 +1382,7 @@ class WolfpackMCPServer {
1306
1382
  description: parsed.description,
1307
1383
  notes: parsed.notes,
1308
1384
  stage: parsed.stage,
1385
+ edits: parsed.edits,
1309
1386
  }, teamSlug);
1310
1387
  return {
1311
1388
  content: [
@@ -1425,6 +1502,7 @@ class WolfpackMCPServer {
1425
1502
  expectedBehavior: parsed.expected_behavior,
1426
1503
  actualBehavior: parsed.actual_behavior,
1427
1504
  tags: parsed.tags,
1505
+ edits: parsed.edits,
1428
1506
  }, teamSlug);
1429
1507
  return {
1430
1508
  content: [
@@ -1456,7 +1534,10 @@ class WolfpackMCPServer {
1456
1534
  }
1457
1535
  case 'get_wiki_page': {
1458
1536
  const parsed = GetWikiPageSchema.parse(args);
1459
- const page = await this.client.getWikiPage(parsed.page_id);
1537
+ const page = await this.client.getWikiPage(parsed.page_id, {
1538
+ mode: parsed.mode,
1539
+ section: parsed.section,
1540
+ });
1460
1541
  if (page) {
1461
1542
  return {
1462
1543
  content: [{ type: 'text', text: JSON.stringify(stripUuids(page), null, 2) }],
@@ -1488,6 +1569,7 @@ class WolfpackMCPServer {
1488
1569
  const page = await this.client.updateWikiPage(parsed.page_id, {
1489
1570
  title: parsed.title,
1490
1571
  content: parsed.content,
1572
+ edits: parsed.edits,
1491
1573
  expectedVersion: parsed.expected_version,
1492
1574
  });
1493
1575
  return {
@@ -1552,6 +1634,7 @@ class WolfpackMCPServer {
1552
1634
  const entry = await this.client.updateJournalEntry(parsed.entry_id, {
1553
1635
  title: parsed.title,
1554
1636
  content: parsed.content,
1637
+ edits: parsed.edits,
1555
1638
  }, teamSlug);
1556
1639
  return {
1557
1640
  content: [
@@ -1605,7 +1688,12 @@ class WolfpackMCPServer {
1605
1688
  }
1606
1689
  case 'update_idea': {
1607
1690
  const parsed = UpdateIdeaSchema.parse(args);
1608
- 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);
1691
+ const idea = await this.client.updateIdea(parsed.idea_id, {
1692
+ title: parsed.title,
1693
+ content: parsed.content,
1694
+ stage: parsed.stage,
1695
+ edits: parsed.edits,
1696
+ }, parsed.project_slug || this.client.getProjectSlug() || undefined);
1609
1697
  return {
1610
1698
  content: [
1611
1699
  {
@@ -50,6 +50,80 @@ const CONTENT_LINKING_HELP = 'CROSS-REFERENCES: In any markdown content, you can
50
50
  '#j123 or #journal-123 for journal entries, #c123 or #case-123 for cases, #p123 or #proc-123 for procedures. ' +
51
51
  'Use @username to mention team members (triggers notifications). ' +
52
52
  'Standard markdown links also work: [link text](/project/{slug}/work-items/123).';
53
+ /**
54
+ * How to change part of a stored body instead of sending it all back (#2679).
55
+ *
56
+ * Said once because seven update tools carry it, and an agent that learned the contract on one
57
+ * of them has to find the same contract on the next.
58
+ */
59
+ const EDITS_HELP = 'SURGICAL EDITS: prefer `edits` to sending `content` in full. It costs a fraction of the ' +
60
+ 'tokens, and it cannot corrupt the parts of the document you did not mean to touch. ' +
61
+ 'Edits apply in order, all or nothing: if any one of them fails, nothing is written. ' +
62
+ '`{"type": "replace", "old": "...", "new": "..."}` swaps one exact run of text, which must ' +
63
+ 'occur exactly once — quote enough of the surrounding lines to make it unique. Whitespace and ' +
64
+ 'indentation are compared exactly. A failed match tells you how many times it matched and ' +
65
+ 'shows the closest lines, so you can correct the quote without re-reading the document.';
66
+ /**
67
+ * The section operations, offered only where the body is markdown and carries a version.
68
+ *
69
+ * `delete_section` removes part of a document; it is not a delete tool in the sense this
70
+ * package's CLAUDE.md forbids, which is about destroying an entity. The same removal has always
71
+ * been expressible by sending a shorter `content`, and nothing here reaches an entity the
72
+ * caller could not already edit.
73
+ */
74
+ const SECTION_EDITS_HELP = 'Markdown sections can also be addressed by heading path, which is stable where a line number ' +
75
+ 'is not: `replace_section`, `append_to_section`, `insert_before_section`, ' +
76
+ '`insert_after_section` and `delete_section` take `section` (e.g. ["Deployment", "Rollback"]) ' +
77
+ 'and, except for the delete, `content`. They act on the heading and everything nested under ' +
78
+ 'it. Because they quote no text of their own they REQUIRE `expected_version` from the read ' +
79
+ 'they are based on. Call get_wiki_page with mode="outline" to see the heading paths.';
80
+ const REPLACE_ONLY_EDIT_TYPES = ['replace'];
81
+ const ALL_EDIT_TYPES = [
82
+ 'replace',
83
+ 'replace_section',
84
+ 'append_to_section',
85
+ 'insert_before_section',
86
+ 'insert_after_section',
87
+ 'delete_section',
88
+ ];
89
+ /** The `edits` property, with the section operations only where they are supported. */
90
+ function editsSchema(sections) {
91
+ return {
92
+ type: 'array',
93
+ description: sections
94
+ ? 'Targeted edits to apply to the stored content instead of replacing it. Mutually exclusive with `content`.'
95
+ : 'Targeted edits to apply to the stored content instead of replacing it. Mutually exclusive with `content`. This content type takes `replace` only.',
96
+ items: {
97
+ type: 'object',
98
+ properties: {
99
+ type: {
100
+ type: 'string',
101
+ enum: sections ? ALL_EDIT_TYPES : REPLACE_ONLY_EDIT_TYPES,
102
+ description: 'Which operation to apply.',
103
+ },
104
+ old: {
105
+ type: 'string',
106
+ description: '`replace`: the text to find. Must occur exactly once. Whitespace is exact.',
107
+ },
108
+ new: { type: 'string', description: '`replace`: what to put in its place.' },
109
+ ...(sections
110
+ ? {
111
+ section: {
112
+ type: 'array',
113
+ items: { type: 'string' },
114
+ description: 'Section operations: the heading path, outermost first. The trailing headings alone are enough while they are unambiguous.',
115
+ },
116
+ content: {
117
+ type: 'string',
118
+ description: 'Section operations other than `delete_section`: the markdown to put there.',
119
+ },
120
+ }
121
+ : {}),
122
+ },
123
+ required: ['type'],
124
+ },
125
+ };
126
+ }
53
127
  /**
54
128
  * The risk vocabulary, said the same way on every tool that carries it (#2633).
55
129
  *
@@ -147,6 +221,65 @@ const SHARED_TOOLS = [
147
221
  required: ['query'],
148
222
  },
149
223
  },
224
+ // Timeline tool (#2684)
225
+ {
226
+ name: 'get_timeline',
227
+ // Takes the read scope of whatever it is asked about — `mcp:wiki:read` for a page,
228
+ // `mcp:work_items:read` for a work item — and checks it per entity type, so it
229
+ // declares none of its own. Reading a page's history is reading the page.
230
+ permission: null,
231
+ description: 'Read what has changed on one piece of content, newest change first: who changed it, when, ' +
232
+ 'and what the change was. Every kind of content with a timeline is read through this one tool. ' +
233
+ 'WHAT YOU GET BACK: each change carries a `summary`, and the fields it touched split two ways — ' +
234
+ '`fieldChanges` (short values, as `from` and `to`) and `diffs` (long or multi-line values, as a ' +
235
+ 'unified `diff -u` patch of the lines that changed). A change with neither, such as an approval ' +
236
+ 'or a comment, is described by its `summary` alone. ' +
237
+ 'SINCE IT WAS LAST APPROVED: approvals are changes on the same timeline, so read the timeline, ' +
238
+ 'find the newest change whose `category` is "approval", and call again with `since` set to its ' +
239
+ '`timestamp` — what comes back is everything that has happened to the content since it was signed off. ' +
240
+ 'PERMISSIONS: this needs the same read permission as reading the content itself (mcp:wiki:read for a ' +
241
+ 'page, mcp:work_items:read for a work item, and so on) and nothing further. Each timeline is that ' +
242
+ "one thing's own history: a case's timeline does not include the work items raised from it, so read " +
243
+ 'those with entity_type "work_item" and the refId of the item you want.',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ entity_type: {
248
+ type: 'string',
249
+ enum: [
250
+ 'wiki_page',
251
+ 'work_item',
252
+ 'issue',
253
+ 'radar_item',
254
+ 'journal_entry',
255
+ 'procedure',
256
+ 'case',
257
+ ],
258
+ description: 'The kind of content whose history you want',
259
+ },
260
+ entity_id: {
261
+ type: 'string',
262
+ description: 'The reference you already hold: the slug for a wiki page, the refId (number) for ' +
263
+ 'everything else — a work item\'s "123", an issue\'s "45".',
264
+ },
265
+ project_slug: {
266
+ type: 'string',
267
+ description: 'Project slug; required with a refId when you belong to more than one project ' +
268
+ '(use list_projects to get slugs)',
269
+ },
270
+ since: {
271
+ type: 'string',
272
+ description: 'Only changes at or after this moment (ISO 8601, e.g. "2026-10-07T09:00:00Z"). Pass an ' +
273
+ "approval change's `timestamp` to see what has changed since the content was approved.",
274
+ },
275
+ limit: {
276
+ type: 'number',
277
+ description: 'Maximum number of changes to return; defaults to 20',
278
+ },
279
+ },
280
+ required: ['entity_type', 'entity_id'],
281
+ },
282
+ },
150
283
  // Work Item tools
151
284
  {
152
285
  name: 'list_work_items',
@@ -282,7 +415,8 @@ const SHARED_TOOLS = [
282
415
  'STATUS: This tool never changes status — editing the description is also how a plan is added before pickup and how the record is tidied after handover (#2634). The response names the status it left the item in. Moving an item is always your own call: pull_work_item to start it, update_work_item to set "doing" when you pick it back up, and "review" with a completion comment when it is done. ' +
283
416
  'CRITICAL: NEVER overwrite or remove the original description text. Preserve all existing content exactly as-is. Append your plan below a "---" separator. You may only modify content you previously added (your plan section). ' +
284
417
  'BEST PRACTICE: Append a plan with markdown checkboxes (- [ ] task). Check off completed tasks (- [x] task) as you progress. Include sections for: Plan (checklist), Approach (strategy), and Notes (discoveries/decisions). ' +
285
- CONTENT_LINKING_HELP,
418
+ CONTENT_LINKING_HELP +
419
+ EDITS_HELP,
286
420
  inputSchema: {
287
421
  type: 'object',
288
422
  properties: {
@@ -295,8 +429,9 @@ const SHARED_TOOLS = [
295
429
  type: 'string',
296
430
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
297
431
  },
432
+ edits: editsSchema(false),
298
433
  },
299
- required: ['work_item_id', 'description'],
434
+ required: ['work_item_id'],
300
435
  },
301
436
  },
302
437
  {
@@ -315,7 +450,8 @@ const SHARED_TOOLS = [
315
450
  'what makes the board show who is working on what. ' +
316
451
  'When moving to "review", add a completion comment via create_work_item_comment. When moving to "blocked", add a comment explaining the blocker. ' +
317
452
  'When you take an item back out of "review" to rework it, set it to "doing" first — an item being changed is not an item waiting to be reviewed. ' +
318
- 'closing_comment is only accepted together with status "closed" — never use it for progress or completion notes.',
453
+ 'closing_comment is only accepted together with status "closed" — never use it for progress or completion notes.' +
454
+ EDITS_HELP,
319
455
  inputSchema: {
320
456
  type: 'object',
321
457
  properties: {
@@ -404,6 +540,7 @@ const SHARED_TOOLS = [
404
540
  type: 'string',
405
541
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
406
542
  },
543
+ edits: editsSchema(false),
407
544
  },
408
545
  required: ['work_item_id'],
409
546
  },
@@ -720,7 +857,8 @@ const SHARED_TOOLS = [
720
857
  name: 'update_radar_item',
721
858
  permission: 'mcp:radar:update',
722
859
  description: 'Update an existing radar/initiative item. Requires mcp:radar:update permission. ' +
723
- CONTENT_LINKING_HELP,
860
+ CONTENT_LINKING_HELP +
861
+ EDITS_HELP,
724
862
  inputSchema: {
725
863
  type: 'object',
726
864
  properties: {
@@ -743,6 +881,7 @@ const SHARED_TOOLS = [
743
881
  type: 'string',
744
882
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
745
883
  },
884
+ edits: editsSchema(false),
746
885
  },
747
886
  required: ['item_id'],
748
887
  },
@@ -951,7 +1090,8 @@ const SHARED_TOOLS = [
951
1090
  'empty array clears it. Tagging an issue that is not assigned to you needs ' +
952
1091
  'mcp:issues:tag_all, and that scope allows a tags-only update — send any other ' +
953
1092
  'field alongside and the usual assignment rule applies. ' +
954
- CONTENT_LINKING_HELP,
1093
+ CONTENT_LINKING_HELP +
1094
+ EDITS_HELP,
955
1095
  inputSchema: {
956
1096
  type: 'object',
957
1097
  properties: {
@@ -999,6 +1139,7 @@ const SHARED_TOOLS = [
999
1139
  type: 'string',
1000
1140
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1001
1141
  },
1142
+ edits: editsSchema(false),
1002
1143
  },
1003
1144
  required: ['issue_id'],
1004
1145
  },
@@ -1036,8 +1177,12 @@ const SHARED_TOOLS = [
1036
1177
  name: 'get_wiki_page',
1037
1178
  permission: 'mcp:wiki:read',
1038
1179
  description: 'Get a specific wiki/documentation page by slug (URL path). Returns full content in markdown. ' +
1039
- 'VERSION: the response carries a `version`. Pass it to update_wiki_page as expected_version when you save, ' +
1180
+ 'LONG PAGES: pass mode="outline" first to see the heading tree and the size of each section, ' +
1181
+ 'then section=["Heading"] to read just the one you need — far cheaper than reading the page. ' +
1182
+ 'VERSION: every response carries a `version`. Pass it to update_wiki_page as expected_version when you save, ' +
1040
1183
  'and read the page again — taking a fresh version — if the save is refused because somebody changed it. ' +
1184
+ 'APPROVALS: `approvals` lists who has signed the page off as it currently reads, with the date each ' +
1185
+ 'approved. It is empty if nobody has, and an approval the page has been edited past is not listed. ' +
1041
1186
  'IMAGES: Image references in the content (e.g. `![alt](/api/files/...)`) can be viewed using the download_image tool.',
1042
1187
  inputSchema: {
1043
1188
  type: 'object',
@@ -1046,6 +1191,20 @@ const SHARED_TOOLS = [
1046
1191
  type: 'string',
1047
1192
  description: 'The page slug (URL path like "getting-started")',
1048
1193
  },
1194
+ mode: {
1195
+ type: 'string',
1196
+ enum: ['full', 'outline'],
1197
+ description: '"full" (default) returns the page content. "outline" returns the heading tree — ' +
1198
+ 'heading path, level, line range and size — and no content.',
1199
+ },
1200
+ section: {
1201
+ type: 'array',
1202
+ items: { type: 'string' },
1203
+ description: 'Read one section instead of the page. A heading path, outermost first, e.g. ' +
1204
+ '["Deployment", "Rollback"]. The trailing headings alone are enough while they are ' +
1205
+ 'unambiguous, so ["Rollback"] usually works. Returns the heading and everything ' +
1206
+ 'nested under it.',
1207
+ },
1049
1208
  },
1050
1209
  required: ['page_id'],
1051
1210
  },
@@ -1079,9 +1238,13 @@ const SHARED_TOOLS = [
1079
1238
  name: 'update_wiki_page',
1080
1239
  permission: 'mcp:wiki:update',
1081
1240
  description: 'Update an existing wiki page. Requires mcp:wiki:update permission. ' +
1082
- 'READ BEFORE YOU WRITE: call get_wiki_page first and pass the `version` it returned as expected_version. ' +
1083
- 'Wiki pages are edited by people at the same time as you, and `content` replaces the whole document, ' +
1084
- 'so a save made from a copy you read earlier would throw away everything written since. ' +
1241
+ EDITS_HELP +
1242
+ ' ' +
1243
+ SECTION_EDITS_HELP +
1244
+ ' READ BEFORE YOU WRITE: wiki pages are edited by people at the same time as you. A `content` write ' +
1245
+ 'replaces the whole document, so a save made from a copy you read earlier would throw away everything ' +
1246
+ 'written since — call get_wiki_page first and pass the `version` it returned as expected_version, ' +
1247
+ 'which is required for `content` and for a section operation. ' +
1085
1248
  'If the save is refused because the page changed, do NOT send the same content again: ' +
1086
1249
  'call get_wiki_page, re-apply your change to what the page says now, and save with the new version. ' +
1087
1250
  CONTENT_LINKING_HELP,
@@ -1098,15 +1261,23 @@ const SHARED_TOOLS = [
1098
1261
  title: { type: 'string', description: 'Updated title' },
1099
1262
  content: {
1100
1263
  type: 'string',
1101
- description: 'Updated content (markdown)',
1264
+ description: 'Updated content (markdown). Replaces the whole page — prefer `edits` for anything smaller.',
1102
1265
  },
1266
+ edits: editsSchema(true),
1103
1267
  expected_version: {
1104
1268
  type: 'string',
1105
- description: 'The `version` get_wiki_page returned for this page. Required: the save is refused if ' +
1106
- 'somebody has changed the page since, so that their edit is not thrown away.',
1269
+ description: 'The `version` from the get_wiki_page read this write is based on. The save is ' +
1270
+ 'refused with a conflict if the page changed since, naming who changed it — re-read ' +
1271
+ 'and re-apply, so that their edit is not thrown away. Required for `content`, and ' +
1272
+ 'for `edits` containing a section operation; a `replace` edit quotes the exact text ' +
1273
+ 'it rewrites, which is a stronger precondition over the only region it touches.',
1107
1274
  },
1108
1275
  },
1109
- required: ['page_id', 'expected_version'],
1276
+ // `expected_version` is required for everything except a `replace` edit, which is the one
1277
+ // request that quotes its own precondition — so the rule is stated on the field and
1278
+ // enforced by the route (#2679 / #2681) rather than written as a schema the caller has to
1279
+ // satisfy before the refusal can explain itself.
1280
+ required: ['page_id'],
1110
1281
  },
1111
1282
  },
1112
1283
  // Journal Entry tools
@@ -1180,7 +1351,8 @@ const SHARED_TOOLS = [
1180
1351
  name: 'update_journal_entry',
1181
1352
  permission: 'mcp:journal:update',
1182
1353
  description: 'Update an existing journal entry. Requires mcp:journal:update permission. ' +
1183
- CONTENT_LINKING_HELP,
1354
+ CONTENT_LINKING_HELP +
1355
+ EDITS_HELP,
1184
1356
  inputSchema: {
1185
1357
  type: 'object',
1186
1358
  properties: {
@@ -1197,6 +1369,7 @@ const SHARED_TOOLS = [
1197
1369
  type: 'string',
1198
1370
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1199
1371
  },
1372
+ edits: editsSchema(false),
1200
1373
  },
1201
1374
  required: ['entry_id'],
1202
1375
  },
@@ -1279,7 +1452,8 @@ const SHARED_TOOLS = [
1279
1452
  description: 'Update an idea, or move it to another stage. The stages are "spark" (just raised), ' +
1280
1453
  '"exploring", "building", "shipped" and "parked" — moving an idea along is how the project ' +
1281
1454
  'sees what became of it. Requires mcp:ideas:update permission. ' +
1282
- CONTENT_LINKING_HELP,
1455
+ CONTENT_LINKING_HELP +
1456
+ EDITS_HELP,
1283
1457
  inputSchema: {
1284
1458
  type: 'object',
1285
1459
  properties: {
@@ -1294,6 +1468,7 @@ const SHARED_TOOLS = [
1294
1468
  type: 'string',
1295
1469
  description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1296
1470
  },
1471
+ edits: editsSchema(false),
1297
1472
  },
1298
1473
  required: ['idea_id'],
1299
1474
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.116",
3
+ "version": "1.0.117",
4
4
  "description": "MCP server for Clachan (formerly Wolfpack), AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",