@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
package/dist/server.js CHANGED
@@ -1,17 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase A/B - MCP server factory.
3
- *
4
- * Builds an `McpServer` with the registered tool set + a handle to
5
- * the orboto REST client. Transport is picked by the process entry
6
- * point (`index.ts`) based on `ORBOTO_MCP_TRANSPORT=stdio|http`.
7
- *
8
- * Keeping the server factory transport-agnostic means `server.ts` is
9
- * the same object whether we ship stdio for Local-Proxy (Phase G
10
- * `@orboto/mcp-cli`) or HTTP-SSE for Self-Hosted-inline.
11
- *
12
- * Tool registrations stay in this file so adding a new tool in
13
- * Phase C is one diff here + one new file in `tools/`. Phase D will
14
- * add `registerResource` / `registerPrompt` calls here too.
15
3
  */
16
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
17
5
  import { VERSION } from './version.js';
@@ -68,12 +56,11 @@ import { listGitAppInstallationsToolConfig, makeListGitAppInstallationsHandler }
68
56
  import { createTicketToolConfig, makeCreateTicketHandler, updateTicketToolConfig, makeUpdateTicketHandler, moveTicketToolConfig, makeMoveTicketHandler, closeTicketToolConfig, makeCloseTicketHandler, deleteTicketToolConfig, makeDeleteTicketHandler, commentToolConfig, makeCommentHandler, updateCommentToolConfig, makeUpdateCommentHandler, deleteCommentToolConfig, makeDeleteCommentHandler, assignToolConfig, makeAssignHandler, unassignToolConfig, makeUnassignHandler, labelTicketToolConfig, makeLabelTicketHandler, unlabelTicketToolConfig, makeUnlabelTicketHandler, setMilestoneToolConfig, makeSetMilestoneHandler, addTicketDependencyToolConfig, makeAddTicketDependencyHandler, removeTicketDependencyToolConfig, makeRemoveTicketDependencyHandler, listTicketDependenciesToolConfig, makeListTicketDependenciesHandler, } from './tools/ticket-writes.js';
69
57
  import { timerStartToolConfig, makeTimerStartHandler, timerStopToolConfig, makeTimerStopHandler, logTimeToolConfig, makeLogTimeHandler, listTimeEntriesToolConfig, makeListTimeEntriesHandler, editTimeEntryToolConfig, makeEditTimeEntryHandler, deleteTimeEntryToolConfig, makeDeleteTimeEntryHandler, } from './tools/time-writes.js';
70
58
  import { listTicketSchedulesToolConfig, makeListTicketSchedulesHandler, scheduleTicketSessionToolConfig, makeScheduleTicketSessionHandler, cancelTicketSessionToolConfig, makeCancelTicketSessionHandler, } from './tools/ticket-schedules.js';
71
- import { checkToolConfig, makeCheckHandler, uncheckToolConfig, makeUncheckHandler, addCheckToolConfig, makeAddCheckHandler, removeCheckToolConfig, makeRemoveCheckHandler, newChecklistToolConfig, makeNewChecklistHandler, } from './tools/checklist-writes.js';
59
+ import { checkToolConfig, makeCheckHandler, uncheckToolConfig, makeUncheckHandler, addCheckToolConfig, makeAddCheckHandler, removeCheckToolConfig, makeRemoveCheckHandler, updateCheckToolConfig, makeUpdateCheckHandler, newChecklistToolConfig, makeNewChecklistHandler, } from './tools/checklist-writes.js';
72
60
  import { listUsersToolConfig, makeListUsersHandler, getAuditLogToolConfig, makeGetAuditLogHandler, triggerBackupToolConfig, makeTriggerBackupHandler, } from './tools/admin-writes.js';
73
61
  import { freeBusyToolConfig, makeFreeBusyHandler } from './tools/free-busy.js';
74
62
  import { createFullBackupToolConfig, makeCreateFullBackupHandler, listBackupsToolConfig, makeListBackupsHandler, downloadBackupToolConfig, makeDownloadBackupHandler, } from './tools/backup.js';
75
63
  import { primerFactListToolConfig, makePrimerFactListHandler, primerFactAddToolConfig, makePrimerFactAddHandler, primerFactUpdateToolConfig, makePrimerFactUpdateHandler, primerFactSupersedeToolConfig, makePrimerFactSupersedeHandler, primerFactVerifyToolConfig, makePrimerFactVerifyHandler, primerFactDeleteToolConfig, makePrimerFactDeleteHandler, } from './tools/primer-facts.js';
76
- // ORB-799 - wrapper-feature-parity gap-close.
77
64
  import { whoamiToolConfig, makeWhoamiHandler } from './tools/identity.js';
78
65
  import { claimToolConfig, makeClaimHandler, unclaimToolConfig, makeUnclaimHandler, } from './tools/claim.js';
79
66
  import { reportFeedbackToolConfig, makeReportFeedbackHandler } from './tools/feedback.js';
@@ -103,18 +90,7 @@ const FALLBACK_WORKING_RULES = [
103
90
  'When you write a git commit that touches a ticket, put the ticket key (e.g. `ORB-42`) in parentheses at the END of the subject line - `feat(auth): add token rotation (ORB-42)`. The orboto git-activity parser links the commit by that key.',
104
91
  'Use sub-tickets for steps large enough to need their own commit / time tracking / review, and checklists for one-liners inside a single ticket\'s scope. Big features (Epic + 3+ phase tickets): create a milestone FIRST, then the Epic, then phase tickets as children on that milestone.',
105
92
  ].join(' ');
