@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,26 +1,5 @@
1
1
  /**
2
2
  * ORB-943 - OAuth bootstrap for the stdio local-proxy.
3
- *
4
- * The stdio proxy (`@orboto/mcp` spawned by Claude Desktop / Cursor) was
5
- * PAT-only: the operator had to paste a long-lived `orb_*` key into the
6
- * client config. This module gives the stdio proxy the SAME short-lived,
7
- * SSO-backed OAuth path the HTTP transport already offers - a browser-assisted
8
- * loopback flow (RFC 8252) so a desktop user connects without ever pasting a
9
- * token. The PAT stays the documented fallback for headless service accounts
10
- * (see index.ts).
11
- *
12
- * Design:
13
- * - No new API surface. This reuses the shipped provider-side OAuth stack
14
- * (discovery + DCR + PKCE + rotating refresh) exactly as an HTTP MCP client
15
- * would: discover -> register -> authorize-in-browser -> exchange -> use
16
- * the access token as the REST Bearer (the api's `authenticate` decorator
17
- * already accepts OAuth access tokens alongside `orb_*` keys).
18
- * - The refresh token is cached on disk (0600) keyed by the instance origin,
19
- * so a subsequent boot refreshes silently instead of re-opening a browser.
20
- * - The pure pieces (PKCE, request building, response parsing, the cache
21
- * round-trip, the token-provider refresh math) are exported so they unit
22
- * test without a browser or a live server. The loopback listener + browser
23
- * open is the thin I/O shell.
24
3
  */
25
4
  import { createHash, randomBytes } from 'node:crypto';
26
5
  import { createServer } from 'node:http';
