@orboto/mcp 0.205.1 → 0.206.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +2 -0
- package/dist/tools/agent-coordination.js +16 -1
- package/dist/tools/agent-coordination.test.js +12 -0
- package/dist/tools/agent-messages.js +1 -1
- package/dist/tools/agent-presence-schema.js +1 -1
- package/dist/tools/session-check.js +26 -0
- package/dist/tools/session-check.test.js +26 -0
- package/dist/tools/work-next.js +4 -4
- package/dist/tools/work-sessions.js +3 -3
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -27,6 +27,7 @@ import { agentBroadcastToolConfig, makeAgentBroadcastHandler, } from './tools/ag
|
|
|
27
27
|
import { agentMessageWorkToolConfig, makeAgentMessageWorkHandler } from './tools/agent-message-work.js';
|
|
28
28
|
import { agentMessagesToolConfig, makeAgentMessagesHandler } from './tools/agent-messages.js';
|
|
29
29
|
import { agentWakesToolConfig, makeAgentWakesHandler } from './tools/agent-wakes.js';
|
|
30
|
+
import { sessionCheckToolConfig, makeSessionCheckHandler } from './tools/session-check.js';
|
|
30
31
|
import { listProjectsToolConfig, makeListProjectsHandler } from './tools/list-projects.js';
|
|
31
32
|
import { getProjectToolConfig, makeGetProjectHandler } from './tools/get-project.js';
|
|
32
33
|
import { projectReadinessToolConfig, makeProjectReadinessHandler } from './tools/project-readiness.js';
|
|
@@ -209,6 +210,7 @@ export async function buildOrbotoMcpServer(opts) {
|
|
|
209
210
|
reg('orboto_message_work', agentMessageWorkToolConfig, makeAgentMessageWorkHandler(client));
|
|
210
211
|
reg('orboto_messages', agentMessagesToolConfig, makeAgentMessagesHandler(client));
|
|
211
212
|
reg('orboto_agent_wakes', agentWakesToolConfig, makeAgentWakesHandler(client));
|
|
213
|
+
reg('orboto_session_check', sessionCheckToolConfig, makeSessionCheckHandler(client));
|
|
212
214
|
reg('orboto_agent_broadcast', agentBroadcastToolConfig, makeAgentBroadcastHandler(client));
|
|
213
215
|
reg('orboto_list_projects', listProjectsToolConfig, makeListProjectsHandler(client));
|
|
214
216
|
reg('orboto_get_project', getProjectToolConfig, makeGetProjectHandler(client));
|
|
@@ -84,11 +84,12 @@ export function makeAgentPresenceHandler(client) {
|
|
|
84
84
|
}
|
|
85
85
|
export const agentNotifyToolConfig = {
|
|
86
86
|
title: 'Notify another agent / user',
|
|
87
|
-
description: '
|
|
87
|
+
description: 'Message a user (bot or human) by email; lands in their inbox (orboto_messages), for humans in-app. kind: request|error = work inside the scope the recipient holds (request needs project or toSessionRef + outcome); info = information; complete = result (needs outcome). payload: free-form JSON; threadId links a reply to the message it answers. toSessionRef (instance short id, session ref or id) reaches ONE session of the account; project scopes an account message to sessions declared on it (ORB-2136).',
|
|
88
88
|
inputSchema: z.object({
|
|
89
89
|
targetEmail: z.string().email(),
|
|
90
90
|
kind: z.enum(['info', 'request', 'complete', 'error']).default('info'),
|
|
91
91
|
subject: z.string().min(1).max(200),
|
|
92
|
+
outcome: z.string().min(10).max(1000).optional(),
|
|
92
93
|
payload: z.record(z.string(), z.unknown()).optional(),
|
|
93
94
|
threadId: z.string().uuid().optional(),
|
|
94
95
|
project: z.string().min(1).max(64).optional().describe('Project key or UUID: scope the message to the recipient session working that project.'),
|
|
@@ -126,8 +127,22 @@ export function makeAgentBroadcastHandler(client) {
|
|
|
126
127
|
};
|
|
127
128
|
};
|
|
128
129
|
}
|
|
130
|
+
/** ORB-2223 - the message-kind definition this versioned sender enforces; the API only warns. */
|
|
131
|
+
export function notifyDefinitionError(args) {
|
|
132
|
+
const kind = args.kind ?? 'info';
|
|
133
|
+
if ((kind === 'request' || kind === 'complete') && !args.outcome?.trim()) {
|
|
134
|
+
return `Invalid arguments: \`outcome\` is required for a ${kind} - the outcome sentence (what must be true when it is done, or what was delivered).`;
|
|
135
|
+
}
|
|
136
|
+
if (kind === 'request' && !args.project && !args.toSessionRef) {
|
|
137
|
+
return 'Invalid arguments: a request needs a target - `project` (key or UUID) or `toSessionRef`.';
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
129
141
|
export function makeAgentNotifyHandler(client) {
|
|
130
142
|
return async (args, extra) => {
|
|
143
|
+
const invalid = notifyDefinitionError(args);
|
|
144
|
+
if (invalid)
|
|
145
|
+
return { isError: true, content: [{ type: 'text', text: invalid }] };
|
|
131
146
|
const senderRef = mcpInstanceToken(args.senderRef, extra);
|
|
132
147
|
const res = await client.post('/v1/agent/notify', { ...args, senderRef }, { instanceToken: mcpInstanceToken(undefined, extra) });
|
|
133
148
|
const target = res.toSessionId ? `${args.targetEmail} session ${res.toSessionId.slice(0, 8)}` : args.targetEmail;
|
|
@@ -135,16 +135,28 @@ describe('orboto_agent_notify', () => {
|
|
|
135
135
|
};
|
|
136
136
|
});
|
|
137
137
|
const handler = makeAgentNotifyHandler(client);
|
|
138
|
+
const refused = await handler({ targetEmail: 'bob@example.com', kind: 'request', subject: 'Please review ORB-42' });
|
|
139
|
+
expect(refused.isError).toBe(true);
|
|
140
|
+
expect(refused.content[0].text).toContain('`outcome` is required for a request');
|
|
141
|
+
const noTarget = await handler({ targetEmail: 'bob@example.com', kind: 'request', subject: 'Please review ORB-42', outcome: 'ORB-42 has a review verdict' });
|
|
142
|
+
expect(noTarget.content[0].text).toContain('`project` (key or UUID) or `toSessionRef`');
|
|
143
|
+
const noOutcome = await handler({ targetEmail: 'bob@example.com', kind: 'complete', subject: 'Done' });
|
|
144
|
+
expect(noOutcome.content[0].text).toContain('`outcome` is required for a complete');
|
|
145
|
+
expect(capturedBody).toHaveLength(0);
|
|
138
146
|
const result = await handler({
|
|
139
147
|
targetEmail: 'bob@example.com',
|
|
140
148
|
kind: 'request',
|
|
141
149
|
subject: 'Please review ORB-42',
|
|
150
|
+
outcome: 'ORB-42 has a review verdict',
|
|
151
|
+
project: 'ORB',
|
|
142
152
|
payload: { ticketKey: 'ORB-42' },
|
|
143
153
|
});
|
|
144
154
|
expect(capturedBody[0]).toMatchObject({
|
|
145
155
|
targetEmail: 'bob@example.com',
|
|
146
156
|
kind: 'request',
|
|
147
157
|
subject: 'Please review ORB-42',
|
|
158
|
+
outcome: 'ORB-42 has a review verdict',
|
|
159
|
+
project: 'ORB',
|
|
148
160
|
payload: { ticketKey: 'ORB-42' },
|
|
149
161
|
});
|
|
150
162
|
expect(result.content[0].text).toContain('notified bob@example.com');
|
|
@@ -13,7 +13,7 @@ import { mcpInstanceToken } from './shared.js';
|
|
|
13
13
|
const PartySchema = z.object({ userId: z.string(), email: z.string(), sessionId: z.string().nullable(), role: z.string().nullable(), label: z.string() });
|
|
14
14
|
export const agentMessagesToolConfig = {
|
|
15
15
|
title: 'Agent inbox and message work',
|
|
16
|
-
description: 'Your inbox as THIS session sees it: mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out, all:true shows the whole account (ORB-2136). messageWork carries durable work ownership - claim before executing. ackIds acks: it completes an unclaimed request
|
|
16
|
+
description: 'Your inbox as THIS session sees it: mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out, all:true shows the whole account (ORB-2136). messageWork carries durable work ownership - claim before executing. ackIds acks: it completes an unclaimed request; claimed work with open steps is refused until messageWork completes or blocks it. dismiss is the third answer: not_mine hides it here and tells the sender, obsolete/duplicate close it for the account (note or duplicateOf required). Never leave mail you read and judged. Reply via orboto_agent_notify with threadId (toSessionRef = from.sessionId reaches that instance).',
|
|
17
17
|
inputSchema: z.object({
|
|
18
18
|
messageWork: z.record(z.unknown()).optional()
|
|
19
19
|
.describe('Work envelope {messageId, mutation, cursor, limit, openOnly}; schema: orboto_api_search.'),
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { AgentSessionScopeSchema } from './agent-session-scope.js';
|
|
3
3
|
const WorkSessionResourceClaimSchema = z.object({
|
|
4
4
|
kind: z.enum(['path', 'named']), value: z.string().min(1).max(500), mode: z.enum(['read', 'write']),
|
|
5
|
-
state: z.enum(['granted', 'waiting']).optional(), requestedAt: z.string().optional(),
|
|
5
|
+
state: z.enum(['granted', 'waiting']).optional(), requestedAt: z.string().optional(), grantedAt: z.string().optional(),
|
|
6
6
|
});
|
|
7
7
|
/** ORB-2209 - one account reference, shared by `actsAs` and `owner`. */
|
|
8
8
|
const AgentAccountRefSchema = z.object({ id: z.string().uuid(), name: z.string().nullable(), email: z.string() });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** ORB-2223 - the completion check: what this session still owes before its turn may end, and the explicit pause. */
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export const sessionCheckToolConfig = {
|
|
4
|
+
title: 'Completion check',
|
|
5
|
+
description: 'Before a turn ends: what this session still owes. verdict continue or may_stop, text says why; it lists, never re-runs. pause with a reason, then resume.',
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
action: z.enum(['check', 'pause', 'resume']).default('check'),
|
|
8
|
+
reason: z.string().min(3).max(500).optional(),
|
|
9
|
+
}).shape,
|
|
10
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
11
|
+
};
|
|
12
|
+
export function makeSessionCheckHandler(client) {
|
|
13
|
+
return async (args) => {
|
|
14
|
+
const action = args.action ?? 'check';
|
|
15
|
+
if (action === 'pause' && !args.reason) {
|
|
16
|
+
return { isError: true, content: [{ type: 'text', text: 'action pause needs a reason: why the session stops on purpose.' }] };
|
|
17
|
+
}
|
|
18
|
+
if (action !== 'check') {
|
|
19
|
+
const res = await client.post(`/agents/session/${action}`, action === 'pause' ? { reason: args.reason } : {});
|
|
20
|
+
const text = res.pause ? `Paused since ${res.pause.pausedAt}: ${res.pause.reason}` : 'Not paused - the completion check holds runnable work again.';
|
|
21
|
+
return { content: [{ type: 'text', text }], structuredContent: res };
|
|
22
|
+
}
|
|
23
|
+
const res = await client.get('/agents/session/unfinished');
|
|
24
|
+
return { content: [{ type: 'text', text: res.text }], structuredContent: res };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { makeSessionCheckHandler } from './session-check.js';
|
|
3
|
+
function textOf(result) {
|
|
4
|
+
return result.content[0].text;
|
|
5
|
+
}
|
|
6
|
+
describe('orboto_session_check', () => {
|
|
7
|
+
it('returns the plain-language completion check and the structured reconciliation', async () => {
|
|
8
|
+
const body = { sessionId: 's1', verdict: 'continue', reason: 'runnable', text: 'orboto completion guard: this session still owns runnable authorized work, so the turn does not end yet.' };
|
|
9
|
+
const get = vi.fn().mockResolvedValue(body);
|
|
10
|
+
const result = await makeSessionCheckHandler({ get })({});
|
|
11
|
+
expect(get).toHaveBeenCalledWith('/agents/session/unfinished');
|
|
12
|
+
expect(textOf(result)).toContain('does not end yet');
|
|
13
|
+
expect(result.structuredContent).toMatchObject({ verdict: 'continue' });
|
|
14
|
+
});
|
|
15
|
+
it('pauses with a reason and resumes, and refuses a pause without one', async () => {
|
|
16
|
+
const post = vi.fn()
|
|
17
|
+
.mockResolvedValueOnce({ sessionId: 's1', pause: { pausedAt: '2026-09-24T20:00:00Z', reason: 'Waiting for the release window' } })
|
|
18
|
+
.mockResolvedValueOnce({ sessionId: 's1', pause: null });
|
|
19
|
+
const handler = makeSessionCheckHandler({ post });
|
|
20
|
+
expect((await handler({ action: 'pause' })).isError).toBe(true);
|
|
21
|
+
expect(textOf(await handler({ action: 'pause', reason: 'Waiting for the release window' }))).toContain('Waiting for the release window');
|
|
22
|
+
expect(post).toHaveBeenCalledWith('/agents/session/pause', { reason: 'Waiting for the release window' });
|
|
23
|
+
expect(textOf(await handler({ action: 'resume' }))).toContain('Not paused');
|
|
24
|
+
expect(post).toHaveBeenLastCalledWith('/agents/session/resume', {});
|
|
25
|
+
});
|
|
26
|
+
});
|
package/dist/tools/work-next.js
CHANGED
|
@@ -27,9 +27,9 @@ export const workNextToolConfig = {
|
|
|
27
27
|
inputSchema: z.object({
|
|
28
28
|
projectKey: z.string().min(1).describe('Project key (e.g. "ACME") or UUID.'),
|
|
29
29
|
agentTag: z.string().min(1).max(64).optional()
|
|
30
|
-
.describe('ORB-1772 - preferred
|
|
30
|
+
.describe('ORB-1772 - preferred routing tag: `agent:<tag>` tickets rank first, foreign `agent:*` last but eligible. Lowercased; often the model or fleet lane name.'),
|
|
31
31
|
role: z.enum(['implementation', 'review', 'preflight', 'integration', 'spec']).optional()
|
|
32
|
-
.describe('Default `implementation
|
|
32
|
+
.describe('Default `implementation`; reserves only a ticket whose (ticket, role) lease is free.'),
|
|
33
33
|
leaseSeconds: z.number().int().min(60).max(86_400).optional()
|
|
34
34
|
.describe('How long the lease should hold without renewal. Default 900 (15 min).'),
|
|
35
35
|
startTimer: z.boolean().optional()
|
|
@@ -37,11 +37,11 @@ export const workNextToolConfig = {
|
|
|
37
37
|
agentSessionToken: z.string().optional()
|
|
38
38
|
.describe('Stable per-agent-instance token. Omit to use this MCP connection\'s own instance id.'),
|
|
39
39
|
resourceClaims: z.array(z.object(ResourceClaimShape)).max(50).optional()
|
|
40
|
-
.describe('ORB-1610 -
|
|
40
|
+
.describe('ORB-1610 - claims to acquire with the reservation; a ticket under a conflicting granted write claim is skipped.'),
|
|
41
41
|
onConflict: z.enum(['reject', 'queue']).optional()
|
|
42
42
|
.describe('Only matters when `resourceClaims` is set on the WINNING candidate. Default `reject`.'),
|
|
43
43
|
peek: z.boolean().optional()
|
|
44
|
-
.describe('ORB-1930 -
|
|
44
|
+
.describe('ORB-1930 - "is there work for me?" without reserving: the same candidate walk, no lease, timer or lane quota; returns `candidate` or the empty reason.'),
|
|
45
45
|
}).shape,
|
|
46
46
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
47
47
|
};
|
|
@@ -363,7 +363,7 @@ export const workFinishToolConfig = {
|
|
|
363
363
|
sessionId: z.string().uuid().describe('The id returned by orboto_work_start / orboto_work_session_start.'),
|
|
364
364
|
outcome: z.enum(['finished', 'cancelled']).optional()
|
|
365
365
|
.describe('`finished` (default) drives the ticket transition below. `cancelled` = abandoned attempt; time is still booked, the ticket is left untouched.'),
|
|
366
|
-
commitSha: z.string().optional().describe('The commit this session produced
|
|
366
|
+
commitSha: z.string().optional().describe('The commit this session produced; an attestation now, verified once git ingestion catches up.'),
|
|
367
367
|
verification: z.object({
|
|
368
368
|
reviewOfSessionId: z.string().uuid().optional().describe('Exact independent implementation work session this review examined.'),
|
|
369
369
|
reviewVerdict: z.enum(['approved', 'rework']).optional().describe('Explicit review outcome, paired with reviewOfSessionId; never inferred from later ticket status.'),
|
|
@@ -373,8 +373,8 @@ export const workFinishToolConfig = {
|
|
|
373
373
|
notes: z.string().optional(),
|
|
374
374
|
}).optional().describe('Which gates you actually ran and what they said. This is the attestation a reviewer reads cold.'),
|
|
375
375
|
targetCategory: z.enum(['todo', 'in_progress', 'in_review', 'done', 'wont_fix']).optional()
|
|
376
|
-
.describe('Default `done
|
|
377
|
-
note: z.string().optional().describe('The completion note
|
|
376
|
+
.describe('Default `done`; `in_review` when a human should look first. Implementation sessions with outcome `finished` only.'),
|
|
377
|
+
note: z.string().optional().describe('The completion note on the ticket; generated from booked time, commit and verification when omitted.'),
|
|
378
378
|
}).shape,
|
|
379
379
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
380
380
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.206.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",
|