@orboto/mcp 0.193.0 → 0.194.1
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
package/dist/oauth-bootstrap.js
CHANGED
|
@@ -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;
|
|
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;
|
|
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 }))
|
|
205
|
-
.mockResolvedValueOnce(new Response('nope', { status: 500 }));
|
|
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: () => { },
|
package/dist/orboto-client.js
CHANGED
|
@@ -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
|
-
*
|
|
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 () => {
|
package/dist/required-rules.js
CHANGED
|
@@ -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;
|
package/dist/resources.test.js
CHANGED
|
@@ -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();
|