@@ -47,9 +26,6 @@ export function generatePkce() {
47
26
  export function generateState() {
48
27
  return randomBytes(16).toString('base64url');
49
28
  }
50
- // ---------------------------------------------------------------------------
51
- // Origin derivation + discovery
52
- // ---------------------------------------------------------------------------
53
29
  /**
54
30
  * The OAuth authorization server lives at the instance ORIGIN, while the stdio
55
31
  * proxy is configured with the REST base (`<origin>/api`). Strip a trailing
@@ -74,9 +50,6 @@ export async function discoverAuthServer(origin, fetchImpl = fetch) {
74
50
  }
75
51
  return meta;
76
52
  }
77
- // ---------------------------------------------------------------------------
78
- // Dynamic Client Registration (RFC 7591)
79
- // ---------------------------------------------------------------------------
80
53
  /** Register a public client for the loopback redirect. Returns the client_id. */
81
54
  export async function registerLoopbackClient(registrationEndpoint, redirectUri, fetchImpl = fetch) {
82
55
  const res = await fetchImpl(registrationEndpoint, {
@@ -99,9 +72,6 @@ export async function registerLoopbackClient(registrationEndpoint, redirectUri,
99
72
  throw new Error('OAuth client registration returned no client_id');
100
73
  return json.client_id;
101
74
  }
102
- // ---------------------------------------------------------------------------
103
- // Authorize URL + token exchange
104
- // ---------------------------------------------------------------------------
105
75
  export function buildAuthorizeUrl(authorizationEndpoint, params) {
106
76
  const u = new URL(authorizationEndpoint);
107
77
  u.searchParams.set('response_type', 'code');
@@ -159,9 +129,6 @@ export async function refreshTokens(tokenEndpoint, params, fetchImpl = fetch) {
159
129
  }
160
130
  return parseTokenResponse(await res.json());
161
131
  }
162
- // ---------------------------------------------------------------------------
163
- // Token cache (0600, keyed by origin)
164
- // ---------------------------------------------------------------------------
165
132
  /** Cache file path. Honours ORBOTO_MCP_TOKEN_CACHE for tests / custom homes. */
166
133
  export function tokenCachePath() {
167
134
  const override = process.env.ORBOTO_MCP_TOKEN_CACHE;
@@ -187,8 +154,6 @@ export function saveCachedGrant(origin, grant, path = tokenCachePath()) {
187
154
  file[origin] = grant;
188
155
  mkdirSync(dirname(path), { recursive: true });
189
156
  writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
190
- // writeFileSync's mode only applies on create; force 0600 on an existing file
191
- // too so a cache written before this code shipped is tightened.
192
157
  try {
193
158
  chmodSync(path, 0o600);
194
159
  }
@@ -219,8 +184,6 @@ export function createTokenProvider(initial, refresh, onRefreshed, now = Date.no
219
184
  throw new Error('OAuth session expired and no refresh token is available - reconnect the client.');
220
185
  }
221
186
  const next = await refresh(current.refreshToken);
222
- // The AS rotates the refresh token; if a response omits it (shouldn't with
223
- // offline_access) keep the prior one so the chain isn't lost.
224
187
  current = { ...next, refreshToken: next.refreshToken ?? current.refreshToken };
225
188
  onRefreshed?.(current);
226
189
  return current.accessToken;
@@ -240,9 +203,6 @@ export function createTokenProvider(initial, refresh, onRefreshed, now = Date.no
240
203
  },
241
204
  };
242
205
  }
243
- // ---------------------------------------------------------------------------
244
- // Interactive loopback flow (the I/O shell)
245
- // ---------------------------------------------------------------------------
246
206
  /** Open a URL in the user's default browser. Returns false if no opener is
247
207
  * available (headless) so the caller can print the URL for manual paste. */
248
208
  export async function openInBrowser(url) {
@@ -333,9 +293,6 @@ export async function runLoopbackAuthorization(opts) {
333
293
  clearTimeout(timeout);
334
294
  reject(e);
335
295
  });
336
- // Bind to an ephemeral loopback port, THEN register the client at that exact
337
- // redirect_uri (the api allows any loopback port at authorize time per RFC
338
- // 8252, but DCR still needs a concrete uri).
339
296
  server.listen(0, '127.0.0.1', async () => {
340
297
  try {
341
298
  const addr = server.address();
@@ -365,9 +322,6 @@ export async function runLoopbackAuthorization(opts) {
365
322
  });
366
323
  });
367
324
  }
368
- // ---------------------------------------------------------------------------
369
- // Top-level bootstrap: cache -> refresh -> interactive, returns a provider
370
- // ---------------------------------------------------------------------------
371
325
  /**
372
326
  * Resolve an OAuth token provider for the stdio proxy. Order:
373
327
  * 1. A cached grant whose refresh token still works -> silent refresh.
@@ -398,7 +352,6 @@ export async function bootstrapOAuth(opts) {
398
352
  return createTokenProvider(tokens, (rt) => refreshTokens(cached.tokenEndpoint, { clientId: cached.clientId, refreshToken: rt }, fetchImpl), (next) => persist(cached.clientId, cached.tokenEndpoint, next));
399
353
  }
400
354
  catch (e) {
401
- // A revoked / expired refresh token drops us back to interactive.
402
355
  log(`[orboto-mcp] cached OAuth session no longer valid (${e.message}); re-authorizing`);
403
356
  clearCachedGrant(origin, cachePath);
404
357
  }
@@ -16,7 +16,6 @@ describe('PKCE', () => {
16
16
  expect(method).toBe('S256');
17
17
  const recomputed = createHash('sha256').update(verifier).digest('base64url');
18
18
  expect(recomputed).toBe(challenge);
19
- // base64url, no padding
20
19
  expect(challenge).not.toContain('=');
21
20
  expect(challenge).not.toContain('+');
22
21
  });
@@ -99,7 +98,6 @@ describe('token cache', () => {
99
98
  expect(loadCachedGrant('https://a.test', path)?.clientId).toBe('c');
100
99
  expect(loadCachedGrant('https://b.test', path)?.refreshToken).toBe('r2');
101
100
  expect(loadCachedGrant('https://missing.test', path)).toBeNull();
102
- // 0600 perms (owner rw only)
103
101
  const mode = statSync(path).mode & 0o777;
104
102
  expect(mode).toBe(0o600);
105
103
  });
@@ -114,12 +112,10 @@ describe('token cache', () => {
114
112
  });
115
113
  });
116
114
  describe('createTokenProvider', () => {
117
- // expiresAt comfortably larger than EXPIRY_SKEW_MS so "fresh" and "within
118
- // skew" are both expressible with a fake clock.
119
115
  const base = { accessToken: 'at0', refreshToken: 'rt0', expiresAt: 10_000_000, scope: 'mcp' };
120
116
  it('returns the cached access token while fresh', async () => {
121
117
  const refresh = vi.fn();
122
- const now = () => 0; // well before expiry
118
+ const now = () => 0;
123
119
  const p = createTokenProvider(base, refresh, undefined, now);
124
120
  expect(await p.getAccessToken()).toBe('at0');
125
121
  expect(refresh).not.toHaveBeenCalled();
@@ -127,7 +123,7 @@ describe('createTokenProvider', () => {
127
123
  it('refreshes when within the expiry skew and rotates the refresh token', async () => {
128
124
  const refresh = vi.fn().mockResolvedValue({ accessToken: 'at1', refreshToken: 'rt1', expiresAt: 10_000_000, scope: 'mcp' });
129
125
  const onRefreshed = vi.fn();
130
- const now = () => base.expiresAt - EXPIRY_SKEW_MS + 1; // inside skew window
126
+ const now = () => base.expiresAt - EXPIRY_SKEW_MS + 1;
131
127
  const p = createTokenProvider(base, refresh, onRefreshed, now);
132
128
  expect(await p.getAccessToken()).toBe('at1');
133
129
  expect(refresh).toHaveBeenCalledWith('rt0');
@@ -152,15 +148,11 @@ describe('createTokenProvider', () => {
152
148
  await expect(p.getAccessToken()).rejects.toThrow(/reconnect the client/);
153
149
  });
154
150
  it('ORB-1419 - single-flights concurrent getAccessToken calls into ONE refresh', async () => {
155
- // Two concurrent callers that both see an expired token must share a single
156
- // in-flight refresh rather than each firing one (which would present the OLD
157
- // refresh token twice and trip server-side reuse-detection).
158
151
  let resolveRefresh = () => { };
159
152
  const refresh = vi.fn().mockImplementation(() => new Promise((r) => { resolveRefresh = r; }));
160
153
  const p = createTokenProvider(base, refresh, undefined, () => base.expiresAt);
161
154
  const a = p.getAccessToken();
162
155
  const b = p.getAccessToken();
163
- // Both callers are now awaiting; only one refresh should have been started.
164
156
  expect(refresh).toHaveBeenCalledOnce();
165
157
  expect(refresh).toHaveBeenCalledWith('rt0');
166
158
  resolveRefresh({ accessToken: 'at-shared', refreshToken: 'rt1', expiresAt: 10_000_000, scope: 'mcp' });
@@ -190,19 +182,15 @@ describe('bootstrapOAuth cached-grant path', () => {
190
182
  });
191
183
  expect(await provider.getAccessToken()).toBe('fresh-at');
192
184
  expect(openBrowser).not.toHaveBeenCalled();
193
- // rotation persisted
194
185
  expect(loadCachedGrant('https://x.test', path)?.refreshToken).toBe('rt1');
195
186
  });
196
187
  it('clears a dead cached grant and falls through to discovery', async () => {
197
188
  saveCachedGrant('https://x.test', {
198
189
  clientId: 'cid', refreshToken: 'dead', scope: 'mcp', tokenEndpoint: 'https://x.test/oauth/token',
199
190
  }, path);
200
- // First call (refresh) 400s; then discovery is attempted (which we fail so
201
- // we don't need to drive the whole interactive flow) - the point is the
202
- // dead grant is cleared and discovery is reached.
203
191
  const fetchImpl = vi.fn()
204
- .mockResolvedValueOnce(new Response('{"error":"invalid_grant"}', { status: 400 })) // refresh
205
- .mockResolvedValueOnce(new Response('nope', { status: 500 })); // discovery
192
+ .mockResolvedValueOnce(new Response('{"error":"invalid_grant"}', { status: 400 }))
193
+ .mockResolvedValueOnce(new Response('nope', { status: 500 }));
206
194
  await expect(bootstrapOAuth({
207
195
  apiBaseUrl: 'https://x.test/api', fetchImpl: fetchImpl,
208
196
  openBrowser: vi.fn(), cachePath: path, log: () => { },
@@ -1,27 +1,6 @@
1
1
  /**
2
2
  * ORB-244 Phase A - thin HTTP client the MCP server uses to talk to
3
3
  * the orboto API.
4
- *
5
- * Deliberately speaks HTTPS-REST rather than importing `@orboto/api`'s
6
- * services directly, for three reasons:
7
- * 1. One code path covers both delivery variants from the ticket -
8
- * Local-Proxy (`npx @orboto/mcp-cli`, running on the dev's laptop,
9
- * pointing at the public orboto URL) AND Self-Hosted-inline
10
- * (separate container in docker-compose, pointing at
11
- * `http://api:3000`). The only difference is the env var.
12
- * 2. The API's `requirePermission` / PBAC cascade runs server-side
13
- * where it belongs. The MCP server is a pure transport adapter;
14
- * it never sees the DB or trust boundary.
15
- * 3. The `orb_*` API-key flow is already wired into the API's
16
- * `authenticate` decorator - reusing it means the existing
17
- * `mcp:use` + `api:use` scope checks, rate limits, and audit
18
- * logs all light up for free.
19
- *
20
- * Every request adds `Authorization: Bearer <apiKey>` (from env) and
21
- * a `User-Agent: orboto-mcp/<version>` header for the admin UI to
22
- * distinguish MCP traffic from regular API traffic. Non-2xx responses
23
- * throw `OrbotoApiError` so tool handlers can translate to MCP's
24
- * `{isError: true}` shape.
25
4
  */
26
5
  import { VERSION } from './version.js';
27
6
  export class OrbotoApiError extends Error {
@@ -50,10 +29,6 @@ export class OrbotoClient {
50
29
  }
51
30
  this.apiKey = config.apiKey;
52
31
  this.tokenProvider = config.tokenProvider;
53
- // User-Agent shape: `orboto-mcp/<version> (claude-desktop)`. The
54
- // suffix is optional metadata so the admin's MCP-usage panel
55
- // (Phase F) can group calls per client family without needing a
56
- // new DB column.
57
32
  const ua = config.userAgentSuffix
58
33
  ? `orboto-mcp/${VERSION} (${config.userAgentSuffix})`
59
34
  : `orboto-mcp/${VERSION}`;
@@ -95,9 +70,6 @@ export class OrbotoClient {
95
70
  if (res.status === 401 && this.tokenProvider) {
96
71
  res = await doFetch(await this.bearer(true));
97
72
  }
98
- // ORB-1727 - the api stamps `x-orboto-agent-mail: <count>` on responses
99
- // while the caller has unread inbox messages. Capture it centrally so
100
- // the tool-response wrapper can nudge without any extra request.
101
73
  const mail = res.headers?.get?.('x-orboto-agent-mail');
102
74
  this.pendingAgentMail = mail ? Number(mail) || 0 : 0;
103
75
  if (!res.ok) {
@@ -241,8 +213,6 @@ export async function preflightMcpSession(client) {
241
213
  if (!status.mcpUseGranted) {
242
214
  throw new Error(`MCP preflight failed: user ${status.userEmail} lacks the mcp:use permission. Ask an admin to grant it.`);
243
215
  }
244
- // ORB-942 - per-user opt-out. Distinct message pointing at the toggle so
245
- // the user knows this is their own setting, not an admin / permission block.
246
216
  if (!status.userMcpEnabled) {
247
217
  throw new Error('MCP preflight failed: you have disabled MCP access for your account. Re-enable it in Profile - Connect an AI Client.');
248
218
  }
@@ -1,14 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase A - orboto REST client unit tests.
3
- *
4
- * We mock `fetch` and cover the shape we promise to every tool
5
- * handler: correct base URL, bearer header, User-Agent string,
6
- * JSON body for write verbs, and the `OrbotoApiError` throw on
7
- * non-2xx with status + body captured.
8
- *
9
- * `preflightMcpSession` is tested too because its three-way
10
- * failure branching (401 / disabled / permission-denied) is the
11
- * most likely regression surface when the API contract drifts.
12
3
  */
13
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
14
5
  import { OrbotoClient, OrbotoApiError, preflightMcpSession } from './orboto-client.js';
@@ -83,8 +74,6 @@ describe('OrbotoClient', () => {
83
74
  const result = await client.post('/action', {});
84
75
  expect(result).toBeUndefined();
85
76
  });
86
- // ORB-799 - postMultipart sends FormData with no Content-Type header
87
- // (fetch chooses the boundary). The bearer + UA headers still apply.
88
77
  it('postMultipart sends FormData body without Content-Type (boundary picked by fetch)', async () => {
89
78
  const spy = mockFetch({ json: async () => ({ docId: 'd1' }) });
90
79
  const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_test' });
@@ -95,7 +84,6 @@ describe('OrbotoClient', () => {
95
84
  const [, init] = spy.mock.calls[0];
96
85
  const headers = init.headers;
97
86
  expect(headers.Authorization).toBe('Bearer orb_test');
98
- // No Content-Type header - fetch sets multipart/form-data; boundary=…
99
87
  expect(headers['Content-Type']).toBeUndefined();
100
88
  expect(init.method).toBe('POST');
101
89
  });
package/dist/prompts.js CHANGED
@@ -1,29 +1,8 @@
1
1
  /**
2
2
  * ORB-244 Phase D - MCP prompts.
3
- *
4
- * Prompts are reusable instruction templates an MCP-aware client
5
- * (Claude Desktop, Cursor) surfaces in its UI as a one-click action.
6
- * Each handler returns `messages[]` that become the start of the
7
- * conversation; the user's model then drives the work, typically
8
- * by calling orboto MCP tools.
9
- *
10
- * Five v1 templates - each a thin wrapper that hands the model a
11
- * focused goal + a tool sequence to execute:
12
- * - plan-sprint(projectKey) - draft a sprint plan from open work
13
- * - triage-my-tickets() - sort caller's open assignments
14
- * - summarize-project(projectKey) - quick project briefing
15
- * - estimate-ticket(ticketKey) - rough effort guess via similar work
16
- * - find-duplicates(ticketKey) - search for overlapping tickets
17
- *
18
- * No backend AI calls - the prompts steer the model, the model
19
- * uses tools. Keeps the surface stateless and the AI provider
20
- * irrelevant (works with any MCP client).
21
3
  */
22
4
  import { z } from 'zod';
23
5
  export function registerOrbotoPrompts(server) {
24
- // -------------------------------------------------------------------------
25
- // plan-sprint
26
- // -------------------------------------------------------------------------
27
6
  server.registerPrompt('plan-sprint', {
28
7
  title: 'Draft a sprint plan',
29
8
  description: 'Build a 2-week sprint plan from a project\'s open backlog and the team\'s recent velocity.',
@@ -52,9 +31,6 @@ export function registerOrbotoPrompts(server) {
52
31
  },
53
32
  }],
54
33
  }));
55
- // -------------------------------------------------------------------------
56
- // triage-my-tickets
57
- // -------------------------------------------------------------------------
58
34
  server.registerPrompt('triage-my-tickets', {
59
35
  title: 'Triage my open tickets',
60
36
  description: 'Sort the caller\'s open assignments and recommend the next 3 to focus on.',
@@ -82,9 +58,6 @@ export function registerOrbotoPrompts(server) {
82
58
  },
83
59
  }],
84
60
  }));
85
- // -------------------------------------------------------------------------
86
- // summarize-project
87
- // -------------------------------------------------------------------------
88
61
  server.registerPrompt('summarize-project', {
89
62
  title: 'Summarise a project',
90
63
  description: 'A 3-sentence briefing on a project - what it is, where it stands, what\'s next.',
@@ -108,9 +81,6 @@ export function registerOrbotoPrompts(server) {
108
81
  },
109
82
  }],
110
83
  }));
111
- // -------------------------------------------------------------------------
112
- // estimate-ticket
113
- // -------------------------------------------------------------------------
114
84
  server.registerPrompt('estimate-ticket', {
115
85
  title: 'Estimate a ticket\'s effort',
116
86
  description: 'Rough effort guess for a ticket based on its description and similar past work.',
@@ -135,9 +105,6 @@ export function registerOrbotoPrompts(server) {
135
105
  },
136
106
  }],
137
107
  }));
