@orboto/mcp 0.192.0 → 0.194.0

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.
Files changed (131) hide show
  1. package/dist/event-bridge.js +1 -47
  2. package/dist/event-bridge.test.js +0 -19
  3. package/dist/http-transport-binding.test.js +1 -4
  4. package/dist/http-transport.js +1 -213
  5. package/dist/http-transport.test.js +0 -56
  6. package/dist/index.js +1 -48
  7. package/dist/input-schema.js +2 -76
  8. package/dist/input-schema.test.js +1 -15
  9. package/dist/instructions-budget.test.js +0 -2
  10. package/dist/manifest-size.test.js +3 -72
  11. package/dist/oauth-bootstrap.js +0 -47
  12. package/dist/oauth-bootstrap.test.js +4 -16
  13. package/dist/orboto-client.js +0 -30
  14. package/dist/orboto-client.test.js +0 -12
  15. package/dist/prompts.js +0 -39
  16. package/dist/protect-text-usage.test.js +1 -9
  17. package/dist/required-rules-wrapper.test.js +0 -5
  18. package/dist/required-rules.js +0 -2
  19. package/dist/resources.js +0 -62
  20. package/dist/resources.test.js +0 -16
  21. package/dist/response-budget.js +2 -150
  22. package/dist/response-budget.test.js +1 -32
  23. package/dist/server.js +4 -180
  24. package/dist/session-nudge.js +0 -9
  25. package/dist/session-nudge.test.js +2 -10
  26. package/dist/session-nudge.transport.test.js +0 -23
  27. package/dist/tool-annotations.test.js +0 -44
  28. package/dist/tool-docs.js +1 -12
  29. package/dist/tool-docs.test.js +2 -8
  30. package/dist/tool-parameters.test.js +0 -19
  31. package/dist/tools/absence-writes.js +1 -17
  32. package/dist/tools/admin-translations.js +0 -11
  33. package/dist/tools/admin-writes.js +0 -28
  34. package/dist/tools/admin-writes.test.js +0 -1
  35. package/dist/tools/agent-coordination.js +2 -24
  36. package/dist/tools/agent-coordination.test.js +0 -2
  37. package/dist/tools/agent-instructions.js +0 -10
  38. package/dist/tools/agent-instructions.test.js +0 -2
  39. package/dist/tools/agent-messages.js +0 -1
  40. package/dist/tools/ai-status.js +0 -14
  41. package/dist/tools/ai-usage.js +0 -10
  42. package/dist/tools/analytics.js +0 -2
  43. package/dist/tools/api-call.js +0 -12
  44. package/dist/tools/api-call.test.js +1 -2
  45. package/dist/tools/api-search.js +1 -6
  46. package/dist/tools/approvals.js +0 -6
  47. package/dist/tools/attach.js +0 -25
  48. package/dist/tools/backup.js +0 -13
  49. package/dist/tools/backup.test.js +1 -2
  50. package/dist/tools/bulk-create.js +1 -25
  51. package/dist/tools/bulk-create.test.js +0 -4
  52. package/dist/tools/bulk-writes.js +69 -61
  53. package/dist/tools/bulk-writes.test.js +34 -4
  54. package/dist/tools/check-similar.js +0 -17
  55. package/dist/tools/checklist-writes.js +82 -27
  56. package/dist/tools/checklist-writes.test.js +69 -1
  57. package/dist/tools/claim.js +1 -71
  58. package/dist/tools/claim.test.js +0 -2
  59. package/dist/tools/critical-path.js +0 -5
  60. package/dist/tools/critical-path.test.js +0 -3
  61. package/dist/tools/cross-project-links.ee.js +1 -24
  62. package/dist/tools/doc-attachments.js +0 -28
  63. package/dist/tools/doc-attachments.test.js +1 -4
  64. package/dist/tools/doc-comments.js +1 -37
  65. package/dist/tools/doc-comments.test.js +1 -2
  66. package/dist/tools/doc-edits.js +1 -31
  67. package/dist/tools/doc-edits.test.js +0 -9
  68. package/dist/tools/doc-export.js +0 -24
  69. package/dist/tools/doc-export.test.js +0 -12
  70. package/dist/tools/doc-revisions.js +0 -23
  71. package/dist/tools/docs-ai.js +0 -34
  72. package/dist/tools/docs-ai.test.js +0 -11
  73. package/dist/tools/docs.js +1 -75
  74. package/dist/tools/docs.test.js +1 -3
  75. package/dist/tools/embedding-status.js +0 -13
  76. package/dist/tools/embedding-status.test.js +0 -1
  77. package/dist/tools/feedback.js +1 -15
  78. package/dist/tools/feedback.test.js +0 -1
  79. package/dist/tools/get-checklists.js +6 -14
  80. package/dist/tools/get-project-primer.js +1 -25
  81. package/dist/tools/get-project.js +0 -17
  82. package/dist/tools/get-ticket.js +1 -61
  83. package/dist/tools/get-timer.js +0 -11
  84. package/dist/tools/git-health-reasons.js +0 -1
  85. package/dist/tools/identity.js +0 -13
  86. package/dist/tools/list-projects.js +0 -20
  87. package/dist/tools/list-projects.test.js +0 -1
  88. package/dist/tools/list-tickets.js +10 -9
  89. package/dist/tools/milestones-crud.test.js +0 -4
  90. package/dist/tools/milestones.js +0 -43
  91. package/dist/tools/my-tickets.js +0 -7
  92. package/dist/tools/phase-b-tools.test.js +14 -21
  93. package/dist/tools/primer-facts.js +1 -49
  94. package/dist/tools/primer-facts.test.js +0 -20
  95. package/dist/tools/project-listings.js +0 -25
  96. package/dist/tools/query.js +1 -12
  97. package/dist/tools/query.test.js +0 -8
  98. package/dist/tools/raci.js +0 -11
  99. package/dist/tools/requirements-spec.js +1 -12
  100. package/dist/tools/response-expand.js +0 -19
  101. package/dist/tools/review-policy.js +0 -24
  102. package/dist/tools/review-policy.test.js +0 -2
  103. package/dist/tools/search.js +1 -13
  104. package/dist/tools/session-start.js +1 -136
  105. package/dist/tools/session-start.test.js +1 -44
  106. package/dist/tools/set-parent.js +1 -19
  107. package/dist/tools/shared.js +0 -41
  108. package/dist/tools/shared.test.js +0 -4
  109. package/dist/tools/similar-projection.js +0 -15
  110. package/dist/tools/ticket-attachments.js +1 -25
  111. package/dist/tools/ticket-schedules.js +0 -9
  112. package/dist/tools/ticket-schedules.test.js +0 -1
  113. package/dist/tools/ticket-writes.js +1 -110
  114. package/dist/tools/ticket-writes.test.js +1 -15
  115. package/dist/tools/time-writes.js +26 -46
  116. package/dist/tools/time-writes.test.js +18 -0
  117. package/dist/tools/update-project.js +1 -27
  118. package/dist/tools/update-project.test.js +1 -1
  119. package/dist/tools/wiki.js +0 -8
  120. package/dist/tools/work-next.js +0 -5
  121. package/dist/tools/work-sessions-start.test.js +0 -2
  122. package/dist/tools/work-sessions.js +0 -48
  123. package/dist/tools/work-sessions.test.js +0 -5
  124. package/dist/toolset.js +2 -81
  125. package/dist/toolset.test.js +0 -20
  126. package/dist/version-guard.test.js +0 -4
  127. package/dist/version.js +0 -12
  128. package/dist/with-metrics-output-schema.test.js +1 -14
  129. package/dist/with-metrics.js +2 -74
  130. package/dist/with-metrics.test.js +7 -29
  131. package/package.json +1 -1