106
- // Static MCP operational hints - tool/resource/prompt usage that does
107
- // not change per workspace. The configurable working-rules are appended
108
- // live below. ORB-1520 - assembled per toolset: the curated manifest
109
- // gets the escape-hatch pointer, the full manifest keeps the
110
- // get_checklists hint (that tool only exists there).
111
93
  function staticMcpHints(toolset) {
112
- // ORB-1805 - the minimal tier serves 8k/16k local models, where the
113
- // instructions block competes with the user's own turn for the same
114
- // few thousand tokens. Everything that is merely NICE to know
115
- // (resources, prompts, checklist semantics, primer) is dropped here:
116
- // `orboto_session_start` delivers the authoritative rules on the first
117
- // call anyway, and `orboto_api_search` reaches the rest.
118
94
  if (toolset === 'minimal') {
119
95
  return [
120
96
  'orboto is a ticket + project management system. Ticket keys look like `PROJ-123`; the first segment is the project key.',
@@ -139,14 +115,6 @@ function staticMcpHints(toolset) {
139
115
  'All writes respect the caller\'s project-level permissions - a 403 means the API rejected the write, not the MCP server.',
140
116
  ].filter((s) => s.length > 0).join(' ');
141
117
  }
142
- // ORB-1177 - MCP clients cap how much of the `instructions` block they
143
- // inject and silently truncate the overflow (ORB-1168 saw a 5k+ string
144
- // cut mid-rule). Enforce our own budget so the high-priority head (hints
145
- // + non-negotiables + the orboto_session_start / orboto://rules pointers)
146
- // always survives, and the workspace rules are cut at a WHOLE-LINE
147
- // boundary with an explicit pointer to the cap-independent full set,
148
- // rather than the client slicing mid-sentence. The complete rules are
149
- // always available via orboto_session_start + the orboto://rules resource.
150
118
  const INSTRUCTIONS_BUDGET = 4000;
151
119
  const RULES_HEADING = 'Working rules for this workspace:\n';
152
120
  const TRUNCATION_MARKER = '\n\n[... rules truncated to fit the client cap - read the COMPLETE rules via the orboto_session_start tool or the orboto://rules resource ...]';
@@ -154,7 +122,7 @@ export function assembleInstructions(head, workingRules, budget = INSTRUCTIONS_B
154
122
  const full = `${head}\n\n${RULES_HEADING}${workingRules}`;
155
123
  if (full.length <= budget)
156
124
  return full;
157
- const room = budget - head.length - RULES_HEADING.length - TRUNCATION_MARKER.length - 2; // 2 = the '\n\n' join
125
+ const room = budget - head.length - RULES_HEADING.length - TRUNCATION_MARKER.length - 2;
158
126
  let kept = '';
159
127
  if (room > 0) {
160
128
  for (const line of workingRules.split('\n')) {
@@ -167,23 +135,10 @@ export function assembleInstructions(head, workingRules, budget = INSTRUCTIONS_B
167
135
  }
168
136
  export async function buildOrbotoMcpServer(opts) {
169
137
  const client = new OrbotoClient(opts);
170
- // ORB-1520 - the manifest mode. Explicit option (per-connection) wins,
171
- // then the process-wide env default, then `curated`.
172
138
  const toolset = resolveToolset(opts.toolset, process.env.ORBOTO_MCP_TOOLSET);
173
- // ORB-1090 - fetch the workspace's configurable working-rules at
174
- // connect so admin edits propagate to every new MCP session. The
175
- // instructions block is the one place every MCP client reliably sees
176
- // the rules (clients don't read the repo's CLAUDE.md). If unavailable,
177
- // keep the connection usable for recovery but gate all other tools.
178
139
  let workingRules = FALLBACK_WORKING_RULES;
179
- // ORB-1471 - the same connect-time fetch also reports whether this
180
- // workspace requires `orboto_session_start` before any other tool call
181
- // (the hard gate). Unknown policy must never silently disable the gate.
182
140
  let requireSessionStart = true;
183
141
  try {
184
- // ORB-1753 - stdio deployments can declare their profile via
185
- // ORBOTO_AGENT_KIND / ORBOTO_MODEL_TIER; api-key standing profiles
186
- // resolve server-side either way.
187
142
  const connectParams = new URLSearchParams();
188
143
  applyAgentProfile(connectParams);
189
144
  const connectQs = connectParams.toString();
@@ -196,33 +151,9 @@ export async function buildOrbotoMcpServer(opts) {
196
151
  workingRules = 'Workspace rules are unavailable. Built-in hints are not a substitute. Call orboto_session_start with forceRules and retry after connectivity or authentication recovers.';
197
152
  }
198
153
  const server = new McpServer({ name: 'orboto', version: VERSION }, {
199
- // ORB-940 - advertise resources/subscribe so MCP-aware clients
200
- // (Claude Desktop, Cursor) wire up live updates instead of
201
- // polling. Even when no bridge is hooked up (stdio sessions)
202
- // the capability stays on; subscriptions just stay quiet.
203
154
  capabilities: {
204
155
  resources: { subscribe: true, listChanged: true },
205
156
  },
206
- // `instructions` appears in the system-prompt-style block some
207
- // MCP clients inject before the user's first message. Static
208
- // operational hints + the live, workspace-configurable working
209
- // rules (ORB-1086).
210
- //
211
- // ORB-1168 - MCP clients cap how much of this block they inject; a
212
- // 5k+ string was being silently truncated mid-rule, so an MCP-only
213
- // agent (no skill / no repo CLAUDE.md) got incomplete rules. Put
214
- // the orientation, a pointer to the full rule set (the
215
- // orboto_session_start tool), and the core non-negotiables
216
- // FIRST so they survive any truncation; the full workspace rules
217
- // follow and only their tail is at risk.
218
- // ORB-1805 - the minimal tier ships the HEAD ONLY: no workspace
219
- // rules block at all. On an 8k window every rule line here is a
220
- // line the user's own turn does not get, and the rules are not
221
- // lost - `orboto_session_start` (which the head demands as the
222
- // first action) returns the complete, authoritative set, and its
223
- // length is then the caller's budget to spend, not a fixed tax on
224
- // every connect. It also makes this block a CONSTANT: a workspace
225
- // that grows its rules cannot grow the minimal manifest.
226
157
  instructions: toolset === 'minimal'
227
158
  ? [
228
159
  staticMcpHints(toolset),
@@ -233,71 +164,34 @@ export async function buildOrbotoMcpServer(opts) {
233
164
  'FIRST ACTION this session: call the `orboto_session_start` tool - it returns the complete, authoritative binding rules you must follow (plus your in-progress work). Re-run it after any context compaction. If the rules below look cut off, `orboto_session_start` and the `orboto://rules` resource always have the full set. (Do NOT use `orboto_list_agent_instructions` to read the rules - that manages rule blocks for admins.) Core non-negotiables: ticket-first (claim or create a ticket before touching code), one commit per ticket with the ticket key in the subject line, push after each commit, and never mark work done that is not actually done.',
234
165
  ].join('\n\n'), workingRules),
235
166
  });
236
- // ORB-1331 - one-time session-start nudge. The state object lives for
237
- // the lifetime of THIS server instance: the HTTP transport builds one
238
- // server per session and the stdio transport one per process, so a
239
- // single flag object is per-session (HTTP) / process-local (stdio)
240
- // without any session-lifecycle plumbing. Shared by reference into
241
- // every tool's dispatch wrapper below.
242
167
  const nudgeState = createNudgeState(requireSessionStart);
243
- // ORB-311 - every tool dispatch posts one row to /admin/mcp/instrument
244
- // via the withMetrics wrapper. `reg` is a one-line shim around
245
- // `server.registerTool` that adds the metrics layer at registration
246
- // time; per-tool files stay metrics-unaware.
247
- // ORB-1520 - the toolset gate. Wraps the metrics-registering shim so a
248
- // tail tool in curated mode simply never registers; the reg() CALL
249
- // SITES below stay untouched (the chat-tools drift guard parses them
250
- // from this file's source and must keep seeing the full set).
251
168
  const regAll = registerWithMetrics(server, client, opts.userAgentSuffix, nudgeState);
252
169
  const reg = (toolName, config, handler) => {
253
170
  if (!toolInToolset(toolName, toolset))
254
171
  return;
255
- // ORB-1805 - the minimal tier drops the human-readable `title`
256
- // (measured 580 characters across its 12 tools). `name` is already
257
- // self-describing (orboto_create_ticket) and a client without a
258
- // title falls back to it, so the display loss is small where the
259
- // token pressure is largest. Curated/full keep their titles.
260
172
  const cfg = toolset === 'minimal' && config && 'title' in config
261
173
  ? (() => { const { title: _title, ...rest } = config; return rest; })()
262
174
  : config;
263
175
  regAll(toolName, cfg, handler);
264
176
  };
265
- // Tools - alphabetical-ish by concept. Each tool file owns its
266
- // input/output schema; the server just glues names to handlers.
267
177
  reg('orboto_ai_status', aiStatusToolConfig, makeAiStatusHandler(client));
268
- reg('orboto_draft_customer_reply', draftCustomerReplyToolConfig, makeDraftCustomerReplyHandler(client)); // ORB-2000
178
+ reg('orboto_draft_customer_reply', draftCustomerReplyToolConfig, makeDraftCustomerReplyHandler(client));
269
179
  reg('orboto_embedding_status', embeddingStatusToolConfig, makeEmbeddingStatusHandler(client));
270
180
  reg('orboto_ai_usage', aiUsageToolConfig, makeAiUsageHandler(client));
271
- // ORB-1093 - session-start / post-compact re-orientation digest.
272
181
  reg('orboto_session_start', sessionStartToolConfig, makeSessionStartHandler(client));
273
- // ORB-1697 - the way back from a budget-truncated response. Serves the
274
- // omitted remainder from the in-process store, so capping a response
275
- // never loses content.
276
182
  reg('orboto_response_expand', responseExpandToolConfig, makeResponseExpandHandler());
277
- // ORB-1741 - lazy full guidance: manifest descriptions are one-line
278
- // summaries (tool-docs.ts); this serves the complete text on demand.
279
183
  reg('orboto_help', helpToolConfig, makeHelpHandler());
280
- // ORB-1518 - Code-Mode escape hatch, discovery half: search the live
281
- // OpenAPI spec / fetch one endpoint's schema on demand. Keeps the whole
282
- // REST surface reachable without a named tool per endpoint.
283
184
  reg('orboto_api_search', apiSearchToolConfig, makeApiSearchHandler(client));
284
- // ORB-1519 - the execute half: structured REST proxy through the
285
- // API's full auth + permission chain (POST /system/api-proxy).
286
185
  reg('orboto_api_call', apiCallToolConfig, makeApiCallHandler(client));
287
- // ORB-1089 - manage the configurable coding-agent rule blocks (admin:ai:write).
288
186
  reg('orboto_list_agent_instructions', listAgentInstructionsToolConfig, makeListAgentInstructionsHandler(client));
289
187
  reg('orboto_create_agent_instruction', createAgentInstructionToolConfig, makeCreateAgentInstructionHandler(client));
290
188
  reg('orboto_update_agent_instruction', updateAgentInstructionToolConfig, makeUpdateAgentInstructionHandler(client));
291
189
  reg('orboto_reset_agent_instruction', resetAgentInstructionToolConfig, makeResetAgentInstructionHandler(client));
292
190
  reg('orboto_delete_agent_instruction', deleteAgentInstructionToolConfig, makeDeleteAgentInstructionHandler(client));
293
- // ORB-705 - Multi-Agent Coordination tools (heartbeat, presence,
294
- // directed notify). Layered on the ORB-704 REST surface.
295
191
  reg('orboto_agent_heartbeat', agentHeartbeatToolConfig, makeAgentHeartbeatHandler(client));
296
192
  reg('orboto_agent_presence', agentPresenceToolConfig, makeAgentPresenceHandler(client));
297
193
  reg('orboto_agent_notify', agentNotifyToolConfig, makeAgentNotifyHandler(client));
298
- // ORB-1727 - the receive half of the store-and-forward inbox.
299
194
  reg('orboto_messages', agentMessagesToolConfig, makeAgentMessagesHandler(client));
300
- // ORB-964 - scoped fan-out for multi-agent coordination.
301
195
  reg('orboto_agent_broadcast', agentBroadcastToolConfig, makeAgentBroadcastHandler(client));
302
196
  reg('orboto_list_projects', listProjectsToolConfig, makeListProjectsHandler(client));
303
197
  reg('orboto_get_project', getProjectToolConfig, makeGetProjectHandler(client));
@@ -317,42 +211,27 @@ export async function buildOrbotoMcpServer(opts) {
317
211
  reg('orboto_requirements_spec', requirementsSpecToolConfig, makeRequirementsSpecHandler(client));
318
212
  reg('orboto_list_doc_spaces', listDocSpacesToolConfig, makeListDocSpacesHandler(client));
319
213
  reg('orboto_get_doc', getDocToolConfig, makeGetDocHandler(client));
320
- // ORB-1342 (epic ORB-1339) - context-efficient doc snippet search +
321
- // targeted edits. Wrap GET /docs/search (ORB-1340) and POST
322
- // /docs/:id/edits (ORB-1341). Prefer these over get_doc + update_doc for
323
- // small changes to a large doc: find a passage by snippet, change it by
324
- // diff, never transfer the full body in either direction.
325
214
  reg('orboto_search_docs', searchDocsToolConfig, makeSearchDocsHandler(client));
326
215
  reg('orboto_edit_doc', editDocToolConfig, makeEditDocHandler(client));
327
216
  reg('orboto_edit_doc_section', editDocSectionToolConfig, makeEditDocSectionHandler(client));
328
- // ORB-912 - doc-spaces CRUD + list docs in a space. Closes the
329
- // first slice of the MCP doc-surface parity gap (epic ORB-911).
330
217
  reg('orboto_create_doc_space', createDocSpaceToolConfig, makeCreateDocSpaceHandler(client));
331
218
  reg('orboto_update_doc_space', updateDocSpaceToolConfig, makeUpdateDocSpaceHandler(client));
332
219
  reg('orboto_delete_doc_space', deleteDocSpaceToolConfig, makeDeleteDocSpaceHandler(client));
333
220
  reg('orboto_list_docs_in_space', listDocsInSpaceToolConfig, makeListDocsInSpaceHandler(client));
334
- // ORB-913 - doc-page CRUD (plain create + update + delete + move).
335
221
  reg('orboto_create_doc', createDocToolConfig, makeCreateDocHandler(client));
336
222
  reg('orboto_update_doc', updateDocToolConfig, makeUpdateDocHandler(client));
337
223
  reg('orboto_delete_doc', deleteDocToolConfig, makeDeleteDocHandler(client));
338
224
  reg('orboto_move_doc', moveDocToolConfig, makeMoveDocHandler(client));
339
- // ORB-914 - doc-attachments (upload + list + delete).
340
225
  reg('orboto_upload_doc_attachment', uploadDocAttachmentToolConfig, makeUploadDocAttachmentHandler(client));
341
226
  reg('orboto_list_doc_attachments', listDocAttachmentsToolConfig, makeListDocAttachmentsHandler(client));
342
227
  reg('orboto_delete_doc_attachment', deleteDocAttachmentToolConfig, makeDeleteDocAttachmentHandler(client));
343
- // ORB-1455 - ticket-attachment read + view (list metadata, fetch bytes /
344
- // image content block). Complements orboto_attach_to_ticket (write-only).
345
228
  reg('orboto_list_ticket_attachments', listTicketAttachmentsToolConfig, makeListTicketAttachmentsHandler(client));
346
229
  reg('orboto_get_attachment', getAttachmentToolConfig, makeGetAttachmentHandler(client));
347
- // ORB-915 - doc export (Markdown + PDF).
348
230
  reg('orboto_export_doc_md', exportDocMdToolConfig, makeExportDocMdHandler(client));
349
231
  reg('orboto_export_doc_pdf', exportDocPdfToolConfig, makeExportDocPdfHandler(client));
350
- // ORB-916 - doc revision history (list + get + restore). Closes
351
- // the last slice of the doc-surface parity gap (epic ORB-911).
352
232
  reg('orboto_list_doc_revisions', listDocRevisionsToolConfig, makeListDocRevisionsHandler(client));
353
233
  reg('orboto_get_doc_revision', getDocRevisionToolConfig, makeGetDocRevisionHandler(client));
354
234
  reg('orboto_restore_doc_revision', restoreDocRevisionToolConfig, makeRestoreDocRevisionHandler(client));
355
- // ORB-917 - doc comments (list + post + resolve + delete).
356
235
  reg('orboto_list_doc_comments', listDocCommentsToolConfig, makeListDocCommentsHandler(client));
357
236
  reg('orboto_post_doc_comment', postDocCommentToolConfig, makePostDocCommentHandler(client));
358
237
  reg('orboto_resolve_doc_comment', resolveDocCommentToolConfig, makeResolveDocCommentHandler(client));
@@ -365,13 +244,8 @@ export async function buildOrbotoMcpServer(opts) {
365
244
  reg('orboto_add_cross_project_link', addCrossProjectLinkToolConfig, makeAddCrossProjectLinkHandler(client));
366
245
  reg('orboto_update_cross_project_link', updateCrossProjectLinkToolConfig, makeUpdateCrossProjectLinkHandler(client));
367
246
  reg('orboto_remove_cross_project_link', removeCrossProjectLinkToolConfig, makeRemoveCrossProjectLinkHandler(client));
368
- // ORB-918 - duplicate-space + resolve-links. Closes the doc-surface
369
- // parity epic (ORB-911) - every doc-API endpoint now has an MCP
370
- // pendant.
371
247
  reg('orboto_duplicate_doc_space', duplicateDocSpaceToolConfig, makeDuplicateDocSpaceHandler(client));
372
248
  reg('orboto_resolve_doc_smart_links', resolveDocSmartLinksToolConfig, makeResolveDocSmartLinksHandler(client));
373
- // ORB-855 - LLM-Wiki tools (ingest / ask / lint / plan-apply / record /
374
- // append-section / flag-stale). Thin wrappers over the Phase B/C/D routes.
375
249
  reg('orboto_wiki_ingest_url', wikiIngestUrlToolConfig, makeWikiIngestUrlHandler(client));
376
250
  reg('orboto_wiki_ask', wikiAskToolConfig, makeWikiAskHandler(client));
377
251
  reg('orboto_wiki_lint', wikiLintToolConfig, makeWikiLintHandler(client));
@@ -381,16 +255,12 @@ export async function buildOrbotoMcpServer(opts) {
381
255
  reg('orboto_wiki_append_section', wikiAppendSectionToolConfig, makeWikiAppendSectionHandler(client));
382
256
  reg('orboto_wiki_flag_stale', wikiFlagStaleToolConfig, makeWikiFlagStaleHandler(client));
383
257
  reg('orboto_wiki_save_answer', wikiSaveAnswerToolConfig, makeWikiSaveAnswerHandler(client));
384
- // ORB-862 - personal AI-preference facts (owner-scoped).
385
258
  reg('orboto_personal_fact_list', personalFactListToolConfig, makePersonalFactListHandler(client));
386
259
  reg('orboto_personal_fact_add', personalFactAddToolConfig, makePersonalFactAddHandler(client));
387
260
  reg('orboto_personal_fact_update', personalFactUpdateToolConfig, makePersonalFactUpdateHandler(client));
388
261
  reg('orboto_personal_fact_delete', personalFactDeleteToolConfig, makePersonalFactDeleteHandler(client));
389
262
  reg('orboto_get_timer', getTimerToolConfig, makeGetTimerHandler(client));
390
263
  reg('orboto_list_git_app_installations', listGitAppInstallationsToolConfig, makeListGitAppInstallationsHandler(client));
391
- // ORB-309 Phase C - write tools (Group 1: ticket mutations).
392
- // Each respects the API's PBAC cascade - a 403 surfaces as
393
- // OrbotoApiError → MCP throws → client sees an isError response.
394
264
  reg('orboto_create_ticket', createTicketToolConfig, makeCreateTicketHandler(client));
395
265
  reg('orboto_update_ticket', updateTicketToolConfig, makeUpdateTicketHandler(client));
396
266
  reg('orboto_move_ticket', moveTicketToolConfig, makeMoveTicketHandler(client));
@@ -404,73 +274,45 @@ export async function buildOrbotoMcpServer(opts) {
404
274
  reg('orboto_label_ticket', labelTicketToolConfig, makeLabelTicketHandler(client));
405
275
  reg('orboto_unlabel_ticket', unlabelTicketToolConfig, makeUnlabelTicketHandler(client));
406
276
  reg('orboto_set_milestone', setMilestoneToolConfig, makeSetMilestoneHandler(client));
407
- // ORB-1037 - RACI agent surfaces: read the matrix + set a person's role.
408
277
  reg('orboto_raci', raciToolConfig, makeRaciHandler(client));
409
278
  reg('orboto_set_raci', setRaciToolConfig, makeSetRaciHandler(client));
410
- // ORB-453 - ticket-dependency tools (3-way-sync gap filed after the
411
- // skill wrapper landed in ORB-452). Same idempotent-on-409/404
412
- // semantics as orboto_assign / orboto_unassign.
413
279
  reg('orboto_add_ticket_dependency', addTicketDependencyToolConfig, makeAddTicketDependencyHandler(client));
414
280
  reg('orboto_remove_ticket_dependency', removeTicketDependencyToolConfig, makeRemoveTicketDependencyHandler(client));
415
281
  reg('orboto_list_ticket_dependencies', listTicketDependenciesToolConfig, makeListTicketDependenciesHandler(client));
416
- // ORB-309 Phase C - Group 2: time tools.
417
282
  reg('orboto_timer_start', timerStartToolConfig, makeTimerStartHandler(client));
418
283
  reg('orboto_timer_stop', timerStopToolConfig, makeTimerStopHandler(client));
419
284
  reg('orboto_log_time', logTimeToolConfig, makeLogTimeHandler(client));
420
285
  reg('orboto_list_time_entries', listTimeEntriesToolConfig, makeListTimeEntriesHandler(client));
421
286
  reg('orboto_edit_time_entry', editTimeEntryToolConfig, makeEditTimeEntryHandler(client));
422
287
  reg('orboto_delete_time_entry', deleteTimeEntryToolConfig, makeDeleteTimeEntryHandler(client));
423
- // ORB-626 - ticket meeting schedules (working sessions + iCal invites).
424
288
  reg('orboto_list_ticket_schedules', listTicketSchedulesToolConfig, makeListTicketSchedulesHandler(client));
425
289
  reg('orboto_schedule_ticket_session', scheduleTicketSessionToolConfig, makeScheduleTicketSessionHandler(client));
426
290
  reg('orboto_cancel_ticket_session', cancelTicketSessionToolConfig, makeCancelTicketSessionHandler(client));
427
- // ORB-309 Phase C - Group 3: checklist writes (ORB-234 surface).
428
291
  reg('orboto_check', checkToolConfig, makeCheckHandler(client));
429
292
  reg('orboto_uncheck', uncheckToolConfig, makeUncheckHandler(client));
430
293
  reg('orboto_add_check', addCheckToolConfig, makeAddCheckHandler(client));
431
294
  reg('orboto_remove_check', removeCheckToolConfig, makeRemoveCheckHandler(client));
295
+ reg('orboto_update_check', updateCheckToolConfig, makeUpdateCheckHandler(client));
432
296
  reg('orboto_new_checklist', newChecklistToolConfig, makeNewChecklistHandler(client));
433
- // ORB-309 Phase C - Group 4: admin-only tools. Each call hits a
434
- // route gated on super-admin / admin:* permissions; a non-admin
435
- // caller's 403 surfaces as a readable error from rewrite403().
436
297
  reg('orboto_list_users', listUsersToolConfig, makeListUsersHandler(client));
437
- // ORB-625 - free/busy availability lookup for assignee selection (read).
438
298
  reg('orboto_free_busy', freeBusyToolConfig, makeFreeBusyHandler(client));
439
299
  reg('orboto_get_audit_log', getAuditLogToolConfig, makeGetAuditLogHandler(client));
440
300
  reg('orboto_trigger_backup', triggerBackupToolConfig, makeTriggerBackupHandler(client));
441
301
  reg('orboto_create_full_backup', createFullBackupToolConfig, makeCreateFullBackupHandler(client));
442
302
  reg('orboto_list_backups', listBackupsToolConfig, makeListBackupsHandler(client));
443
303
  reg('orboto_download_backup', downloadBackupToolConfig, makeDownloadBackupHandler(client));
444
- // ORB-510 / ORB-513 - primer-fact tools. Wraps the ORB-511 REST
445
- // surface so agents can record structured project facts that the
446
- // primer renderer (ORB-512) surfaces at session start. The skill
447
- // rule (ORB-514) tells agents *when* to record; these are the *how*.
448
304
  reg('orboto_primer_fact_list', primerFactListToolConfig, makePrimerFactListHandler(client));
449
305
  reg('orboto_primer_fact_add', primerFactAddToolConfig, makePrimerFactAddHandler(client));
450
306
  reg('orboto_primer_fact_update', primerFactUpdateToolConfig, makePrimerFactUpdateHandler(client));
451
307
  reg('orboto_primer_fact_supersede', primerFactSupersedeToolConfig, makePrimerFactSupersedeHandler(client));
452
308
  reg('orboto_primer_fact_verify', primerFactVerifyToolConfig, makePrimerFactVerifyHandler(client));
453
309
  reg('orboto_primer_fact_delete', primerFactDeleteToolConfig, makePrimerFactDeleteHandler(client));
454
- // ORB-799 - wrapper-feature-parity gap-close. Eight clusters:
455
- // 1. Identity (whoami)
456
- // 2. Composite (claim, unclaim)
457
- // 3. Milestone CRUD (create, close, update)
458
- // 4. Project listings (statuses, labels)
459
- // 5. Bulk writes (patch, move, close, comment, assign, unassign)
460
- // 6. Docs-AI (ask, ingest-url, ingest-file)
461
- // 7. Attachments (attach-to-ticket)
462
- // 8. Re-parenting (set-parent - symmetric to set_milestone)
463
310
  reg('orboto_whoami', whoamiToolConfig, makeWhoamiHandler(client));
464
311
  reg('orboto_claim', claimToolConfig, makeClaimHandler(client));
465
312
  reg('orboto_unclaim', unclaimToolConfig, makeUnclaimHandler(client));
466
- // ORB-1910 - feedback / bug report to the operator through the tenant relay.
467
313
  reg('orboto_report_feedback', reportFeedbackToolConfig, makeReportFeedbackHandler(client));
468
- // ORB-1223 - approval / sign-off gates.
469
314
  reg('orboto_list_approvals', listApprovalsToolConfig, makeListApprovalsHandler(client));
470
315
  reg('orboto_approval_decide', approvalDecideToolConfig, makeApprovalDecideHandler(client));
471
- // ORB-1615 - structural review policy: fingerprint, consult-before-invoking
472
- // policy check, and recording a review decision. Rule CONFIGURATION stays
473
- // out of MCP (project-settings admin, same as approval-policy CRUD above).
474
316
  reg('orboto_review_fingerprint', reviewFingerprintToolConfig, makeReviewFingerprintHandler(client));
475
317
  reg('orboto_review_policy_check', reviewPolicyCheckToolConfig, makeReviewPolicyCheckHandler(client));
476
318
  reg('orboto_review_approval_record', reviewApprovalRecordToolConfig, makeReviewApprovalRecordHandler(client));
@@ -483,8 +325,6 @@ export async function buildOrbotoMcpServer(opts) {
483
325
  reg('orboto_bulk_patch_tickets', bulkPatchTicketsToolConfig, makeBulkPatchTicketsHandler(client));
484
326
  reg('orboto_bulk_move_tickets', bulkMoveTicketsToolConfig, makeBulkMoveTicketsHandler(client));
485
327
  reg('orboto_bulk_close_tickets', bulkCloseTicketsToolConfig, makeBulkCloseTicketsHandler(client));
486
- // ORB-1694 - the two longest measured consecutive-call runs (create 49x,
487
- // add_dependency 27x) collapse into one call each.
488
328
  reg('orboto_bulk_create_tickets', bulkCreateTicketsToolConfig, makeBulkCreateTicketsHandler(client));
489
329
  reg('orboto_bulk_add_ticket_dependencies', bulkAddTicketDependenciesToolConfig, makeBulkAddTicketDependenciesHandler(client));
490
330
  reg('orboto_bulk_comment_tickets', bulkCommentTicketsToolConfig, makeBulkCommentTicketsHandler(client));
@@ -499,17 +339,12 @@ export async function buildOrbotoMcpServer(opts) {
499
339
  reg('orboto_create_project', createProjectToolConfig, makeCreateProjectHandler(client));
500
340
  reg('orboto_archive_project', archiveProjectToolConfig, makeArchiveProjectHandler(client));
501
341
  reg('orboto_check_similar', checkSimilarToolConfig, makeCheckSimilarHandler(client));
502
- // ORB-1609 - work sessions: the lease/timer/presence/evidence record.
503
342
  reg('orboto_work_session_start', workSessionStartToolConfig, makeWorkSessionStartHandler(client));
504
- // ORB-1611 - the one-call bundled pickup: acquire + full context bundle.
505
343
  reg('orboto_work_start', workStartToolConfig, makeWorkStartHandler(client));
506
344
  reg('orboto_work_session_finish', workSessionFinishToolConfig, makeWorkSessionFinishHandler(client));
507
- // ORB-1612 - the one-call bundled exit: finish + ticket transition + note.
508
345
  reg('orboto_work_finish', workFinishToolConfig, makeWorkFinishHandler(client));
509
- // ORB-1613 - collision-aware dispatch pull for worker pools.
510
346
  reg('orboto_work_next', workNextToolConfig, makeWorkNextHandler(client));
511
347
  reg('orboto_work_sessions', workSessionsToolConfig, makeWorkSessionsHandler(client));
512
- // ORB-1610 - resource claims: path globs + named exclusive resources.
513
348
  reg('orboto_work_session_claims_add', workSessionClaimsAddToolConfig, makeWorkSessionClaimsAddHandler(client));
514
349
  reg('orboto_work_session_claims_release', workSessionClaimsReleaseToolConfig, makeWorkSessionClaimsReleaseHandler(client));
515
350
  reg('orboto_admin_translation_list', listAdminTranslationsToolConfig, makeListAdminTranslationsHandler(client));
@@ -517,23 +352,12 @@ export async function buildOrbotoMcpServer(opts) {
517
352
  reg('orboto_admin_translation_revert', revertTranslationToolConfig, makeAdminRevertTranslationHandler(client));
518
353
  reg('orboto_admin_agent_drift_list', listAgentDriftToolConfig, makeListAgentDriftHandler(client));
519
354
  reg('orboto_admin_agent_drift_resolve', resolveAgentDriftToolConfig, makeResolveAgentDriftHandler(client));
520
- // ORB-310 Phase D - read-only `orboto://…` URI resources +
521
- // task-shaped Prompt templates the MCP client offers in its UI.
522
355
  registerOrbotoResources(server, client);
523
356
  registerOrbotoPrompts(server);
524
- // ORB-940 - resources/subscribe + resources/unsubscribe. The Set
525
- // is shared by reference with the transport's event-bridge, which
526
- // is the actual sender of `notifications/resources/updated`. We
527
- // resolve immediately with an empty result either way - the spec
528
- // allows that, and the bridge does the real work.
529
357
  if (opts.subscriptions) {
530
358
  const subs = opts.subscriptions;
531
359
  server.server.setRequestHandler(SubscribeRequestSchema, async (req) => {
532
360
  subs.add(req.params.uri);
533
- // ORB-940 follow-up - stderr-log every subscribe/unsubscribe so
534
- // operators can see in the MCP container log whether the AI
535
- // client actually opts into live updates (vs. just reading the
536
- // resource once). Diagnostic only - never throw from here.
537
361
  try {
538
362
  process.stderr.write(`[orboto-mcp] subscribe → ${req.params.uri} (total subs: ${subs.size})\n`);
539
363
  }
@@ -52,15 +52,6 @@ export const SESSION_START_GATE_MESSAGE = 'This workspace requires you to load i
52
52
  * Advance the gate state for one dispatch and report whether this
53
53
  * dispatch must be REFUSED (returned an instructive error without running the
54
54
  * handler).
55
- *
56
- * Semantics when the gate is enabled: every tool call other than
57
- * `orboto_session_start` is refused until `orboto_session_start` has run once
58
- * this session. A rule refresh locks the session until its result succeeds
59
- * (and is itself never gated). When the gate is disabled, nothing is ever
60
- * refused (returns false) - default behaviour is unchanged.
61
- *
62
- * The metrics wrapper records successful completion separately; dispatch
63
- * alone is not evidence of rule delivery.
64
55
  */
65
56
  export function shouldGate(state, toolName) {
66
57
  if (toolName === SESSION_START_TOOL) {
@@ -11,7 +11,6 @@ describe('shouldNudge (ORB-1331)', () => {
11
11
  it('nudges on the first tool call when it is not session_start, then never again', () => {
12
12
  const state = createNudgeState();
13
13
  expect(shouldNudge(state, 'orboto_list_projects')).toBe(true);
14
- // Every subsequent call is clean regardless of tool.
15
14
  expect(shouldNudge(state, 'orboto_get_ticket')).toBe(false);
16
15
  expect(shouldNudge(state, 'orboto_list_projects')).toBe(false);
17
16
  expect(shouldNudge(state, SESSION_START_TOOL)).toBe(false);
@@ -19,14 +18,12 @@ describe('shouldNudge (ORB-1331)', () => {
19
18
  it('never nudges when the first call IS session_start', () => {
20
19
  const state = createNudgeState();
21
20
  expect(shouldNudge(state, SESSION_START_TOOL)).toBe(false);
22
- // and does not resurface on later calls either.
23
21
  expect(shouldNudge(state, 'orboto_list_projects')).toBe(false);
24
22
  });
25
23
  it('isolates state between sessions/processes (each fresh state nudges once)', () => {
26
24
  const a = createNudgeState();
27
25
  const b = createNudgeState();
28
26
  expect(shouldNudge(a, 'orboto_list_projects')).toBe(true);
29
- // b is untouched by a - its own first call still nudges.
30
27
  expect(shouldNudge(b, 'orboto_list_projects')).toBe(true);
31
28
  expect(shouldNudge(a, 'orboto_list_projects')).toBe(false);
32
29
  expect(shouldNudge(b, 'orboto_list_projects')).toBe(false);
@@ -34,21 +31,18 @@ describe('shouldNudge (ORB-1331)', () => {
34
31
  });
35
32
  describe('shouldGate (ORB-1471)', () => {
36
33
  it('never gates when the gate is disabled (default), whatever the tool', () => {
37
- const state = createNudgeState(); // gateEnabled defaults false
34
+ const state = createNudgeState();
38
35
  expect(shouldGate(state, 'orboto_list_projects')).toBe(false);
39
36
  expect(shouldGate(state, 'orboto_create_ticket')).toBe(false);
40
37
  expect(shouldGate(state, SESSION_START_TOOL)).toBe(false);
41
38
  });
42
39
  it('when enabled, refuses every non-session-start tool until session_start runs', () => {
43
40
  const state = createNudgeState(true);
44
- // Pre session_start: every other tool is gated.
45
41
  expect(shouldGate(state, 'orboto_list_projects')).toBe(true);
46
42
  expect(shouldGate(state, 'orboto_create_ticket')).toBe(true);
47
- // Dispatch is not proof of delivery; only successful completion unlocks.
48
43
  expect(shouldGate(state, SESSION_START_TOOL)).toBe(false);
49
44
  expect(shouldGate(state, 'orboto_list_projects')).toBe(true);
50
45
  recordSessionStartResult(state, SESSION_START_TOOL, true);
51
- // Post session_start: everything passes.
52
46
  expect(shouldGate(state, 'orboto_list_projects')).toBe(false);
53
47
  expect(shouldGate(state, 'orboto_create_ticket')).toBe(false);
54
48
  });
@@ -76,7 +70,7 @@ describe('SESSION_START_NUDGE text (ORB-1331)', () => {
76
70
  it('is English, ASCII-only, and free of em/en-dashes', () => {
77
71
  // eslint-disable-next-line no-control-regex
78
72
  expect(/^[\x00-\x7F]*$/.test(SESSION_START_NUDGE)).toBe(true);
79
- expect(SESSION_START_NUDGE).not.toMatch(/[\u2013\u2014]/); // en/em dash
73
+ expect(SESSION_START_NUDGE).not.toMatch(/[\u2013\u2014]/);
80
74
  expect(SESSION_START_NUDGE).toContain('orboto_session_start');
81
75
  });
82
76
  });
@@ -90,9 +84,7 @@ describe('prependNudge (ORB-1331)', () => {
90
84
  expect(out.content).toHaveLength(2);
91
85
  expect(out.content[0]).toEqual({ type: 'text', text: SESSION_START_NUDGE });
92
86
  expect(out.content[1]).toEqual({ type: 'text', text: 'tool output' });
93
- // structuredContent consumers are unaffected - same reference-equal payload.
94
87
  expect(out.structuredContent).toBe(original.structuredContent);
95
- // input is not mutated.
96
88
  expect(original.content).toHaveLength(1);
97
89
  });
98
90
  it('preserves isError on an error result', () => {
@@ -1,30 +1,11 @@
1
1
  /**
2
2
  * ORB-1331 - end-to-end transport proof of the session-start nudge.
3
- *
4
- * Drives the REAL server (buildOrbotoMcpServer → registerWithMetrics →
5
- * withMetrics → prependNudge) over an in-memory MCP transport with a
6
- * genuine MCP Client, so the whole JSON-RPC dispatch path is exercised,
7
- * not just the helper in isolation.
8
- *
9
- * Transport semantics both reduce to the lifetime of the per-build nudge
10
- * state:
11
- * - HTTP: the transport calls buildOrbotoMcpServer once PER SESSION, so
12
- * each session is an independent server instance with its own state.
13
- * - stdio: buildOrbotoMcpServer is called once PER PROCESS, so a single
14
- * server instance's state is process-local.
15
- * We model a "session"/"process" as one built server + one connected
16
- * client, and assert the nudge fires exactly once on the first non-
17
- * session_start call and never when the first call is session_start.
18
3
  */
19
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
20
5
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
6
  import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
22
7
  import { buildOrbotoMcpServer } from './server.js';
23
8
  import { SESSION_START_NUDGE } from './session-nudge.js';
24
- // Every API call the tools make resolves to a benign empty payload so
25
- // the handlers succeed without a live backend. list_projects → [] →
26
- // "no projects" text + valid structuredContent; session_start's sub-
27
- // rules read is required and carries an explicit valid empty ruleset.
28
9
  function mockApi() {
29
10
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => ({
30
11
  ok: true,
@@ -76,7 +57,6 @@ describe('ORB-1331 - HTTP transport (per-session nudge)', () => {
76
57
  const client = await connectSession();
77
58
  const first = await client.callTool({ name: 'orboto_list_projects', arguments: {} });
78
59
  expect(firstText(first)).toContain(SESSION_START_NUDGE);
79
- // structuredContent must be untouched by the text-block prepend.
80
60
  expect(first.structuredContent).toMatchObject({ projects: [], total: 0 });
81
61
  const second = await client.callTool({ name: 'orboto_list_projects', arguments: {} });
82
62
  expect(firstText(second)).not.toContain(SESSION_START_NUDGE);
@@ -93,15 +73,12 @@ describe('ORB-1331 - HTTP transport (per-session nudge)', () => {
93
73
  const b = await connectSession();
94
74
  const aFirst = await a.callTool({ name: 'orboto_list_projects', arguments: {} });
95
75
  expect(firstText(aFirst)).toContain(SESSION_START_NUDGE);
96
- // b is a distinct built server → distinct state → still nudges.
97
76
  const bFirst = await b.callTool({ name: 'orboto_list_projects', arguments: {} });
98
77
  expect(firstText(bFirst)).toContain(SESSION_START_NUDGE);
99
78
  });
100
79
  });
101
80
  describe('ORB-1331 - stdio transport (process-local nudge)', () => {
102
81
  it('fires once for the process: first non-session_start call nudged, all later calls clean', async () => {
103
- // stdio builds one server per process → one nudge state reused for
104
- // every call on this single connected client.
105
82
  const client = await connectSession();
106
83
  const first = await client.callTool({ name: 'orboto_list_projects', arguments: {} });
107
84
  expect(firstText(first)).toContain(SESSION_START_NUDGE);
@@ -1,37 +1,5 @@
1
1
  /**
2
2
  * ORB-1669 - safety-annotation ratchet for the MCP tool surface.
3
- *
4
- * MCP clients use `annotations` to decide whether a tool call needs a
5
- * confirmation step. The hints DEFAULT badly for us: per the MCP spec a
6
- * tool with no annotations is read-only=false / destructive=true /
7
- * idempotent=false - so an unannotated read looks dangerous, and (worse,
8
- * because it is the silent direction) a write annotated only with
9
- * `{ readOnlyHint: false }` still reports destructive=true. Before this
10
- * ticket 90 of 168 tools carried nothing at all and `orboto_bulk_close`
11
- * was indistinguishable from `orboto_get_ticket`.
12
- *
13
- * This test enumerates the tools exactly as a client sees them -
14
- * `tools/list` over an in-memory transport, not a source scan - and
15
- * fails the build on a new tool that skips its annotations. There is no
16
- * allowlist on purpose: unlike the dark-mode / i18n ratchets there is no
17
- * legitimate "sanctioned exception", every tool can answer these
18
- * questions.
19
- *
20
- * THE CONVENTION (apply it when you add a tool):
21
- *
22
- * readOnlyHint: true - performs no writes at all.
23
- * destructiveHint: true - the call can DELETE or REMOVE a row, DISCARD
24
- * content, take over state owned by someone
25
- * else, or mutate many tickets at once. If ANY
26
- * argument combination can do that, the tool is
27
- * destructive - the hint is per-tool and cannot
28
- * be conditioned on a flag (this is why
29
- * `orboto_claim` is destructive: `sole=true`
30
- * strips every other assignee).
31
- * destructiveHint: false- purely additive, or a reversible single-entity
32
- * field/state update.
33
- * idempotentHint: true - repeating the call with identical arguments
34
- * has no further effect.
35
3
  */
36
4
  import { describe, it, expect, beforeAll } from 'vitest';
37
5
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -48,11 +16,6 @@ function unsafeTakeoverTools(list) {
48
16
  .map((tool) => tool.name);
49
17
  }
50
18
  beforeAll(async () => {
51
- // Port 1 is never listened on -> the connect-time /agent-instructions
52
- // fetch fails immediately and the server falls back to its built-in
53
- // rules. No network, no fixture server, no timeout.
54
- // ORB-1520 made the CURATED manifest the default; the annotation contract
55
- // covers every registered tool, so this ratchet lists the FULL toolset.
56
19
  const server = await buildOrbotoMcpServer({ baseUrl: 'http://127.0.0.1:1', apiKey: 'orb_test', toolset: 'full' });
57
20
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
58
21
  const client = new Client({ name: 'annotation-ratchet', version: '0' });
@@ -62,8 +25,6 @@ beforeAll(async () => {
62
25
  });
63
26
  describe('MCP tool safety annotations (ORB-1669)', () => {
64
27
  it('enumerates the full tool surface', () => {
65
- // Guards the guard: if the in-memory handshake ever silently returns
66
- // nothing, every expectation below would vacuously pass.
67
28
  expect(tools.length).toBeGreaterThan(150);
68
29
  });
69
30
  it('every tool declares annotations with an explicit readOnlyHint', () => {
@@ -74,9 +35,6 @@ describe('MCP tool safety annotations (ORB-1669)', () => {
74
35
  'Add an `annotations` block to the tool config in apps/mcp/src/tools/ - see the convention at the top of this file.').toEqual([]);
75
36
  });
76
37
  it('every write tool declares an explicit destructiveHint', () => {
77
- // The load-bearing one. `{ readOnlyHint: false }` alone leaves
78
- // destructiveHint defaulting to TRUE, so a harmless write advertises
79
- // itself as destructive and the signal stops meaning anything.
80
38
  const offenders = tools
81
39
  .filter((t) => t.annotations?.readOnlyHint === false)
82
40
  .filter((t) => typeof t.annotations?.destructiveHint !== 'boolean')
@@ -92,10 +50,8 @@ describe('MCP tool safety annotations (ORB-1669)', () => {
92
50
  });
93
51
  it('every takeover-capable tool is a destructive write for all argument combinations', () => {
94
52
  const capable = tools.filter((tool) => Object.prototype.hasOwnProperty.call(tool.inputSchema?.properties ?? {}, 'takeover'));
95
- // Pin the actual siblings so losing schema discovery cannot pass vacuously.
96
53
  expect(capable.map((tool) => tool.name)).toEqual(expect.arrayContaining(['orboto_work_start', 'orboto_work_session_start']));
97
54
  expect(unsafeTakeoverTools(tools)).toEqual([]);
98
- // Reservation without a takeover path is not an account-state takeover.
99
55
  expect(tools.find((tool) => tool.name === 'orboto_work_next')?.annotations).toMatchObject({ readOnlyHint: false, destructiveHint: false });
100
56
  });
101
57
  it('the takeover ratchet catches missing, false and contradictory hints on future tools', () => {