138
- // -------------------------------------------------------------------------
139
- // find-duplicates
140
- // -------------------------------------------------------------------------
141
108
  server.registerPrompt('find-duplicates', {
142
109
  title: 'Find duplicate tickets',
143
110
  description: 'Look for tickets that overlap with the given one - likely duplicates or competing work.',
@@ -165,9 +132,6 @@ export function registerOrbotoPrompts(server) {
165
132
  },
166
133
  }],
167
134
  }));
168
- // -------------------------------------------------------------------------
169
- // ORB-855 - wiki-ingest: guided URL ingest flow.
170
- // -------------------------------------------------------------------------
171
135
  server.registerPrompt('wiki-ingest', {
172
136
  title: 'Ingest a URL into the wiki',
173
137
  description: 'Guided flow: import a URL into an LLM-Wiki space and confirm what the curation produced.',
@@ -190,9 +154,6 @@ export function registerOrbotoPrompts(server) {
190
154
  },
191
155
  }],
192
156
  }));
193
- // -------------------------------------------------------------------------
194
- // ORB-855 - wiki-maintain: guided lint + fix flow.
195
- // -------------------------------------------------------------------------
196
157
  server.registerPrompt('wiki-maintain', {
197
158
  title: 'Maintain a wiki (lint + fix)',
198
159
  description: 'Run the lint pass on an LLM-Wiki space and propose fixes for the open issues.',
@@ -1,15 +1,7 @@
1
1
  /**
2
2
  * ORB-1818 - `PROTECT_TEXT_META` is a protection, not an escape hatch.
3
3
  *
4
- * A handler that sets it keeps its text half out of the shrinker, so a
5
- * tool could use it to opt out of the central response budget - the one
6
- * thing ORB-1697 exists to prevent. It is sanctioned for exactly one
7
- * payload: the complete binding workspace rules, which must never be cut
8
- * for either client class (Claude Code keeps the structured half,
9
- * text-only clients keep the Markdown one).
10
- *
11
- * This ratchet fails the build when any other source file sets it, the
12
- * same way the dark-mode / i18n / audit checks fail on a new violation.
4
+ * @see ORB-1697
13
5
  */
14
6
  import { describe, it, expect } from 'vitest';
15
7
  import { readdirSync, readFileSync, statSync } from 'node:fs';
@@ -54,11 +54,6 @@ async function fixture() {
54
54
  });
55
55
  return { run, cache, ruleReads: () => ruleReads, set: (body, code = 200) => { rules = body; status = code; }, setNext: (body) => { next = body; } };
56
56
  }