@@ -1,14 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase B - `orboto_get_project`.
3
- *
4
- * Returns metadata + milestones + labels + members for a project.
5
- * Accepts the human-readable project key (`ACME`), case-insensitive
6
- * - the API's `GET /projects/by-key/:key` does the lookup, the rest
7
- * of the hydration (milestones + labels + members) comes from the
8
- * `/projects/:id/*` endpoints that take the resolved UUID.
9
- *
10
- * Members endpoint returns a nested `{user: {...}, role: {...}}`
11
- * shape; we flatten here into a card-friendly line.
12
3
  */
13
4
  import { z } from 'zod';
14
5
  import { resolveProjectByKey } from './shared.js';
@@ -23,9 +14,6 @@ export const getProjectToolConfig = {
23
14
  export function makeGetProjectHandler(client) {
24
15
  return async ({ projectKey }) => {
25
16
  const project = await resolveProjectByKey(client, projectKey);
26
- // Parallel fan-out - the API doesn't have a single aggregate route
27
- // for milestones+labels+members, and three parallel fetches beat a
28
- // chain on the default Node HTTP pool.
29
17
  const [milestones, labels, members] = await Promise.all([
30
18
  client.get(`/projects/${project.id}/milestones`),
31
19
  client.get(`/projects/${project.id}/labels`),
@@ -39,7 +27,6 @@ export function makeGetProjectHandler(client) {
39
27
  ...milestones.map((m) => ` - ${m.milestoneKey ? `${m.milestoneKey} · ` : ''}${m.name} [${m.status}]${m.endDate ? ` due ${m.endDate}` : ''}`),
40
28
  '',
41
29
  `Labels: ${labels.map((l) => l.name).join(', ') || '(none)'}`,
42
- // ORB-1040 - only mention RACI when the project opted in.
43
30
  project.raciEnabled ? 'RACI: enabled' : null,
44
31
  '',
45
32
  `Members (${members.length}):`,
@@ -52,15 +39,11 @@ export function makeGetProjectHandler(client) {
52
39
  content: [{ type: 'text', text: lines.join('\n') }],
53
40
  structuredContent: {
54
41
  project: {
55
- // ORB-1042 - surface the UUID for the rare APIs that still need it
56
- // (most agent surfaces accept the key).
57
42
  id: project.id,
58
43
  key: project.key,
59
44
  name: project.name,
60
45
  status: project.status,
61
46
  description: project.description,
62
- // ORB-1040 - RACI is opt-in; agents must not raise/set RACI here
63
- // unless this is true.
64
47
  raciEnabled: project.raciEnabled ?? false,
65
48
  },
66
49
  milestones,
@@ -1,17 +1,7 @@
1
1
  /**
2
2
  * ORB-244 Phase B - `orboto_get_ticket`.
3
3
  *
4
- * Returns a ticket's full context - description, comments, assignees,
5
- * labels, checklists, git activity - in a shape the model can
6
- * reason about without follow-up calls. Comments and checklists are
7
- * fetched in parallel with the ticket payload; git activity is
8
- * skipped when the ticket's `gitActivityCount` is 0 so we don't waste
9
- * a round-trip on the common case.
10
- *
11
- * ORB-272: `/tickets/:id/comments` is cursor-paginated. We pull the
12
- * first page (50 by default) - if the ticket has more, a footer line
13
- * nudges the user to open it in the UI. AI agents asking "give me
14
- * the full history" beyond 50 is a rare enough case not to fan out.
4
+ * @see ORB-272
15
5
  */
16
6
  import { z } from 'zod';
17
7
  import { OrbotoApiError } from '../orboto-client.js';
@@ -33,29 +23,12 @@ export function makeGetTicketHandler(client) {
33
23
  const ticket = await resolveTicketByKey(client, ticketKey);
34
24
  const inc = new Set(include ?? []);
35
25
  const parentId = ticket.parentTicketId ?? null;
36
- // ORB-1698 - the default response is the decision CARD: identity +
37
- // description + progress + counts. The heavy blocks (comment bodies,
38
- // git rows, checklist items) are fetched only when asked for via
39
- // `include` - measured, they were the bulk of a 1.02 Mtok carry cost.
40
- // Children + attachments have no count on the enriched row, so their
41
- // (cheap, metadata-only) fetches stay - the RESPONSE carries only the
42
- // count unless included.
43
26
  const [enriched, parent, childrenPage, attachments, commentsPage, checklists, gitActivity] = await Promise.all([
44
- // ORB-1023 - `resolveTicketByKey` hits the by-key endpoint, which
45
- // returns a BARE ticket row (no statusCategory, assignees, labels,
46
- // milestoneName, counts). Re-fetch the enriched by-id shape; falls
47
- // back to the bare row on a 404.
48
27
  client.get(`/projects/${ticket.projectId}/tickets/${ticket.id}`).catch(swallow404(null)),
49
- // Parent ticket - only fetched when set. Lets the model say
50
- // "this is sub-ticket of [ACME-10]" without a second tool call.
51
28
  parentId
52
29
  ? client.get(`/projects/${ticket.projectId}/tickets/${parentId}`).catch(swallow404(null))
53
30
  : Promise.resolve(null),
54
- // Children via the parentTicketId filter (API-side, O(children)).
55
- // Cap at 50; anything bigger should use `orboto_list_tickets
56
- // --parentTicketKey` and paginate explicitly.
57
31
  client.get(`/projects/${ticket.projectId}/tickets?parentTicketId=${ticket.id}&limit=50`).catch(swallow404({ items: [], nextCursor: null })),
58
- // ORB-1455 - attachments metadata so the agent knows files exist.
59
32
  client.get(`/tickets/${ticket.id}/attachments`).catch(swallow404([])),
60
33
  inc.has('comments')
61
34
  ? client.get(`/tickets/${ticket.id}/comments?limit=${COMMENT_PAGE_SIZE}`).catch(swallow404({ items: [], nextCursor: null }))
@@ -70,8 +43,6 @@ export function makeGetTicketHandler(client) {
70
43
  const comments = commentsPage.items;
71
44
  const hasMoreComments = !!commentsPage.nextCursor;
72
45
  const children = childrenPage.items;
73
- // ORB-1023 - prefer the enriched by-id row (statusCategory, assignees,
74
- // labels, milestoneName); fall back to the bare resolver row.
75
46
  const full = enriched ?? ticket;
76
47
  const commentCount = full.commentCount ?? (inc.has('comments') ? comments.length : 0);
77
48
  const gitCount = full.gitActivityCount ?? (inc.has('git') ? gitActivity.length : 0);
@@ -92,7 +63,6 @@ export function makeGetTicketHandler(client) {
92
63
  return {
93
64
  content: [{ type: 'text', text: formatTicket(full, inc, comments, hasMoreComments, checklists, gitActivity, parent, children, attachments, includeHint) }],
94
65
  structuredContent: {
95
- // ORB-1179 - surface the uuid alongside the key.
96
66
  id: full.id,
97
67
  key: full.ticketKey,
98
68
  title: full.title,
@@ -103,22 +73,14 @@ export function makeGetTicketHandler(client) {
103
73
  : null,
104
74
  priority: full.priority,
105
75
  type: full.type,
106
- // ORB-1608 - role-aware commit policy. The API defaults unset
107
- // rows to 'implementation'.
108
76
  deliveryMode: full.deliveryMode ?? 'implementation',
109
77
  dueDate: full.dueDate,
110
78
  startDate: full.startDate,
111
79
  isPrivate: full.isPrivate,
112
80
  estimatedTimeMinutes: full.estimatedTimeMinutes,
113
81
  loggedMinutes: full.loggedMinutes ?? 0,
114
- // ORB-1605 - true when in_review, zero ingested git_activities,
115
- // but the project HAS an active git connection: closing
116
- // verification may be blocked on stalled ingestion.
117
82
  waitingForGitIngestion: full.waitingForGitIngestion ?? false,
118
83
  description: full.description ?? null,
119
- // Hierarchy - null when no parent, array of summary rows for
120
- // children (empty array when none). Sub-ticket consumers can
121
- // decide to call orboto_get_ticket on each for the full detail.
122
84
  parentTicket: parent ? {
123
85
  key: parent.ticketKey,
124
86
  title: parent.title,
@@ -135,14 +97,10 @@ export function makeGetTicketHandler(client) {
135
97
  })),
136
98
  } : {}),
137
99
  assignees: full.assignees ?? [],
138
- // ORB-1034 - RACI roster (R/A/C/I); opt-in via include: ["raci"]
139
- // (assignees above stay the always-on Responsible+Accountable set).
140
100
  ...(inc.has('raci') ? {
141
101
  raci: (full.raci ?? []).map((r) => ({ userId: r.userId, fullName: r.fullName, role: r.role })),
142
102
  } : {}),
143
103
  labels: (full.labels ?? []).map((l) => l.name),
144
- // ORB-1698 - counts always; bodies opt-in. checklistProgress is the
145
- // aggregate from the enriched row (items via include).
146
104
  commentCount,
147
105
  gitActivityCount: gitCount,
148
106
  attachmentCount: attachments.length,
@@ -167,10 +125,6 @@ export function makeGetTicketHandler(client) {
167
125
  items: cl.items.map((i) => ({
168
126
  content: i.content,
169
127
  done: i.effectiveCompleted,
170
- // When the item links to another ticket, `effectiveCompleted`
171
- // mirrors that ticket's status-category instead of this item's
172
- // own checkbox. Surface the link so the model can explain
173
- // why the item is/isn't done.
174
128
  linkedTicket: i.linkedTicketKey ? {
175
129
  key: i.linkedTicketKey,
176
130
  title: i.linkedTicketTitle,
@@ -189,8 +143,6 @@ export function makeGetTicketHandler(client) {
189
143
  createdAt: g.createdAt,
190
144
  })),
191
145
  } : {}),
192
- // ORB-1455 - attachments (id + metadata). Feed an id to
193
- // orboto_get_attachment to view an image or fetch the bytes.
194
146
  ...(inc.has('attachments') ? {
195
147
  attachments: attachments.map((a) => ({
196
148
  id: a.id,
@@ -215,18 +167,12 @@ function formatTicket(ticket, inc, comments, hasMoreComments, checklists, gitAct
215
167
  const header = [
216
168
  `[${ticket.ticketKey}] ${ticket.title}`,
217
169
  `Status: ${ticket.statusName ?? ticket.status} Priority: ${ticket.priority} Type: ${ticket.type}`,
218
- // ORB-1608 - only shown when it deviates from the 'implementation'
219
- // default, so a fresh ticket's card stays uncluttered.
220
170
  ticket.deliveryMode && ticket.deliveryMode !== 'implementation'
221
171
  ? `Delivery mode: ${ticket.deliveryMode}`
222
172
  : null,
223
- // ORB-1605 - surface the stalled-ingestion signal right in the
224
- // header so an agent checking "is this really done?" sees it
225
- // before reading the (currently empty) git-activity section.
226
173
  ticket.waitingForGitIngestion
227
174
  ? '⏳ Waiting for Git ingestion - this project has an active git connection but no commits/PRs have landed for this ticket yet. Closing verification may be blocked on stalled ingestion, not on unfinished work - check manually before assuming it is unlinked.'
228
175
  : null,
229
- // ORB-1023 - show the milestone name (not the UUID) when set.
230
176
  ticket.milestoneId ? `Milestone: ${ticket.milestoneName ?? '(unnamed)'}` : null,
231
177
  ticket.dueDate ? `Due: ${ticket.dueDate}` : null,
232
178
  parent ? `Parent: [${parent.ticketKey}] ${parent.title} (${parent.statusName ?? parent.status})` : null,
@@ -236,8 +182,6 @@ function formatTicket(ticket, inc, comments, hasMoreComments, checklists, gitAct
236
182
  ticket.assignees && ticket.assignees.length > 0
237
183
  ? `Assignees: ${ticket.assignees.map((a) => a.fullName || a.email).join(', ')}`
238
184
  : 'Assignees: (unassigned)',
239
- // ORB-1034 - RACI summary, shown only when the project uses RACI and
240
- // someone holds a non-Responsible role (Accountable / Consulted / Informed).
241
185
  ticket.raci && ticket.raci.some((r) => r.role !== 'R')
242
186
  ? `RACI: ${['A', 'R', 'C', 'I']
243
187
  .map((role) => {
@@ -270,8 +214,6 @@ function formatTicket(ticket, inc, comments, hasMoreComments, checklists, gitAct
270
214
  const progressLabel = `${cl.progress.done}/${cl.progress.total}`;
271
215
  checklistLines.push(`### ${cl.title} (${progressLabel})${cl.triggersDone ? ' · triggers done' : ''}`);
272
216
  for (const i of cl.items) {
273
- // Linked-ticket suffix on items that track another ticket -
274
- // lets the model say "item X is done because [ACME-42] shipped".
275
217
  const link = i.linkedTicketKey
276
218
  ? ` ↪ [${i.linkedTicketKey}] ${i.linkedTicketTitle ?? ''} (${i.linkedTicketStatusCategory ?? 'unknown'})`
277
219
  : '';
@@ -302,8 +244,6 @@ function formatTicket(ticket, inc, comments, hasMoreComments, checklists, gitAct
302
244
  gitLines.push(`- ${g.type} ${g.state ? `[${g.state}]` : ''} ${g.title}${g.url ? ` - ${g.url}` : ''}`);
303
245
  }
304
246
  }
305
- // ORB-1455 - attachments so the agent knows files exist. Feed an id to
306
- // orboto_get_attachment to view an image or fetch the bytes.
307
247
  const attachmentLines = [];
308
248
  if (!inc.has('attachments') && attachments.length > 0) {
309
249
  attachmentLines.push('', `Attachments: ${attachments.length}`);
@@ -1,15 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase B - `orboto_get_timer`.
3
- *
4
- * Returns the caller's active stopwatch (or `null` when none is
5
- * running). Useful for the model to answer "am I still tracking
6
- * time on that bug from earlier?" without guessing.
7
- *
8
- * API shape: `GET /time/timer` returns `ActiveTimer | null` directly
9
- * (not wrapped). ActiveTimer carries `ticketTitle` and `projectId`
10
- * as joined fields but no `ticketKey` - so the ticket identifier is
11
- * resolved the cheap way: first 8 chars of the UUID when no title
12
- * is available, just the title otherwise.
13
3
  */
14
4
  import { z } from 'zod';
15
5
  export const getTimerToolConfig = {
@@ -20,7 +10,6 @@ export const getTimerToolConfig = {
20
10
  };
21
11
  export function makeGetTimerHandler(client) {
22
12
  return async () => {
23
- // `/time/timer` returns the timer row directly, or `null`.
24
13
  const timer = await client.get('/time/timer');
25
14
  if (!timer) {
26
15
  return {
@@ -1,4 +1,3 @@
1
- // Shared rendering vocabulary for session-start and work-session bundles.
2
1
  export const GIT_HEALTH_REASON_TEXT = {
3
2
  connection_inactive: 'connection is deactivated',
4
3
  app_installation_suspended: 'GitHub App installation is suspended',
@@ -1,15 +1,5 @@
1
1
  /**
2
2
  * ORB-799 - identity / debug tools.
3
- *
4
- * `orboto_whoami` - return the authenticated principal as `{id, email,
5
- * fullName, isBot}`. Mirrors `orboto.mjs whoami`. The wrapper hits the
6
- * existing `/users/me` route directly; we do the same so a stale-token
7
- * 401 surfaces the same way it does for every other tool.
8
- *
9
- * Useful for debugging "which API key is this session actually using?",
10
- * which is the most common confusion when an agent has multiple MCP
11
- * configs (`claude-desktop`, `cursor`, `gemini-cli`) wired to different
12
- * `orb_*` keys.
13
3
  */
14
4
  import { z } from 'zod';
15
5
  export const whoamiToolConfig = {
@@ -22,7 +12,6 @@ export const whoamiToolConfig = {
22
12
  fullName: z.string().nullable(),
23
13
  isBot: z.boolean(),
24
14
  workspaceLocale: z.string().nullable(),
25
- // ORB-1671 - both identities of a delegated act-as connection.
26
15
  actingAuthorizedBy: z.object({ id: z.string(), email: z.string(), fullName: z.string() }).nullable(),
27
16
  }).shape,
28
17
  annotations: { readOnlyHint: true, idempotentHint: true },
@@ -40,8 +29,6 @@ export function makeWhoamiHandler(client) {
40
29
  if (workspaceLocale) {
41
30
  lines.push(` workspace language: ${workspaceLocale} (write tickets in this language)`);
42
31
  }
43
- // ORB-1671 - an act-as connection reports BOTH identities so the agent
44
- // (and a human debugging it) can see who it acts as and who authorised it.
45
32
  if (me.actingAuthorizedBy) {
46
33
  lines.push(` acting as this bot, authorised by: ${me.actingAuthorizedBy.fullName} <${me.actingAuthorizedBy.email}>`);
47
34
  }
@@ -1,14 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase A reference tool - `orboto_list_projects`.
3
- *
4
- * Maps to `GET /projects` in the orboto API, which already filters by
5
- * the caller's visibility via the PBAC cascade. The MCP server is a
6
- * transport adapter - it doesn't re-implement the ACL.
7
- *
8
- * Rest of the read-tool suite follows the same shape (see
9
- * ticket ORB-244 Phase B). Keeping this first tool narrow on
10
- * purpose - it's the one we wire + verify end-to-end before
11
- * scaling out.
12
3
  */
13
4
  import { z } from 'zod';
14
5
  export const listProjectsToolConfig = {
@@ -17,19 +8,12 @@ export const listProjectsToolConfig = {
17
8
  + 'If you are after one project, pass `query` to filter by key/name instead of pulling the whole list - '
18
9
  + 'and note you can usually use a project key directly with other tools without listing at all. '
19
10
  + 'When the result says it is partial, refine with `query` rather than re-calling.',
20
- // ORB-1109 - query + limit so a small-context agent (self-hosted /
21
- // local 32k models over MCP) can narrow instead of ingesting every
22
- // project.
23
11
  inputSchema: z.object({
24
12
  query: z.string().optional().describe('Substring matched against key or name.'),
25
13
  limit: z.number().int().min(1).max(200).optional().describe('Max projects. Default 50.'),
26
14
  }).shape,
27
- // Well-formed output schema so MCP clients that honour it can
28
- // surface structured data. Adding fields is safe; removing them is a
29
- // breaking change for pinned client configs.
30
15
  outputSchema: z.object({
31
16
  projects: z.array(z.object({
32
- // ORB-1179 - uuid alongside the key for downstream write tools.
33
17
  id: z.string(),
34
18
  key: z.string(),
35
19
  name: z.string(),
@@ -52,10 +36,6 @@ export function makeListProjectsHandler(client) {
52
36
  const cap = Math.min(limit ?? 50, 200);
53
37
  const shown = matched.slice(0, cap);
54
38
  const rows = shown.map((p) => ({ id: p.id, key: p.key, name: p.name, status: p.status, description: p.description }));
55
- // Text content is what the model reads; structured content is what
56
- // the client UI renders. The text stays description-free (compact)
57
- // and ends with a count line so a small model knows whether it has
58
- // the whole set.
59
39
  const lines = rows.map((r) => `- ${r.key} - ${r.name} (${r.status})`);
60
40
  const partial = shown.length < matched.length;
61
41
  const footer = matched.length === 0
@@ -32,7 +32,6 @@ describe('tools/list-projects', () => {
32
32
  });
33
33
  expect(result.structuredContent).toEqual({
34
34
  projects: [
35
- // ORB-1179 - uuid surfaced alongside the key
36
35
  { id: 'p1', key: 'ACME', name: 'Acme', status: 'active', description: 'Customer portal' },
37
36
  { id: 'p2', key: 'TOOL', name: 'Internal Tools', status: 'draft', description: null },
38
37
  ],
@@ -12,7 +12,7 @@ import { resolveProjectByKey, resolveTicketByKey, ticketLine, agentTicketListRow
12
12
  import { resolveMilestoneByNameOrId } from './milestones.js';
13
13
  export const listTicketsToolConfig = {
14
14
  title: 'List tickets',
15
- description: 'List tickets in a project, optionally filtered by status category, milestone name, or assignee email. Returns up to 50 tickets per call.',
15
+ description: 'List tickets in a project, optionally filtered by status category, milestone name, or assignee email. Set unscheduled=true for the product backlog (everything not planned into a milestone yet). Returns up to 50 tickets per call.',
16
16
  inputSchema: z.object({
17
17
  projectKey: z.string().min(1).describe('Project key (e.g. "ACME").'),
18
18
  statusCategory: z
@@ -23,6 +23,10 @@ export const listTicketsToolConfig = {
23
23
  .string()
24
24
  .optional()
25
25
  .describe('Key (ORB-M3), name, or UUID. Omit for all, backlog included.'),
26
+ unscheduled: z
27
+ .boolean()
28
+ .optional()
29
+ .describe('true = only the backlog: tickets with no milestone. Cannot be combined with `milestone`.'),
26
30
  assigneeEmail: z
27
31
  .string()
28
32
  .optional()
@@ -43,18 +47,16 @@ export function makeListTicketsHandler(client) {
43
47
  qs.set('limit', String(input.limit ?? 25));
44
48
  if (input.statusCategory)
45
49
  qs.set('statusCategory', input.statusCategory);
46
- // Milestone + assignee need a UUID on the API; resolve them here
47
- // from the project payload instead of forcing the caller to paste
48
- // a UUID.
50
+ if (input.unscheduled && input.milestone) {
51
+ throw new Error('Pass either `milestone` or `unscheduled: true`, not both.');
52
+ }
53
+ if (input.unscheduled)
54
+ qs.set('unscheduled', 'true');
49
55
  if (input.milestone) {
50
- // ORB-1696 - shared resolver: key (ORB-M3), name or UUID, ambiguous
51
- // name -> explicit error. Matches create_ticket/set_milestone/OQL.
52
56
  const m = await resolveMilestoneByNameOrId(client, project.id, input.milestone);
53
57
  qs.set('milestoneId', m.id);
54
58
  }
55
59
  if (input.assigneeEmail) {
56
- // Members endpoint returns `{userId, user: {email, ...}, role: {...}}`;
57
- // we need to peek inside `user` to match by email.
58
60
  const members = await client.get(`/projects/${project.id}/members`);
59
61
  const member = members.find((x) => x.user.email.toLowerCase() === input.assigneeEmail.toLowerCase());
60
62
  if (!member)
@@ -78,7 +80,6 @@ export function makeListTicketsHandler(client) {
78
80
  project: { key: project.key },
79
81
  count: page.items.length,
80
82
  hasMore: !!page.nextCursor,
81
- // ORB-1699 - shared lean row; verbose restores uuid/labels/minutes.
82
83
  tickets: page.items.map((t) => agentTicketListRow(t, input.verbose ?? false)),
83
84
  },
84
85
  };
@@ -46,9 +46,6 @@ describe('orboto_create_milestone', () => {
46
46
  const res = await makeCreateMilestoneHandler(client)({
47
47
  projectKey: 'ACME', name: 'v2.0',
48
48
  });
49
- // ORB-1825 - startDate/endDate are nullable().optional() on the API's
50
- // create body now; an absent key means the same thing as null, so the
51
- // tool no longer forces `?? null` and the wire body simply omits them.
52
49
  expect(calls[1]).toMatchObject({
53
50
  method: 'POST',
54
51
  url: 'https://orboto.example.com/projects/p1/milestones',
@@ -118,7 +115,6 @@ describe('orboto_close_milestone', () => {
118
115
  await makeCloseMilestoneHandler(client)({
119
116
  projectKey: 'ACME', milestone: '11111111-2222-3333-4444-555555555555',
120
117
  });
121
- // PATCH URL should hit the UUID, not the first row.
122
118
  expect(calls[2].url).toContain('/milestones/11111111-2222-3333-4444-555555555555');
123
119
  });
124
120
  it('throws a clear error when milestone is not found', async () => {
@@ -1,21 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase B - milestone tools, expanded in ORB-799 with CRUD.
3
- *
4
- * `orboto_list_milestones` and `orboto_get_milestone` share this file
5
- * because they're cheap neighbours (same API root, same resolution
6
- * chain). The `get` tool also pulls the `/progress` endpoint so the
7
- * model sees ticket-count breakdowns alongside the metadata.
8
- *
9
- * ORB-799 added the write half of the surface to close wrapper-parity:
10
- *
11
- * - orboto_create_milestone - mirrors `orboto.mjs create-milestone`.
12
- * `startDate`/`endDate` are both optional; a milestone without dates
13
- * is a legitimate object (analytics/Gantt/templates treat null dates
14
- * as "no dates").
15
- * - orboto_close_milestone - close + optional archive. Resolves the
16
- * milestone by name OR UUID against the includeClosed=true list so
17
- * re-closing an already-closed milestone is idempotent.
18
- * - orboto_update_milestone - patch name / dates / private flag.
19
3
  */
20
4
  import { z } from 'zod';
21
5
  import { OrbotoApiError } from '../orboto-client.js';
@@ -24,9 +8,6 @@ const UUID_RE = /^[0-9a-f-]{36}$/i;
24
8
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
25
9
  /** ORB-1059 - a milestone key looks like `ORB-M3`. */
26
10
  const MILESTONE_KEY_RE = /^[A-Za-z0-9]+-M\d+$/i;
27
- // ---------------------------------------------------------------------------
28
- // orboto_list_milestones
29
- // ---------------------------------------------------------------------------
30
11
  export const listMilestonesToolConfig = {
31
12
  title: 'List milestones',
32
13
  description: 'List milestones in a project, newest first.',
@@ -51,7 +32,6 @@ export function makeListMilestonesHandler(client) {
51
32
  structuredContent: {
52
33
  project: { key: project.key },
53
34
  milestones: milestones.map((m) => ({
54
- // ORB-1179 - surface the uuid alongside the milestone key.
55
35
  id: m.id,
56
36
  milestoneKey: m.milestoneKey ?? null,
57
37
  name: m.name,
@@ -64,9 +44,6 @@ export function makeListMilestonesHandler(client) {
64
44
  };
65
45
  };
66
46
  }
67
- // ---------------------------------------------------------------------------
68
- // orboto_get_milestone
69
- // ---------------------------------------------------------------------------
70
47
  export const getMilestoneToolConfig = {
71
48
  title: 'Get milestone details',
72
49
  description: 'Return milestone metadata plus ticket-count breakdown by status (to do / in progress / done / …).',
@@ -79,12 +56,7 @@ export const getMilestoneToolConfig = {
79
56
  export function makeGetMilestoneHandler(client) {
80
57
  return async ({ projectKey, milestone }) => {
81
58
  const project = await resolveProjectByKey(client, projectKey);
82
- // ORB-1068 - resolve key / name / UUID through the shared resolver
83
- // instead of an exact-name find, so `ORB-M19` works here too.
84
59
  const m = await resolveMilestoneByNameOrId(client, project.id, milestone);
85
- // Progress comes from a separate endpoint; tolerate 404 gracefully
86
- // in case a future API rename drops it so the tool still returns
87
- // the metadata half.
88
60
  const progress = await client.get(`/projects/${project.id}/milestones/${m.id}/progress`).catch((err) => {
89
61
  if (err instanceof OrbotoApiError && err.status === 404)
90
62
  return null;
@@ -121,9 +93,6 @@ export function makeGetMilestoneHandler(client) {
121
93
  };
122
94
  };
123
95
  }
124
- // ---------------------------------------------------------------------------
125
- // orboto_create_milestone - ORB-799
126
- // ---------------------------------------------------------------------------
127
96
  export const createMilestoneToolConfig = {
128
97
  title: 'Create a milestone',
129
98
  description: 'Create a milestone in a project. `startDate` + `endDate` are optional - omit them (or pass null) for a milestone with no dates. The caller must have `milestone:create`.',
@@ -163,9 +132,6 @@ export function makeCreateMilestoneHandler(client) {
163
132
  };
164
133
  };
165
134
  }
166
- // ---------------------------------------------------------------------------
167
- // orboto_close_milestone - ORB-799
168
- // ---------------------------------------------------------------------------
169
135
  /** Resolve a milestone by name OR UUID against the includeClosed list,
170
136
  * so closing an already-completed milestone (re-close), archiving a
171
137
  * completed milestone, and re-pointing a ticket onto any milestone all
@@ -183,7 +149,6 @@ export async function resolveMilestoneByNameOrId(client, projectId, nameOrId) {
183
149
  }
184
150
  return byId;
185
151
  }
186
- // ORB-1059 - accept the human-readable key (`ORB-M3`), case-insensitive.
187
152
  if (MILESTONE_KEY_RE.test(nameOrId)) {
188
153
  const byKey = all.find((x) => x.milestoneKey?.toLowerCase() === nameOrId.toLowerCase());
189
154
  if (!byKey) {
@@ -191,11 +156,6 @@ export async function resolveMilestoneByNameOrId(client, projectId, nameOrId) {
191
156
  }
192
157
  return byKey;
193
158
  }
194
- // ORB-1826 - exact (raw) name match wins first; falls back to a unique
195
- // normalised match (HTML-entity-decoded, trimmed, whitespace-collapsed,
196
- // casefolded) so `"QA &amp; Testing"` resolves against a milestone
197
- // literally named `"QA & Testing"`. Ambiguous either way still errors,
198
- // listing the candidates.
199
159
  const { match, ambiguous } = resolveByName(all, nameOrId, (m) => m.name);
200
160
  if (ambiguous) {
201
161
  const list = ambiguous.map((m) => `"${m.name}" (${m.id})`).join(', ');
@@ -236,9 +196,6 @@ export function makeCloseMilestoneHandler(client) {
236
196
  };
237
197
  };
238
198
  }
239
- // ---------------------------------------------------------------------------
240
- // orboto_update_milestone - ORB-799
241
- // ---------------------------------------------------------------------------
242
199
  export const updateMilestoneToolConfig = {
243
200
  title: 'Update a milestone\'s fields',
244
201
  description: 'Patch a milestone (`name`, `customerSummary`, `startDate`, `endDate`, `isPrivate`). At least one field must be set. `customerSummary` is the customer-facing text shown in the customer project report instead of any internal text. Use `orboto_close_milestone` to flip status to completed/archived - this tool intentionally does NOT touch the status field so closing remains a clear semantic operation.',
@@ -21,10 +21,6 @@ export const myTicketsToolConfig = {
21
21
  }).shape,
22
22
  annotations: { readOnlyHint: true, idempotentHint: true },
23
23
  };
24
- // The API route's `statuses` param is the uppercase LEGACY status
25
- // enum (TODO/IN_PROGRESS/IN_REVIEW/DONE/WONT_FIX). The MCP tool
26
- // accepts the lower-case category enum the rest of the surface uses;
27
- // map here.
28
24
  const CATEGORY_TO_LEGACY = {
29
25
  todo: 'TODO',
30
26
  in_progress: 'IN_PROGRESS',
@@ -40,8 +36,6 @@ export function makeMyTicketsHandler(client) {
40
36
  qs.set('statuses', CATEGORY_TO_LEGACY[input.statusCategory]);
41
37
  }
42
38
  else {
43
- // Default: exclude done + wont_fix so "what am I working on?"
44
- // returns open work rather than a lifetime history.
45
39
  qs.set('statuses', 'TODO,IN_PROGRESS,IN_REVIEW');
46
40
  }
47
41
  const page = await client.get(`/users/me/assigned-tickets?${qs}`);
@@ -57,7 +51,6 @@ export function makeMyTicketsHandler(client) {
57
51
  structuredContent: {
58
52
  count: page.items.length,
59
53
  hasMore: !!page.nextCursor,
60
- // ORB-1699 - shared lean row; verbose restores uuid/labels/minutes.
61
54
  tickets: page.items.map((t) => agentTicketListRow(t, input.verbose ?? false)),
62
55
  },
63
56
  };