@orboto/mcp 0.209.1 → 0.210.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/server.js CHANGED
@@ -74,6 +74,7 @@ import { primerFactListToolConfig, makePrimerFactListHandler, primerFactAddToolC
74
74
  import { whoamiToolConfig, makeWhoamiHandler } from './tools/identity.js';
75
75
  import { claimToolConfig, makeClaimHandler, unclaimToolConfig, makeUnclaimHandler, } from './tools/claim.js';
76
76
  import { reportFeedbackToolConfig, makeReportFeedbackHandler } from './tools/feedback.js';
77
+ import { feedbackReplyToolConfig, makeFeedbackReplyHandler } from './tools/feedback-reply.js';
77
78
  import { listApprovalsToolConfig, makeListApprovalsHandler, approvalDecideToolConfig, makeApprovalDecideHandler, } from './tools/approvals.js';
78
79
  import { reviewFingerprintToolConfig, makeReviewFingerprintHandler, reviewPolicyCheckToolConfig, makeReviewPolicyCheckHandler, reviewApprovalRecordToolConfig, makeReviewApprovalRecordHandler, } from './tools/review-policy.js';
79
80
  import { createMilestoneToolConfig, makeCreateMilestoneHandler, closeMilestoneToolConfig, makeCloseMilestoneHandler, updateMilestoneToolConfig, makeUpdateMilestoneHandler, } from './tools/milestones.js';
@@ -339,6 +340,7 @@ export async function buildOrbotoMcpServer(opts) {
339
340
  reg('orboto_claim', claimToolConfig, makeClaimHandler(client));
340
341
  reg('orboto_unclaim', unclaimToolConfig, makeUnclaimHandler(client));
341
342
  reg('orboto_report_feedback', reportFeedbackToolConfig, makeReportFeedbackHandler(client));
343
+ reg('orboto_feedback_reply', feedbackReplyToolConfig, makeFeedbackReplyHandler(client));
342
344
  reg('orboto_list_approvals', listApprovalsToolConfig, makeListApprovalsHandler(client));
343
345
  reg('orboto_approval_decide', approvalDecideToolConfig, makeApprovalDecideHandler(client));
344
346
  reg('orboto_review_fingerprint', reviewFingerprintToolConfig, makeReviewFingerprintHandler(client));
package/dist/tool-docs.js CHANGED
@@ -12,12 +12,20 @@ export const SUMMARY_MAX_CHARS = 220;
12
12
  * name the key inputs.
13
13
  */
14
14
  const SUMMARY_OVERRIDES = {
15
- orboto_create_full_backup: 'Start a full-workspace backup job (all projects, users, config, attachments); returns the job id to poll via orboto_list_backups.',
15
+ orboto_create_full_backup: 'Start a full-workspace backup job; returns the job id to poll via orboto_list_backups.',
16
16
  orboto_get_ticket: 'Fetch one ticket by key or UUID with description, status, assignees, checklists and comments.',
17
- orboto_requirements_spec: 'Generate a structured requirements spec for a project or milestone from its tickets (scope, actors, functional + non-functional requirements).',
17
+ orboto_requirements_spec: 'Generate a structured requirements spec for a project or milestone from its tickets.',
18
18
  orboto_update_doc_space: 'Update a doc space\'s name, description, icon, project binding or access mode (open/restricted) by space id.',
19
19
  orboto_search_docs: 'Full-text search over doc/wiki pages (query, optional space or project filter); returns matching pages with snippets.',
20
- orboto_critical_path: 'Compute the dependency-based critical path for a project or milestone: the blocking chain of tickets that determines the earliest finish.',
20
+ orboto_critical_path: 'Compute the critical path (blocking dependency chain) for a project or milestone.',
21
+ /** ORB-2237 - shortened summaries that pay for `orboto_feedback_reply`; each tool's full text stays in `getToolDoc`. */
22
+ orboto_agent_broadcast: 'Fan-out a message to every agent in a workspace, project or topic scope.',
23
+ orboto_api_search: 'Discover REST API endpoints on demand - the escape hatch for any orboto endpoint.',
24
+ orboto_get_doc: 'Return a doc\'s content (Markdown) plus its backlinks.',
25
+ orboto_primer_fact_list: 'List structured project facts feeding the AI primer.',
26
+ orboto_bulk_move_tickets: 'Move every matching ticket to the same status category.',
27
+ orboto_resolve_doc_smart_links: 'Resolve doc/ticket/milestone/project smart-link references to their title + URL.',
28
+ orboto_admin_translation_revert: 'Restore the pre-translation title + description and clear the translation marker.',
21
29
  };
22
30
  /** Runtime registry: tool name -> full guidance text, captured at
23
31
  * registration. Module-global on purpose - the HTTP transport builds one
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ORB-2237 - `orboto_feedback_reply`: the operator's public reply to a
3
+ * tenant report, one step (POST /tickets/:id/feedback-reply).
4
+ */
5
+ import { z } from 'zod';
6
+ export const feedbackReplyToolConfig = {
7
+ title: 'Reply to feedback report',
8
+ description: 'Post the operator\'s public reply on a feedback ticket. Synced to the tenant. Refuses secrets.',
9
+ inputSchema: z.object({
10
+ ticketKey: z.string().min(1).max(64),
11
+ content: z.string().min(1).max(20_000),
12
+ }).shape,
13
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
14
+ };
15
+ export function makeFeedbackReplyHandler(client) {
16
+ return async (args) => {
17
+ const res = await client.post(`/tickets/${encodeURIComponent(args.ticketKey)}/feedback-reply`, { content: args.content });
18
+ return {
19
+ content: [{ type: 'text', text: `Reply posted on ${args.ticketKey} (comment ${res.commentId}, ${res.repliedAt}).` }],
20
+ structuredContent: res,
21
+ };
22
+ };
23
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ORB-2237 - `orboto_feedback_reply` unit test.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { OrbotoClient } from '../orboto-client.js';
6
+ import { makeFeedbackReplyHandler } from './feedback-reply.js';
7
+ beforeEach(() => { vi.restoreAllMocks(); });
8
+ afterEach(() => { vi.restoreAllMocks(); });
9
+ const client = new OrbotoClient({ baseUrl: 'http://api.test', apiKey: 'orb_test' });
10
+ describe('orboto_feedback_reply', () => {
11
+ it('posts the reply to the feedback-reply route and reports the comment id', async () => {
12
+ const calls = [];
13
+ vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
14
+ calls.push({ url: url.toString(), body: init?.body ? JSON.parse(init.body) : undefined });
15
+ return {
16
+ ok: true, status: 200, statusText: 'OK',
17
+ json: async () => ({ commentId: 'c-1', repliedAt: '2026-09-28T10:00:00.000Z' }),
18
+ text: async () => '',
19
+ };
20
+ });
21
+ const result = await makeFeedbackReplyHandler(client)({ ticketKey: 'FBK-1', content: 'Fixed in the next release.' });
22
+ expect(calls[0].url).toContain('/tickets/FBK-1/feedback-reply');
23
+ expect(calls[0].body).toEqual({ content: 'Fixed in the next release.' });
24
+ expect(result.structuredContent).toEqual({ commentId: 'c-1', repliedAt: '2026-09-28T10:00:00.000Z' });
25
+ expect(result.content[0]).toMatchObject({ type: 'text' });
26
+ });
27
+ });
@@ -1,6 +1,7 @@
1
1
  /** ORB-2225 - the wiki as the agents' knowledge base: ask it first, file durable facts in it. */
2
2
  import { z } from 'zod';
3
3
  import { mcpInstanceToken } from './shared.js';
4
+ import { droppedLines } from './wiki.js';
4
5
  export const knowledgeAskToolConfig = {
5
6
  title: 'Ask the wiki',
6
7
  description: 'Ask your session\'s project wikis and the Operations wiki before asking a person; cite docs by key.',
@@ -30,6 +31,6 @@ export const knowledgeAddToolConfig = {
30
31
  export function makeKnowledgeAddHandler(client) {
31
32
  return async (input, extra) => {
32
33
  const res = await client.post('/knowledge/add', input, { instanceToken: mcpInstanceToken(undefined, extra) });
33
- return { content: [{ type: 'text', text: res.text }], structuredContent: res };
34
+ return { content: [{ type: 'text', text: `${res.text}${droppedLines(res.droppedOps ?? [])}` }], structuredContent: res };
34
35
  };
35
36
  }
@@ -26,6 +26,14 @@ describe('ORB-2225 orboto_knowledge_ask and orboto_knowledge_add', () => {
26
26
  expect(textOf(result)).toContain('ACME-D7');
27
27
  expect(result.structuredContent).toMatchObject({ action: 'recorded' });
28
28
  });
29
+ it('files a fact and lists the ops the wiki agent dropped, as wiki record does', async () => {
30
+ const droppedOps = [{ title: 'Runner', target: 'ACME-D99', reason: 'unknown_target' }];
31
+ const post = vi.fn().mockResolvedValue({ action: 'recorded', text: 'Recorded in the ACME-S1 wiki: ACME-D7 "Build host". Cite it by key.', pages: [], source: null, droppedOps });
32
+ const result = await makeKnowledgeAddHandler({ post })({ title: 'Build host', content: 'build.example.org', projectKey: 'ACME' });
33
+ expect(textOf(result)).toContain('Ops dropped: 1');
34
+ expect(textOf(result)).toContain('- Runner (target ACME-D99): unknown_target');
35
+ expect(result.structuredContent).toMatchObject({ droppedOps });
36
+ });
29
37
  it('both tools carry a title and annotations; ask is read-only, add is not', () => {
30
38
  expect(knowledgeAskToolConfig.title).toBeTruthy();
31
39
  expect(knowledgeAskToolConfig.annotations.readOnlyHint).toBe(true);
@@ -71,4 +79,9 @@ describe('ORB-2225 orboto_wiki_runs', () => {
71
79
  expect(textOf(await handler({ spaceId: 'ACME-S1', action: 'retry', runId: '11111111-0000-4000-8000-000000000000' }))).toContain('ran again: applied');
72
80
  expect(post).toHaveBeenCalledWith('/spaces/ACME-S1/llm-wiki/ingest-runs/11111111-0000-4000-8000-000000000000/retry', {});
73
81
  });
82
+ it('ORB-2226 - a run that left planner ops out says how many', async () => {
83
+ const get = vi.fn().mockResolvedValue({ runs: [{ id: 'r2', sourceKey: 'ACME-D10', sourceTitle: 'Deploy notes', status: 'applied', error: null, droppedOps: [{ title: 'Deploy', target: 'ACME-D99', reason: 'unknown_target' }], createdAt: '2026-09-27T09:00:00Z' }] });
84
+ const handler = makeWikiRunsHandler({ get, post: vi.fn() });
85
+ expect(textOf(await handler({ spaceId: 'ACME-S1' }))).toContain('r2 applied ACME-D10 "Deploy notes" 2026-09-27T09:00:00Z (ops dropped: 1)');
86
+ });
74
87
  });
@@ -69,9 +69,15 @@ export function makeWikiLintHandler(client) {
69
69
  return text(`Lint complete: ${res.issues.length} open issue(s), ${res.resolved} auto-resolved.\n${lines.join('\n')}`, { issues: res.issues, detected: res.detected, resolved: res.resolved });
70
70
  };
71
71
  }
72
+ /** ORB-2226 - planner ops whose target page matched no docId, doc key or title. */
73
+ export function droppedLines(dropped) {
74
+ if (dropped.length === 0)
75
+ return '';
76
+ return `\nOps dropped: ${dropped.length}\n${dropped.map((d) => `- ${d.title}${d.target ? ` (target ${d.target})` : ''}: ${d.reason}`).join('\n')}`;
77
+ }
72
78
  export const wikiPlanUpdateToolConfig = {
73
79
  title: 'Plan a wiki edit (dry-run, no writes)',
74
- description: 'Turn a natural-language instruction into page operations (create/patch/append) WITHOUT writing anything. Returns a planId valid for 15 minutes plus the proposed ops. Review the ops, then call orboto_wiki_apply_plan to commit. Wraps POST /spaces/:id/docs/plan-update.',
80
+ description: 'Turn a natural-language instruction into page operations (create/patch/append) WITHOUT writing anything. Returns a planId valid for 15 minutes plus the proposed ops, and droppedOps: ops whose target page matched no docId, doc key or title. Review the ops, then call orboto_wiki_apply_plan to commit. Wraps POST /spaces/:id/docs/plan-update.',
75
81
  inputSchema: z.object({
76
82
  spaceId: z.string().uuid().describe('The wiki space to edit.'),
77
83
  instruction: z.string().min(1).max(4000).describe('What to change, in plain language.'),
@@ -86,7 +92,8 @@ export function makeWikiPlanUpdateHandler(client) {
86
92
  body.sourceDocId = input.sourceDocId;
87
93
  const res = await client.post(`/spaces/${input.spaceId}/docs/plan-update`, body);
88
94
  const ops = res.ops.map((o, i) => `${i + 1}. ${o.op} ${o.title ?? ''} - ${o.summary}`).join('\n');
89
- return text(`Plan ${res.planId} (expires ${res.expiresAt}):\n${ops}\n\nApply with orboto_wiki_apply_plan(planId).`, { planId: res.planId, ops: res.ops, expiresAt: res.expiresAt });
95
+ const droppedOps = res.droppedOps ?? [];
96
+ return text(`Plan ${res.planId} (expires ${res.expiresAt}):\n${ops}${droppedLines(droppedOps)}\n\nApply with orboto_wiki_apply_plan(planId).`, { planId: res.planId, ops: res.ops, droppedOps, expiresAt: res.expiresAt });
90
97
  };
91
98
  }
92
99
  export const wikiApplyPlanToolConfig = {
@@ -121,7 +128,8 @@ export function makeWikiRecordHandler(client) {
121
128
  planBody.sourceDocId = input.sourceDocId;
122
129
  const plan = await client.post(`/spaces/${input.spaceId}/docs/plan-update`, planBody);
123
130
  const applied = await client.post(`/spaces/${input.spaceId}/docs/apply-plan`, { planId: plan.planId });
124
- return text(`Recorded: ${applied.touchedDocs.length} page(s) updated.`, { planId: plan.planId, touchedDocs: applied.touchedDocs });
131
+ const droppedOps = plan.droppedOps ?? [];
132
+ return text(`Recorded: ${applied.touchedDocs.length} page(s) updated.${droppedLines(droppedOps)}`, { planId: plan.planId, touchedDocs: applied.touchedDocs, droppedOps });
125
133
  };
126
134
  }
127
135
  export const wikiAppendSectionToolConfig = {
@@ -196,7 +204,7 @@ export function makeWikiRunsHandler(client) {
196
204
  }
197
205
  const qs = input.status ? `?status=${input.status}` : '';
198
206
  const res = await client.get(`/spaces/${encodeURIComponent(input.spaceId)}/llm-wiki/ingest-runs${qs}`);
199
- const lines = res.runs.map((r) => `- ${r.id} ${r.status} ${r.sourceKey ?? ''} "${r.sourceTitle}" ${r.createdAt}${r.error ? ` - ${r.error}` : ''}`);
207
+ const lines = res.runs.map((r) => `- ${r.id} ${r.status} ${r.sourceKey ?? ''} "${r.sourceTitle}" ${r.createdAt}${r.droppedOps?.length ? ` (ops dropped: ${r.droppedOps.length})` : ''}${r.error ? ` - ${r.error}` : ''}`);
200
208
  return text(lines.length ? lines.join('\n') : 'No ingest runs.', res);
201
209
  };
202
210
  }
@@ -57,6 +57,13 @@ describe('orboto_wiki_plan_update + apply_plan', () => {
57
57
  expect(calls[0].url).toBe(`https://orboto.example.com/spaces/${SPACE}/docs/plan-update`);
58
58
  expect(res.content[0].text).toContain('Plan p1');
59
59
  });
60
+ it('ORB-2226 - plan-update names the ops the planner target could not be resolved for', async () => {
61
+ stubJSON([{ json: { planId: 'p1', ops: [], droppedOps: [{ title: 'Runbook', target: 'ghost page', reason: 'unknown_target' }], expiresAt: '2026-01-01T00:15:00Z' } }]);
62
+ const res = await makeWikiPlanUpdateHandler(client)({ spaceId: SPACE, instruction: 'update the runbook' });
63
+ const out = res.content[0].text;
64
+ expect(out).toContain('Ops dropped: 1');
65
+ expect(out).toContain('- Runbook (target ghost page): unknown_target');
66
+ });
60
67
  it('apply-plan reports touched docs', async () => {
61
68
  stubJSON([{ json: { touchedDocs: [DOC] } }]);
62
69
  const res = await makeWikiApplyPlanHandler(client)({ spaceId: SPACE, planId: 'p1' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orboto/mcp",
3
- "version": "0.209.1",
3
+ "version": "0.210.1",
4
4
  "description": "Model Context Protocol server for orboto - exposes tickets, projects, milestones, time, docs, primer-facts, alerts and absences as MCP tools for Claude Desktop / Cursor / Copilot Chat / any MCP-aware AI client.",
5
5
  "license": "MIT",
6
6
  "type": "module",