@orboto/mcp 0.182.5 → 0.183.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.
@@ -55,7 +55,14 @@ afterEach(() => { vi.restoreAllMocks(); });
55
55
  * schema to the minimum (one describe() on `page`); ceiling re-pinned to
56
56
  * 32,500 (~6 % headroom), deliberately, for that one addition.
57
57
  */
58
- const CURATED_MAX_CHARS = 32_500;
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
+ const CURATED_MAX_CHARS = 37_500;
59
66
  const FULL_MAX_CHARS = 155_000;
60
67
  /**
61
68
  * ORB-1805 - the estimator the ticket measured the failure with
@@ -91,7 +98,7 @@ const MINIMAL_MAX_TOKENS = 3_100;
91
98
  */
92
99
  // ORB-1910 - re-pinned with CURATED_MAX_CHARS (32,500 / 3.6): the curated set
93
100
  // gained orboto_report_feedback; measured 8,514 estimated tokens.
94
- const CURATED_SCHEMA_MAX_TOKENS = 9_050;
101
+ const CURATED_SCHEMA_MAX_TOKENS = 10_450; // ORB-1959: measured 10,198 with the coordination tools
95
102
  async function measuredManifest(toolset) {
96
103
  const server = await buildOrbotoMcpServer({
97
104
  baseUrl: 'https://orboto.example.com',
@@ -129,8 +136,9 @@ describe('ORB-1521 - eager-load manifest size', () => {
129
136
  // eslint-disable-next-line no-console
130
137
  console.log(`[manifest-size] curated: ${count} tools, ${chars} chars (~${estTokens(chars)} tokens)`);
131
138
  // 31 = the ORB-1741-dieted set + the two ORB-1694 bulk tools (each
132
- // REPLACES dozens of per-call responses, so they earn their slot).
133
- expect(count).toBeLessThanOrEqual(32);
139
+ // REPLACES dozens of per-call responses, so they earn their slot);
140
+ // ORB-1910 added report_feedback, ORB-1959 the four coordination tools.
141
+ expect(count).toBeLessThanOrEqual(36);
134
142
  expect(chars).toBeLessThanOrEqual(CURATED_MAX_CHARS);
135
143
  expect(estTokens(chars)).toBeLessThanOrEqual(CURATED_SCHEMA_MAX_TOKENS);
136
144
  });
@@ -17,7 +17,7 @@ import { z } from 'zod';
17
17
  import { mcpInstanceToken } from './shared.js';
18
18
  export const agentHeartbeatToolConfig = {
19
19
  title: 'Agent heartbeat (Multi-Agent Coordination)',
20
- description: 'Register or refresh this agent\'s presence in the workspace. Call on startup AND every ~30 s thereafter; rows missing for >90 s count as offline. Returns the `sessionToken` to persist + send on subsequent heartbeats so the same row is bumped instead of churning new ones. `status` can be `idle` (default), `working` (set `workingOnTicketId` too if relevant), or `blocked`. `capabilities` is a free-form list of strings the operator can use to filter (e.g. `["read-only", "writes-tickets", "writes-code"]`). `clientInfo` describes the runtime - fill `name` with the agent runtime (`claude-desktop`, `claude-code`, `dispatcher-daemon`, `cursor`, ...).',
20
+ description: 'Register or refresh this agent\'s presence. Call on startup and every ~30 s; rows older than 90 s count as offline. Persist the returned sessionToken and send it on later heartbeats. status: idle (default) | working (+workingOnTicketId) | blocked. capabilities: free-form strings for operator filters. clientInfo.name = the runtime (claude-code, cursor, ...).',
21
21
  inputSchema: z.object({
22
22
  sessionToken: z.string().nullable().optional(),
23
23
  status: z.enum(['idle', 'working', 'blocked']).optional(),
@@ -96,7 +96,7 @@ export function makeAgentPresenceHandler(client) {
96
96
  }
97
97
  export const agentNotifyToolConfig = {
98
98
  title: 'Notify another agent / user',
99
- description: 'Dispatch a fire-and-forget message to a target user (bot or human) identified by email. The recipient sees it via the standard notification channel - in-app for human users, MCP push for subscribed agents (ORB-706). Use cases: lead agent telling a worker a sub-task is ready; a worker reporting back to the orchestrator; a CI agent pinging a reviewer when a PR is staged. The `kind` field tags the semantic intent so the recipient can filter (`info` for general updates, `request` when a response is expected, `complete` when reporting a finished sub-task). `payload` is a free-form jsonb blob.',
99
+ description: 'Send a fire-and-forget message to a user (bot or human) by email; it lands in their inbox (orboto_messages) and, for humans, in-app. kind: info | request (answer expected) | complete (sub-task done) | error. payload: free-form JSON; threadId links a reply to the message it answers.',
100
100
  inputSchema: z.object({
101
101
  targetEmail: z.string().email(),
102
102
  kind: z.enum(['info', 'request', 'complete', 'error']).default('info'),
@@ -106,10 +106,10 @@ export const agentNotifyToolConfig = {
106
106
  threadId: z.string().uuid().optional(),
107
107
  // ORB-1732 - optional project scope: address "the agent working project
108
108
  // X" when the recipient identity runs multiple sessions.
109
- project: z.string().min(1).max(64).optional().describe('Project key (ORB) or UUID: scope the message to the recipient session working that project. Recipients fetching with a project filter see scoped messages for their project plus unscoped ones.'),
109
+ project: z.string().min(1).max(64).optional().describe('Project key or UUID: scope the message to the recipient session working that project.'),
110
110
  // ORB-1742 - defaults to this MCP session's instance token so a shared
111
111
  // identity never wakes itself with its own outbound mail.
112
- senderRef: z.string().min(1).max(128).optional().describe('Sender-session ref for self-echo exclusion. Defaults to this MCP session automatically - only override when sending on behalf of another session.'),
112
+ senderRef: z.string().min(1).max(128).optional().describe('Sender-session ref for self-echo exclusion; defaults to this MCP session.'),
113
113
  }).shape,
114
114
  outputSchema: z.object({
115
115
  ok: z.literal(true),
@@ -122,7 +122,7 @@ export const agentNotifyToolConfig = {
122
122
  // ---------------------------------------------------------------------------
123
123
  export const agentBroadcastToolConfig = {
124
124
  title: 'Scoped broadcast to other agents',
125
- description: 'Fan-out a message to every subscribed agent in a scope. `scopeType=workspace` reaches every internal member; `scopeType=project` reaches members of `scopeId` (project UUID); `scopeType=topic` reaches everyone subscribed to the topic string. The recipient sees it via a `notifications/resources/updated` push on `orboto://broadcast/<scope>/<scope_id>`. Use for lead-agent → workers (workspace), project-team status updates (project), or ad-hoc cross-cutting coordination (topic). Retention is 24 h - late subscribers can replay via the resource read.',
125
+ description: 'Fan-out to every agent in a scope: workspace (all internal members), project (members of scopeId = project UUID) or topic (any string). Each recipient gets an inbox copy (orboto_messages, ackable); live subscribers also get a resources/updated push on orboto://broadcast/<scope>/<scope_id>. The scope replay keeps agent_broadcast_retention_days (default 7).',
126
126
  inputSchema: z.object({
127
127
  scopeType: z.enum(['workspace', 'project', 'topic']),
128
128
  scopeId: z.string().default(''),
@@ -11,13 +11,13 @@ import { z } from 'zod';
11
11
  import { mcpInstanceToken } from './shared.js';
12
12
  export const agentMessagesToolConfig = {
13
13
  title: 'Fetch / acknowledge your agent inbox',
14
- description: 'Read the messages other agents sent you while you were not connected (store-and-forward inbox; sending happens via orboto_agent_notify). Default lists UNREAD messages and marks them delivered; pass ackIds to acknowledge messages as read (acked messages stop triggering the pending-mail pointer on tool responses and prune after 30 days). Reply by calling orboto_agent_notify with threadId = the message id you are answering.',
14
+ description: 'Your store-and-forward inbox: messages other agents sent you plus broadcasts to your scopes (payload.broadcast names the scope). Default = unread, marked delivered on fetch; ackIds marks read. Reply via orboto_agent_notify with threadId = the message id.',
15
15
  inputSchema: z.object({
16
16
  all: z.boolean().default(false).describe('true = include already-read messages'),
17
17
  limit: z.number().int().min(1).max(200).default(50),
18
- project: z.string().min(1).max(64).optional().describe('Project key or UUID: narrow to messages scoped to this project PLUS unscoped ones. Pass the project you are working when your identity runs multiple sessions; only ack messages that are yours.'),
18
+ project: z.string().min(1).max(64).optional().describe('Project key or UUID: scoped messages for this project plus unscoped ones. Ack only messages that are yours.'),
19
19
  // ORB-1742 - self-echo exclusion, on by default for MCP sessions.
20
- includeOwnSends: z.boolean().default(false).describe('true = ALSO list messages this very session sent (they are hidden by default so a shared identity never wakes itself with its own outbound mail).'),
20
+ includeOwnSends: z.boolean().default(false).describe('true = also list messages this session sent (hidden by default so a shared identity never wakes itself).'),
21
21
  ackIds: z.array(z.string().uuid()).max(200).optional().describe('Message ids to mark as read'),
22
22
  }).shape,
23
23
  outputSchema: z.object({
@@ -14,7 +14,7 @@ const mb = (n) => `${Math.round((n / 1024 / 1024) * 10) / 10} MB`;
14
14
  // ---------------------------------------------------------------------------
15
15
  export const createFullBackupToolConfig = {
16
16
  title: 'Create + download a full workspace backup',
17
- description: 'Create an on-demand FULL workspace backup (database + storage) and return the ZIP as a base64 application/zip resource attachment - create, wait and download in one call (the server runs the export as a background run and this tool polls it; large workspaces can take minutes). Requires admin:backup:export. For the scheduled named jobs use orboto_trigger_backup instead.',
17
+ description: 'Create an on-demand FULL workspace backup (database + storage) and return the ZIP as a base64 application/zip resource attachment - create, wait and download in one call (the server runs the export as a background run and this tool polls it; large workspaces can take minutes). Rows and file references share one snapshot; missing or incomplete files fail the run, never return a partial ZIP. Requires admin:backup:export. For the scheduled named jobs use orboto_trigger_backup instead.',
18
18
  inputSchema: z.object({}).shape,
19
19
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
20
20
  };
@@ -68,20 +68,22 @@ describe('orboto_create_full_backup (ORB-1301 / async since ORB-1717)', () => {
68
68
  vi.useRealTimers();
69
69
  }
70
70
  });
71
- it('a failed run surfaces the error instead of a partial ZIP', async () => {
71
+ it.each(['disk full', 'Backup source object is missing, unreadable or incomplete'])('a failed run surfaces %s instead of a partial ZIP', async (message) => {
72
72
  vi.useFakeTimers();
73
+ const download = vi.spyOn(client, 'getBinary');
73
74
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, _init) => {
74
75
  const u = url.toString();
75
76
  if (u.endsWith('/admin/backup/full')) {
76
77
  return { ok: true, status: 202, statusText: 'Accepted', headers: new Headers({ 'content-type': 'application/json' }), json: async () => ({ runId: 'run-2' }), text: async () => '' };
77
78
  }
78
- return { ok: true, status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'application/json' }), json: async () => ({ status: 'failed', errorMessage: 'disk full' }), text: async () => '' };
79
+ return { ok: true, status: 200, statusText: 'OK', headers: new Headers({ 'content-type': 'application/json' }), json: async () => ({ status: 'failed', errorMessage: message }), text: async () => '' };
79
80
  });
80
81
  try {
81
82
  const pending = makeCreateFullBackupHandler(client)();
82
- const assertion = expect(pending).rejects.toThrow('disk full');
83
+ const assertion = expect(pending).rejects.toThrow(message);
83
84
  await vi.advanceTimersByTimeAsync(6_000);
84
85
  await assertion;
86
+ expect(download).not.toHaveBeenCalled();
85
87
  }
86
88
  finally {
87
89
  vi.useRealTimers();
package/dist/toolset.js CHANGED
@@ -81,7 +81,10 @@ export const MINIMAL_TOOLS = new Set([
81
81
  'orboto_close_ticket',
82
82
  'orboto_timer_start',
83
83
  'orboto_timer_stop',
84
- // Everything else, on demand
84
+ // Everything else, on demand (ORB-1959 measured orboto_messages at ~3k
85
+ // chars - a fifth of this tier's whole window - so the inbox stays in
86
+ // curated, where the default connection lives; minimal is the explicit
87
+ // small-model opt-in and reaches the inbox through orboto_api_call).
85
88
  'orboto_api_search',
86
89
  'orboto_api_call',
87
90
  ]);
@@ -118,6 +121,17 @@ export const CURATED_TOOLS = new Set([
118
121
  'orboto_timer_stop', // 772
119
122
  'orboto_log_time', // 130
120
123
  'orboto_get_timer', // 43
124
+ // ORB-1959 - agent coordination on the DEFAULT connection. Without
125
+ // these four an agent on curated could neither receive nor acknowledge
126
+ // mail (broadcasts included, they land in the inbox since ORB-1959),
127
+ // announce itself (heartbeat / presence) nor reach another agent - the
128
+ // workspace rules demand exactly that, and every fresh `mcp add` forgot
129
+ // the ?toolset=full workaround. Four small schemas; measured cost in
130
+ // manifest-size.test.ts.
131
+ 'orboto_messages',
132
+ 'orboto_agent_notify',
133
+ 'orboto_agent_broadcast',
134
+ 'orboto_agent_heartbeat',
121
135
  // Transport plumbing + the escape hatch itself
122
136
  'orboto_response_expand',
123
137
  // ORB-1741 - the way back from one-line manifest summaries: full
@@ -54,6 +54,13 @@ describe('toolInToolset', () => {
54
54
  expect(toolInToolset('orboto_claim', 'curated')).toBe(true);
55
55
  expect(toolInToolset('orboto_api_call', 'curated')).toBe(true);
56
56
  });
57
+ // ORB-1959 - coordination is part of the default connection, not an opt-in.
58
+ it('curated carries the four agent-coordination tools; minimal stays the 12-tool loop', () => {
59
+ for (const name of ['orboto_messages', 'orboto_agent_notify', 'orboto_agent_broadcast', 'orboto_agent_heartbeat']) {
60
+ expect(toolInToolset(name, 'curated'), name).toBe(true);
61
+ expect(toolInToolset(name, 'minimal'), name).toBe(false);
62
+ }
63
+ });
57
64
  it('minimal admits only the daily loop + the escape hatch', () => {
58
65
  expect(toolInToolset('orboto_claim', 'minimal')).toBe(true);
59
66
  expect(toolInToolset('orboto_api_search', 'minimal')).toBe(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orboto/mcp",
3
- "version": "0.182.5",
3
+ "version": "0.183.0",
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",