@orboto/mcp 0.178.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/LICENSE.md +21 -0
- package/README.md +123 -0
- package/dist/event-bridge.js +250 -0
- package/dist/event-bridge.test.js +165 -0
- package/dist/http-transport-binding.test.js +151 -0
- package/dist/http-transport.js +608 -0
- package/dist/http-transport.test.js +408 -0
- package/dist/index.js +128 -0
- package/dist/input-schema.js +293 -0
- package/dist/input-schema.test.js +198 -0
- package/dist/instructions-budget.test.js +34 -0
- package/dist/manifest-size.test.js +192 -0
- package/dist/mcp-instrument.js +24 -0
- package/dist/oauth-bootstrap.js +413 -0
- package/dist/oauth-bootstrap.test.js +212 -0
- package/dist/orboto-client.js +250 -0
- package/dist/orboto-client.test.js +141 -0
- package/dist/prompts.js +219 -0
- package/dist/prompts.test.js +79 -0
- package/dist/protect-text-usage.test.js +38 -0
- package/dist/resources.js +260 -0
- package/dist/resources.test.js +191 -0
- package/dist/response-budget.js +515 -0
- package/dist/response-budget.test.js +343 -0
- package/dist/server.js +547 -0
- package/dist/session-nudge.js +81 -0
- package/dist/session-nudge.test.js +100 -0
- package/dist/session-nudge.transport.test.js +100 -0
- package/dist/tool-annotations.test.js +97 -0
- package/dist/tool-docs.js +64 -0
- package/dist/tool-docs.test.js +90 -0
- package/dist/tool-parameters.test.js +145 -0
- package/dist/tools/absence-writes.js +93 -0
- package/dist/tools/absence-writes.test.js +106 -0
- package/dist/tools/admin-translations.js +83 -0
- package/dist/tools/admin-writes.js +174 -0
- package/dist/tools/admin-writes.test.js +120 -0
- package/dist/tools/agent-coordination.js +160 -0
- package/dist/tools/agent-coordination.test.js +156 -0
- package/dist/tools/agent-drift.js +79 -0
- package/dist/tools/agent-drift.test.js +87 -0
- package/dist/tools/agent-instructions.js +172 -0
- package/dist/tools/agent-instructions.test.js +124 -0
- package/dist/tools/agent-messages.js +75 -0
- package/dist/tools/ai-status.js +55 -0
- package/dist/tools/ai-status.test.js +43 -0
- package/dist/tools/ai-usage.js +68 -0
- package/dist/tools/analytics.js +72 -0
- package/dist/tools/analytics.test.js +71 -0
- package/dist/tools/api-call.js +72 -0
- package/dist/tools/api-call.test.js +104 -0
- package/dist/tools/api-search.js +77 -0
- package/dist/tools/api-search.test.js +90 -0
- package/dist/tools/approvals.js +68 -0
- package/dist/tools/approvals.test.js +48 -0
- package/dist/tools/attach.js +108 -0
- package/dist/tools/attach.test.js +117 -0
- package/dist/tools/backup.js +93 -0
- package/dist/tools/backup.test.js +108 -0
- package/dist/tools/bulk-create.js +213 -0
- package/dist/tools/bulk-create.test.js +135 -0
- package/dist/tools/bulk-writes.js +339 -0
- package/dist/tools/bulk-writes.test.js +239 -0
- package/dist/tools/check-similar.js +84 -0
- package/dist/tools/check-similar.test.js +101 -0
- package/dist/tools/checklist-writes.js +202 -0
- package/dist/tools/checklist-writes.test.js +184 -0
- package/dist/tools/claim.js +236 -0
- package/dist/tools/claim.test.js +207 -0
- package/dist/tools/critical-path.js +83 -0
- package/dist/tools/critical-path.test.js +114 -0
- package/dist/tools/cross-project-links.ee.js +124 -0
- package/dist/tools/cross-project-links.ee.test.js +163 -0
- package/dist/tools/customer-report.js +88 -0
- package/dist/tools/customer-report.test.js +55 -0
- package/dist/tools/doc-attachments.js +184 -0
- package/dist/tools/doc-attachments.test.js +136 -0
- package/dist/tools/doc-comments.js +228 -0
- package/dist/tools/doc-comments.test.js +192 -0
- package/dist/tools/doc-edits.js +229 -0
- package/dist/tools/doc-edits.test.js +218 -0
- package/dist/tools/doc-export.js +87 -0
- package/dist/tools/doc-export.test.js +109 -0
- package/dist/tools/doc-revisions.js +133 -0
- package/dist/tools/doc-revisions.test.js +103 -0
- package/dist/tools/docs-ai.js +171 -0
- package/dist/tools/docs-ai.test.js +160 -0
- package/dist/tools/docs.js +557 -0
- package/dist/tools/docs.test.js +365 -0
- package/dist/tools/embedding-status.js +100 -0
- package/dist/tools/embedding-status.test.js +86 -0
- package/dist/tools/free-busy.js +49 -0
- package/dist/tools/free-busy.test.js +46 -0
- package/dist/tools/get-checklists.js +72 -0
- package/dist/tools/get-project-primer.js +58 -0
- package/dist/tools/get-project.js +77 -0
- package/dist/tools/get-ticket.js +321 -0
- package/dist/tools/get-timer.js +62 -0
- package/dist/tools/help.js +44 -0
- package/dist/tools/identity.js +60 -0
- package/dist/tools/identity.test.js +67 -0
- package/dist/tools/list-git-app-installations.js +45 -0
- package/dist/tools/list-projects.js +72 -0
- package/dist/tools/list-projects.test.js +67 -0
- package/dist/tools/list-tickets.js +86 -0
- package/dist/tools/milestones-crud.test.js +154 -0
- package/dist/tools/milestones.js +279 -0
- package/dist/tools/milestones.test.js +60 -0
- package/dist/tools/my-tickets.js +65 -0
- package/dist/tools/personal-facts.js +72 -0
- package/dist/tools/personal-facts.test.js +45 -0
- package/dist/tools/phase-b-tools.test.js +530 -0
- package/dist/tools/portfolio.js +56 -0
- package/dist/tools/primer-facts.js +346 -0
- package/dist/tools/primer-facts.test.js +353 -0
- package/dist/tools/project-listings.js +116 -0
- package/dist/tools/project-listings.test.js +106 -0
- package/dist/tools/query.js +65 -0
- package/dist/tools/query.test.js +128 -0
- package/dist/tools/raci.js +108 -0
- package/dist/tools/raci.test.js +70 -0
- package/dist/tools/requirements-spec.js +89 -0
- package/dist/tools/requirements-spec.test.js +62 -0
- package/dist/tools/response-expand.js +104 -0
- package/dist/tools/review-policy.js +105 -0
- package/dist/tools/review-policy.test.js +71 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/session-start.js +435 -0
- package/dist/tools/session-start.test.js +568 -0
- package/dist/tools/set-parent.js +76 -0
- package/dist/tools/set-parent.test.js +115 -0
- package/dist/tools/shared.js +226 -0
- package/dist/tools/shared.test.js +169 -0
- package/dist/tools/similar-projection.js +30 -0
- package/dist/tools/ticket-attachments.js +131 -0
- package/dist/tools/ticket-attachments.test.js +106 -0
- package/dist/tools/ticket-schedules.js +112 -0
- package/dist/tools/ticket-schedules.test.js +102 -0
- package/dist/tools/ticket-writes.js +778 -0
- package/dist/tools/ticket-writes.test.js +643 -0
- package/dist/tools/time-writes.js +238 -0
- package/dist/tools/time-writes.test.js +138 -0
- package/dist/tools/update-project.js +165 -0
- package/dist/tools/update-project.test.js +172 -0
- package/dist/tools/wiki.js +185 -0
- package/dist/tools/wiki.test.js +104 -0
- package/dist/tools/work-sessions-next.test.js +150 -0
- package/dist/tools/work-sessions-start.test.js +174 -0
- package/dist/tools/work-sessions.js +709 -0
- package/dist/tools/work-sessions.test.js +358 -0
- package/dist/toolset.js +149 -0
- package/dist/toolset.test.js +150 -0
- package/dist/version-guard.test.js +51 -0
- package/dist/version.js +28 -0
- package/dist/with-metrics-mail-nudge.test.js +41 -0
- package/dist/with-metrics-output-schema.test.js +91 -0
- package/dist/with-metrics.js +211 -0
- package/dist/with-metrics.test.js +230 -0
- package/package.json +66 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-1615 - structural review policy tools.
|
|
3
|
+
*
|
|
4
|
+
* - orboto_review_fingerprint - normalize + hash raw diff text into a
|
|
5
|
+
* canonical fingerprint (+ real size
|
|
6
|
+
* metrics), server-side (one algorithm,
|
|
7
|
+
* not one per client).
|
|
8
|
+
* - orboto_review_policy_check - the consult-before-invoking-a-model
|
|
9
|
+
* call: what risk level does this ticket carry, and (if a fingerprint
|
|
10
|
+
* is supplied) is there already a valid approval for THIS exact diff.
|
|
11
|
+
* - orboto_review_approval_record - record an approve/reject decision
|
|
12
|
+
* against a ticket's diff fingerprint.
|
|
13
|
+
*
|
|
14
|
+
* Rule CONFIGURATION (path pattern / deliveryMode / size -> risk level)
|
|
15
|
+
* stays out of the MCP surface, same call as approval_policies' CRUD in
|
|
16
|
+
* tools/approvals.ts: it's project-settings admin config, not something an
|
|
17
|
+
* autonomous agent tool should expose.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
import { resolveTicketByKey } from './shared.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// orboto_review_fingerprint
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export const reviewFingerprintToolConfig = {
|
|
25
|
+
title: 'Compute a canonical diff fingerprint',
|
|
26
|
+
description: 'Normalize + hash raw diff text (e.g. `git diff` output) into a canonical fingerprint, server-side - so every agent hashes the same way instead of each fudging its own. Whitespace-only / blank-line-only edits and file ORDER are invisible to the hash (an approval survives a reflow); any real content change, a permission/mode change, or an added/removed/renamed file is NOT (an approval never survives those). Also returns real size metrics (filesChanged/linesAdded/linesRemoved/paths) derived from the same diff. Use the returned `fingerprint` with `orboto_review_policy_check` and `orboto_review_approval_record`.',
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
diff: z.string().min(1).describe('Raw unified diff text, e.g. the output of `git diff` / `git diff --no-color`.'),
|
|
29
|
+
}).shape,
|
|
30
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
31
|
+
};
|
|
32
|
+
export function makeReviewFingerprintHandler(client) {
|
|
33
|
+
return async ({ diff }) => {
|
|
34
|
+
const result = await client.post('/review-policy/fingerprint', { diff });
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: `Fingerprint ${result.fingerprint} (${result.algo}) - ${result.filesChanged} file(s), +${result.linesAdded}/-${result.linesRemoved}.`,
|
|
39
|
+
}],
|
|
40
|
+
structuredContent: result,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// orboto_review_policy_check
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export const reviewPolicyCheckToolConfig = {
|
|
48
|
+
title: 'Check a ticket\'s review policy before invoking a reviewer',
|
|
49
|
+
description: 'Consult a ticket\'s structural review policy BEFORE spawning a review session or invoking a model. Called with no diff context (the common case, right after picking up a ticket) it resolves the per-ticket override or a deliveryMode-only rule; passing `paths`/`linesChanged` (from `orboto_review_fingerprint`) additionally matches path/size-scoped rules and, if `fingerprint` is passed too, checks whether a VALID approval already covers this exact diff - so a low-risk ticket, or one already reviewed at this fingerprint, can close without spawning a reviewer. `riskLevel: "required"` with `source: "fail_safe"` means the policy engine itself errored - treat that as "review required", never as "no review needed".',
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
ticketKey: z.string().min(3).describe('Ticket key, e.g. ORB-42.'),
|
|
52
|
+
fingerprint: z.string().min(8).optional().describe('From orboto_review_fingerprint - checks for an existing valid approval at this exact diff.'),
|
|
53
|
+
paths: z.array(z.string()).max(500).optional().describe('Changed file paths (from orboto_review_fingerprint) - refines the match against path-scoped rules.'),
|
|
54
|
+
linesChanged: z.number().int().nonnegative().optional().describe('Total changed lines (linesAdded + linesRemoved) - refines the match against size-scoped rules.'),
|
|
55
|
+
}).shape,
|
|
56
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
57
|
+
};
|
|
58
|
+
export function makeReviewPolicyCheckHandler(client) {
|
|
59
|
+
return async ({ ticketKey, fingerprint, paths, linesChanged }) => {
|
|
60
|
+
const ticket = await resolveTicketByKey(client, ticketKey);
|
|
61
|
+
const decision = await client.post(`/projects/${ticket.projectId}/tickets/${ticket.id}/review-policy/check`, { fingerprint, paths, linesChanged });
|
|
62
|
+
const parts = [`[${ticket.ticketKey}] review risk: ${decision.riskLevel} (${decision.source}${decision.matchedRuleName ? `: "${decision.matchedRuleName}"` : ''}).`];
|
|
63
|
+
if (decision.evaluationError)
|
|
64
|
+
parts.push(`Policy engine error - treat as required: ${decision.evaluationError}`);
|
|
65
|
+
if (decision.fingerprintChecked) {
|
|
66
|
+
parts.push(decision.hasValidApproval ? 'A valid approval already covers this exact diff.' : 'No valid approval for this diff - the fingerprint changed or none was recorded.');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: parts.join(' ') }],
|
|
70
|
+
structuredContent: { ticketKey: ticket.ticketKey, ...decision },
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// orboto_review_approval_record
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export const reviewApprovalRecordToolConfig = {
|
|
78
|
+
title: 'Record a review decision against a diff fingerprint',
|
|
79
|
+
description: 'Record your review verdict (approve/reject) against a ticket\'s diff fingerprint (from orboto_review_fingerprint). A recorded APPROVAL is reusable: orboto_review_policy_check reports it as valid for the SAME fingerprint, letting a later finish/close skip re-review - until the diff changes, which produces a different fingerprint and naturally stops matching. Requires ticket:record_review_approval.',
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
ticketKey: z.string().min(3).describe('Ticket key, e.g. ORB-42.'),
|
|
82
|
+
fingerprint: z.string().min(8).describe('From orboto_review_fingerprint.'),
|
|
83
|
+
decision: z.enum(['approved', 'rejected']).describe('Your review verdict.'),
|
|
84
|
+
note: z.string().max(2000).optional().describe('Optional review comment.'),
|
|
85
|
+
filesChanged: z.number().int().nonnegative().optional(),
|
|
86
|
+
linesAdded: z.number().int().nonnegative().optional(),
|
|
87
|
+
linesRemoved: z.number().int().nonnegative().optional(),
|
|
88
|
+
paths: z.array(z.string()).max(500).optional(),
|
|
89
|
+
}).shape,
|
|
90
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
91
|
+
};
|
|
92
|
+
export function makeReviewApprovalRecordHandler(client) {
|
|
93
|
+
return async (args) => {
|
|
94
|
+
const ticket = await resolveTicketByKey(client, args.ticketKey);
|
|
95
|
+
const { ticketKey: _ticketKey, ...body } = args;
|
|
96
|
+
const approval = await client.post(`/projects/${ticket.projectId}/tickets/${ticket.id}/review-approvals`, body);
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: `Recorded ${approval.decision} on [${ticket.ticketKey}] for fingerprint ${approval.fingerprint}.`,
|
|
101
|
+
}],
|
|
102
|
+
structuredContent: { ticketKey: ticket.ticketKey, approval: approval },
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-1615 - `orboto_review_fingerprint` / `orboto_review_policy_check` /
|
|
3
|
+
* `orboto_review_approval_record` unit tests.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { OrbotoClient } from '../orboto-client.js';
|
|
7
|
+
import { makeReviewFingerprintHandler, makeReviewPolicyCheckHandler, makeReviewApprovalRecordHandler, } from './review-policy.js';
|
|
8
|
+
beforeEach(() => { vi.restoreAllMocks(); });
|
|
9
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
10
|
+
function stub(responses) {
|
|
11
|
+
const calls = [];
|
|
12
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
13
|
+
calls.push({ url: url.toString(), method: init?.method ?? 'GET', body: init?.body ? JSON.parse(init.body) : undefined });
|
|
14
|
+
const r = responses.shift();
|
|
15
|
+
if (!r)
|
|
16
|
+
throw new Error(`unexpected extra fetch ${init?.method ?? 'GET'} ${url}`);
|
|
17
|
+
return { ok: r.ok ?? true, status: r.status ?? 200, statusText: 'OK', json: async () => ('json' in r ? r.json : {}), text: async () => '' };
|
|
18
|
+
});
|
|
19
|
+
return calls;
|
|
20
|
+
}
|
|
21
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
22
|
+
const PROJ = { id: 'p1', key: 'ACME', name: 'Acme', description: null, status: 'active' };
|
|
23
|
+
const TICKET = { id: 't1', projectId: 'p1', ticketKey: 'ACME-1', ticketNumber: 1, title: 'Bug', description: null, status: 'IN_PROGRESS', statusName: 'In Progress', statusCategory: 'in_progress', type: 'bug', priority: 'normal', estimatedTimeMinutes: 0, dueDate: null, isPrivate: false };
|
|
24
|
+
describe('orboto_review_fingerprint', () => {
|
|
25
|
+
it('computes a fingerprint + size metrics from raw diff text', async () => {
|
|
26
|
+
const calls = stub([{ json: { fingerprint: 'abc123', algo: 'sha256-diff-v1', filesChanged: 1, linesAdded: 1, linesRemoved: 1, paths: ['src/foo.ts'] } }]);
|
|
27
|
+
const res = await makeReviewFingerprintHandler(client)({ diff: 'diff --git a/src/foo.ts b/src/foo.ts\n-a\n+b\n' });
|
|
28
|
+
expect(calls[0].url).toContain('/review-policy/fingerprint');
|
|
29
|
+
expect(calls[0].method).toBe('POST');
|
|
30
|
+
expect(res.structuredContent?.fingerprint).toBe('abc123');
|
|
31
|
+
expect(res.content[0].text).toContain('abc123');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('orboto_review_policy_check', () => {
|
|
35
|
+
it('reports the resolved risk level and whether a valid approval exists', async () => {
|
|
36
|
+
const decision = {
|
|
37
|
+
riskLevel: 'required', source: 'rule', matchedRuleName: 'api routes', evaluationError: null,
|
|
38
|
+
fingerprintChecked: true, hasValidApproval: false, latestApproval: null,
|
|
39
|
+
};
|
|
40
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: decision }]);
|
|
41
|
+
const res = await makeReviewPolicyCheckHandler(client)({ ticketKey: 'ACME-1', fingerprint: 'abc123' });
|
|
42
|
+
const check = calls.find((c) => c.method === 'POST');
|
|
43
|
+
expect(check?.url).toContain('/projects/p1/tickets/t1/review-policy/check');
|
|
44
|
+
expect(check?.body).toMatchObject({ fingerprint: 'abc123' });
|
|
45
|
+
expect(res.structuredContent).toMatchObject({ riskLevel: 'required', hasValidApproval: false });
|
|
46
|
+
expect(res.content[0].text).toContain('required');
|
|
47
|
+
expect(res.content[0].text).toContain('No valid approval');
|
|
48
|
+
});
|
|
49
|
+
it('surfaces the fail-safe direction distinctly when the policy engine errored', async () => {
|
|
50
|
+
const decision = {
|
|
51
|
+
riskLevel: 'required', source: 'fail_safe', matchedRuleName: null, evaluationError: 'db timeout',
|
|
52
|
+
fingerprintChecked: false, hasValidApproval: false, latestApproval: null,
|
|
53
|
+
};
|
|
54
|
+
stub([{ json: PROJ }, { json: TICKET }, { json: decision }]);
|
|
55
|
+
const res = await makeReviewPolicyCheckHandler(client)({ ticketKey: 'ACME-1' });
|
|
56
|
+
expect(res.content[0].text).toContain('treat as required');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('orboto_review_approval_record', () => {
|
|
60
|
+
it('records an approval against a fingerprint', async () => {
|
|
61
|
+
const approval = { id: 'ra1', ticketId: 't1', fingerprint: 'abc123', decision: 'approved', reviewerLabel: 'reviewer@orboto.test', note: null, revokedAt: null, createdAt: '2026-01-01T00:00:00Z' };
|
|
62
|
+
const calls = stub([{ json: PROJ }, { json: TICKET }, { json: approval }]);
|
|
63
|
+
const res = await makeReviewApprovalRecordHandler(client)({ ticketKey: 'ACME-1', fingerprint: 'abc123', decision: 'approved', note: 'looks good' });
|
|
64
|
+
const record = calls.find((c) => c.method === 'POST' && c.url.includes('review-approvals'));
|
|
65
|
+
expect(record?.body).toMatchObject({ fingerprint: 'abc123', decision: 'approved', note: 'looks good' });
|
|
66
|
+
// ticketKey must NOT leak into the request body sent to the API - the
|
|
67
|
+
// route resolves the ticket from the URL, not the payload.
|
|
68
|
+
expect(record?.body).not.toHaveProperty('ticketKey');
|
|
69
|
+
expect(res.content[0].text).toContain('approved');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-244 Phase B - `orboto_search`.
|
|
3
|
+
*
|
|
4
|
+
* Unified full-text search across tickets, comments, and docs. Maps
|
|
5
|
+
* to the existing `/search` route (Phase 21 global search) which
|
|
6
|
+
* already respects PBAC visibility (private tickets, internal
|
|
7
|
+
* comments, doc ACL).
|
|
8
|
+
*
|
|
9
|
+
* ORB-272: /search is cursor-paginated. Response shape is
|
|
10
|
+
* `{items, nextCursor, total}`. We surface `total` so the model
|
|
11
|
+
* knows how many hits exist globally, but don't expose `cursor` on
|
|
12
|
+
* the tool input - repeat MCP tool calls to walk a search cursor is
|
|
13
|
+
* an awkward UX. Users who need to see more narrow the query or
|
|
14
|
+
* open the full-page /search in the web UI.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { resolveProjectByKey } from './shared.js';
|
|
18
|
+
export const searchToolConfig = {
|
|
19
|
+
title: 'Search across orboto',
|
|
20
|
+
description: 'Full-text search across tickets, comments, and docs. Honours the caller\'s visibility - private tickets and internal comments never appear unless the caller can already see them. Recall (ORB-1695): when the strict all-terms pass finds nothing, the server automatically retries with an OR-relaxed pass plus semantic (embedding) recall - the response\'s `pass` field says which pass produced the hits, and relaxed-pass hits deserve a skeptical read (they matched SOME terms or only the meaning, not all terms). Still prefer a single distinctive STABLE token (file/component/error-string fragment like "AdminCodesPage") and search the SYMPTOM, not your intended fix.',
|
|
21
|
+
inputSchema: z.object({
|
|
22
|
+
query: z.string().min(1).describe('Search terms, e.g. "queue worker retry".'),
|
|
23
|
+
types: z.array(z.enum(['ticket', 'comment', 'doc'])).optional()
|
|
24
|
+
.describe('Restrict to entity types. Omit for all.'),
|
|
25
|
+
projectKey: z.string().optional().describe('Restrict to one project.'),
|
|
26
|
+
limit: z.number().int().min(1).max(50).default(15),
|
|
27
|
+
}).shape,
|
|
28
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
29
|
+
};
|
|
30
|
+
export function makeSearchHandler(client) {
|
|
31
|
+
return async (input) => {
|
|
32
|
+
const qs = new URLSearchParams();
|
|
33
|
+
qs.set('q', input.query);
|
|
34
|
+
qs.set('limit', String(input.limit ?? 15));
|
|
35
|
+
if (input.types && input.types.length > 0)
|
|
36
|
+
qs.set('types', input.types.join(','));
|
|
37
|
+
if (input.projectKey) {
|
|
38
|
+
const project = await resolveProjectByKey(client, input.projectKey);
|
|
39
|
+
qs.set('projectId', project.id);
|
|
40
|
+
}
|
|
41
|
+
const res = await client.get(`/search?${qs}`);
|
|
42
|
+
const shown = res.items.length;
|
|
43
|
+
const hasMore = !!res.nextCursor && res.total > shown;
|
|
44
|
+
const headerHint = hasMore ? ` (showing ${shown} of ${res.total})` : '';
|
|
45
|
+
const relaxedNote = res.pass === 'relaxed'
|
|
46
|
+
? ' [relaxed pass: no row matched ALL terms - these matched some terms or the meaning]'
|
|
47
|
+
: '';
|
|
48
|
+
const text = res.items.length === 0
|
|
49
|
+
? `No hits for "${input.query}".`
|
|
50
|
+
: `Hits${headerHint}${relaxedNote}:\n\n` + res.items.map((h) => {
|
|
51
|
+
const tag = h.type.toUpperCase();
|
|
52
|
+
// ORB-1084 - non-ticket hits carry the FULL id: the truncated
|
|
53
|
+
// form was unusable as input for the doc write tools.
|
|
54
|
+
const ident = h.ticketKey ?? h.id;
|
|
55
|
+
const project = h.projectName ? ` · ${h.projectName}` : '';
|
|
56
|
+
return `- [${tag} ${ident}${project}] ${h.title}\n ${h.excerpt}`;
|
|
57
|
+
}).join('\n\n');
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text }],
|
|
60
|
+
structuredContent: {
|
|
61
|
+
total: res.total,
|
|
62
|
+
shown,
|
|
63
|
+
hasMore,
|
|
64
|
+
pass: res.pass ?? 'strict',
|
|
65
|
+
hits: res.items.map((h) => ({
|
|
66
|
+
type: h.type,
|
|
67
|
+
id: h.id, // ORB-1084 - full id, usable as write-tool input
|
|
68
|
+
title: h.title,
|
|
69
|
+
excerpt: h.excerpt,
|
|
70
|
+
ticketKey: h.ticketKey ?? null,
|
|
71
|
+
projectName: h.projectName,
|
|
72
|
+
spaceName: h.spaceName,
|
|
73
|
+
url: h.url,
|
|
74
|
+
})),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|