wolfpack-mcp 1.0.104 → 1.0.106

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`
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * Agent builder MCP tool definitions and handlers.
3
3
  * Registered only when the API key has the `agent_builder` capability.
4
+ *
5
+ * Each entry declares the scope its route already checks, so a key granted the
6
+ * capability on `mcp:agents:read` is offered the reads and not the writes it
7
+ * would be refused (#2493). `list_organisations` is the one `permission: null`
8
+ * here: `GET /mcp/organisations` checks no scope, being the discovery call the
9
+ * rest of the family needs to name an organisation at all.
4
10
  */
5
11
  import { z } from 'zod';
6
12
  import { config } from './config.js';
@@ -34,12 +40,18 @@ export const AGENT_BUILDER_TOOLS = [
34
40
  // ─── Group 0: Organisations ─────────────────────────────────────────────
35
41
  {
36
42
  name: 'list_organisations',
43
+ permission: null,
44
+ capability: 'agent_builder',
45
+ stdioOnly: true,
37
46
  description: 'List organisations you belong to. Use org_slug parameter in other agent builder tools when you belong to multiple organisations.',
38
47
  inputSchema: { type: 'object', properties: {} },
39
48
  },
40
49
  // ─── Group 1: Agent CRUD ─────────────────────────────────────────────────
41
50
  {
42
51
  name: 'list_agents',
52
+ permission: 'mcp:agents:read',
53
+ capability: 'agent_builder',
54
+ stdioOnly: true,
43
55
  description: 'List agents. Returns name, status, template, and assigned projects. ' +
44
56
  'scope "org" (the default) lists the organisation\'s agents; scope "personal" lists your own ' +
45
57
  'My Agents from My Space, which belong to no organisation. Call it twice to see both.',
@@ -57,6 +69,9 @@ export const AGENT_BUILDER_TOOLS = [
57
69
  },
58
70
  {
59
71
  name: 'get_agent',
72
+ permission: 'mcp:agents:read',
73
+ capability: 'agent_builder',
74
+ stdioOnly: true,
60
75
  description: 'Get full details for an agent including config, instructions, LLM model, and linked skills. ' +
61
76
  'Takes an organisation agent or one of your own personal agents.',
62
77
  inputSchema: {
@@ -70,6 +85,9 @@ export const AGENT_BUILDER_TOOLS = [
70
85
  },
71
86
  {
72
87
  name: 'create_agent',
88
+ permission: 'mcp:agents:create',
89
+ capability: 'agent_builder',
90
+ stdioOnly: true,
73
91
  description: 'Create a new agent from a container image. Use list_container_images to see available images. ' +
74
92
  'With personal=true, creates one of your own My Agents instead of an organisation agent: ' +
75
93
  'it needs a handle, and only an image opened to personal use (list_container_images with ' +
@@ -98,6 +116,9 @@ export const AGENT_BUILDER_TOOLS = [
98
116
  },
99
117
  {
100
118
  name: 'update_agent',
119
+ permission: 'mcp:agents:update',
120
+ capability: 'agent_builder',
121
+ stdioOnly: true,
101
122
  description: "Update an agent's config, instructions, or LLM model. Omit fields to leave them unchanged.",
102
123
  inputSchema: {
103
124
  type: 'object',
@@ -144,6 +165,9 @@ export const AGENT_BUILDER_TOOLS = [
144
165
  },
145
166
  {
146
167
  name: 'list_agent_aliases',
168
+ permission: 'mcp:agents:read',
169
+ capability: 'agent_builder',
170
+ stdioOnly: true,
147
171
  description: 'List the aliases of an agent. Aliases share the source agent definition but have their own identity and queue.',
148
172
  inputSchema: {
149
173
  type: 'object',
@@ -156,6 +180,9 @@ export const AGENT_BUILDER_TOOLS = [
156
180
  },
157
181
  {
158
182
  name: 'create_agent_alias',
183
+ permission: 'mcp:agents:create',
184
+ capability: 'agent_builder',
185
+ stdioOnly: true,
159
186
  description: 'Create an alias of an agent, so the same definition can run work in parallel. ' +
160
187
  'An agent runs one session at a time, so N concurrent workers means N aliases. ' +
161
188
  'The alias live-tracks the source for container image, prompts, instructions, LLM, tools, skills and tasks — edit those once on the source. ' +
@@ -173,6 +200,9 @@ export const AGENT_BUILDER_TOOLS = [
173
200
  },
174
201
  {
175
202
  name: 'get_agent_mcp_selections',
203
+ permission: 'mcp:agents:read',
204
+ capability: 'agent_builder',
205
+ stdioOnly: true,
176
206
  description: "List the template MCP servers this agent has opted out of. Returns { disabled: string[] } where each string is the MCP server key from the agent's template.",
177
207
  inputSchema: {
178
208
  type: 'object',
@@ -185,6 +215,9 @@ export const AGENT_BUILDER_TOOLS = [
185
215
  },
186
216
  {
187
217
  name: 'update_agent_mcp_selections',
218
+ permission: 'mcp:agents:update',
219
+ capability: 'agent_builder',
220
+ stdioOnly: true,
188
221
  description: 'Replace the list of template MCP servers this agent has opted out of. ' +
189
222
  'Pass an empty array to re-enable every template server. Template servers ' +
190
223
  'not in the list stay enabled; those in the list are disabled for this agent.',
@@ -205,6 +238,9 @@ export const AGENT_BUILDER_TOOLS = [
205
238
  // ─── Group 2: Project Assignment ─────────────────────────────────────────
206
239
  {
207
240
  name: 'list_agent_projects',
241
+ permission: 'mcp:agents:read',
242
+ capability: 'agent_builder',
243
+ stdioOnly: true,
208
244
  description: 'List projects assigned to an agent.',
209
245
  inputSchema: {
210
246
  type: 'object',
@@ -217,6 +253,9 @@ export const AGENT_BUILDER_TOOLS = [
217
253
  },
218
254
  {
219
255
  name: 'assign_agent_to_project',
256
+ permission: 'mcp:agents:update',
257
+ capability: 'agent_builder',
258
+ stdioOnly: true,
220
259
  description: 'Assign an agent to a project by slug.',
221
260
  inputSchema: {
222
261
  type: 'object',
@@ -230,6 +269,9 @@ export const AGENT_BUILDER_TOOLS = [
230
269
  },
231
270
  {
232
271
  name: 'remove_agent_from_project',
272
+ permission: 'mcp:agents:update',
273
+ capability: 'agent_builder',
274
+ stdioOnly: true,
233
275
  description: 'Remove an agent from a project.',
234
276
  inputSchema: {
235
277
  type: 'object',
@@ -244,6 +286,9 @@ export const AGENT_BUILDER_TOOLS = [
244
286
  // ─── Group 3: Sessions ────────────────────────────────────────────────────
245
287
  {
246
288
  name: 'list_agent_sessions',
289
+ permission: 'mcp:agents:read',
290
+ capability: 'agent_builder',
291
+ stdioOnly: true,
247
292
  description: 'List sessions for an agent. Filter by status: running, stopped, failed.',
248
293
  inputSchema: {
249
294
  type: 'object',
@@ -263,6 +308,9 @@ export const AGENT_BUILDER_TOOLS = [
263
308
  },
264
309
  {
265
310
  name: 'get_agent_session',
311
+ permission: 'mcp:agents:read',
312
+ capability: 'agent_builder',
313
+ stdioOnly: true,
266
314
  description: 'Get session metadata and recent events (last 20). Does NOT include full conversation — use get_agent_session_conversation for that.',
267
315
  inputSchema: {
268
316
  type: 'object',
@@ -276,6 +324,9 @@ export const AGENT_BUILDER_TOOLS = [
276
324
  },
277
325
  {
278
326
  name: 'get_agent_session_conversation',
327
+ permission: 'mcp:agents:read',
328
+ capability: 'agent_builder',
329
+ stdioOnly: true,
279
330
  description: 'Get the full conversation history for a session (paginated). Can be large for long-running sessions.',
280
331
  inputSchema: {
281
332
  type: 'object',
@@ -291,6 +342,9 @@ export const AGENT_BUILDER_TOOLS = [
291
342
  },
292
343
  {
293
344
  name: 'run_agent',
345
+ permission: 'mcp:agents:create',
346
+ capability: 'agent_builder',
347
+ stdioOnly: true,
294
348
  description: 'Queue an ad-hoc session with an inline prompt. This is async — it returns a queue entry ID immediately. ' +
295
349
  'Use list_agent_queue or list_agent_sessions to track progress.',
296
350
  inputSchema: {
@@ -314,6 +368,9 @@ export const AGENT_BUILDER_TOOLS = [
314
368
  },
315
369
  {
316
370
  name: 'stop_agent_session',
371
+ permission: 'mcp:agents:update',
372
+ capability: 'agent_builder',
373
+ stdioOnly: true,
317
374
  description: 'Stop a running agent session.',
318
375
  inputSchema: {
319
376
  type: 'object',
@@ -327,6 +384,9 @@ export const AGENT_BUILDER_TOOLS = [
327
384
  },
328
385
  {
329
386
  name: 'resume_agent_session',
387
+ permission: 'mcp:agents:create',
388
+ capability: 'agent_builder',
389
+ stdioOnly: true,
330
390
  description: 'Resume a completed session with a follow-up prompt. The agent continues with full prior context.',
331
391
  inputSchema: {
332
392
  type: 'object',
@@ -342,6 +402,9 @@ export const AGENT_BUILDER_TOOLS = [
342
402
  // ─── Group 4: Tasks & Queue ───────────────────────────────────────────────
343
403
  {
344
404
  name: 'list_agent_tasks',
405
+ permission: 'mcp:agents:read',
406
+ capability: 'agent_builder',
407
+ stdioOnly: true,
345
408
  description: 'List defined tasks for an agent.',
346
409
  inputSchema: {
347
410
  type: 'object',
@@ -354,6 +417,9 @@ export const AGENT_BUILDER_TOOLS = [
354
417
  },
355
418
  {
356
419
  name: 'get_agent_task',
420
+ permission: 'mcp:agents:read',
421
+ capability: 'agent_builder',
422
+ stdioOnly: true,
357
423
  description: 'Get details of a specific agent task.',
358
424
  inputSchema: {
359
425
  type: 'object',
@@ -367,6 +433,9 @@ export const AGENT_BUILDER_TOOLS = [
367
433
  },
368
434
  {
369
435
  name: 'create_agent_task',
436
+ permission: 'mcp:agents:create',
437
+ capability: 'agent_builder',
438
+ stdioOnly: true,
370
439
  description: 'Create a new task for an agent.',
371
440
  inputSchema: {
372
441
  type: 'object',
@@ -404,6 +473,9 @@ export const AGENT_BUILDER_TOOLS = [
404
473
  },
405
474
  {
406
475
  name: 'update_agent_task',
476
+ permission: 'mcp:agents:update',
477
+ capability: 'agent_builder',
478
+ stdioOnly: true,
407
479
  description: "Update a task's prompt, sort order, model pin, or disabled tools/MCP servers.",
408
480
  inputSchema: {
409
481
  type: 'object',
@@ -440,6 +512,9 @@ export const AGENT_BUILDER_TOOLS = [
440
512
  },
441
513
  {
442
514
  name: 'run_agent_task',
515
+ permission: 'mcp:agents:create',
516
+ capability: 'agent_builder',
517
+ stdioOnly: true,
443
518
  description: 'Queue a saved task for immediate execution. Async — returns a queue entry ID. ' +
444
519
  'Use list_agent_queue to track progress.',
445
520
  inputSchema: {
@@ -469,6 +544,9 @@ export const AGENT_BUILDER_TOOLS = [
469
544
  },
470
545
  {
471
546
  name: 'list_agent_queue',
547
+ permission: 'mcp:agents:read',
548
+ capability: 'agent_builder',
549
+ stdioOnly: true,
472
550
  description: 'List queue entries for an agent. Use this to track progress after run_agent or run_agent_task.',
473
551
  inputSchema: {
474
552
  type: 'object',
@@ -486,6 +564,9 @@ export const AGENT_BUILDER_TOOLS = [
486
564
  },
487
565
  {
488
566
  name: 'cancel_queue_entry',
567
+ permission: 'mcp:agents:update',
568
+ capability: 'agent_builder',
569
+ stdioOnly: true,
489
570
  description: 'Cancel a queued (not yet running) queue entry.',
490
571
  inputSchema: {
491
572
  type: 'object',
@@ -500,6 +581,9 @@ export const AGENT_BUILDER_TOOLS = [
500
581
  // ─── Group 4b: Schedules ─────────────────────────────────────────────────
501
582
  {
502
583
  name: 'list_agent_schedules',
584
+ permission: 'mcp:agents:read',
585
+ capability: 'agent_builder',
586
+ stdioOnly: true,
503
587
  description: 'List all schedules for an agent. Each schedule defines when and how often the agent wakes up, ' +
504
588
  'and which tasks to run.',
505
589
  inputSchema: {
@@ -513,6 +597,9 @@ export const AGENT_BUILDER_TOOLS = [
513
597
  },
514
598
  {
515
599
  name: 'create_agent_schedule',
600
+ permission: 'mcp:agents:create',
601
+ capability: 'agent_builder',
602
+ stdioOnly: true,
516
603
  description: 'Create a new schedule for an agent. A schedule defines frequency, working hours, timezone, ' +
517
604
  'and which tasks to run. Schedules are created disabled by default.',
518
605
  inputSchema: {
@@ -580,6 +667,9 @@ export const AGENT_BUILDER_TOOLS = [
580
667
  },
581
668
  {
582
669
  name: 'update_agent_schedule',
670
+ permission: 'mcp:agents:update',
671
+ capability: 'agent_builder',
672
+ stdioOnly: true,
583
673
  description: "Update a schedule's configuration or task assignments.",
584
674
  inputSchema: {
585
675
  type: 'object',
@@ -641,6 +731,9 @@ export const AGENT_BUILDER_TOOLS = [
641
731
  // ─── Group 5: Skills (authoring) ─────────────────────────────────────────
642
732
  {
643
733
  name: 'create_skill',
734
+ permission: 'mcp:skills:create',
735
+ capability: 'agent_builder',
736
+ stdioOnly: true,
644
737
  description: 'Create a skill. By default creates an org-level skill. ' +
645
738
  'Pass agent_id to create an agent-private skill scoped to that agent only.',
646
739
  inputSchema: {
@@ -669,6 +762,9 @@ export const AGENT_BUILDER_TOOLS = [
669
762
  },
670
763
  {
671
764
  name: 'update_skill',
765
+ permission: 'mcp:skills:update',
766
+ capability: 'agent_builder',
767
+ stdioOnly: true,
672
768
  description: "Update a skill's instructions or description.",
673
769
  inputSchema: {
674
770
  type: 'object',
@@ -684,6 +780,9 @@ export const AGENT_BUILDER_TOOLS = [
684
780
  },
685
781
  {
686
782
  name: 'create_skill_resource',
783
+ permission: 'mcp:skills:update',
784
+ capability: 'agent_builder',
785
+ stdioOnly: true,
687
786
  description: 'Add a text resource to a skill (script, reference, or asset).',
688
787
  inputSchema: {
689
788
  type: 'object',
@@ -704,6 +803,9 @@ export const AGENT_BUILDER_TOOLS = [
704
803
  },
705
804
  {
706
805
  name: 'update_skill_resource',
806
+ permission: 'mcp:skills:update',
807
+ capability: 'agent_builder',
808
+ stdioOnly: true,
707
809
  description: "Update a skill resource's content.",
708
810
  inputSchema: {
709
811
  type: 'object',
@@ -720,6 +822,9 @@ export const AGENT_BUILDER_TOOLS = [
720
822
  },
721
823
  {
722
824
  name: 'set_agent_skills',
825
+ permission: 'mcp:agents:update',
826
+ capability: 'agent_builder',
827
+ stdioOnly: true,
723
828
  description: 'Replace the full set of skills assigned to an agent. ' +
724
829
  'Use list_org_skills to find skill IDs, then pass all desired skill IDs here.',
725
830
  inputSchema: {
@@ -739,6 +844,9 @@ export const AGENT_BUILDER_TOOLS = [
739
844
  // ─── Group 5b: Skills (reading) ─────────────────────────────────────────
740
845
  {
741
846
  name: 'list_agent_skills',
847
+ permission: 'mcp:skills:read',
848
+ capability: 'agent_builder',
849
+ stdioOnly: true,
742
850
  description: 'List all skills for a specific agent: agent-private skills + linked org skills + built-in skills. ' +
743
851
  'Returns full skill objects with IDs. Agent-scoped skills have agentProfileId set. ' +
744
852
  'Use this to see what skills an agent has and get IDs for update_skill.',
@@ -752,6 +860,9 @@ export const AGENT_BUILDER_TOOLS = [
752
860
  },
753
861
  {
754
862
  name: 'get_skill_detail',
863
+ permission: 'mcp:skills:read',
864
+ capability: 'agent_builder',
865
+ stdioOnly: true,
755
866
  description: 'Get the full content of a skill by name. ' +
756
867
  'Returns instructions, metadata, and a list of attached resources. ' +
757
868
  'With agent_id: resolves agent-private → org → system (use after list_agent_skills). ' +
@@ -774,6 +885,9 @@ export const AGENT_BUILDER_TOOLS = [
774
885
  },
775
886
  {
776
887
  name: 'list_org_skills',
888
+ permission: 'mcp:skills:read',
889
+ capability: 'agent_builder',
890
+ stdioOnly: true,
777
891
  description: 'List all skills in the org skill library (org-level + system-level). ' +
778
892
  'Returns ID, name, description, and whether the skill is built-in. ' +
779
893
  'Use skill IDs with set_agent_skills to assign skills to an agent.',
@@ -782,6 +896,9 @@ export const AGENT_BUILDER_TOOLS = [
782
896
  // ─── Group 6: Secrets ─────────────────────────────────────────────────────
783
897
  {
784
898
  name: 'list_agent_secrets',
899
+ permission: 'mcp:agents:read',
900
+ capability: 'agent_builder',
901
+ stdioOnly: true,
785
902
  description: 'List secret names for an agent, with the note recording where each value came from. ' +
786
903
  'Values are never returned.',
787
904
  inputSchema: {
@@ -795,6 +912,9 @@ export const AGENT_BUILDER_TOOLS = [
795
912
  },
796
913
  {
797
914
  name: 'set_agent_secret',
915
+ permission: 'mcp:agents:create',
916
+ capability: 'agent_builder',
917
+ stdioOnly: true,
798
918
  description: 'Create or update a secret for an agent. ' +
799
919
  'Name must be uppercase letters, digits, and underscores (e.g. MY_API_KEY). ' +
800
920
  'Omit value to change only shared_with_aliases or comment on an existing secret.',
@@ -822,6 +942,9 @@ export const AGENT_BUILDER_TOOLS = [
822
942
  // ─── Group 7: Discovery ───────────────────────────────────────────────────
823
943
  {
824
944
  name: 'list_container_images',
945
+ permission: 'mcp:agents:read',
946
+ capability: 'agent_builder',
947
+ stdioOnly: true,
825
948
  description: 'List agent-capable container images assigned to the organisation. Use image IDs when calling create_agent. ' +
826
949
  'With personal=true, lists the images a personal agent may run instead — the bring-your-own-token ' +
827
950
  'images opened to personal use, which are not an organisation catalogue.',
@@ -838,6 +961,9 @@ export const AGENT_BUILDER_TOOLS = [
838
961
  },
839
962
  {
840
963
  name: 'list_llm_models',
964
+ permission: 'mcp:agents:read',
965
+ capability: 'agent_builder',
966
+ stdioOnly: true,
841
967
  description: 'List LLM providers and models available to the organisation. ' +
842
968
  'Use provider, model and family IDs when calling create_agent, update_agent, ' +
843
969
  'create_agent_task or update_agent_task.',
@@ -4,8 +4,11 @@
4
4
  * Registered only when the API key has the `agent_observer` capability, and
5
5
  * stdio only — like the agent-self, agent-memory and agent-builder families,
6
6
  * these are tools an agent uses about the fleet it runs in, not tools an
7
- * integration calls over the remote transport, so they have no catalogue entry
8
- * and no generated remote copy to drift from.
7
+ * integration calls over the remote transport, so they are `stdioOnly` and have
8
+ * no generated remote copy to drift from. They are still catalogue entries
9
+ * (#2493): declaring the scope is what lets the stdio tool list tell the two
10
+ * observation tiers apart, and a tier a key cannot call is a tier it is not told
11
+ * it has.
9
12
  *
10
13
  * The backend decides what comes back: a session is visible to an observer only
11
14
  * where the observer is a member of a project the run was scoped to. Nothing in
@@ -22,6 +25,9 @@ const PROJECT_BOUND = 'You see a session only if it ran in a project you are a m
22
25
  export const AGENT_OBSERVE_TOOLS = [
23
26
  {
24
27
  name: 'list_observed_sessions',
28
+ permission: 'mcp:agents:observe',
29
+ capability: 'agent_observer',
30
+ stdioOnly: true,
25
31
  description: "List other agents' sessions across the projects you are a member of, newest first. " +
26
32
  'Returns status, trigger, timings, error text, token use and the projects each run was ' +
27
33
  'scoped to — no transcripts, no container logs, no system prompts. ' +
@@ -50,6 +56,9 @@ export const AGENT_OBSERVE_TOOLS = [
50
56
  },
51
57
  {
52
58
  name: 'get_observed_session',
59
+ permission: 'mcp:agents:observe',
60
+ capability: 'agent_observer',
61
+ stdioOnly: true,
53
62
  description: 'Read one observed session by id. Same fields as the listing. ' +
54
63
  'Not found if the session ran in no project of yours.',
55
64
  inputSchema: {
@@ -62,6 +71,9 @@ export const AGENT_OBSERVE_TOOLS = [
62
71
  },
63
72
  {
64
73
  name: 'get_observed_session_events',
74
+ permission: 'mcp:agents:observe',
75
+ capability: 'agent_observer',
76
+ stdioOnly: true,
65
77
  description: "Read an observed session's activity log: which tools ran, in order, with the one-line " +
66
78
  'summary each hook wrote. This is how a run looks from outside — the same tool repeating, ' +
67
79
  'or nothing at all for a long time, is what "stuck" looks like. ' +
@@ -78,6 +90,9 @@ export const AGENT_OBSERVE_TOOLS = [
78
90
  },
79
91
  {
80
92
  name: 'get_observed_session_conversation',
93
+ permission: 'mcp:agents:observe_transcripts',
94
+ capability: 'agent_observer',
95
+ stdioOnly: true,
81
96
  description: "Read an observed session's conversation. Requires the separate " +
82
97
  'mcp:agents:observe_transcripts permission, which most observers do not have and do not ' +
83
98
  'need: a transcript carries repository contents, shell output and the agent’s system ' +
@@ -3,9 +3,22 @@ function text(data) {
3
3
  return JSON.stringify(data, null, 2);
4
4
  }
5
5
  // ─── Tool definitions ──────────────────────────────────────────────────────────
6
+ /**
7
+ * An agent reading and recording its own context, stdio only.
8
+ *
9
+ * `permission: null` throughout, and it is a decision rather than a gap (#2493):
10
+ * `McpSelfController` calls no `checkMcpPermission` at all, because the gate is
11
+ * being an agent — every route resolves an `AgentProfile` from the caller and
12
+ * refuses anyone else — and no `MCP_PERMISSIONS` member names that. The
13
+ * capability is what withholds these, and for memory it is withheld from an agent
14
+ * whose memory is off (#1776).
15
+ */
6
16
  export const AGENT_SELF_TOOLS = [
7
17
  {
8
18
  name: 'get_self',
19
+ permission: null,
20
+ capability: 'agent_self',
21
+ stdioOnly: true,
9
22
  description: 'Get your own agent profile with full composed context: system prompt, instructions, skills, LLM model, and assigned projects. ' +
10
23
  'Use this to understand how you are configured and what capabilities you have. ' +
11
24
  'Essential for self-reflection and improvement proposals.',
@@ -13,6 +26,9 @@ export const AGENT_SELF_TOOLS = [
13
26
  },
14
27
  {
15
28
  name: 'get_own_sessions',
29
+ permission: null,
30
+ capability: 'agent_self',
31
+ stdioOnly: true,
16
32
  description: 'List your own recent sessions. Returns session metadata including status, trigger type, and timestamps. ' +
17
33
  'Use this to review your recent activity for retrospective analysis.',
18
34
  inputSchema: {
@@ -31,6 +47,9 @@ export const AGENT_SELF_TOOLS = [
31
47
  export const AGENT_MEMORY_TOOLS = [
32
48
  {
33
49
  name: 'list_memories',
50
+ permission: null,
51
+ capability: 'agent_memory',
52
+ stdioOnly: true,
34
53
  description: 'List the persistent memory entries this session can see: your global ones, plus those scoped to the projects this session runs in. ' +
35
54
  'Memory persists across sessions and is scoped to you. ' +
36
55
  'Use this to recall observations, patterns, and notes from previous work.',
@@ -38,6 +57,9 @@ export const AGENT_MEMORY_TOOLS = [
38
57
  },
39
58
  {
40
59
  name: 'get_memory',
60
+ permission: null,
61
+ capability: 'agent_memory',
62
+ stdioOnly: true,
41
63
  description: 'Get a specific memory entry by key. Returns the content and metadata for a single memory entry.',
42
64
  inputSchema: {
43
65
  type: 'object',
@@ -49,6 +71,9 @@ export const AGENT_MEMORY_TOOLS = [
49
71
  },
50
72
  {
51
73
  name: 'save_memory',
74
+ permission: null,
75
+ capability: 'agent_memory',
76
+ stdioOnly: true,
52
77
  description: 'Save a persistent memory entry by key. Creates the entry if it does not exist, or updates it if it does. ' +
53
78
  'Use this to persist observations, learnings, and notes across sessions. ' +
54
79
  'To clear a memory entry, save it with empty content. ' +
@@ -14,6 +14,9 @@ function text(data) {
14
14
  export const BROWSER_TOOLS = [
15
15
  {
16
16
  name: 'browser_request_control',
17
+ permission: 'mcp:browser:control',
18
+ capability: 'browser_control',
19
+ stdioOnly: true,
17
20
  description: 'Ask the person you are chatting with for permission to drive their browser. ' +
18
21
  'They see your reason in the conversation with Allow and Deny buttons, and this call waits for their answer. ' +
19
22
  'You MUST call this and be granted control before any other browser tool will work — there is no way around it, ' +
@@ -32,6 +35,9 @@ export const BROWSER_TOOLS = [
32
35
  },
33
36
  {
34
37
  name: 'browser_snapshot',
38
+ permission: 'mcp:browser:control',
39
+ capability: 'browser_control',
40
+ stdioOnly: true,
35
41
  description: 'See the page: its URL, title, visible text, and the interactive elements you can act on. ' +
36
42
  'Each element comes back with a "ref" — pass that ref to browser_click or browser_type. ' +
37
43
  'Refs are only valid until the page changes, so take a fresh snapshot after every action.',
@@ -39,6 +45,9 @@ export const BROWSER_TOOLS = [
39
45
  },
40
46
  {
41
47
  name: 'browser_click',
48
+ permission: 'mcp:browser:control',
49
+ capability: 'browser_control',
50
+ stdioOnly: true,
42
51
  description: 'Click an element on the page. The visitor sees the pointer travel to it before it is clicked.',
43
52
  inputSchema: {
44
53
  type: 'object',
@@ -50,6 +59,9 @@ export const BROWSER_TOOLS = [
50
59
  },
51
60
  {
52
61
  name: 'browser_type',
62
+ permission: 'mcp:browser:control',
63
+ capability: 'browser_control',
64
+ stdioOnly: true,
53
65
  description: 'Type text into an input, textarea or select on the page, replacing whatever it holds.',
54
66
  inputSchema: {
55
67
  type: 'object',
@@ -62,6 +74,9 @@ export const BROWSER_TOOLS = [
62
74
  },
63
75
  {
64
76
  name: 'browser_scroll',
77
+ permission: 'mcp:browser:control',
78
+ capability: 'browser_control',
79
+ stdioOnly: true,
65
80
  description: 'Scroll the page vertically to bring more of it into view.',
66
81
  inputSchema: {
67
82
  type: 'object',
@@ -76,6 +91,9 @@ export const BROWSER_TOOLS = [
76
91
  },
77
92
  {
78
93
  name: 'browser_release_control',
94
+ permission: 'mcp:browser:control',
95
+ capability: 'browser_control',
96
+ stdioOnly: true,
79
97
  description: 'Hand the browser back when you are done, so the visitor stops seeing the "agent in control" frame on their screen. ' +
80
98
  'Always do this once the task is finished. They can also stop you themselves at any time.',
81
99
  inputSchema: { type: 'object', properties: {} },
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import { z } from 'zod';
6
6
  import { createRequire } from 'module';
7
7
  import { readFile, stat } from 'fs/promises';
@@ -10,12 +10,12 @@ import { WolfpackClient } from './client.js';
10
10
  import { productName } from './brand.js';
11
11
  import { allTasksChecked, getWorkItemReminders } from './workItemReminders.js';
12
12
  import { validateConfig, config } from './config.js';
13
- import { AGENT_BUILDER_TOOLS, handleAgentBuilderTool } from './agentBuilderTools.js';
13
+ import { handleAgentBuilderTool } from './agentBuilderTools.js';
14
14
  import { handleProcedureTool } from './procedureTools.js';
15
15
  import { stdioTools, toolNamesFor, withProductName } from './toolCatalogue.js';
16
- import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS, handleAgentSelfTool } from './agentSelfTools.js';
17
- import { AGENT_OBSERVE_TOOLS, handleAgentObserveTool } from './agentObserveTools.js';
18
- import { BROWSER_TOOLS, handleBrowserTool } from './browserTools.js';
16
+ import { handleAgentSelfTool } from './agentSelfTools.js';
17
+ import { handleAgentObserveTool } from './agentObserveTools.js';
18
+ import { handleBrowserTool } from './browserTools.js';
19
19
  import { resolveRadarItemId } from './resolveRadarItemId.js';
20
20
  import { SERVER_INSTRUCTIONS } from './serverInstructions.js';
21
21
  import { fetch as proxyFetch } from './proxyFetch.js';
@@ -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)'),
@@ -833,16 +864,11 @@ class WolfpackMCPServer {
833
864
  await this.fetchCapabilities();
834
865
  }
835
866
  return {
836
- // The catalogue carries a token where the product's name belongs (#2466); it is
837
- // filled in here, as the tools are advertised.
838
- tools: withProductName([
839
- ...stdioTools(this.capabilities, this.permissions),
840
- ...(this.capabilities.includes('agent_self') ? AGENT_SELF_TOOLS : []),
841
- ...(this.capabilities.includes('agent_memory') ? AGENT_MEMORY_TOOLS : []),
842
- ...(this.capabilities.includes('agent_observer') ? AGENT_OBSERVE_TOOLS : []),
843
- ...(this.capabilities.includes('agent_builder') ? AGENT_BUILDER_TOOLS : []),
844
- ...(this.capabilities.includes('browser_control') ? BROWSER_TOOLS : []),
845
- ], productName),
867
+ // Every family is a catalogue entry, so one call covers them all — gated on
868
+ // the capability and on the key's own scopes (#2493). The catalogue carries a
869
+ // token where the product's name belongs (#2466); it is filled in here, as the
870
+ // tools are advertised.
871
+ tools: withProductName(stdioTools(this.capabilities, this.permissions), productName),
846
872
  };
847
873
  });
848
874
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -1512,6 +1538,71 @@ class WolfpackMCPServer {
1512
1538
  ],
1513
1539
  };
1514
1540
  }
1541
+ // Idea handlers (the Idea Factory)
1542
+ case 'list_ideas': {
1543
+ const parsed = ListIdeasSchema.parse(args);
1544
+ const result = await this.client.listIdeas({
1545
+ teamSlug: parsed.project_slug || this.client.getProjectSlug() || undefined,
1546
+ sort: parsed.sort,
1547
+ limit: parsed.limit,
1548
+ offset: parsed.offset,
1549
+ });
1550
+ const spent = result.boostSpentToday
1551
+ ? 'Your boost for today in this project is already spent.'
1552
+ : 'Your boost for today in this project is unspent.';
1553
+ let text = `${spent}\n\n${JSON.stringify(stripUuids(result.items), null, 2)}`;
1554
+ if (result.total > result.items.length) {
1555
+ text = `Note: Showing ${result.items.length} of ${result.total} ideas. Use limit/offset for pagination.\n\n${text}`;
1556
+ }
1557
+ return { content: [{ type: 'text', text }] };
1558
+ }
1559
+ case 'get_idea': {
1560
+ const parsed = GetIdeaSchema.parse(args);
1561
+ const idea = await this.client.getIdea(parsed.idea_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
1562
+ return {
1563
+ content: [{ type: 'text', text: JSON.stringify(stripUuids(idea), null, 2) }],
1564
+ };
1565
+ }
1566
+ case 'create_idea': {
1567
+ const parsed = CreateIdeaSchema.parse(args);
1568
+ const idea = await this.client.createIdea({
1569
+ title: parsed.title,
1570
+ content: parsed.content,
1571
+ teamSlug: parsed.project_slug || this.client.getProjectSlug() || undefined,
1572
+ });
1573
+ return {
1574
+ content: [
1575
+ {
1576
+ type: 'text',
1577
+ text: `Created idea #${idea.refId}: ${idea.title}\n\n${JSON.stringify(stripUuids(idea), null, 2)}`,
1578
+ },
1579
+ ],
1580
+ };
1581
+ }
1582
+ case 'update_idea': {
1583
+ const parsed = UpdateIdeaSchema.parse(args);
1584
+ 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);
1585
+ return {
1586
+ content: [
1587
+ {
1588
+ type: 'text',
1589
+ text: `Updated idea #${idea.refId}: ${idea.title} (${idea.stage})\n\n${JSON.stringify(stripUuids(idea), null, 2)}`,
1590
+ },
1591
+ ],
1592
+ };
1593
+ }
1594
+ case 'boost_idea': {
1595
+ const parsed = BoostIdeaSchema.parse(args);
1596
+ const idea = await this.client.boostIdea(parsed.idea_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
1597
+ return {
1598
+ content: [
1599
+ {
1600
+ type: 'text',
1601
+ text: `Boosted idea #${idea.refId}: ${idea.title} now has ${idea.boostCount} boost(s). That was your boost for today in this project.`,
1602
+ },
1603
+ ],
1604
+ };
1605
+ }
1515
1606
  // Comment handlers
1516
1607
  case 'list_work_item_comments': {
1517
1608
  const parsed = ListWorkItemCommentsSchema.parse(args);
@@ -1941,14 +2032,14 @@ class WolfpackMCPServer {
1941
2032
  }
1942
2033
  // Check agent self tools
1943
2034
  if (this.capabilities.includes('agent_self')) {
1944
- const selfToolNames = AGENT_SELF_TOOLS.map((t) => t.name);
2035
+ const selfToolNames = toolNamesFor('agent_self');
1945
2036
  if (selfToolNames.includes(name)) {
1946
2037
  return handleAgentSelfTool(name, args, this.client);
1947
2038
  }
1948
2039
  }
1949
2040
  // Check memory tools (separately gated — #1776)
1950
2041
  if (this.capabilities.includes('agent_memory')) {
1951
- const memoryToolNames = AGENT_MEMORY_TOOLS.map((t) => t.name);
2042
+ const memoryToolNames = toolNamesFor('agent_memory');
1952
2043
  if (memoryToolNames.includes(name)) {
1953
2044
  return handleAgentSelfTool(name, args, this.client);
1954
2045
  }
@@ -1956,14 +2047,14 @@ class WolfpackMCPServer {
1956
2047
  // Check observation tools (#2388 — other agents' runs, in the
1957
2048
  // observer's own projects)
1958
2049
  if (this.capabilities.includes('agent_observer')) {
1959
- const observeToolNames = AGENT_OBSERVE_TOOLS.map((t) => t.name);
2050
+ const observeToolNames = toolNamesFor('agent_observer');
1960
2051
  if (observeToolNames.includes(name)) {
1961
2052
  return handleAgentObserveTool(name, args, this.client);
1962
2053
  }
1963
2054
  }
1964
2055
  // Check browser control tools (#2288 — chat sessions only)
1965
2056
  if (this.capabilities.includes('browser_control')) {
1966
- const browserToolNames = BROWSER_TOOLS.map((t) => t.name);
2057
+ const browserToolNames = toolNamesFor('browser_control');
1967
2058
  if (browserToolNames.includes(name)) {
1968
2059
  return handleBrowserTool(name, args, this.client);
1969
2060
  }
@@ -67,6 +67,30 @@ describe('the tool list served over stdio', () => {
67
67
  expect(names).not.toContain('update_work_item');
68
68
  expect(names).not.toContain('create_work_item_comment');
69
69
  }, 60_000);
70
+ it('offers an observer key the tiers of observation its scopes reach', async () => {
71
+ // #2493 — the observe family was appended after `stdioTools` and filtered on
72
+ // the capability alone, so a coach holding `observe` was advertised the
73
+ // transcript tool and refused on every call. The two scopes are one
74
+ // capability by design (#2388), which is exactly why the tool list has to
75
+ // tell them apart.
76
+ const names = await toolNamesFrom(await stubBackend({
77
+ capabilities: ['agent_observer'],
78
+ permissions: ['mcp:agents:observe'],
79
+ }));
80
+ expect(names).toContain('list_observed_sessions');
81
+ expect(names).toContain('get_observed_session_events');
82
+ expect(names).not.toContain('get_observed_session_conversation');
83
+ }, 60_000);
84
+ it('offers a read-only builder key its reads and none of its writes', async () => {
85
+ // #2493, the same gap on the largest family: `agent_builder` is granted on
86
+ // `mcp:agents:read` alone, so every write in it was advertised to a key that
87
+ // cannot call one.
88
+ const names = await toolNamesFrom(await stubBackend({ capabilities: ['agent_builder'], permissions: ['mcp:agents:read'] }));
89
+ expect(names).toContain('list_agents');
90
+ expect(names).toContain('list_organisations');
91
+ expect(names).not.toContain('create_agent');
92
+ expect(names).not.toContain('update_agent');
93
+ }, 60_000);
70
94
  it('is not stripped when the capabilities round trip fails', async () => {
71
95
  // #1488 the other way up: a gateway outage means the key's scopes are
72
96
  // unknown, not that the key holds none. Stripping the list on a 503 would
@@ -16,11 +16,17 @@
16
16
  * `rootDir: ./src`. A generated, never-hand-edited copy is what one source of
17
17
  * truth looks like across that boundary.
18
18
  *
19
- * The agent-builder, agent-self, agent-memory and browser-control families are
20
- * deliberately not here: they exist on the stdio transport only (see
21
- * `agentBuilderTools.ts`, `agentSelfTools.ts`, `browserTools.ts`), so they have
22
- * no second declaration to drift from.
19
+ * The agent-self, agent-memory, agent-observer, agent-builder and browser-control
20
+ * families stay in the files that hold their handlers, and are appended to the
21
+ * catalogue from there. They exist on the stdio transport only, which `stdioOnly`
22
+ * records — but being stdio-only is no reason to declare no scope, and while they
23
+ * sat outside the catalogue they declared none, so neither the scope filter
24
+ * (#2428) nor the scope-is-required rule (#2345) reached them (#2493).
23
25
  */
26
+ import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS } from './agentSelfTools.js';
27
+ import { AGENT_OBSERVE_TOOLS } from './agentObserveTools.js';
28
+ import { AGENT_BUILDER_TOOLS } from './agentBuilderTools.js';
29
+ import { BROWSER_TOOLS } from './browserTools.js';
24
30
  /**
25
31
  * The token a description carries where the product's name belongs (#2466).
26
32
  *
@@ -74,7 +80,8 @@ const DEFINITION_DESCRIPTION = 'Workflow definition: { nodes: [{id, type, positi
74
80
  'setVariable, createPage, createJournalEntry, llmPrompt, jsonata, agentTask, runContainer), data.label, and ' +
75
81
  "data.config holding that type's settings — the step body belongs in data.config.description, not data.description. " +
76
82
  'Use activityType "task" for a step a person just reads and ticks off.';
77
- export const TOOL_CATALOGUE = [
83
+ /** The tools both transports serve. */
84
+ const SHARED_TOOLS = [
78
85
  // Project tools
79
86
  {
80
87
  name: 'list_projects',
@@ -1146,6 +1153,124 @@ export const TOOL_CATALOGUE = [
1146
1153
  required: ['entry_id'],
1147
1154
  },
1148
1155
  },
1156
+ // Idea Factory tools (#2473). Gated on the `ideas` capability, which the backend
1157
+ // grants a key holding mcp:ideas:read once the `idea-factory` flag is on for it.
1158
+ // No archive tool: archiving is the Idea Factory's delete, and MCP does not
1159
+ // surface deletes (packages/mcp/CLAUDE.md).
1160
+ {
1161
+ name: 'list_ideas',
1162
+ permission: 'mcp:ideas:read',
1163
+ capability: 'ideas',
1164
+ description: 'List the live ideas in a project (the Idea Factory). Use when user asks about "ideas", ' +
1165
+ '"the idea factory", "what has been suggested", or "what are we thinking about". ' +
1166
+ 'Each idea carries a stage (spark, exploring, building, shipped, parked) and a boostCount — ' +
1167
+ 'the number of members who have voted for it. The response also says whether your own boost ' +
1168
+ 'for today in this project has already been spent.',
1169
+ inputSchema: {
1170
+ type: 'object',
1171
+ properties: {
1172
+ sort: {
1173
+ type: 'string',
1174
+ description: 'Ordering: "popular" (default, most boosted first), "updated", or "newest"',
1175
+ },
1176
+ limit: { type: 'number', description: 'Maximum number of ideas to return' },
1177
+ offset: { type: 'number', description: 'Number of ideas to skip' },
1178
+ project_slug: {
1179
+ type: 'string',
1180
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1181
+ },
1182
+ },
1183
+ },
1184
+ },
1185
+ {
1186
+ name: 'get_idea',
1187
+ permission: 'mcp:ideas:read',
1188
+ capability: 'ideas',
1189
+ description: 'Get a single idea by its refId, with its full content, stage, tags, boost count and the ' +
1190
+ 'refIds of any work items being built from it.',
1191
+ inputSchema: {
1192
+ type: 'object',
1193
+ properties: {
1194
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1195
+ project_slug: {
1196
+ type: 'string',
1197
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1198
+ },
1199
+ },
1200
+ required: ['idea_id'],
1201
+ },
1202
+ },
1203
+ {
1204
+ name: 'create_idea',
1205
+ permission: 'mcp:ideas:create',
1206
+ capability: 'ideas',
1207
+ description: 'Raise a new idea in the Idea Factory. Use when user wants to "suggest", "raise an idea", ' +
1208
+ '"add to the idea factory", or "capture a thought". A new idea starts at the "spark" stage ' +
1209
+ 'with no boosts. Requires mcp:ideas:create permission. ' +
1210
+ CONTENT_LINKING_HELP,
1211
+ inputSchema: {
1212
+ type: 'object',
1213
+ properties: {
1214
+ title: { type: 'string', description: 'Idea title' },
1215
+ content: { type: 'string', description: 'Idea content (markdown)' },
1216
+ // No tag_ids: the service takes tag UUIDs and the MCP surface strips UUIDs
1217
+ // from every response, so an agent never holds one to send back. Tag an
1218
+ // idea in the UI.
1219
+ project_slug: {
1220
+ type: 'string',
1221
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1222
+ },
1223
+ },
1224
+ required: ['title', 'content'],
1225
+ },
1226
+ },
1227
+ {
1228
+ name: 'update_idea',
1229
+ permission: 'mcp:ideas:update',
1230
+ capability: 'ideas',
1231
+ description: 'Update an idea, or move it to another stage. The stages are "spark" (just raised), ' +
1232
+ '"exploring", "building", "shipped" and "parked" — moving an idea along is how the project ' +
1233
+ 'sees what became of it. Requires mcp:ideas:update permission. ' +
1234
+ CONTENT_LINKING_HELP,
1235
+ inputSchema: {
1236
+ type: 'object',
1237
+ properties: {
1238
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1239
+ title: { type: 'string', description: 'Updated title' },
1240
+ content: { type: 'string', description: 'Updated content (markdown)' },
1241
+ stage: {
1242
+ type: 'string',
1243
+ description: 'New stage: spark, exploring, building, shipped or parked',
1244
+ },
1245
+ project_slug: {
1246
+ type: 'string',
1247
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1248
+ },
1249
+ },
1250
+ required: ['idea_id'],
1251
+ },
1252
+ },
1253
+ {
1254
+ name: 'boost_idea',
1255
+ permission: 'mcp:ideas:boost',
1256
+ capability: 'ideas',
1257
+ description: 'Vote for an idea by spending your boost. Use when user wants to "vote for", "boost", ' +
1258
+ '"back" or "+1" an idea. You get ONE boost per project per day, whichever idea you spend ' +
1259
+ 'it on, so it is a choice between ideas rather than a click — spending it again the same ' +
1260
+ 'day is refused rather than counted. Requires mcp:ideas:boost permission, which is separate ' +
1261
+ 'from mcp:ideas:update: voting is participation, not editing.',
1262
+ inputSchema: {
1263
+ type: 'object',
1264
+ properties: {
1265
+ idea_id: { type: 'string', description: 'The idea refId (number)' },
1266
+ project_slug: {
1267
+ type: 'string',
1268
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1269
+ },
1270
+ },
1271
+ required: ['idea_id'],
1272
+ },
1273
+ },
1149
1274
  // Comment tools
1150
1275
  {
1151
1276
  name: 'list_work_item_comments',
@@ -2152,6 +2277,14 @@ export const TOOL_CATALOGUE = [
2152
2277
  },
2153
2278
  },
2154
2279
  ];
2280
+ export const TOOL_CATALOGUE = [
2281
+ ...SHARED_TOOLS,
2282
+ ...AGENT_SELF_TOOLS,
2283
+ ...AGENT_MEMORY_TOOLS,
2284
+ ...AGENT_OBSERVE_TOOLS,
2285
+ ...AGENT_BUILDER_TOOLS,
2286
+ ...BROWSER_TOOLS,
2287
+ ];
2155
2288
  const toTool = ({ name, description, inputSchema }) => ({
2156
2289
  name,
2157
2290
  description,
@@ -2181,13 +2314,20 @@ export function toolNamesFor(capability) {
2181
2314
  return TOOL_CATALOGUE.filter((t) => t.capability === capability).map((t) => t.name);
2182
2315
  }
2183
2316
  /**
2184
- * The catalogue as the remote transport advertises it: every tool, taking the
2185
- * `remote` override where one is set. This is what the generated backend copy holds.
2317
+ * The catalogue as the remote transport advertises it: every tool but the
2318
+ * stdio-only ones, taking the `remote` override where one is set. This is what
2319
+ * the generated backend copy holds.
2186
2320
  */
2187
2321
  export function remoteTools() {
2188
- return TOOL_CATALOGUE.map((t) => ({ ...toTool(t), ...t.remote }));
2322
+ return TOOL_CATALOGUE.filter((t) => !t.stdioOnly).map((t) => ({ ...toTool(t), ...t.remote }));
2189
2323
  }
2190
- /** The scope the remote transport requires per tool, derived from the catalogue. */
2191
- export function remoteToolPermissions() {
2324
+ /**
2325
+ * The scope each tool requires, derived from the catalogue. The stdio-only tools
2326
+ * are in here too, though the remote server can never dispatch one: the scope is a
2327
+ * fact about the tool rather than about a transport, and this map is what the
2328
+ * quarterly prompt-injection probe reads to tell an induced tool that is inside
2329
+ * the agent's grant from one that is outside it (#2493).
2330
+ */
2331
+ export function toolPermissions() {
2192
2332
  return Object.fromEntries(TOOL_CATALOGUE.map((t) => [t.name, t.permission]));
2193
2333
  }
@@ -2,8 +2,11 @@ import { describe, it, expect } from 'vitest';
2
2
  import { readFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import * as catalogue from './toolCatalogue.js';
5
- import { TOOL_CATALOGUE, remoteTools, stdioTools } from './toolCatalogue.js';
5
+ import { TOOL_CATALOGUE, remoteTools, toolPermissions, stdioTools, toolNamesFor, } from './toolCatalogue.js';
6
6
  import { BROWSER_TOOLS } from './browserTools.js';
7
+ import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS } from './agentSelfTools.js';
8
+ import { AGENT_OBSERVE_TOOLS } from './agentObserveTools.js';
9
+ import { AGENT_BUILDER_TOOLS } from './agentBuilderTools.js';
7
10
  // @ts-expect-error — a plain .mjs build script, deliberately not part of this program
8
11
  import { render, GENERATED_PATH } from '../../../scripts/generate-mcp-catalogue.mjs';
9
12
  const repoFile = (path) => readFileSync(join(__dirname, '../../..', path), 'utf8');
@@ -61,13 +64,16 @@ describe('the MCP tool catalogue', () => {
61
64
  .map((param) => `${tool.name}.${param}`));
62
65
  expect(unforwarded).toEqual([]);
63
66
  });
64
- it('offers the procedure tools over stdio only to a key with that capability', () => {
65
- const withProcedures = stdioTools(['procedures']).map((t) => t.name);
67
+ // Every capability in the catalogue, not just `procedures`: a family added with
68
+ // its `capability` left off is offered to every key, and nothing else notices.
69
+ 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) => {
70
+ const gated = TOOL_CATALOGUE.filter((t) => t.capability === capability).map((t) => t.name);
71
+ const withIt = stdioTools([capability]).map((t) => t.name);
66
72
  const without = stdioTools([]).map((t) => t.name);
67
- expect(withProcedures).toContain('start_case');
68
- expect(without).not.toContain('start_case');
73
+ expect(withIt).toEqual(expect.arrayContaining(gated));
74
+ gated.forEach((name) => expect(without).not.toContain(name));
69
75
  // Gating is the only difference: nothing else drops out.
70
- expect(withProcedures.length - without.length).toBe(TOOL_CATALOGUE.filter((t) => t.capability === 'procedures').length);
76
+ expect(withIt.length - without.length).toBe(gated.length);
71
77
  });
72
78
  it('offers a tool over stdio only to a key holding its scope', () => {
73
79
  // #2428 — the list told an agent it could do things the 403 then refused.
@@ -77,11 +83,42 @@ describe('the MCP tool catalogue', () => {
77
83
  expect(readOnly).not.toContain('create_issue');
78
84
  });
79
85
  it('keeps a tool that deliberately requires no scope of its own', () => {
80
- // `permission: null` is a decision, not a gap, so an empty key still gets these.
81
- const unscoped = TOOL_CATALOGUE.filter((t) => t.permission === null).map((t) => t.name);
86
+ // `permission: null` is a decision, not a gap, so an empty key still gets these
87
+ // the ones no capability gates, which is every family a plain key can reach.
88
+ const unscoped = TOOL_CATALOGUE.filter((t) => t.permission === null && !t.capability).map((t) => t.name);
82
89
  expect(unscoped.length).toBeGreaterThan(0);
83
90
  expect(stdioTools([], []).map((t) => t.name)).toEqual(unscoped);
84
91
  });
92
+ it('serves a stdio-only tool over stdio alone, and still declares its scope', () => {
93
+ // #2493 — the five families used to be appended after `stdioTools`, so they
94
+ // reached neither the scope filter nor the scope-is-required rule. They are
95
+ // catalogue entries now, which must not put them in front of a remote client:
96
+ // the remote server has no dispatch case for one, and advertising it would be
97
+ // a promise the product cannot keep.
98
+ const stdioOnly = TOOL_CATALOGUE.filter((t) => t.stdioOnly);
99
+ expect(stdioOnly.map((t) => t.name)).toContain('get_observed_session_conversation');
100
+ expect(remoteTools().map((t) => t.name)).not.toContain('get_observed_session_conversation');
101
+ // The scope still reaches the generated map, which is what the quarterly
102
+ // prompt-injection probe reads to judge whether an induced tool is in scope.
103
+ const permissions = toolPermissions();
104
+ for (const tool of stdioOnly) {
105
+ expect(permissions[tool.name], tool.name).toBe(tool.permission);
106
+ expect(tool.capability, tool.name).toBeDefined();
107
+ }
108
+ });
109
+ it.each([
110
+ ['agent_self', AGENT_SELF_TOOLS],
111
+ ['agent_memory', AGENT_MEMORY_TOOLS],
112
+ ['agent_observer', AGENT_OBSERVE_TOOLS],
113
+ ['agent_builder', AGENT_BUILDER_TOOLS],
114
+ ['browser_control', BROWSER_TOOLS],
115
+ ])('names the whole %s family for the stdio dispatch to route on', (capability, family) => {
116
+ // The stdio server routes a call to a family's handler by asking the catalogue
117
+ // for that capability's names, rather than by importing the array (#2493). A
118
+ // tool whose `capability` does not match the file it lives in would then be
119
+ // advertised and dispatched to the wrong handler — or to none.
120
+ expect(toolNamesFor(capability)).toEqual(family.map((t) => t.name));
121
+ });
85
122
  it('honours a wildcard the way the server does when it checks the call', () => {
86
123
  // Filtering has to read a scope exactly as `checkMcpPermission` does, or a
87
124
  // key on `mcp:*` loses tools it is entitled to call.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.104",
3
+ "version": "1.0.106",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "clean": "",
15
15
  "build": "tsc",
16
16
  "dev": "tsx watch src/index.ts",
17
+ "generate:catalogue": "tsx ../../scripts/generate-mcp-catalogue.mjs",
17
18
  "start": "node dist/index.js",
18
19
  "test": "vitest",
19
20
  "lint": "eslint . --max-warnings 0",