wolfpack-mcp 1.0.95 → 1.0.97

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.
@@ -0,0 +1,146 @@
1
+ import { z } from 'zod';
2
+ function text(data) {
3
+ return JSON.stringify(data, null, 2);
4
+ }
5
+ /**
6
+ * Browser control for chatbot agents (#2288): see and drive the page the chat
7
+ * widget is embedded in.
8
+ *
9
+ * Gated by the `browser_control` capability, which the backend grants only to a
10
+ * key bound to a chat session — a scheduled run or a coder never sees these.
11
+ * The backend gates every call on the visitor's own grant as well, so a tool
12
+ * called out of turn is refused there rather than here.
13
+ */
14
+ export const BROWSER_TOOLS = [
15
+ {
16
+ name: 'browser_request_control',
17
+ description: 'Ask the person you are chatting with for permission to drive their browser. ' +
18
+ 'They see your reason in the conversation with Allow and Deny buttons, and this call waits for their answer. ' +
19
+ 'You MUST call this and be granted control before any other browser tool will work — there is no way around it, ' +
20
+ 'and asking repeatedly after a refusal is not acceptable. ' +
21
+ 'Ask only when driving the page is genuinely the best way to help; explaining what to click is usually better.',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ reason: {
26
+ type: 'string',
27
+ description: 'What you want to do on their page, in one plain sentence — this is what they read before deciding (e.g. "Fill in the booking form with the dates we agreed").',
28
+ },
29
+ },
30
+ required: ['reason'],
31
+ },
32
+ },
33
+ {
34
+ name: 'browser_snapshot',
35
+ description: 'See the page: its URL, title, visible text, and the interactive elements you can act on. ' +
36
+ 'Each element comes back with a "ref" — pass that ref to browser_click or browser_type. ' +
37
+ 'Refs are only valid until the page changes, so take a fresh snapshot after every action.',
38
+ inputSchema: { type: 'object', properties: {} },
39
+ },
40
+ {
41
+ name: 'browser_click',
42
+ description: 'Click an element on the page. The visitor sees the pointer travel to it before it is clicked.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ ref: { type: 'string', description: 'The ref of the element, from browser_snapshot' },
47
+ },
48
+ required: ['ref'],
49
+ },
50
+ },
51
+ {
52
+ name: 'browser_type',
53
+ description: 'Type text into an input, textarea or select on the page, replacing whatever it holds.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ ref: { type: 'string', description: 'The ref of the field, from browser_snapshot' },
58
+ text: { type: 'string', description: 'The text to enter' },
59
+ },
60
+ required: ['ref', 'text'],
61
+ },
62
+ },
63
+ {
64
+ name: 'browser_scroll',
65
+ description: 'Scroll the page vertically to bring more of it into view.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ deltaY: {
70
+ type: 'number',
71
+ description: 'Pixels to scroll by — positive scrolls down, negative up',
72
+ },
73
+ },
74
+ required: ['deltaY'],
75
+ },
76
+ },
77
+ {
78
+ name: 'browser_release_control',
79
+ description: 'Hand the browser back when you are done, so the visitor stops seeing the "agent in control" frame on their screen. ' +
80
+ 'Always do this once the task is finished. They can also stop you themselves at any time.',
81
+ inputSchema: { type: 'object', properties: {} },
82
+ },
83
+ ];
84
+ const RequestControlSchema = z.object({ reason: z.string() });
85
+ const ClickSchema = z.object({ ref: z.string() });
86
+ const TypeSchema = z.object({ ref: z.string(), text: z.string() });
87
+ const ScrollSchema = z.object({ deltaY: z.coerce.number() });
88
+ /** A refused or failed command is reported as a tool error, so the agent reads
89
+ * it as something that did not happen rather than as a result. */
90
+ function commandOutcome(result) {
91
+ if (!result.ok) {
92
+ return {
93
+ content: [{ type: 'text', text: result.error ?? 'The command failed' }],
94
+ isError: true,
95
+ };
96
+ }
97
+ return { content: [{ type: 'text', text: text(result.result ?? { done: true }) }] };
98
+ }
99
+ export async function handleBrowserTool(name, args, client) {
100
+ switch (name) {
101
+ case 'browser_request_control': {
102
+ const { reason } = RequestControlSchema.parse(args);
103
+ const { outcome } = await client.requestBrowserControl(reason);
104
+ if (outcome === 'granted') {
105
+ return {
106
+ content: [
107
+ {
108
+ type: 'text',
109
+ text: 'Granted. Take a browser_snapshot to see the page, and call browser_release_control when you are done.',
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ return {
115
+ content: [
116
+ {
117
+ type: 'text',
118
+ text: outcome === 'denied'
119
+ ? 'The visitor declined. Do not ask again unless they bring it up — help them another way.'
120
+ : 'The visitor did not answer. Carry on without the browser.',
121
+ },
122
+ ],
123
+ isError: true,
124
+ };
125
+ }
126
+ case 'browser_snapshot':
127
+ return commandOutcome(await client.runBrowserCommand({ action: 'snapshot' }));
128
+ case 'browser_click': {
129
+ const { ref } = ClickSchema.parse(args);
130
+ return commandOutcome(await client.runBrowserCommand({ action: 'click', ref }));
131
+ }
132
+ case 'browser_type': {
133
+ const parsed = TypeSchema.parse(args);
134
+ return commandOutcome(await client.runBrowserCommand({ action: 'type', ...parsed }));
135
+ }
136
+ case 'browser_scroll': {
137
+ const { deltaY } = ScrollSchema.parse(args);
138
+ return commandOutcome(await client.runBrowserCommand({ action: 'scroll', deltaY }));
139
+ }
140
+ case 'browser_release_control':
141
+ await client.releaseBrowserControl();
142
+ return { content: [{ type: 'text', text: 'Browser control handed back to the visitor.' }] };
143
+ default:
144
+ throw new Error(`Unknown browser tool: ${name}`);
145
+ }
146
+ }
package/dist/client.js CHANGED
@@ -490,6 +490,9 @@ export class WolfpackClient {
490
490
  async createWorkItemComment(workItemId, data, teamSlug) {
491
491
  return this.api.post(this.withTeamSlug(`/work-items/${workItemId}/comments`, teamSlug), data);
492
492
  }
493
+ async askWorkItemQuestion(workItemId, question, teamSlug) {
494
+ return this.api.post(this.withTeamSlug(`/work-items/${workItemId}/question`, teamSlug), { question });
495
+ }
493
496
  async createIssueComment(issueId, data, teamSlug) {
494
497
  return this.api.post(this.withTeamSlug(`/issues/${issueId}/comments`, teamSlug), data);
495
498
  }
@@ -978,6 +981,18 @@ export class WolfpackClient {
978
981
  async saveMemory(key, content) {
979
982
  return this.api.put(`/self/memories/${encodeURIComponent(key)}`, { content });
980
983
  }
984
+ // ─── Browser control (#2288) ───────────────────────────────────────────────
985
+ // No chat is named on any of these: the backend derives it from the session
986
+ // this key is bound to, so the agent cannot address another visitor's page.
987
+ async requestBrowserControl(reason) {
988
+ return this.api.post('/browser/request', { reason });
989
+ }
990
+ async runBrowserCommand(command) {
991
+ return this.api.post('/browser/command', command);
992
+ }
993
+ async releaseBrowserControl() {
994
+ await this.api.post('/browser/release', {});
995
+ }
981
996
  close() {
982
997
  // No cleanup needed for API client
983
998
  }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { validateConfig, config } from './config.js';
12
12
  import { AGENT_BUILDER_TOOLS, handleAgentBuilderTool } from './agentBuilderTools.js';
13
13
  import { PROCEDURE_TOOLS, handleProcedureTool } from './procedureTools.js';
14
14
  import { AGENT_SELF_TOOLS, AGENT_MEMORY_TOOLS, handleAgentSelfTool } from './agentSelfTools.js';
15
+ import { BROWSER_TOOLS, handleBrowserTool } from './browserTools.js';
15
16
  import { resolveRadarItemId } from './resolveRadarItemId.js';
16
17
  import { SERVER_INSTRUCTIONS } from './serverInstructions.js';
17
18
  import { fetch as proxyFetch } from './proxyFetch.js';
@@ -538,6 +539,14 @@ const CreateWorkItemCommentSchema = z.object({
538
539
  .optional()
539
540
  .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
540
541
  });
542
+ const AskWorkItemQuestionSchema = z.object({
543
+ work_item_id: refIdString().describe('The work item refId (number)'),
544
+ question: z.string().describe('The question (markdown)'),
545
+ project_slug: z
546
+ .string()
547
+ .optional()
548
+ .describe('Project slug (required for multi-project users, use list_projects to get slugs)'),
549
+ });
541
550
  const CreateIssueCommentSchema = z.object({
542
551
  issue_id: refIdString().describe('The issue refId (number)'),
543
552
  content: z.string().describe('Comment content (markdown)'),
@@ -1814,6 +1823,8 @@ class WolfpackMCPServer {
1814
1823
  '2) Completion summaries when moving to "review" (what was done, files changed, testing notes). ' +
1815
1824
  '3) Important observations or decisions that should be visible in the activity history. ' +
1816
1825
  'USE DESCRIPTION (update_work_progress) FOR: Plans, checklists, and progress tracking that need to be updated over time. ' +
1826
+ 'NOT FOR QUESTIONS you need answered before you can continue the item you are working: a reply cannot reach a ' +
1827
+ 'running session, so a question asked here is one you keep working past. Use ask_work_item_question, which pauses the work. ' +
1817
1828
  'Comments are typically not used in personal projects. ' +
1818
1829
  CONTENT_LINKING_HELP,
1819
1830
  inputSchema: {
@@ -1832,6 +1843,35 @@ class WolfpackMCPServer {
1832
1843
  required: ['work_item_id', 'content'],
1833
1844
  },
1834
1845
  },
1846
+ {
1847
+ name: 'ask_work_item_question',
1848
+ description: 'Ask a question about the work item you are working on and PAUSE until it is answered. ' +
1849
+ 'For when you cannot sensibly continue without an answer from a person — an ambiguous requirement, a decision ' +
1850
+ 'that is theirs to make, something only they know. The question is posted as a comment addressed to the ' +
1851
+ 'item\'s creator, and the item is marked as waiting: it stays in "doing" with you, your schedule does not ' +
1852
+ 'wake you for it until someone else comments (that comment is the answer), and the session ending on it is ' +
1853
+ 'not counted as an abandonment. AFTER CALLING THIS, STOP: commit and push anything worth keeping, make sure ' +
1854
+ 'the plan in the description records where you got to, and end your session without taking other work — ' +
1855
+ 'a reply cannot reach a running session, and pull_work_item refuses while the item waits. When you are ' +
1856
+ "woken again, get_work_item shows the question and its answer. Only the item's leading user may ask, and " +
1857
+ 'only while the item is in "doing". In a chat session ask in your reply instead — the next message is the answer. ' +
1858
+ 'Requires mcp:work_items:update permission.',
1859
+ inputSchema: {
1860
+ type: 'object',
1861
+ properties: {
1862
+ work_item_id: { type: 'string', description: 'The work item refId (number)' },
1863
+ question: {
1864
+ type: 'string',
1865
+ description: 'The question (markdown). Give the context a reader needs to answer it in one reply: what you found, the options you see, what you would do by default.',
1866
+ },
1867
+ project_slug: {
1868
+ type: 'string',
1869
+ description: 'Project slug (required for multi-project users, use list_projects to get slugs)',
1870
+ },
1871
+ },
1872
+ required: ['work_item_id', 'question'],
1873
+ },
1874
+ },
1835
1875
  {
1836
1876
  name: 'create_issue_comment',
1837
1877
  description: 'Add a comment to an issue. Requires mcp:comments:create permission. ' +
@@ -2428,6 +2468,7 @@ class WolfpackMCPServer {
2428
2468
  ...(this.capabilities.includes('agent_self') ? AGENT_SELF_TOOLS : []),
2429
2469
  ...(this.capabilities.includes('agent_memory') ? AGENT_MEMORY_TOOLS : []),
2430
2470
  ...(this.capabilities.includes('agent_builder') ? AGENT_BUILDER_TOOLS : []),
2471
+ ...(this.capabilities.includes('browser_control') ? BROWSER_TOOLS : []),
2431
2472
  ],
2432
2473
  };
2433
2474
  });
@@ -2512,7 +2553,7 @@ class WolfpackMCPServer {
2512
2553
  const workItem = await this.client.getWorkItem(parsed.work_item_id, parsed.project_slug || this.client.getProjectSlug() || undefined);
2513
2554
  if (workItem) {
2514
2555
  let text = JSON.stringify(stripUuids(workItem), null, 2);
2515
- const reminders = getWorkItemReminders(workItem.status, workItem.description, workItem.approved);
2556
+ const reminders = getWorkItemReminders(workItem.status, workItem.description, workItem.approved, workItem.question);
2516
2557
  if (reminders.length > 0) {
2517
2558
  text = `${reminders.join('\n\n')}\n\n${text}`;
2518
2559
  }
@@ -3089,6 +3130,19 @@ class WolfpackMCPServer {
3089
3130
  ],
3090
3131
  };
3091
3132
  }
3133
+ case 'ask_work_item_question': {
3134
+ const parsed = AskWorkItemQuestionSchema.parse(args);
3135
+ const teamSlug = parsed.project_slug || this.client.getProjectSlug() || undefined;
3136
+ const result = await this.client.askWorkItemQuestion(parsed.work_item_id, parsed.question, teamSlug);
3137
+ return {
3138
+ content: [
3139
+ {
3140
+ type: 'text',
3141
+ text: `${result.notice}\n\n${JSON.stringify(stripUuids(result.comment), null, 2)}`,
3142
+ },
3143
+ ],
3144
+ };
3145
+ }
3092
3146
  case 'create_issue_comment': {
3093
3147
  const parsed = CreateIssueCommentSchema.parse(args);
3094
3148
  const teamSlug = parsed.project_slug || this.client.getProjectSlug() || undefined;
@@ -3485,6 +3539,13 @@ class WolfpackMCPServer {
3485
3539
  return handleAgentSelfTool(name, args, this.client);
3486
3540
  }
3487
3541
  }
3542
+ // Check browser control tools (#2288 — chat sessions only)
3543
+ if (this.capabilities.includes('browser_control')) {
3544
+ const browserToolNames = BROWSER_TOOLS.map((t) => t.name);
3545
+ if (browserToolNames.includes(name)) {
3546
+ return handleBrowserTool(name, args, this.client);
3547
+ }
3548
+ }
3488
3549
  // Check agent builder tools
3489
3550
  if (this.capabilities.includes('agent_builder')) {
3490
3551
  return handleAgentBuilderTool(name, args, this.client);
@@ -30,9 +30,23 @@ const STATUS_REMINDERS = {
30
30
  'starting, so the board shows the work is under way again. If you are reviewing it, leave it where it is.',
31
31
  };
32
32
  // Reminders to prepend to a get_work_item response, most urgent first.
33
- export function getWorkItemReminders(status, description, approved) {
33
+ export function getWorkItemReminders(status, description, approved, question) {
34
34
  const reminders = [];
35
- if (status === 'pending' && approved === false) {
35
+ // A question replaces the status reminder: "doing" would say to finish the work,
36
+ // and the work is paused until someone answers (#2289)
37
+ if (status === 'doing' && question) {
38
+ if (question.answer === null) {
39
+ reminders.push('REMINDER: This work item is waiting for an answer to the question its leading agent asked ' +
40
+ '(see "question" below). Nobody has answered yet. If you are that agent, do not work on it ' +
41
+ 'and do not take other work: end your session, and your schedule wakes you once the answer is in.');
42
+ }
43
+ else {
44
+ reminders.push('REMINDER: The question asked on this work item has been answered — the answer is in ' +
45
+ '"question.answer" below, and the comments carry any discussion since. Resume the work with ' +
46
+ 'that answer; the plan in the description says where it got to.');
47
+ }
48
+ }
49
+ else if (status === 'pending' && approved === false) {
36
50
  // Overrides the generic pending reminder: approval only gates unassigned items
37
51
  reminders.push('REMINDER: This backlog item has NOT been approved. If it is assigned to you, that assignment ' +
38
52
  'overrides approval — pull it with pull_work_item and start work. If it is not assigned to you, ' +
@@ -55,6 +55,17 @@ describe('getWorkItemReminders', () => {
55
55
  expect(reminders).toEqual([expect.stringContaining('set status to "doing"')]);
56
56
  expect(reminders[0]).toContain('If you are reviewing it, leave it where it is');
57
57
  });
58
+ // #2289: a question outranks the "doing" reminder — the work is paused until answered
59
+ it('tells an agent whose question is unanswered to stop and wait', () => {
60
+ const reminders = getWorkItemReminders('doing', PLAN, undefined, { answer: null });
61
+ expect(reminders).toEqual([expect.stringContaining('waiting for an answer')]);
62
+ expect(reminders[0]).toContain('end your session');
63
+ });
64
+ it('points an agent whose question was answered at the answer', () => {
65
+ const reminders = getWorkItemReminders('doing', PLAN, undefined, { answer: 'Option B.' });
66
+ expect(reminders).toEqual([expect.stringContaining('has been answered')]);
67
+ expect(reminders[0]).toContain('"question.answer"');
68
+ });
58
69
  it('adds the no-plan reminder when the description has no plan', () => {
59
70
  const reminders = getWorkItemReminders('new', 'Just a description');
60
71
  expect(reminders).toHaveLength(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfpack-mcp",
3
- "version": "1.0.95",
3
+ "version": "1.0.97",
4
4
  "description": "MCP server for Wolfpack AI-enhanced software delivery tools",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",