57
- // ORB-1948 - each test spawns the REAL wrapper CLI (a 6,000-line module,
58
- // about 1 s locally); on a saturated CI host (tag run: boot gate, three
59
- // image builds, CLI binaries and the MCP split in parallel) that exceeded
60
- // vitest's 5 s default and blocked the v0.182.1 rollout. The budget is per
61
- // test and generous on purpose - a hang still fails, only slower.
62
57
  const SPAWN_TEST_TIMEOUT_MS = 60_000;
63
58
  describe('wrapper required rules through the real CLI', () => {
64
59
  it('does not turn malformed work-next responses into an idle result', async () => {
@@ -69,8 +69,6 @@ export async function loadRequiredRules(client, path, knownHash) {
69
69
  }, 10_000);
70
70
  });
71
71
  try {
72
- // Race the whole operation: OAuth token/keychain acquisition can stall
73
- // before fetch starts and therefore cannot observe fetch's abort signal.
74
72
  value = await Promise.race([client.get(path, { signal: controller.signal }), timeout]);
75
73
  }
76
74
  catch (error) {
package/dist/resources.js CHANGED
@@ -1,42 +1,11 @@
1
1
  /**
2
2
  * ORB-244 Phase D - MCP resources.
3
- *
4
- * Resources let MCP-aware clients (Claude Desktop, Cursor) read
5
- * orboto content as static blobs without explicitly invoking a tool.
6
- * Where tools are RPCs ("do this thing"), resources are URIs ("here
7
- * is content at this address").
8
- *
9
- * Four URI templates exposed:
10
- * orboto://ticket/{ticketKey} - rendered Markdown of the ticket
11
- * orboto://doc/{docId} - doc body (Markdown)
12
- * orboto://project/{projectKey} - project summary
13
- * orboto://search/{query} - search results as Markdown
14
- *
15
- * No `list` callback for tickets/docs because the candidate set is
16
- * unbounded (every ticket, every doc) - the URI templates are
17
- * sufficient. Clients discover content via tools first, then read a
18
- * specific resource. The required `list: undefined` pattern keeps
19
- * the SDK happy.
20
- *
21
- * Implementation note: each resource handler delegates to the same
22
- * REST endpoints the tools use; the only difference is response
23
- * shape (Markdown text vs. structured tool result). Sharing logic
24
- * with tools/*.ts would couple the two surfaces too tightly - for
25
- * now the resources are independent thin renderers.
26
3
  */
27
4
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
28
5
  import { OrbotoApiError } from './orboto-client.js';
29
6
  import { loadRequiredRules } from './required-rules.js';
30
7
  import { resolveProjectByKey, resolveTicketByKey } from './tools/shared.js';
31
8
  export function registerOrbotoResources(server, client) {
32
- // -------------------------------------------------------------------------
33
- // orboto://rules
34
- //
35
- // ORB-1177 - the COMPLETE assembled binding rules, cap-independent. The
36
- // MCP `instructions` block is budgeted + may be truncated by the client
37
- // (ORB-1168); this resource is never truncated, so a client can fetch
38
- // the full rule set on demand. Same source orboto_session_start reads.
39
- // -------------------------------------------------------------------------
40
9
  server.registerResource('rules', new ResourceTemplate('orboto://rules', { list: undefined }), {
41
10
  title: 'Workspace agent rules (complete)',
42
11
  description: 'The complete, assembled binding rules you must follow as an agent in this workspace - cap-independent (the MCP instructions block may be truncated; this resource is not). orboto_session_start returns the same rules plus your in-progress work.',
@@ -48,9 +17,6 @@ export function registerOrbotoResources(server, client) {
48
17
  contents: [{ uri: uri.href, mimeType: 'text/markdown', text: `# orboto workspace agent rules\n\n${rules}` }],
49
18
  };
50
19
  });
51
- // -------------------------------------------------------------------------
52
- // orboto://ticket/{ticketKey}
53
- // -------------------------------------------------------------------------
54
20
  server.registerResource('ticket', new ResourceTemplate('orboto://ticket/{ticketKey}', { list: undefined }), {
55
21
  title: 'orboto ticket',
56
22
  description: 'A single ticket with description, assignees, comments, and labels - rendered as Markdown.',
@@ -72,9 +38,6 @@ export function registerOrbotoResources(server, client) {
72
38
  }],
73
39
  };
74
40
  });
75
- // -------------------------------------------------------------------------
76
- // orboto://doc/{docId}
77
- // -------------------------------------------------------------------------
78
41
  server.registerResource('doc', new ResourceTemplate('orboto://doc/{docId}', { list: undefined }), {
79
42
  title: 'orboto doc',
80
43
  description: 'A wiki page from a doc space - Markdown body.',
@@ -92,9 +55,6 @@ export function registerOrbotoResources(server, client) {
92
55
  contents: [{ uri: uri.href, mimeType: 'text/markdown', text: lines.join('\n') }],
93
56
  };
94
57
  });
95
- // -------------------------------------------------------------------------
96
- // orboto://project/{projectKey}
97
- // -------------------------------------------------------------------------
98
58
  server.registerResource('project', new ResourceTemplate('orboto://project/{projectKey}', { list: undefined }), {
99
59
  title: 'orboto project',
100
60
  description: 'Project metadata, milestones, members.',
@@ -114,13 +74,6 @@ export function registerOrbotoResources(server, client) {
114
74
  }],
115
75
  };
116
76
  });
