wolfpack-mcp 1.0.102 → 1.0.104

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/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/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 || 'https://wolfpacks.work/api/mcp',
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('Please set WOLFPACK_API_KEY to your API key from the Wolfpack application');
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", "review", "ready", "blocked", "completed". Use "pending" or "backlog" for backlog items. Use "all" to include completed/closed. Default excludes completed/closed.'),
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", "review", "ready", "blocked", "completed". Defaults to "new".'),
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'])
@@ -805,6 +807,8 @@ class WolfpackMCPServer {
805
807
  server;
806
808
  client;
807
809
  capabilities = [];
810
+ /** The key's scopes, or null while they are unknown — see `stdioTools` (#2428). */
811
+ permissions = null;
808
812
  capabilitiesLoaded = false;
809
813
  capabilitiesFetch = null;
810
814
  constructor() {
@@ -829,14 +833,16 @@ class WolfpackMCPServer {
829
833
  await this.fetchCapabilities();
830
834
  }
831
835
  return {
832
- tools: [
833
- ...stdioTools(this.capabilities),
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),
834
840
  ...(this.capabilities.includes('agent_self') ? AGENT_SELF_TOOLS : []),
835
841
  ...(this.capabilities.includes('agent_memory') ? AGENT_MEMORY_TOOLS : []),
836
842
  ...(this.capabilities.includes('agent_observer') ? AGENT_OBSERVE_TOOLS : []),
837
843
  ...(this.capabilities.includes('agent_builder') ? AGENT_BUILDER_TOOLS : []),
838
844
  ...(this.capabilities.includes('browser_control') ? BROWSER_TOOLS : []),
839
- ],
845
+ ], productName),
840
846
  };
841
847
  });
