wolfpack-mcp 1.0.103 → 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 +43 -0
- package/dist/brand.js +13 -0
- package/dist/client.js +27 -0
- package/dist/config.js +3 -2
- package/dist/index.js +106 -6
- package/dist/serverInstructions.js +5 -4
- package/dist/toolCatalogue.js +146 -8
- package/dist/toolCatalogue.test.js +13 -7
- package/package.json +1 -1
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/brand.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the product calls itself, and the domain it is served from (#2466).
|
|
3
|
+
*
|
|
4
|
+
* This package is published and installed on its own, away from the repository, so —
|
|
5
|
+
* unlike the Vite apps — it cannot read `brand.json`. It takes the values from the
|
|
6
|
+
* environment, with the fallbacks below pinned to `brand.json` by
|
|
7
|
+
* `scripts/check-product-brand.js`.
|
|
8
|
+
*
|
|
9
|
+
* `WOLFPACK_*` variables are this package's public configuration contract with the MCP
|
|
10
|
+
* clients that already set them, so they are deliberately not renamed here.
|
|
11
|
+
*/
|
|
12
|
+
export const productName = process.env.PRODUCT_NAME || 'Wolfpack';
|
|
13
|
+
export const productDomain = process.env.PRODUCT_DOMAIN || 'wolfpacks.work';
|
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/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { productDomain, productName } from './brand.js';
|
|
1
2
|
export const config = {
|
|
2
|
-
apiUrl: process.env.WOLFPACK_API_URL ||
|
|
3
|
+
apiUrl: process.env.WOLFPACK_API_URL || `https://${productDomain}/api/mcp`,
|
|
3
4
|
apiKey: process.env.WOLFPACK_API_KEY,
|
|
4
5
|
projectSlug: process.env.WOLFPACK_PROJECT_SLUG || process.env.WOLFPACK_TEAM_SLUG,
|
|
5
6
|
orgSlug: process.env.WOLFPACK_ORG_SLUG,
|
|
@@ -8,7 +9,7 @@ export const config = {
|
|
|
8
9
|
export function validateConfig() {
|
|
9
10
|
if (!config.apiKey) {
|
|
10
11
|
console.error('Error: WOLFPACK_API_KEY environment variable is required');
|
|
11
|
-
console.error(
|
|
12
|
+
console.error(`Please set WOLFPACK_API_KEY to your API key from the ${productName} application`);
|
|
12
13
|
console.error('Example: WOLFPACK_API_KEY=wfp_sk_... (user) or wfp_ak_... (agent)');
|
|
13
14
|
process.exit(1);
|
|
14
15
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,12 @@ import { createRequire } from 'module';
|
|
|
7
7
|
import { readFile, stat } from 'fs/promises';
|
|
8
8
|
import { basename, extname, resolve } from 'path';
|
|
9
9
|
import { WolfpackClient } from './client.js';
|
|
10
|
+
import { productName } from './brand.js';
|
|
10
11
|
import { allTasksChecked, getWorkItemReminders } from './workItemReminders.js';
|
|
11
12
|
import { validateConfig, config } from './config.js';
|
|
12
13
|
import { AGENT_BUILDER_TOOLS, handleAgentBuilderTool } from './agentBuilderTools.js';
|
|
13
14
|
import { handleProcedureTool } from './procedureTools.js';
|
|
14
|
-
import { stdioTools, toolNamesFor } from './toolCatalogue.js';
|
|
15
|
+
import { stdioTools, toolNamesFor, withProductName } from './toolCatalogue.js';
|
|
15
16
|
import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS, handleAgentSelfTool } from './agentSelfTools.js';
|
|
16
17
|
import { AGENT_OBSERVE_TOOLS, handleAgentObserveTool } from './agentObserveTools.js';
|
|
17
18
|
import { BROWSER_TOOLS, handleBrowserTool } from './browserTools.js';
|
|
@@ -65,7 +66,7 @@ const ListWorkItemsSchema = z.object({
|
|
|
65
66
|
status: z.coerce
|
|
66
67
|
.string()
|
|
67
68
|
.optional()
|
|
68
|
-
.describe('Filter by status. Board columns: "new", "doing", "
|
|
69
|
+
.describe('Filter by status. Board columns: "new", "doing", "paused", "blocked", "review", "ready", "completed". Use "pending" or "backlog" for backlog items. Use "all" to include completed/closed. Default excludes completed/closed.'),
|
|
69
70
|
assigned_to_id: z.coerce
|
|
70
71
|
.string()
|
|
71
72
|
.optional()
|
|
@@ -121,6 +122,7 @@ const VALID_STATUSES = [
|
|
|
121
122
|
'pending',
|
|
122
123
|
'new',
|
|
123
124
|
'doing',
|
|
125
|
+
'paused',
|
|
124
126
|
'blocked',
|
|
125
127
|
'review',
|
|
126
128
|
'ready',
|
|
@@ -375,7 +377,7 @@ const CreateWorkItemSchema = z.object({
|
|
|
375
377
|
status: z
|
|
376
378
|
.enum(VALID_STATUSES)
|
|
377
379
|
.optional()
|
|
378
|
-
.describe('Initial status: "pending" (backlog), "new" (to do), "doing", "
|
|
380
|
+
.describe('Initial status: "pending" (backlog), "new" (to do), "doing", "paused", "blocked", "review", "ready", "completed". Defaults to "new".'),
|
|
379
381
|
priority: z.number().optional().describe('Priority level (0-4, higher is more important)'),
|
|
380
382
|
size: z
|
|
381
383
|
.enum(['S', 'M', 'L'])
|
|
@@ -532,6 +534,37 @@ const UpdateJournalEntrySchema = z.object({
|
|
|
532
534
|
.optional()
|
|
533
535
|
.describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
|
|
534
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
|
+
});
|
|
535
568
|
// Comment schemas
|
|
536
569
|
const ListWorkItemCommentsSchema = z.object({
|
|
537
570
|
work_item_id: refIdString().describe('The work item refId (number)'),
|
|
@@ -831,14 +864,16 @@ class WolfpackMCPServer {
|
|
|
831
864
|
await this.fetchCapabilities();
|
|
832
865
|
}
|
|
833
866
|
return {
|
|
834
|
-
|
|
867
|
+
// The catalogue carries a token where the product's name belongs (#2466); it is
|
|
868
|
+
// filled in here, as the tools are advertised.
|
|
869
|
+
tools: withProductName([
|
|
835
870
|
...stdioTools(this.capabilities, this.permissions),
|
|
836
871
|
...(this.capabilities.includes('agent_self') ? AGENT_SELF_TOOLS : []),
|
|
837
872
|
...(this.capabilities.includes('agent_memory') ? AGENT_MEMORY_TOOLS : []),
|
|
838
873
|
...(this.capabilities.includes('agent_observer') ? AGENT_OBSERVE_TOOLS : []),
|
|
839
874
|
...(this.capabilities.includes('agent_builder') ? AGENT_BUILDER_TOOLS : []),
|
|
840
875
|
...(this.capabilities.includes('browser_control') ? BROWSER_TOOLS : []),
|
|
841
|
-
],
|
|
876
|
+
], productName),
|
|
842
877
|
};
|
|
843
878
|
});
|
|
844
879
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -1508,6 +1543,71 @@ class WolfpackMCPServer {
|
|
|
1508
1543
|
],
|
|
1509
1544
|
};
|
|
1510
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
|
+
}
|
|
1511
1611
|
// Comment handlers
|
|
1512
1612
|
case 'list_work_item_comments': {
|
|
1513
1613
|
const parsed = ListWorkItemCommentsSchema.parse(args);
|
|
@@ -2072,7 +2172,7 @@ class WolfpackMCPServer {
|
|
|
2072
2172
|
// Connect stdio transport FIRST so MCP clients don't deadlock waiting for initialize
|
|
2073
2173
|
const transport = new StdioServerTransport();
|
|
2074
2174
|
await this.server.connect(transport);
|
|
2075
|
-
console.error(
|
|
2175
|
+
console.error(`${productName} MCP Server v${CURRENT_VERSION} started`);
|
|
2076
2176
|
if (!this.capabilitiesLoaded) {
|
|
2077
2177
|
console.error('Starting with base tools only; retrying capabilities fetch in background');
|
|
2078
2178
|
void this.recoverCapabilities();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { productName } from './brand.js';
|
|
1
2
|
/**
|
|
2
3
|
* Server instructions, returned in the MCP `initialize` result.
|
|
3
4
|
*
|
|
@@ -12,9 +13,9 @@
|
|
|
12
13
|
* server — the two servers have no shared package, and already keep their tool
|
|
13
14
|
* descriptions in step by hand.
|
|
14
15
|
*/
|
|
15
|
-
export const SERVER_INSTRUCTIONS =
|
|
16
|
+
export const SERVER_INSTRUCTIONS = `${productName} is this project's tracker. Its references override the usual GitHub reading of "#N".
|
|
16
17
|
|
|
17
|
-
- A bare "#123" — "work on #123", "look at #42" — is ALWAYS a
|
|
18
|
+
- A bare "#123" — "work on #123", "look at #42" — is ALWAYS a ${productName} work item. Call get_work_item. Do not run \`gh issue view\`, and do not search GitHub for it.
|
|
18
19
|
- Read a number as a GitHub issue or pull request only when the user says GitHub, PR, or gh — "the GitHub issue #123", "PR #123".
|
|
19
|
-
- Every other
|
|
20
|
-
- On its own, "issue" means a
|
|
20
|
+
- Every other ${productName} type needs its prefix and is never a bare number: #i123 issues, #r123 roadmap/initiatives, #j123 journal entries, #c123 cases, #p123 procedures. Wiki pages are addressed by path, e.g. /docs/setup.
|
|
21
|
+
- On its own, "issue" means a ${productName} issue (#i123). A GitHub issue is only ever called a GitHub issue.`;
|
package/dist/toolCatalogue.js
CHANGED
|
@@ -21,6 +21,22 @@
|
|
|
21
21
|
* `agentBuilderTools.ts`, `agentSelfTools.ts`, `browserTools.ts`), so they have
|
|
22
22
|
* no second declaration to drift from.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* The token a description carries where the product's name belongs (#2466).
|
|
26
|
+
*
|
|
27
|
+
* The catalogue is serialised into the backend's generated copy, so a description cannot
|
|
28
|
+
* interpolate the name — it would be baked in at generation time, and the copy would go
|
|
29
|
+
* stale on a rename with nothing to notice. Each transport fills the token in as it
|
|
30
|
+
* advertises its tools instead.
|
|
31
|
+
*/
|
|
32
|
+
export const PRODUCT_NAME_TOKEN = '__PRODUCT_NAME__';
|
|
33
|
+
/** The same tools, as a person reading their client's tool list should see them. */
|
|
34
|
+
export function withProductName(tools, productName) {
|
|
35
|
+
return tools.map((tool) => ({
|
|
36
|
+
...tool,
|
|
37
|
+
description: tool.description?.split(PRODUCT_NAME_TOKEN).join(productName),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
24
40
|
// Cross-reference syntax for linking between content items in markdown fields
|
|
25
41
|
const CONTENT_LINKING_HELP = 'CROSS-REFERENCES: In any markdown content, you can link to other items using these patterns: ' +
|
|
26
42
|
'#123, #w123, or #work-123 for work items (DEFAULT — bare #N always means a work item), ' +
|
|
@@ -89,9 +105,9 @@ export const TOOL_CATALOGUE = [
|
|
|
89
105
|
// Checks each entity type itself and drops the types it may not read, so a
|
|
90
106
|
// key holding only `mcp:wiki:read` still searches pages.
|
|
91
107
|
permission: null,
|
|
92
|
-
description: 'Ranked full-text search across project content: wiki pages, work items, cases, journal entries, issues and the
|
|
108
|
+
description: 'Ranked full-text search across project content: wiki pages, work items, cases, journal entries, issues and the __PRODUCT_NAME__ user manual. ' +
|
|
93
109
|
'Title matches outrank body matches; results include a snippet, entity type, and refId/slug for follow-up calls ' +
|
|
94
|
-
'(get_work_item, get_issue, get_wiki_page, ...). Results with entityType "manual" are user manual sections — platform documentation on how
|
|
110
|
+
'(get_work_item, get_issue, get_wiki_page, ...). Results with entityType "manual" are user manual sections — platform documentation on how __PRODUCT_NAME__ features work. ' +
|
|
95
111
|
'Use this to find existing content before creating new items, or when the user asks to "find" or "look up" something without saying where it lives.',
|
|
96
112
|
inputSchema: {
|
|
97
113
|
type: 'object',
|
|
@@ -132,7 +148,7 @@ export const TOOL_CATALOGUE = [
|
|
|
132
148
|
'with a non-empty blockedBy is NOT claimable however approved it is — take the work it names ' +
|
|
133
149
|
'first; the response says so too. ' +
|
|
134
150
|
'TERMINOLOGY: "board" and "kanban" are synonymous - both refer to the Kanban board of work items. ' +
|
|
135
|
-
'The board has columns: "new" (to do), "doing" (in progress), "review" (pending review), "ready" (code done, awaiting deployment), "
|
|
151
|
+
'The board has columns: "new" (to do), "doing" (in progress), "paused" (set aside by a human), "blocked", "review" (pending review), "ready" (code done, awaiting deployment), "completed" (deployed). ' +
|
|
136
152
|
'The "backlog" or "pending" status represents items not yet on the board. ' +
|
|
137
153
|
'By default, completed/closed items are excluded - use status="all" to include them. ' +
|
|
138
154
|
'IMPORTANT: Work items are NOT the same as issues. Work items live on the Kanban board/backlog. ' +
|
|
@@ -147,7 +163,7 @@ export const TOOL_CATALOGUE = [
|
|
|
147
163
|
},
|
|
148
164
|
status: {
|
|
149
165
|
type: 'string',
|
|
150
|
-
description: 'Filter by status. Board columns: "new", "doing", "
|
|
166
|
+
description: 'Filter by status. Board columns: "new", "doing", "paused", "blocked", "review", "ready", "completed". ' +
|
|
151
167
|
'Use "pending" or "backlog" for backlog items not on board. ' +
|
|
152
168
|
'Use "all" to include completed/closed items. Default excludes completed/closed.',
|
|
153
169
|
},
|
|
@@ -208,8 +224,9 @@ export const TOOL_CATALOGUE = [
|
|
|
208
224
|
'WORKFLOW: When asked to work on an item, check its status and follow the required state transitions ' +
|
|
209
225
|
'(pending→pull first, new→doing, review→doing when you are picking the work back up, ' +
|
|
210
226
|
'ready/completed/closed→new→doing, then review when done). ' +
|
|
211
|
-
'AGENTS: a "blocked" item is not yours to restart — a human clears the blocker
|
|
212
|
-
'
|
|
227
|
+
'AGENTS: a "blocked" or "paused" item is not yours to restart — a human clears the blocker, and a ' +
|
|
228
|
+
'human decides when paused work resumes. Leave it where it is, comment if you can help, and take ' +
|
|
229
|
+
'the next claimable item instead. ' +
|
|
213
230
|
'PLANNING: Check if the description contains a plan (markdown checklist). If not, APPEND one using update_work_progress - preserve all original description text and add your plan below a "---" separator. ' +
|
|
214
231
|
'FORMS: Procedure-created work items may include formDefinition (field definitions with name, label, type, required, options) and formValues (current values). Use submit_work_item_form to fill in form values. ' +
|
|
215
232
|
'REVIEW CHECKS: items under review may carry review checks (code-review, security-review, ...); see list_work_item_checks and claim/complete_work_item_check. ' +
|
|
@@ -264,7 +281,8 @@ export const TOOL_CATALOGUE = [
|
|
|
264
281
|
'STATUS WORKFLOW: "pending" (backlog) → "new" (to do) → "doing" (in progress) → "review" (work done) → "ready" (awaiting deployment) → "completed" (deployed). ' +
|
|
265
282
|
'Use "blocked" when work cannot proceed. AGENTS: you cannot start un-started work here — ' +
|
|
266
283
|
'a status change on a "pending" or "new" item that is not assigned to you is refused. ' +
|
|
267
|
-
'Nor can you move an item OUT of "blocked": clearing a blocker
|
|
284
|
+
'Nor can you move an item OUT of "blocked" or "paused": clearing a blocker, and resuming work a ' +
|
|
285
|
+
'human set aside, are human decisions. ' +
|
|
268
286
|
'Take it with pull_work_item, which assigns it to you and puts it in "doing"; that is ' +
|
|
269
287
|
'what makes the board show who is working on what. ' +
|
|
270
288
|
'When moving to "review", add a completion comment via create_work_item_comment. When moving to "blocked", add a comment explaining the blocker. ' +
|
|
@@ -291,6 +309,7 @@ export const TOOL_CATALOGUE = [
|
|
|
291
309
|
'pending',
|
|
292
310
|
'new',
|
|
293
311
|
'doing',
|
|
312
|
+
'paused',
|
|
294
313
|
'blocked',
|
|
295
314
|
'review',
|
|
296
315
|
'ready',
|
|
@@ -803,6 +822,7 @@ export const TOOL_CATALOGUE = [
|
|
|
803
822
|
'pending',
|
|
804
823
|
'new',
|
|
805
824
|
'doing',
|
|
825
|
+
'paused',
|
|
806
826
|
'blocked',
|
|
807
827
|
'review',
|
|
808
828
|
'ready',
|
|
@@ -810,7 +830,7 @@ export const TOOL_CATALOGUE = [
|
|
|
810
830
|
'closed',
|
|
811
831
|
'archived',
|
|
812
832
|
],
|
|
813
|
-
description: 'Initial status: "pending" (backlog), "new" (to do), "doing", "
|
|
833
|
+
description: 'Initial status: "pending" (backlog), "new" (to do), "doing", "paused", "blocked", "review", "ready", "completed", "closed", "archived". Defaults to "new".',
|
|
814
834
|
},
|
|
815
835
|
priority: {
|
|
816
836
|
type: 'number',
|
|
@@ -1126,6 +1146,124 @@ export const TOOL_CATALOGUE = [
|
|
|
1126
1146
|
required: ['entry_id'],
|
|
1127
1147
|
},
|
|
1128
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
|
+
},
|
|
1129
1267
|
// Comment tools
|
|
1130
1268
|
{
|
|
1131
1269
|
name: 'list_work_item_comments',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { readFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import
|
|
4
|
+
import * as catalogue from './toolCatalogue.js';
|
|
5
|
+
import { TOOL_CATALOGUE, remoteTools, stdioTools } from './toolCatalogue.js';
|
|
5
6
|
import { BROWSER_TOOLS } from './browserTools.js';
|
|
6
7
|
// @ts-expect-error — a plain .mjs build script, deliberately not part of this program
|
|
7
8
|
import { render, GENERATED_PATH } from '../../../scripts/generate-mcp-catalogue.mjs';
|
|
@@ -16,7 +17,9 @@ const repoFile = (path) => readFileSync(join(__dirname, '../../..', path), 'utf8
|
|
|
16
17
|
describe('the MCP tool catalogue', () => {
|
|
17
18
|
it("is what the backend's generated copy holds", () => {
|
|
18
19
|
// The one check that makes the other declaration a copy rather than a rival.
|
|
19
|
-
|
|
20
|
+
// The whole module, exactly as the generator imports it: passing named exports
|
|
21
|
+
// one by one is how the copy went stale when the generator grew a new one (#2466).
|
|
22
|
+
expect(readFileSync(GENERATED_PATH, 'utf8')).toBe(render(catalogue));
|
|
20
23
|
});
|
|
21
24
|
it('gives every tool a name no other tool has', () => {
|
|
22
25
|
const names = TOOL_CATALOGUE.map((t) => t.name);
|
|
@@ -58,13 +61,16 @@ describe('the MCP tool catalogue', () => {
|
|
|
58
61
|
.map((param) => `${tool.name}.${param}`));
|
|
59
62
|
expect(unforwarded).toEqual([]);
|
|
60
63
|
});
|
|
61
|
-
|
|
62
|
-
|
|
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);
|
|
63
69
|
const without = stdioTools([]).map((t) => t.name);
|
|
64
|
-
expect(
|
|
65
|
-
expect(without).not.toContain(
|
|
70
|
+
expect(withIt).toEqual(expect.arrayContaining(gated));
|
|
71
|
+
gated.forEach((name) => expect(without).not.toContain(name));
|
|
66
72
|
// Gating is the only difference: nothing else drops out.
|
|
67
|
-
expect(
|
|
73
|
+
expect(withIt.length - without.length).toBe(gated.length);
|
|
68
74
|
});
|
|
69
75
|
it('offers a tool over stdio only to a key holding its scope', () => {
|
|
70
76
|
// #2428 — the list told an agent it could do things the 403 then refused.
|