117
- // -------------------------------------------------------------------------
118
- // orboto://search/{query}
119
- //
120
- // Note: clients URL-encode `query` automatically. Special chars in
121
- // a natural-language query are fine; the URI template handles
122
- // unescaping.
123
- // -------------------------------------------------------------------------
124
77
  server.registerResource('search', new ResourceTemplate('orboto://search/{query}', { list: undefined }), {
125
78
  title: 'orboto search',
126
79
  description: 'Full-text search across tickets, comments, docs - visibility-filtered.',
@@ -150,15 +103,6 @@ export function registerOrbotoResources(server, client) {
150
103
  }],
151
104
  };
152
105
  });
153
- // -------------------------------------------------------------------------
154
- // orboto://user/me/notifications
155
- //
156
- // ORB-706 - read the calling user's recent notifications.
157
- // Subscribable: every `notification:new` event for this user
158
- // fires a `resources/updated` push. Used by agents that want to
159
- // react to mentions / agent_message / status-change pings in
160
- // real time.
161
- // -------------------------------------------------------------------------
162
106
  server.registerResource('user-notifications', new ResourceTemplate('orboto://user/me/notifications', { list: undefined }), {
163
107
  title: 'My notifications',
164
108
  description: 'Calling user\'s recent notifications. Subscribable - every new notification fires resources/updated. Use this to react to mentions, agent_message (ORB-705), and status-change pings in real time.',
@@ -189,12 +133,6 @@ export function registerOrbotoResources(server, client) {
189
133
  }],
190
134
  };