842
848
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -2032,6 +2038,7 @@ class WolfpackMCPServer {
2032
2038
  .getCapabilities()
2033
2039
  .then((caps) => {
2034
2040
  this.capabilities = caps.capabilities;
2041
+ this.permissions = caps.permissions ?? null;
2035
2042
  this.capabilitiesLoaded = true;
2036
2043
  if (this.capabilities.length > 0) {
2037
2044
  console.error(`Capabilities: ${this.capabilities.join(', ')}`);
@@ -2069,7 +2076,7 @@ class WolfpackMCPServer {
2069
2076
  // Connect stdio transport FIRST so MCP clients don't deadlock waiting for initialize
2070
2077
  const transport = new StdioServerTransport();
2071
2078
  await this.server.connect(transport);
2072
- console.error(`Wolfpack MCP Server v${CURRENT_VERSION} started`);
2079
+ console.error(`${productName} MCP Server v${CURRENT_VERSION} started`);
2073
2080
  if (!this.capabilitiesLoaded) {
2074
2081
  console.error('Starting with base tools only; retrying capabilities fetch in background');
2075
2082
  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 = `Wolfpack is this project's tracker. Its references override the usual GitHub reading of "#N".
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 Wolfpack work item. Call get_work_item. Do not run \`gh issue view\`, and do not search GitHub for it.
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 Wolfpack 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.
20
- - On its own, "issue" means a Wolfpack issue (#i123). A GitHub issue is only ever called a GitHub issue.`;
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.`;
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
+ import { createServer } from 'http';
5
+ import { fileURLToPath } from 'url';
6
+ /**
7
+ * #2428 — the tool list a key is actually served, over a real stdio transport.
8
+ *
9
+ * `stdioTools` filtering in isolation proves nothing about what an agent sees:
10
+ * the defect was that `index.ts` never asked the backend for the key's scopes
11
+ * and never passed them on. So this drives the real server the way a client
12
+ * does, against a stub standing in for `/api/mcp/capabilities`, and reads the
13
+ * tool list off `listTools()`.
14
+ */
15
+ const entryPoint = fileURLToPath(new URL('./index.ts', import.meta.url));
16
+ let backend;
17
+ /** A backend answering `/capabilities` with `body`, or failing when it is null. */
18
+ const stubBackend = async (body) => {
19
+ backend = createServer((req, res) => {
20
+ if (body && req.url?.endsWith('/capabilities')) {
21
+ res.writeHead(200, { 'Content-Type': 'application/json' });
22
+ res.end(JSON.stringify(body));
23
+ return;
24
+ }
25
+ if (req.url?.endsWith('/teams')) {
26
+ res.writeHead(200, { 'Content-Type': 'application/json' });
27
+ res.end(JSON.stringify({ teams: [] }));
28
+ return;
29
+ }
30
+ res.writeHead(503).end();
31
+ });
32
+ await new Promise((resolve) => backend.listen(0, '127.0.0.1', resolve));
33
+ return `http://127.0.0.1:${backend.address().port}/api/mcp`;
34
+ };
35
+ const toolNamesFrom = async (apiUrl) => {
36
+ const client = new Client({ name: 'test', version: '1.0.0' });
37
+ const transport = new StdioClientTransport({
38
+ // tsx directly, not via npx: the transport kills the process it spawned,
39
+ // and an npx wrapper would leave the server orphaned behind it.
40
+ command: process.execPath,
41
+ args: ['--import', 'tsx', entryPoint],
42
+ env: {
43
+ ...process.env,
44
+ WOLFPACK_API_KEY: 'wfp_sk_test',
45
+ WOLFPACK_API_URL: apiUrl,
46
+ WOLFPACK_PROJECT_SLUG: '',
47
+ WOLFPACK_TEAM_SLUG: '',
48
+ WOLFPACK_ORG_SLUG: '',
49
+ },
50
+ });
51
+ await client.connect(transport);
52
+ try {
53
+ return (await client.listTools()).tools.map((t) => t.name);
54
+ }
55
+ finally {
56
+ await client.close();
57
+ }
58
+ };
59
+ afterEach(async () => {
60
+ await new Promise((resolve) => backend?.close(() => resolve()) ?? resolve());
61
+ backend = undefined;
62
+ });
63
+ describe('the tool list served over stdio', () => {
64
+ it('offers a read-only key the tools it may call and not the ones it may not', async () => {
65
+ const names = await toolNamesFrom(await stubBackend({ capabilities: ['project'], permissions: ['mcp:work_items:read'] }));
66
+ expect(names).toContain('list_work_items');
67
+ expect(names).not.toContain('update_work_item');
68
+ expect(names).not.toContain('create_work_item_comment');
69
+ }, 60_000);
70
+ it('is not stripped when the capabilities round trip fails', async () => {
71
+ // #1488 the other way up: a gateway outage means the key's scopes are
72
+ // unknown, not that the key holds none. Stripping the list on a 503 would
73
+ // be a worse regression than the one this item fixes.
74
+ const names = await toolNamesFrom(await stubBackend(null));
75
+ expect(names).toContain('list_work_items');
76
+ expect(names).toContain('update_work_item');
77
+ }, 60_000);
78
+ });
@@ -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 Wolfpack user manual. ' +
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 Wolfpack features work. ' +
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), "blocked", "completed" (deployed). ' +
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", "review", "ready", "blocked", "completed". ' +
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. Leave it where it is, ' +
212
- 'comment if you can help clear it, and take the next claimable item instead. ' +
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 is a human decision. ' +
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", "review", "ready", "blocked", "completed", "closed", "archived". Defaults to "new".',
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',
@@ -2137,9 +2157,24 @@ const toTool = ({ name, description, inputSchema }) => ({
2137
2157
  description,
2138
2158
  inputSchema,
2139
2159
  });
2140
- /** The catalogue tools the stdio transport offers a key holding `capabilities`. */
2141
- export function stdioTools(capabilities) {
2142
- return TOOL_CATALOGUE.filter((t) => !t.capability || capabilities.includes(t.capability)).map(toTool);
2160
+ /**
2161
+ * Whether `permissions` carries `required`, read exactly as the backend reads it
2162
+ * in `checkMcpPermission` an exact match, or a `…:*` wildcard above it.
2163
+ */
2164
+ const holdsScope = (permissions, required) => permissions.some((p) => p === required || (p.endsWith(':*') && required.startsWith(p.slice(0, -1))));
2165
+ /**
2166
+ * The catalogue tools the stdio transport offers a key holding `capabilities`
2167
+ * and `permissions`.
2168
+ *
2169
+ * `permissions` absent means the key's scopes are not known — the `/capabilities`
2170
+ * round trip has not succeeded yet (#1488), or the backend predates #2428 — and
2171
+ * everything the capabilities allow is advertised, as it always was. An empty
2172
+ * array is the opposite: a key that holds no scopes, and is offered only the
2173
+ * tools that deliberately require none.
2174
+ */
2175
+ export function stdioTools(capabilities, permissions) {
2176
+ return TOOL_CATALOGUE.filter((t) => (!t.capability || capabilities.includes(t.capability)) &&
2177
+ (!t.permission || !permissions || holdsScope(permissions, t.permission))).map(toTool);
2143
2178
  }
2144
2179
  /** The names a capability gates, for routing a call to that family's handler. */
2145
2180
  export function toolNamesFor(capability) {
@@ -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 { TOOL_CATALOGUE, remoteTools, remoteToolPermissions, stdioTools } from './toolCatalogue.js';
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
- expect(readFileSync(GENERATED_PATH, 'utf8')).toBe(render({ remoteTools, remoteToolPermissions }));
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);
@@ -66,6 +69,32 @@ describe('the MCP tool catalogue', () => {
66
69
  // Gating is the only difference: nothing else drops out.
67
70
  expect(withProcedures.length - without.length).toBe(TOOL_CATALOGUE.filter((t) => t.capability === 'procedures').length);
68
71
  });
72
+ it('offers a tool over stdio only to a key holding its scope', () => {
73
+ // #2428 — the list told an agent it could do things the 403 then refused.
74
+ const readOnly = stdioTools(['project'], ['mcp:work_items:read']).map((t) => t.name);
75
+ expect(readOnly).toContain('list_work_items');
76
+ expect(readOnly).not.toContain('update_work_item');
77
+ expect(readOnly).not.toContain('create_issue');
78
+ });
79
+ 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);
82
+ expect(unscoped.length).toBeGreaterThan(0);
83
+ expect(stdioTools([], []).map((t) => t.name)).toEqual(unscoped);
84
+ });
85
+ it('honours a wildcard the way the server does when it checks the call', () => {
86
+ // Filtering has to read a scope exactly as `checkMcpPermission` does, or a
87
+ // key on `mcp:*` loses tools it is entitled to call.
88
+ expect(stdioTools([], ['mcp:*']).map((t) => t.name)).toEqual(stdioTools([]).map((t) => t.name));
89
+ expect(stdioTools([], ['mcp:work_items:*']).map((t) => t.name)).toContain('update_work_item');
90
+ expect(stdioTools([], ['mcp:work_items:*']).map((t) => t.name)).not.toContain('create_issue');
91
+ });
92
+ it("advertises everything when the key's scopes are unknown", () => {
93
+ // A `/capabilities` round trip that has not succeeded yet (#1488) must not
94
+ // strip the list — an outage is not an answer about what the key may do.
95
+ expect(stdioTools(['project']).map((t) => t.name)).toEqual(stdioTools(['project'], null).map((t) => t.name));
96
+ expect(stdioTools(['project'], null).map((t) => t.name)).toContain('update_work_item');
97
+ });
69
98
  it('takes the remote override only where one is set', () => {
70
99
  // upload_image is the single tool whose two clients cannot share a contract.
71
100
  const overridden = TOOL_CATALOGUE.filter((t) => t.remote).map((t) => t.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",