@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.
- package/dist/event-bridge.js +1 -47
- package/dist/event-bridge.test.js +0 -19
- package/dist/http-transport-binding.test.js +1 -4
- package/dist/http-transport.js +1 -213
- package/dist/http-transport.test.js +0 -56
- package/dist/index.js +1 -48
- package/dist/input-schema.js +2 -76
- package/dist/input-schema.test.js +1 -15
- package/dist/instructions-budget.test.js +0 -2
- package/dist/manifest-size.test.js +3 -72
- package/dist/oauth-bootstrap.js +0 -47
- package/dist/oauth-bootstrap.test.js +4 -16
- package/dist/orboto-client.js +0 -30
- package/dist/orboto-client.test.js +0 -12
- package/dist/prompts.js +0 -39
- package/dist/protect-text-usage.test.js +1 -9
- package/dist/required-rules-wrapper.test.js +0 -5
- package/dist/required-rules.js +0 -2
- package/dist/resources.js +0 -62
- package/dist/resources.test.js +0 -16
- package/dist/response-budget.js +2 -150
- package/dist/response-budget.test.js +1 -32
- package/dist/server.js +4 -180
- package/dist/session-nudge.js +0 -9
- package/dist/session-nudge.test.js +2 -10
- package/dist/session-nudge.transport.test.js +0 -23
- package/dist/tool-annotations.test.js +0 -44
- package/dist/tool-docs.js +1 -12
- package/dist/tool-docs.test.js +2 -8
- package/dist/tool-parameters.test.js +0 -19
- package/dist/tools/absence-writes.js +1 -17
- package/dist/tools/admin-translations.js +0 -11
- package/dist/tools/admin-writes.js +0 -28
- package/dist/tools/admin-writes.test.js +0 -1
- package/dist/tools/agent-coordination.js +2 -24
- package/dist/tools/agent-coordination.test.js +0 -2
- package/dist/tools/agent-instructions.js +0 -10
- package/dist/tools/agent-instructions.test.js +0 -2
- package/dist/tools/agent-messages.js +0 -1
- package/dist/tools/ai-status.js +0 -14
- package/dist/tools/ai-usage.js +0 -10
- package/dist/tools/analytics.js +0 -2
- package/dist/tools/api-call.js +0 -12
- package/dist/tools/api-call.test.js +1 -2
- package/dist/tools/api-search.js +1 -6
- package/dist/tools/approvals.js +0 -6
- package/dist/tools/attach.js +0 -25
- package/dist/tools/backup.js +0 -13
- package/dist/tools/backup.test.js +1 -2
- package/dist/tools/bulk-create.js +1 -25
- package/dist/tools/bulk-create.test.js +0 -4
- package/dist/tools/bulk-writes.js +69 -61
- package/dist/tools/bulk-writes.test.js +34 -4
- package/dist/tools/check-similar.js +0 -17
- package/dist/tools/checklist-writes.js +82 -27
- package/dist/tools/checklist-writes.test.js +69 -1
- package/dist/tools/claim.js +1 -71
- package/dist/tools/claim.test.js +0 -2
- package/dist/tools/critical-path.js +0 -5
- package/dist/tools/critical-path.test.js +0 -3
- package/dist/tools/cross-project-links.ee.js +1 -24
- package/dist/tools/doc-attachments.js +0 -28
- package/dist/tools/doc-attachments.test.js +1 -4
- package/dist/tools/doc-comments.js +1 -37
- package/dist/tools/doc-comments.test.js +1 -2
- package/dist/tools/doc-edits.js +1 -31
- package/dist/tools/doc-edits.test.js +0 -9
- package/dist/tools/doc-export.js +0 -24
- package/dist/tools/doc-export.test.js +0 -12
- package/dist/tools/doc-revisions.js +0 -23
- package/dist/tools/docs-ai.js +0 -34
- package/dist/tools/docs-ai.test.js +0 -11
- package/dist/tools/docs.js +1 -75
- package/dist/tools/docs.test.js +1 -3
- package/dist/tools/embedding-status.js +0 -13
- package/dist/tools/embedding-status.test.js +0 -1
- package/dist/tools/feedback.js +1 -15
- package/dist/tools/feedback.test.js +0 -1
- package/dist/tools/get-checklists.js +6 -14
- package/dist/tools/get-project-primer.js +1 -25
- package/dist/tools/get-project.js +0 -17
- package/dist/tools/get-ticket.js +1 -61
- package/dist/tools/get-timer.js +0 -11
- package/dist/tools/git-health-reasons.js +0 -1
- package/dist/tools/identity.js +0 -13
- package/dist/tools/list-projects.js +0 -20
- package/dist/tools/list-projects.test.js +0 -1
- package/dist/tools/list-tickets.js +10 -9
- package/dist/tools/milestones-crud.test.js +0 -4
- package/dist/tools/milestones.js +0 -43
- package/dist/tools/my-tickets.js +0 -7
- package/dist/tools/phase-b-tools.test.js +14 -21
- package/dist/tools/primer-facts.js +1 -49
- package/dist/tools/primer-facts.test.js +0 -20
- package/dist/tools/project-listings.js +0 -25
- package/dist/tools/query.js +1 -12
- package/dist/tools/query.test.js +0 -8
- package/dist/tools/raci.js +0 -11
- package/dist/tools/requirements-spec.js +1 -12
- package/dist/tools/response-expand.js +0 -19
- package/dist/tools/review-policy.js +0 -24
- package/dist/tools/review-policy.test.js +0 -2
- package/dist/tools/search.js +1 -13
- package/dist/tools/session-start.js +1 -136
- package/dist/tools/session-start.test.js +1 -44
- package/dist/tools/set-parent.js +1 -19
- package/dist/tools/shared.js +0 -41
- package/dist/tools/shared.test.js +0 -4
- package/dist/tools/similar-projection.js +0 -15
- package/dist/tools/ticket-attachments.js +1 -25
- package/dist/tools/ticket-schedules.js +0 -9
- package/dist/tools/ticket-schedules.test.js +0 -1
- package/dist/tools/ticket-writes.js +1 -110
- package/dist/tools/ticket-writes.test.js +1 -15
- package/dist/tools/time-writes.js +26 -46
- package/dist/tools/time-writes.test.js +18 -0
- package/dist/tools/update-project.js +1 -27
- package/dist/tools/update-project.test.js +1 -1
- package/dist/tools/wiki.js +0 -8
- package/dist/tools/work-next.js +0 -5
- package/dist/tools/work-sessions-start.test.js +0 -2
- package/dist/tools/work-sessions.js +0 -48
- package/dist/tools/work-sessions.test.js +0 -5
- package/dist/toolset.js +2 -81
- package/dist/toolset.test.js +0 -20
- package/dist/version-guard.test.js +0 -4
- package/dist/version.js +0 -12
- package/dist/with-metrics-output-schema.test.js +1 -14
- package/dist/with-metrics.js +2 -74
- package/dist/with-metrics.test.js +7 -29
- package/package.json +1 -1
|
@@ -26,10 +26,6 @@ describe('ORB-1175 - stale MCP session recovery', () => {
|
|
|
26
26
|
body: NON_INIT,
|
|
27
27
|
});
|
|
28
28
|
expect(res.status).toBe(404);
|
|
29
|
-
// ORB-1324 - NO WWW-Authenticate: the token is fine, only the session is
|
|
30
|
-
// gone. Attaching an auth challenge made clients kick off a manual OAuth
|
|
31
|
-
// re-auth instead of the automatic re-initialise the 404 already signals.
|
|
32
|
-
// (Token expiry is still caught on the re-init path's preflight.)
|
|
33
29
|
expect(res.headers.get('www-authenticate')).toBeNull();
|
|
34
30
|
const body = await res.json();
|
|
35
31
|
expect(body.error).toMatch(/reinitialize/i);
|
|
@@ -68,7 +64,6 @@ describe('ORB-1424 - GET /mcp OAuth-discovery probe', () => {
|
|
|
68
64
|
});
|
|
69
65
|
expect(res.status).toBe(405);
|
|
70
66
|
expect(res.headers.get('allow')).toBe('POST, DELETE');
|
|
71
|
-
// A held token needs no auth challenge on the 405.
|
|
72
67
|
expect(res.headers.get('www-authenticate')).toBeNull();
|
|
73
68
|
});
|
|
74
69
|
});
|
|
@@ -96,7 +91,6 @@ describe('ORB-941 - graceful close of in-flight MCP sessions on kill-switch', ()
|
|
|
96
91
|
expect(b.log).toHaveBeenCalledOnce();
|
|
97
92
|
expect(a.close).toHaveBeenCalledOnce();
|
|
98
93
|
expect(b.close).toHaveBeenCalledOnce();
|
|
99
|
-
// The reason is surfaced to the client in the notification payload.
|
|
100
94
|
expect(a.log.mock.calls[0][0]).toMatchObject({ level: 'warning' });
|
|
101
95
|
expect(a.log.mock.calls[0][0].data).toContain('disabled by admin.');
|
|
102
96
|
});
|
|
@@ -108,13 +102,9 @@ describe('ORB-941 - graceful close of in-flight MCP sessions on kill-switch', ()
|
|
|
108
102
|
expect(s.close).toHaveBeenCalledOnce();
|
|
109
103
|
});
|
|
110
104
|
});
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// ORB-1353 - session resilience: persist across restarts + auto-adopt.
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
105
|
describe('ORB-1353 - classifyUnknownSession (pure decision)', () => {
|
|
115
106
|
it('keeps the 404 re-init contract when auth is invalid', () => {
|
|
116
107
|
expect(classifyUnknownSession({ hasValidAuth: false, isPersistedForCaller: false })).toBe('reinit-404');
|
|
117
|
-
// Even a "persisted" id must not resurrect without valid auth.
|
|
118
108
|
expect(classifyUnknownSession({ hasValidAuth: false, isPersistedForCaller: true })).toBe('reinit-404');
|
|
119
109
|
});
|
|
120
110
|
it('rehydrates a persisted session owned by the caller (layer 1)', () => {
|
|
@@ -134,25 +124,17 @@ describe('ORB-1353 - clientInfoLabel', () => {
|
|
|
134
124
|
expect(clientInfoLabel(null)).toBeUndefined();
|
|
135
125
|
});
|
|
136
126
|
});
|
|
137
|
-
// A controllable in-process fake of the api endpoints the transport touches:
|
|
138
|
-
// preflight (/system/mcp/status), the working-rules fetch, and the event
|
|
139
|
-
// bridge's SSE endpoint. DB-free - the persisted-session store is injected
|
|
140
|
-
// separately so a "restart" is just clearing the in-memory registry.
|
|
141
127
|
function fakeApi(opts = {}) {
|
|
142
128
|
const state = {
|
|
143
129
|
enabled: opts.enabled ?? true,
|
|
144
130
|
mcpUseGranted: opts.mcpUseGranted ?? true,
|
|
145
|
-
// ORB-942 - per-user MCP opt-out. Defaults on so existing flows are unaffected.
|
|
146
131
|
userMcpEnabled: opts.userMcpEnabled ?? true,
|
|
147
|
-
// ORB-1470 - record every Authorization header the api sees so a test can
|
|
148
|
-
// assert which bearer the transport forwarded upstream.
|
|
149
132
|
seenAuth: [],
|
|
150
133
|
};
|
|
151
134
|
const api = createNodeServer((req, res) => {
|
|
152
135
|
const url = req.url ?? '';
|
|
153
136
|
if (url.startsWith('/system/mcp/status')) {
|
|
154
137
|
state.seenAuth.push((req.headers.authorization ?? ''));
|
|
155
|
-
// A token of `Bearer invalid` models an expired/invalid credential.
|
|
156
138
|
if ((req.headers.authorization ?? '') === 'Bearer invalid') {
|
|
157
139
|
res.writeHead(401, { 'content-type': 'application/json' });
|
|
158
140
|
res.end(JSON.stringify({ error: 'invalid token' }));
|
|
@@ -173,7 +155,6 @@ function fakeApi(opts = {}) {
|
|
|
173
155
|
return;
|
|
174
156
|
}
|
|
175
157
|
if (url.startsWith('/sse/mcp-events')) {
|
|
176
|
-
// Empty stream - the bridge reconnects on close, cleared by session close.
|
|
177
158
|
res.writeHead(200, { 'content-type': 'text/event-stream' });
|
|
178
159
|
res.end();
|
|
179
160
|
return;
|
|
@@ -208,7 +189,6 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
208
189
|
let api = null;
|
|
209
190
|
const cleanups = [];
|
|
210
191
|
afterEach(async () => {
|
|
211
|
-
// Close any live MCP sessions so their event-bridge retry timers don't leak.
|
|
212
192
|
for (const c of cleanups.splice(0))
|
|
213
193
|
c();
|
|
214
194
|
if (server) {
|
|
@@ -245,18 +225,13 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
245
225
|
it('rehydrates the SAME session id after an in-memory restart (layer 1)', async () => {
|
|
246
226
|
const store = fakeStore();
|
|
247
227
|
const { base, internals } = await startWithFakeApi(store.store);
|
|
248
|
-
// 1. Initialise a session and capture its id.
|
|
249
228
|
const initRes = await post(base, INIT_BODY, { authorization: 'Bearer orb_alice' });
|
|
250
229
|
expect(initRes.status).toBe(200);
|
|
251
230
|
const sid = initRes.headers.get('mcp-session-id');
|
|
252
231
|
await initRes.text();
|
|
253
232
|
expect(sid).toBeTruthy();
|
|
254
|
-
// The freshly-minted session was persisted.
|
|
255
233
|
await vi.waitFor(() => expect(store.rows.has(sid)).toBe(true));
|
|
256
|
-
// 2. Simulate an api restart: the in-memory registry is wiped, the store
|
|
257
|
-
// (DB) survives.
|
|
258
234
|
internals.sessions.clear();
|
|
259
|
-
// 3. The next call with the OLD id + valid auth rehydrates under the SAME id.
|
|
260
235
|
const res = await post(base, NON_INIT, { authorization: 'Bearer orb_alice', 'mcp-session-id': sid });
|
|
261
236
|
expect(res.status).toBe(200);
|
|
262
237
|
expect(res.headers.get('mcp-session-id')).toBe(sid);
|
|
@@ -273,7 +248,6 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
273
248
|
await res.text();
|
|
274
249
|
expect(newId).toBeTruthy();
|
|
275
250
|
expect(newId).not.toBe(stale);
|
|
276
|
-
// Adoption is recorded old -> new.
|
|
277
251
|
expect(store.register).toHaveBeenCalledWith('orb_bob', expect.objectContaining({ sessionId: newId, adoptedFrom: stale }));
|
|
278
252
|
});
|
|
279
253
|
it('keeps the ORB-1324 404 contract for an unknown id WITHOUT valid auth', async () => {
|
|
@@ -284,7 +258,6 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
284
258
|
expect(res.headers.get('www-authenticate')).toBeNull();
|
|
285
259
|
const bodyText = await res.text();
|
|
286
260
|
expect(bodyText).toMatch(/reinitialize/i);
|
|
287
|
-
// No session was adopted for an unauthenticated request.
|
|
288
261
|
expect(store.register).not.toHaveBeenCalled();
|
|
289
262
|
});
|
|
290
263
|
it('refuses to adopt when the MCP kill-switch is OFF (returns 404, not a session)', async () => {
|
|
@@ -297,9 +270,6 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
297
270
|
});
|
|
298
271
|
it('refuses to adopt when the per-user MCP opt-out is OFF (ORB-942, returns 404)', async () => {
|
|
299
272
|
const store = fakeStore();
|
|
300
|
-
// Workspace is enabled but the caller flipped their own users.mcp_enabled
|
|
301
|
-
// off - the shared preflight refuses, so the stale id falls to the 404
|
|
302
|
-
// re-init branch and no session is (re)established.
|
|
303
273
|
const { base } = await startWithFakeApi(store.store, fakeApi({ enabled: true, userMcpEnabled: false }));
|
|
304
274
|
const res = await post(base, NON_INIT, { authorization: 'Bearer orb_dave', 'mcp-session-id': 'sess_stale' });
|
|
305
275
|
expect(res.status).toBe(404);
|
|
@@ -307,19 +277,6 @@ describe('ORB-1353 - persisted-session resilience (transport, fake api)', () =>
|
|
|
307
277
|
expect(store.register).not.toHaveBeenCalled();
|
|
308
278
|
});
|
|
309
279
|
});
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
// ORB-1470 - a long-lived MCP session must follow the client's OAuth access-
|
|
312
|
-
// token rotation, not pin itself to the token captured at session creation.
|
|
313
|
-
//
|
|
314
|
-
// Root cause of the field 401s: the transport bound each session's OrbotoClient
|
|
315
|
-
// (+ tool-handler client + SSE bridge) to the bearer seen at creation and
|
|
316
|
-
// reused it for the session's whole life. An OAuth access token has a 1h TTL and
|
|
317
|
-
// the client rotates it; once the pinned token aged out the api answered 401
|
|
318
|
-
// "OAuth access token expired" on the NEXT tool call - even though that request
|
|
319
|
-
// carried a valid refreshed bearer. If the session was initialised with an
|
|
320
|
-
// already-aged cached token, that was only minutes after connect. The fix makes
|
|
321
|
-
// the per-request bearer authoritative via a mutable per-session token holder.
|
|
322
|
-
// ---------------------------------------------------------------------------
|
|
323
280
|
describe('ORB-1470 - per-session bearer follows the client\'s token rotation', () => {
|
|
324
281
|
let api = null;
|
|
325
282
|
afterEach(async () => {
|
|
@@ -357,25 +314,17 @@ describe('ORB-1470 - per-session bearer follows the client\'s token rotation', (
|
|
|
357
314
|
it('adopts the bearer presented on each request and forwards the fresh token upstream (no pinned-token 401)', async () => {
|
|
358
315
|
const store = fakeStore();
|
|
359
316
|
const { base, internals, state } = await startWithFakeApi(store.store);
|
|
360
|
-
// 1. Connect: initialise a session with the first access token (v1).
|
|
361
317
|
const initRes = await post(base, INIT_BODY, { authorization: 'Bearer orb_tok_v1' });
|
|
362
318
|
expect(initRes.status).toBe(200);
|
|
363
319
|
const sid = initRes.headers.get('mcp-session-id');
|
|
364
320
|
await initRes.text();
|
|
365
321
|
expect(sid).toBeTruthy();
|
|
366
|
-
// The session's mutable holder starts at the creation-time bearer.
|
|
367
322
|
const session = internals.sessions.get(sid);
|
|
368
323
|
expect(session.tokenHolder.current).toBe('orb_tok_v1');
|
|
369
|
-
// 2. The client's OAuth layer rotates its access token (v1 -> v2) and sends
|
|
370
|
-
// the next request on the SAME session id with the fresh bearer.
|
|
371
324
|
const res2 = await post(base, NON_INIT, { authorization: 'Bearer orb_tok_v2', 'mcp-session-id': sid });
|
|
372
325
|
expect(res2.status).toBe(200);
|
|
373
326
|
await res2.text();
|
|
374
|
-
// The session adopted the rotated bearer. Before the fix the session stayed
|
|
375
|
-
// pinned to v1 and would 401 "expired" once v1 aged out.
|
|
376
327
|
expect(session.tokenHolder.current).toBe('orb_tok_v2');
|
|
377
|
-
// End-to-end: the session's actual OrbotoClient now forwards v2 upstream, so
|
|
378
|
-
// the api never sees the stale v1 token again.
|
|
379
328
|
state.seenAuth.length = 0;
|
|
380
329
|
await session.client.get('/system/mcp/status');
|
|
381
330
|
expect(state.seenAuth).toContain('Bearer orb_tok_v2');
|
|
@@ -384,16 +333,11 @@ describe('ORB-1470 - per-session bearer follows the client\'s token rotation', (
|
|
|
384
333
|
it('adopts the current bearer when rehydrating a session after a restart', async () => {
|
|
385
334
|
const store = fakeStore();
|
|
386
335
|
const { base, internals, state } = await startWithFakeApi(store.store);
|
|
387
|
-
// Connect with v1, then simulate an api restart wiping the in-memory map.
|
|
388
336
|
const initRes = await post(base, INIT_BODY, { authorization: 'Bearer orb_tok_v1' });
|
|
389
337
|
const sid = initRes.headers.get('mcp-session-id');
|
|
390
338
|
await initRes.text();
|
|
391
339
|
internals.sessions.clear();
|
|
392
|
-
// The client (now on a refreshed v2) reconnects under the SAME id.
|
|
393
340
|
const res = await post(base, NON_INIT, { authorization: 'Bearer orb_tok_v1', 'mcp-session-id': sid });
|
|
394
|
-
// ^ rehydration is keyed on the persisted-store owner (token == userId in
|
|
395
|
-
// the fake), so the rehydrate itself uses the presented token. Then a
|
|
396
|
-
// follow-up request rotates to v2.
|
|
397
341
|
expect(res.status).toBe(200);
|
|
398
342
|
await res.text();
|
|
399
343
|
const res2 = await post(base, NON_INIT, { authorization: 'Bearer orb_tok_v2', 'mcp-session-id': sid });
|
package/dist/index.js
CHANGED
|
@@ -2,34 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ORB-244 Phase A - entry point for `@orboto/mcp`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* ORBOTO_MCP_TRANSPORT=stdio (default) - JSON-RPC over stdin/stdout,
|
|
7
|
-
* used by the Local-Proxy delivery variant. Claude Desktop spawns
|
|
8
|
-
* this process via the `claude_desktop_config.json` snippet and
|
|
9
|
-
* talks over its own stdio pair.
|
|
10
|
-
* ORBOTO_MCP_TRANSPORT=http - Streamable HTTP per MCP
|
|
11
|
-
* spec. Used by the Self-Hosted-inline + Cloud-Managed variants
|
|
12
|
-
* (separate container listening on a port the reverse proxy maps
|
|
13
|
-
* to `/mcp`). Sessions carry `mcp-session-id` for server→client
|
|
14
|
-
* notifications.
|
|
15
|
-
*
|
|
16
|
-
* Config (env-only - no config file):
|
|
17
|
-
* ORBOTO_API_URL required - base URL of the orboto API
|
|
18
|
-
* ORBOTO_API_KEY optional - `orb_*` API key with `mcp:use`
|
|
19
|
-
* scope (stdio mode). Service-account fallback;
|
|
20
|
-
* when omitted, stdio mode bootstraps via OAuth
|
|
21
|
-
* (ORB-943) so a desktop user connects through the
|
|
22
|
-
* workspace login without pasting a token. Per-
|
|
23
|
-
* session bearer is read from the Authorization
|
|
24
|
-
* header in http mode.
|
|
25
|
-
* ORBOTO_AUTH optional - `pat` | `oauth`. Defaults to `pat`
|
|
26
|
-
* when ORBOTO_API_KEY is set, else `oauth`. Force
|
|
27
|
-
* `oauth` to run the browser bootstrap even with a
|
|
28
|
-
* key present.
|
|
29
|
-
* ORBOTO_MCP_TRANSPORT optional - `stdio` (default) | `http`
|
|
30
|
-
* ORBOTO_MCP_PORT optional - port for http transport, default 3100
|
|
31
|
-
* ORBOTO_MCP_CLIENT optional - client hint for User-Agent (e.g.
|
|
32
|
-
* `claude-desktop`, `cursor`).
|
|
5
|
+
* @see ORB-943
|
|
33
6
|
*/
|
|
34
7
|
import { buildOrbotoMcpServer } from './server.js';
|
|
35
8
|
import { OrbotoClient, preflightMcpSession } from './orboto-client.js';
|
|
@@ -45,12 +18,6 @@ function requireEnv(name) {
|
|
|
45
18
|
async function main() {
|
|
46
19
|
const transport = (process.env.ORBOTO_MCP_TRANSPORT ?? 'stdio').toLowerCase();
|
|
47
20
|
if (transport === 'stdio') {
|
|
48
|
-
// Local-Proxy mode - one MCP client, one process, one identity. The
|
|
49
|
-
// identity is either a static `orb_*` PAT (service-account fallback) OR,
|
|
50
|
-
// when no key is configured, an OAuth session bootstrapped through the
|
|
51
|
-
// workspace login (ORB-943) so a desktop user never pastes a token. All
|
|
52
|
-
// config read at boot; no per-request auth needed because the only user of
|
|
53
|
-
// this stdio pair is the client that spawned us.
|
|
54
21
|
const baseUrl = requireEnv('ORBOTO_API_URL');
|
|
55
22
|
const apiKey = process.env.ORBOTO_API_KEY;
|
|
56
23
|
const userAgentSuffix = process.env.ORBOTO_MCP_CLIENT;
|
|
@@ -60,8 +27,6 @@ async function main() {
|
|
|
60
27
|
console.error('[orboto-mcp] ORBOTO_AUTH=pat requires ORBOTO_API_KEY. Unset it to use OAuth, or provide a key.');
|
|
61
28
|
process.exit(2);
|
|
62
29
|
}
|
|
63
|
-
// clientConfig carries either the PAT or the OAuth token provider; both
|
|
64
|
-
// shapes satisfy OrbotoClientConfig.
|
|
65
30
|
let clientConfig;
|
|
66
31
|
if (authMode === 'oauth') {
|
|
67
32
|
try {
|
|
@@ -77,9 +42,6 @@ async function main() {
|
|
|
77
42
|
else {
|
|
78
43
|
clientConfig = { baseUrl, userAgentSuffix, apiKey };
|
|
79
44
|
}
|
|
80
|
-
// Preflight BEFORE spinning up the transport - so a
|
|
81
|
-
// mis-configured install fails loudly to stderr instead of
|
|
82
|
-
// silently hanging on stdin waiting for JSON-RPC frames.
|
|
83
45
|
const preflightClient = new OrbotoClient(clientConfig);
|
|
84
46
|
try {
|
|
85
47
|
const { userEmail } = await preflightMcpSession(preflightClient);
|
|
@@ -95,23 +57,14 @@ async function main() {
|
|
|
95
57
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
96
58
|
const stdio = new StdioServerTransport();
|
|
97
59
|
await server.connect(stdio);
|
|
98
|
-
// The SDK writes its own "connected" log to stderr; we don't
|
|
99
|
-
// echo anything here - stdout is reserved for JSON-RPC frames
|
|
100
|
-
// and an accidental console.log would corrupt the protocol.
|
|
101
60
|
return;
|
|
102
61
|
}
|
|
103
62
|
if (transport === 'http') {
|
|
104
|
-
// Self-Hosted + Cloud-Managed mode. The bearer token comes from
|
|
105
|
-
// the caller (Claude Desktop / Cursor) on every POST - a per-
|
|
106
|
-
// session server is built so each session carries its own
|
|
107
|
-
// API-key scoped OrbotoClient.
|
|
108
63
|
const port = Number(process.env.ORBOTO_MCP_PORT ?? '3100');
|
|
109
64
|
const baseUrl = requireEnv('ORBOTO_API_URL');
|
|
110
65
|
const { createHttpServer } = await import('./http-transport.js');
|
|
111
66
|
const httpServer = createHttpServer({ baseUrl });
|
|
112
67
|
httpServer.listen(port, () => {
|
|
113
|
-
// stderr so self-hosted operators can tail the container log
|
|
114
|
-
// without parsing the JSON on stdout (stdio mode).
|
|
115
68
|
// eslint-disable-next-line no-console
|
|
116
69
|
console.error(`[orboto-mcp] http listening on :${port} → ${baseUrl}`);
|
|
117
70
|
});
|
package/dist/input-schema.js
CHANGED
|
@@ -1,60 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1692 - strict tool inputs + the aliases agents actually send.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* validation failures where the agent guessed a plausible-but-wrong
|
|
6
|
-
* parameter name (`body` for `text`, `parentKey` for `parentTicketKey`,
|
|
7
|
-
* ...). Worse, zod strips unknown keys by default, so a wrong key on an
|
|
8
|
-
* otherwise-valid call VANISHED silently - `create_ticket` with
|
|
9
|
-
* `parentKey` created ORB-1684 with no parent and no warning.
|
|
10
|
-
*
|
|
11
|
-
* Fix, applied centrally at registration (with-metrics `reg()`):
|
|
12
|
-
* 1. every tool input becomes `.strict()` - an unknown key errors,
|
|
13
|
-
* naming the offender;
|
|
14
|
-
* 2. the measured aliases resolve silently to the canonical field via a
|
|
15
|
-
* GUARDED global rename: `alias -> canonical` fires only when the
|
|
16
|
-
* canonical field exists in that tool's shape, the alias does not,
|
|
17
|
-
* and the caller did not also send the canonical - so a tool whose
|
|
18
|
-
* REAL field is `body` (agent-instruction blocks) is never touched;
|
|
19
|
-
* 3. `orboto_update_ticket` folds flat patch fields into `patch` when no
|
|
20
|
-
* `patch` was sent - agents routinely write `{ticketKey, description}`.
|
|
21
|
-
*
|
|
22
|
-
* ORB-1817 - "we do not force keys that make no sense - key and query are
|
|
23
|
-
* sensible names" (operator). Extends the alias table with the sensible
|
|
24
|
-
* spellings a caller actually reaches for (`key`, `query`, `projectId`,
|
|
25
|
-
* ...) plus one DYNAMIC alias - `id` resolves to the tool's own single
|
|
26
|
-
* id-shaped parameter (`ticketKey` / `docId` / `milestone` / `commentId`)
|
|
27
|
-
* only when exactly one of those exists in that tool's shape - and adds a
|
|
28
|
-
* teaching error: when a call still can't be resolved, the thrown message
|
|
29
|
-
* is a structured JSON block naming every parameter the tool accepts plus
|
|
30
|
-
* a Levenshtein<=2 guess for each unrecognized key (same distance the mjs
|
|
31
|
-
* wrapper's `suggestFlag` uses for `--flag` typos). The failure is also
|
|
32
|
-
* logged through the same `/admin/mcp/instrument` path a handler error
|
|
33
|
-
* uses - see the module doc on `mcp-instrument.ts` for why that needs a
|
|
34
|
-
* dedicated leaf module instead of importing from with-metrics.ts.
|
|
35
|
-
*
|
|
36
|
-
* SDK mechanics: `registerTool` accepts either a raw shape or a zod
|
|
37
|
-
* schema. A top-level `z.preprocess(...)` has no `.shape`, which would
|
|
38
|
-
* make the SDK advertise an EMPTY input schema in tools/list. Stamping
|
|
39
|
-
* the inner object's `.shape` onto the effects schema keeps the
|
|
40
|
-
* advertisement byte-identical to the strict object (verified: the SDK's
|
|
41
|
-
* `normalizeObjectSchema` only checks `.shape !== undefined`, and its
|
|
42
|
-
* json-schema emitter unwraps effects with pipeStrategy "input").
|
|
43
|
-
*
|
|
44
|
-
* ORB-1817 - the preprocess callback is also where the teaching error is
|
|
45
|
-
* thrown (verified against @modelcontextprotocol/sdk 1.29's zod-compat +
|
|
46
|
-
* server/mcp.js): a `preprocess` effect calls `effect.transform(data,
|
|
47
|
-
* checkCtx)` BEFORE the inner schema ever parses, and unlike `refine` /
|
|
48
|
-
* `transform`, a synchronous throw from inside it is NOT caught by zod -
|
|
49
|
-
* it propagates out of `ZodEffects._parse`, out of the SDK's
|
|
50
|
-
* `safeParseAsync` (converted to a rejected promise, since that's an
|
|
51
|
-
* `async function`), and lands in the SDK's own `CallToolRequestSchema`
|
|
52
|
-
* handler's outer try/catch, which for a plain (non-McpError) `Error`
|
|
53
|
-
* returns `{isError: true, content: [{type: 'text', text: error.message}]}`
|
|
54
|
-
* with NO extra wrapping. That is what lets us hand back exactly our
|
|
55
|
-
* structured JSON with nothing else attached - `.superRefine()` cannot do
|
|
56
|
-
* this (it never even runs once the base object parse has already failed,
|
|
57
|
-
* which is the common case: a missing-required or unrecognized-key input).
|
|
4
|
+
* @see ORB-1684, ORB-1817
|
|
58
5
|
*/
|
|
59
6
|
import { z } from 'zod';
|
|
60
7
|
import { postLogEntry, redactSecrets } from './mcp-instrument.js';
|
|
@@ -73,16 +20,12 @@ export const GLOBAL_INPUT_ALIASES = {
|
|
|
73
20
|
milestoneKey: 'milestone',
|
|
74
21
|
ticket: 'ticketKey',
|
|
75
22
|
project: 'projectKey',
|
|
76
|
-
// ORB-1817 - measured 2026-09-03: both cost one retry in the same
|
|
77
|
-
// session. "key" and "query" are what a reasonable caller types.
|
|
78
23
|
key: 'ticketKey',
|
|
79
24
|
query: 'oql',
|
|
80
25
|
projectId: 'projectKey',
|
|
81
26
|
milestoneId: 'milestone',
|
|
82
27
|
comment: 'text',
|
|
83
28
|
message: 'text',
|
|
84
|
-
// Bulk tools all take `ticketKeys` (plural) - singular-sounding `keys`
|
|
85
|
-
// is the natural typo/guess.
|
|
86
29
|
keys: 'ticketKeys',
|
|
87
30
|
max: 'limit',
|
|
88
31
|
page: 'cursor',
|
|
@@ -112,7 +55,6 @@ function applyAliases(toolName, shape, value) {
|
|
|
112
55
|
out = { ...rest, [canonical]: aliased };
|
|
113
56
|
}
|
|
114
57
|
}
|
|
115
|
-
// ORB-1817 - `id` -> the tool's single id-shaped parameter.
|
|
116
58
|
if ('id' in out && !('id' in shape)) {
|
|
117
59
|
const candidates = ID_LIKE_CANONICALS.filter((c) => c in shape && !(c in out));
|
|
118
60
|
if (candidates.length === 1) {
|
|
@@ -121,9 +63,6 @@ function applyAliases(toolName, shape, value) {
|
|
|
121
63
|
out = { ...rest, [canonical]: id };
|
|
122
64
|
}
|
|
123
65
|
}
|
|
124
|
-
// update_ticket: flat fields become an implicit patch. Only when the
|
|
125
|
-
// caller sent no patch at all - a partial patch plus flat extras stays
|
|
126
|
-
// an error (ambiguous intent must not be guessed).
|
|
127
66
|
if (toolName === 'orboto_update_ticket' && !('patch' in out)) {
|
|
128
67
|
const patch = {};
|
|
129
68
|
const rest = {};
|
|
@@ -142,10 +81,6 @@ function applyAliases(toolName, shape, value) {
|
|
|
142
81
|
}
|
|
143
82
|
return out;
|
|
144
83
|
}
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
// ORB-1817 - teaching error: structured `expected` block + closest-name
|
|
147
|
-
// guess, built when the strict + alias-resolved value STILL doesn't parse.
|
|
148
|
-
// ---------------------------------------------------------------------------
|
|
149
84
|
/** Levenshtein-1/2 match, same algorithm skills/orboto/scripts/orboto.mjs
|
|
150
85
|
* uses for `--flag` typos (`suggestFlag` / `levenshtein`) - keep them in
|
|
151
86
|
* sync if the distance metric ever changes. */
|
|
@@ -247,12 +182,7 @@ function buildTeachingError(toolName, shape, issues) {
|
|
|
247
182
|
* validates against. Returns a schema whose `.shape` is stamped so
|
|
248
183
|
* tools/list advertisement stays identical to the plain object form.
|
|
249
184
|
*
|
|
250
|
-
*
|
|
251
|
-
* them) - when present, a validation failure that survives aliasing is
|
|
252
|
-
* logged through the SAME `/admin/mcp/instrument` path a handler error
|
|
253
|
-
* uses (ORB-1817 Part C), because the SDK validates input BEFORE
|
|
254
|
-
* with-metrics.ts's handler wrapper ever runs - this preprocess step is
|
|
255
|
-
* the only place that ever sees the failure.
|
|
185
|
+
* @see ORB-1817
|
|
256
186
|
*/
|
|
257
187
|
export function buildStrictInputSchema(toolName, shape, client, clientHint) {
|
|
258
188
|
const inner = z.object(shape).strict();
|
|
@@ -273,12 +203,8 @@ export function buildStrictInputSchema(toolName, shape, client, clientHint) {
|
|
|
273
203
|
clientHint,
|
|
274
204
|
});
|
|
275
205
|
}
|
|
276
|
-
// See the module doc: thrown here (inside a "preprocess" effect, not
|
|
277
|
-
// a refinement/transform), this is NOT caught by zod - it becomes the
|
|
278
|
-
// exact isError text the caller sees.
|
|
279
206
|
throw new Error(message);
|
|
280
207
|
}, inner);
|
|
281
|
-
// SDK contract (see module doc): .shape must exist for advertisement.
|
|
282
208
|
effects.shape = inner.shape;
|
|
283
209
|
return effects;
|
|
284
210
|
}
|
|
@@ -2,18 +2,7 @@
|
|
|
2
2
|
* ORB-1692 / ORB-1817 - strict inputs + measured aliases + the teaching
|
|
3
3
|
* error.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* measurement table must resolve to the canonical field, unknown keys
|
|
7
|
-
* must error naming the offender, and the ORB-1684 silent-drop case
|
|
8
|
-
* (create_ticket with parentKey) must never come back.
|
|
9
|
-
*
|
|
10
|
-
* ORB-1817 - a call that still can't be resolved after aliasing now
|
|
11
|
-
* THROWS (from every parse entry point, `.safeParse()` included - see
|
|
12
|
-
* the module doc on `buildStrictInputSchema` for why: the teaching error
|
|
13
|
-
* is thrown from inside a zod "preprocess" effect, which runs before the
|
|
14
|
-
* sync/async branch even exists). That's an intentional behaviour change
|
|
15
|
-
* from plain zod (whose `.safeParse()` never throws) - every test below
|
|
16
|
-
* that used to check `parsed.success === false` now checks the throw.
|
|
5
|
+
* @see ORB-1684
|
|
17
6
|
*/
|
|
18
7
|
import { describe, it, expect, vi } from 'vitest';
|
|
19
8
|
import { z } from 'zod';
|
|
@@ -105,7 +94,6 @@ describe('alias guard: canonical-field collisions are never renamed', () => {
|
|
|
105
94
|
}
|
|
106
95
|
});
|
|
107
96
|
it('alias is ignored when the caller also sent the canonical field', () => {
|
|
108
|
-
// both present: no rename happens, so `body` is an unknown key -> error
|
|
109
97
|
const body = throwsWith(schemaFor('orboto_comment', commentToolConfig), { ticketKey: 'ORB-1', text: 'real', body: 'stray' });
|
|
110
98
|
expect(body.unrecognized).toEqual(['body']);
|
|
111
99
|
});
|
|
@@ -121,7 +109,6 @@ describe('ORB-1817: extended alias table', () => {
|
|
|
121
109
|
{ tool: 'orboto_bulk_move_tickets', config: bulkMoveTicketsToolConfig, send: { keys: ['ORB-1', 'ORB-2'], statusCategory: 'done' }, expectKey: 'ticketKeys', expectValue: ['ORB-1', 'ORB-2'] },
|
|
122
110
|
{ tool: 'orboto_query', config: queryToolConfig, send: { oql: 'project = ORB', max: 10 }, expectKey: 'limit', expectValue: 10 },
|
|
123
111
|
{ tool: 'orboto_query', config: queryToolConfig, send: { oql: 'project = ORB', page: 'c1' }, expectKey: 'cursor', expectValue: 'c1' },
|
|
124
|
-
// `id` -> the tool's single id-shaped parameter (ticketKey here).
|
|
125
112
|
{ tool: 'orboto_get_ticket', config: getTicketToolConfig, send: { id: 'ORB-1' }, expectKey: 'ticketKey', expectValue: 'ORB-1' },
|
|
126
113
|
{ tool: 'orboto_get_doc', config: getDocToolConfig, send: { id: 'ORB-D12' }, expectKey: 'docId', expectValue: 'ORB-D12' },
|
|
127
114
|
];
|
|
@@ -136,7 +123,6 @@ describe('ORB-1817: extended alias table', () => {
|
|
|
136
123
|
});
|
|
137
124
|
}
|
|
138
125
|
it('`id` is left alone (and errors) when the tool has more than one id-shaped parameter', () => {
|
|
139
|
-
// orboto_update_comment has BOTH ticketKey and commentId - ambiguous.
|
|
140
126
|
const body = throwsWith(schemaFor('orboto_update_comment', updateCommentToolConfig), { id: 'ORB-1', text: 'hi' });
|
|
141
127
|
expect(body.unrecognized).toEqual(['id']);
|
|
142
128
|
});
|
|
@@ -23,12 +23,10 @@ describe('assembleInstructions (ORB-1177)', () => {
|
|
|
23
23
|
expect(out).toContain('orboto://rules');
|
|
24
24
|
expect(out).toContain('orboto_session_start');
|
|
25
25
|
expect(out).toContain('truncated');
|
|
26
|
-
// Every kept rule line is a COMPLETE original line (no mid-line cut).
|
|
27
26
|
const body = out.slice(`${head}\n\n${HEADING}`.length).split('\n\n[... rules truncated')[0];
|
|
28
27
|
const kept = body.length ? body.split('\n') : [];
|
|
29
28
|
for (const line of kept)
|
|
30
29
|
expect(lines).toContain(line);
|
|
31
|
-
// And it actually kept fewer than all lines (proves truncation happened).
|
|
32
30
|
expect(kept.length).toBeLessThan(lines.length);
|
|
33
31
|
});
|
|
34
32
|
});
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ORB-1521 - manifest-size measurement + regression ratchet (epic
|
|
3
3
|
* ORB-1517).
|
|
4
|
-
*
|
|
5
|
-
* Measures the REAL `tools/list` payload an eager-loading client pays
|
|
6
|
-
* at connect time - through an actual MCP client over an in-memory
|
|
7
|
-
* transport, so the numbers include the SDK's zod->JSON-schema
|
|
8
|
-
* conversion, exactly what Codex/Cursor/Claude Desktop receive.
|
|
9
|
-
*
|
|
10
|
-
* The curated assertion is the RATCHET (mirroring the dark-mode / i18n
|
|
11
|
-
* shrink-only checks): if someone re-registers the tail into the
|
|
12
|
-
* default manifest - or the curated set quietly balloons - this fails
|
|
13
|
-
* before a release ships the token bill. Raising CURATED_MAX_CHARS is a
|
|
14
|
-
* conscious decision to grow the default connect cost, not a formality.
|
|
15
4
|
*/
|
|
16
5
|
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
17
6
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -37,34 +26,9 @@ afterEach(() => { vi.restoreAllMocks(); });
|
|
|
37
26
|
* 20 %. Shrink-only: raising either number is a conscious decision to
|
|
38
27
|
* grow every session's connect cost.
|
|
39
28
|
*
|
|
40
|
-
* ORB-1805,
|
|
41
|
-
* advertised `__truncation` block: minimal = 12 tools / 9,440 chars;
|
|
42
|
-
* curated = 31 tools / 27,945 chars (was 34,015); full = 175 tools /
|
|
43
|
-
* 142,366 chars (was 152,348).
|
|
44
|
-
*
|
|
45
|
-
* ORB-1669, landed 2026-09-04: every tool carries an MCP `annotations`
|
|
46
|
-
* block (readOnlyHint / destructiveHint / idempotentHint) - clients use
|
|
47
|
-
* them for confirmation UX, so they are spec conformance, not payload
|
|
48
|
-
* fat. Measured cost: minimal +688 chars (12 tools), curated +1,276,
|
|
49
|
-
* full +9,836 (175 tools, ~56 chars each). FULL_MAX_CHARS and
|
|
50
|
-
* MINIMAL_MAX_TOKENS were raised by exactly that, deliberately.
|
|
51
|
-
*
|
|
52
|
-
* ORB-1910 (2026-09-05): orboto_report_feedback joins the curated set (32
|
|
53
|
-
* tools) - an agent that hits a bug must be able to report it without
|
|
54
|
-
* ?toolset=full. Measured curated = 30,652 chars after trimming the tool's
|
|
55
|
-
* schema to the minimum (one describe() on `page`); ceiling re-pinned to
|
|
56
|
-
* 32,500 (~6 % headroom), deliberately, for that one addition.
|
|
29
|
+
* @see ORB-1805, ORB-1669, ORB-1910
|
|
57
30
|
*/
|
|
58
|
-
// ORB-1959 - re-pinned at 37,500 (measured 36,714): the four agent
|
|
59
|
-
// coordination tools (messages, notify, broadcast, heartbeat) joined the
|
|
60
|
-
// curated tier so an agent on the DEFAULT connection can fetch, act on and
|
|
61
|
-
// ack its mail - the binding workspace rule the old manifest made impossible
|
|
62
|
-
// to follow. Their descriptions were cut to the load-bearing facts first
|
|
63
|
-
// (286 chars); the remaining ~4.2k chars are input/output schemas and the
|
|
64
|
-
// per-tool keys the SDK emits. Decision by the operator, 2026-09-06.
|
|
65
31
|
const CURATED_MAX_CHARS = 37_500;
|
|
66
|
-
// ORB-2000 - orboto_draft_customer_reply joins the full tier (measured
|
|
67
|
-
// 155,669 chars with it); ceiling re-pinned with ~6 % headroom.
|
|
68
32
|
const FULL_MAX_CHARS = 165_000;
|
|
69
33
|
/**
|
|
70
34
|
* ORB-1805 - the estimator the ticket measured the failure with
|
|
@@ -85,22 +49,8 @@ const MINIMAL_MAX_TOKENS = 3_100;
|
|
|
85
49
|
/**
|
|
86
50
|
* The curated tier is ratcheted on its TOOL SCHEMAS (the instructions
|
|
87
51
|
* block has its own budget, enforced in instructions-budget.test.ts).
|
|
88
|
-
*
|
|
89
|
-
* Why this is not the ticket's aspirational 6,000: measured 2026-09-01,
|
|
90
|
-
* the curated manifest with EVERY description and title stripped out is
|
|
91
|
-
* still 17,379 chars (~4,827 tokens) - 31 tools' names, parameter names,
|
|
92
|
-
* types, enums, plus ~2,700 chars of per-tool `$schema` and `execution`
|
|
93
|
-
* keys the MCP SDK emits and we do not control. A 6,000-token ceiling
|
|
94
|
-
* would leave ~4,200 chars for all 31 tools' text combined, i.e. the
|
|
95
|
-
* one-line summaries and NOTHING else - no "Key (ORB-M3), name, or
|
|
96
|
-
* UUID", no "YYYY-MM-DD", no "Default: task". Those pay for themselves
|
|
97
|
-
* in avoided failed calls. The tier that genuinely fits a small window
|
|
98
|
-
* is `minimal`; curated is the desktop-client default, and this ceiling
|
|
99
|
-
* holds the 18 % the diet won.
|
|
100
52
|
*/
|
|
101
|
-
|
|
102
|
-
// gained orboto_report_feedback; measured 8,514 estimated tokens.
|
|
103
|
-
const CURATED_SCHEMA_MAX_TOKENS = 10_450; // ORB-1959: measured 10,198 with the coordination tools
|
|
53
|
+
const CURATED_SCHEMA_MAX_TOKENS = 10_450;
|
|
104
54
|
async function measuredManifest(toolset) {
|
|
105
55
|
const server = await buildOrbotoMcpServer({
|
|
106
56
|
baseUrl: 'https://orboto.example.com',
|
|
@@ -111,9 +61,6 @@ async function measuredManifest(toolset) {
|
|
|
111
61
|
const client = new Client({ name: 'manifest-measure', version: '0.0.0' });
|
|
112
62
|
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
|
113
63
|
try {
|
|
114
|
-
// Walk pagination defensively; the SDK serves all tools in one page
|
|
115
|
-
// today but the measurement must not silently undercount if that
|
|
116
|
-
// changes.
|
|
117
64
|
let cursor;
|
|
118
65
|
const tools = [];
|
|
119
66
|
do {
|
|
@@ -121,9 +68,6 @@ async function measuredManifest(toolset) {
|
|
|
121
68
|
tools.push(...page.tools);
|
|
122
69
|
cursor = page.nextCursor;
|
|
123
70
|
} while (cursor);
|
|
124
|
-
// `_instructions` verified against @modelcontextprotocol/sdk 1.29.
|
|
125
|
-
// Assert non-empty at the call sites so an SDK rename fails loudly
|
|
126
|
-
// instead of scoring the block as free.
|
|
127
71
|
const instructions = String(server.server._instructions ?? '');
|
|
128
72
|
return { count: tools.length, chars: JSON.stringify(tools).length, instructionsChars: instructions.length };
|
|
129
73
|
}
|
|
@@ -137,9 +81,6 @@ describe('ORB-1521 - eager-load manifest size', () => {
|
|
|
137
81
|
const { count, chars } = await measuredManifest();
|
|
138
82
|
// eslint-disable-next-line no-console
|
|
139
83
|
console.log(`[manifest-size] curated: ${count} tools, ${chars} chars (~${estTokens(chars)} tokens)`);
|
|
140
|
-
// 31 = the ORB-1741-dieted set + the two ORB-1694 bulk tools (each
|
|
141
|
-
// REPLACES dozens of per-call responses, so they earn their slot);
|
|
142
|
-
// ORB-1910 added report_feedback, ORB-1959 the four coordination tools.
|
|
143
84
|
expect(count).toBeLessThanOrEqual(36);
|
|
144
85
|
expect(chars).toBeLessThanOrEqual(CURATED_MAX_CHARS);
|
|
145
86
|
expect(estTokens(chars)).toBeLessThanOrEqual(CURATED_SCHEMA_MAX_TOKENS);
|
|
@@ -150,11 +91,7 @@ describe('ORB-1521 - eager-load manifest size', () => {
|
|
|
150
91
|
// eslint-disable-next-line no-console
|
|
151
92
|
console.log(`[manifest-size] full: ${full.count} tools, ${full.chars} chars (~${estTokens(full.chars)} tokens)`);
|
|
152
93
|
expect(full.count).toBeGreaterThan(150);
|
|
153
|
-
// ORB-1741 - the full manifest is ratcheted too: the diet's savings
|
|
154
|
-
// live mostly here (per-tool description essays), so this is where
|
|
155
|
-
// regression would land first.
|
|
156
94
|
expect(full.chars).toBeLessThanOrEqual(FULL_MAX_CHARS);
|
|
157
|
-
// The reduction claim the epic makes - keep it honest in CI.
|
|
158
95
|
expect(full.chars / curated.chars).toBeGreaterThan(3);
|
|
159
96
|
});
|
|
160
97
|
});
|
|
@@ -174,7 +111,7 @@ describe('ORB-1805 - minimal manifest fits a small context window', () => {
|
|
|
174
111
|
+ ` + instructions ${instructionsChars} chars (~${estTokens(instructionsChars)} tokens)`
|
|
175
112
|
+ ` = ~${estTokens(total)} tokens`);
|
|
176
113
|
expect(count).toBe(12);
|
|
177
|
-
expect(instructionsChars).toBeGreaterThan(100);
|
|
114
|
+
expect(instructionsChars).toBeGreaterThan(100);
|
|
178
115
|
expect(estTokens(total)).toBeLessThanOrEqual(MINIMAL_MAX_TOKENS);
|
|
179
116
|
});
|
|
180
117
|
it('minimal is a strict subset of curated, which is a strict subset of full', async () => {
|
|
@@ -186,12 +123,6 @@ describe('ORB-1805 - minimal manifest fits a small context window', () => {
|
|
|
186
123
|
expect(minimal.chars).toBeLessThan(curated.chars);
|
|
187
124
|
});
|
|
188
125
|
it('minimal instructions are a CONSTANT - a long workspace rule set cannot grow them', async () => {
|
|
189
|
-
// The tier ships the head only: no workspace rules block, so a
|
|
190
|
-
// workspace with a 200-line rule set costs the same as an empty one.
|
|
191
|
-
// (The rules are not lost - orboto_session_start returns them, on
|
|
192
|
-
// the caller's budget rather than as a connect-time tax.) Curated,
|
|
193
|
-
// by contrast, embeds them up to its 4k budget - assert BOTH so this
|
|
194
|
-
// stays a real difference and not a coincidence of the fixture.
|
|
195
126
|
const offline = await measuredManifest('minimal');
|
|
196
127
|
const hugeRules = Array.from({ length: 200 }, (_, i) => `rule ${i}: ${'x'.repeat(60)}`).join('\n');
|
|
197
128
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|