191
135
  });
192
- // -------------------------------------------------------------------------
193
- // ORB-855 - LLM-Wiki resources. The index + log are the space's singleton
194
- // kind='index'/'log' docs; a page is any doc by id (namespaced under the
195
- // space). A `page/` discriminator keeps the page template from colliding
196
- // with the static index/log segments.
197
- // -------------------------------------------------------------------------
198
136
  const layerDoc = async (spaceId, kind) => {
199
137
  const list = await client.get(`/spaces/${spaceId}/docs`);
200
138
  return list.find((d) => d.kind === kind) ?? null;
@@ -1,15 +1,5 @@
1
1
  /**
2
2
  * ORB-244 Phase D - resources unit tests.
3
- *
4
- * The MCP SDK's resource-template handlers are normal functions
5
- * once registered. Pulling them off the server's internal registry
6
- * would couple us to SDK internals; instead we register against a
7
- * real McpServer and read back via the protocol-shaped
8
- * `_registeredResourceTemplates` accessor. If the SDK renames it,
9
- * the test fails loudly, but that's the point - we want to know.
10
- *
11
- * Each test exercises the URI → handler-output path with the
12
- * stubbed orboto REST client.
13
3
  */
14
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
15
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -48,9 +38,7 @@ function getTemplateHandler(server, name) {
48
38
  describe('orboto:// ticket resource', () => {
49
39
  it('renders a ticket as Markdown with description + comments', async () => {
50
40
  stub([
51
- // resolveTicketByKey: project lookup
52
41
  { json: { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' } },
53
- // resolveTicketByKey: ticket
54
42
  {
55
43
  json: {
56
44
  id: 't1', projectId: 'p1', ticketKey: 'ACME-7', title: 'Login bug',
@@ -61,7 +49,6 @@ describe('orboto:// ticket resource', () => {
61
49
  labels: [{ id: 'l1', name: 'bug' }],
62
50
  },
63
51
  },
64
- // comments page
65
52
  {
66
53
  json: {
67
54
  items: [
@@ -124,11 +111,8 @@ describe('orboto:// doc resource', () => {
124
111
  describe('orboto:// project resource', () => {
125
112
  it('aggregates project + milestones + members into Markdown', async () => {
126
113
  stub([
127
- // resolveProjectByKey
128
114
  { json: { id: 'p1', key: 'ACME', name: 'Acme Inc', description: 'CRM build', status: 'active' } },
129
- // milestones
130
115
  { json: [{ id: 'm1', name: 'v1', status: 'active', startDate: '2026-04-01', endDate: '2026-05-01' }] },
131
- // members
132
116
  { json: [{ user: { email: 'ada@acme', fullName: 'Ada' }, role: { name: 'developer' } }] },
133
117
  ]);
134
118
  const server = buildServerWithResources();