@orboto/mcp 0.215.0 → 0.216.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/claude-context.js +6 -1
- package/dist/claude-context.test.js +7 -1
- package/dist/index.js +2 -3
- package/dist/manifest-size.test.js +2 -0
- package/dist/tail-labels.js +2 -0
- package/dist/tools/agent-coordination.js +1 -1
- package/dist/tools/agent-coordination.test.js +10 -0
- package/dist/tools/agent-notice-tail.test.js +16 -0
- package/dist/tools/agent-who.js +2 -1
- package/dist/tools/agent-who.test.js +3 -0
- package/dist/tools/session-start.js +3 -2
- package/dist/tools/session-start.test.js +17 -0
- package/dist/tools/shared.js +7 -0
- package/package.json +1 -1
package/dist/claude-context.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* nothing - the ledger then says "unknown", never an estimate.
|
|
6
6
|
*/
|
|
7
7
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
8
|
-
import { homedir } from 'node:os';
|
|
8
|
+
import { homedir, hostname } from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
export const CLAUDE_CONTEXT_MAX_AGE_MS = 10 * 60_000;
|
|
11
11
|
export function claudeContextDir() {
|
|
@@ -46,3 +46,8 @@ export function readClaudeContext(dir, now = Date.now(), root = claudeContextDir
|
|
|
46
46
|
}
|
|
47
47
|
return fresh.length === 1 ? fresh[0] : null;
|
|
48
48
|
}
|
|
49
|
+
/** ORB-2465 - the channel heartbeat's clientInfo: the wake channel, the host `--host` filters on, and the context report. */
|
|
50
|
+
export function channelClientInfo(context, host = hostname()) {
|
|
51
|
+
const value = host.trim().toLowerCase().slice(0, 200);
|
|
52
|
+
return { channel: 'claude-code', ...(value ? { host: value } : {}), ...(context ?? {}) };
|
|
53
|
+
}
|
|
@@ -6,7 +6,7 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { claudeContextDir, readClaudeContext } from './claude-context.js';
|
|
9
|
+
import { channelClientInfo, claudeContextDir, readClaudeContext } from './claude-context.js';
|
|
10
10
|
let root;
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
root = mkdtempSync(path.join(tmpdir(), 'orboto-ctx-'));
|
|
@@ -41,3 +41,9 @@ describe('ORB-2149 - the report directory', () => {
|
|
|
41
41
|
expect(claudeContextDir().endsWith(path.join('.orboto', 'claude-context'))).toBe(true);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
|
+
describe('ORB-2465 - the channel heartbeat names its host', () => {
|
|
45
|
+
it('sends the lower-cased host next to the channel and the context report', () => {
|
|
46
|
+
expect(channelClientInfo({ contextTokens: 10 }, ' Build-Host-A ')).toEqual({ channel: 'claude-code', host: 'build-host-a', contextTokens: 10 });
|
|
47
|
+
expect(channelClientInfo(null, '')).toEqual({ channel: 'claude-code' });
|
|
48
|
+
});
|
|
49
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -71,10 +71,9 @@ async function main() {
|
|
|
71
71
|
const outdated = cliOutdatedNotice(process.env.ORBOTO_MCP_CLI_OUTDATED);
|
|
72
72
|
if (outdated)
|
|
73
73
|
void inbox.deliverNotice(CLI_OUTDATED_NOTICE, outdated);
|
|
74
|
-
const { readClaudeContext } = await import('./claude-context.js');
|
|
74
|
+
const { readClaudeContext, channelClientInfo } = await import('./claude-context.js');
|
|
75
75
|
const beat = () => {
|
|
76
|
-
|
|
77
|
-
preflightClient.post('/v1/agent/heartbeat', { clientInfo: { channel: 'claude-code', ...(context ?? {}) } }, { instanceToken })
|
|
76
|
+
preflightClient.post('/v1/agent/heartbeat', { clientInfo: channelClientInfo(readClaudeContext(process.cwd())) }, { instanceToken })
|
|
78
77
|
.catch((err) => { console.error(`[orboto-mcp] channel heartbeat failed: ${err.message}`); });
|
|
79
78
|
};
|
|
80
79
|
beat();
|
|
@@ -47,6 +47,8 @@ afterEach(() => { vi.restoreAllMocks(); });
|
|
|
47
47
|
* verdicts themselves ride the response, not the manifest.
|
|
48
48
|
* ORB-2338 added no named tool; the transcript rides the escape hatch and
|
|
49
49
|
* the resource.
|
|
50
|
+
* ORB-2465 added no named tool: the live notice rides the escape hatch
|
|
51
|
+
* (full = 171,194 chars, 6 free).
|
|
50
52
|
*
|
|
51
53
|
* @see ORB-1805, ORB-1669, ORB-1910
|
|
52
54
|
*/
|
package/dist/tail-labels.js
CHANGED
|
@@ -20,6 +20,8 @@ export const TAIL_LABELS = [
|
|
|
20
20
|
{ method: 'PATCH', path: new RegExp(`^/projects/${SEG}/chat-bot/routes/${SEG}$`), label: 'orboto_chat_bot_route' },
|
|
21
21
|
{ method: 'DELETE', path: new RegExp(`^/projects/${SEG}/chat-bot/routes/${SEG}$`), label: 'orboto_chat_bot_route' },
|
|
22
22
|
{ method: 'POST', path: /^\/chat-bot\/links\/(confirm|[^/]+\/revoke)$/, label: 'orboto_chat_bot_link' },
|
|
23
|
+
{ method: 'POST', path: /^\/v1\/agent\/broadcast\/live$/, label: 'orboto_agent_notice' },
|
|
24
|
+
{ method: 'GET', path: new RegExp(`^/v1/agent/broadcasts/live(/${SEG})?$`), label: 'orboto_agent_notice_status' },
|
|
23
25
|
];
|
|
24
26
|
/** The metrics label of one tool call: the stand-in tool name for a labelled api_call, else the tool name itself. */
|
|
25
27
|
export function metricsToolName(toolName, args) {
|
|
@@ -192,7 +192,7 @@ export function makeAgentNotifyHandler(client) {
|
|
|
192
192
|
const routedTo = res.routedTo ?? (res.toSessionId ? 'session' : 'account');
|
|
193
193
|
const note = res.routingNote ? ` - ${res.routingNote}` : '';
|
|
194
194
|
return {
|
|
195
|
-
content: [{ type: 'text', text: `${res.queued ? 'queued for' : 'notified'} ${target} (message ${res.messageId}, routed to ${routedTo}${note})` }],
|
|
195
|
+
content: [{ type: 'text', text: `${res.queued ? 'queued for' : 'notified'} ${target} (message ${res.messageId}, routed to ${routedTo}${note})${res.hint ? `\nhint: ${res.hint}` : ''}` }],
|
|
196
196
|
structuredContent: {
|
|
197
197
|
ok: true, messageId: res.messageId, toSessionId: res.toSessionId ?? null, routedTo,
|
|
198
198
|
...(res.routingNote ? { routingNote: res.routingNote } : {}),
|
|
@@ -201,6 +201,16 @@ describe('orboto_agent_notify routing (ORB-2263)', () => {
|
|
|
201
201
|
expect(result.structuredContent).toMatchObject({ routedTo: 'account', routingNote: expect.stringContaining('no session declared HIVE') });
|
|
202
202
|
expect(result.content[0].text).toContain('routed to account - no session declared HIVE');
|
|
203
203
|
});
|
|
204
|
+
it('ORB-2465: passes the agent-broadcast live hint on in the text, not in the output schema', async () => {
|
|
205
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({
|
|
206
|
+
ok: true, status: 200, statusText: 'OK',
|
|
207
|
+
json: async () => ({ ok: true, messageId: '00000000-0000-4000-8000-000000000000', toSessionId: null, routedTo: 'account', hint: 'account mail without --project wakes no particular session; send orboto agent-broadcast live' }),
|
|
208
|
+
text: async () => '',
|
|
209
|
+
}));
|
|
210
|
+
const result = await makeAgentNotifyHandler(client)({ targetEmail: 'bot@example.org', kind: 'info', subject: 'restart' });
|
|
211
|
+
expect(result.content[0].text).toContain('\nhint: account mail without --project');
|
|
212
|
+
expect(result.structuredContent).not.toHaveProperty('hint');
|
|
213
|
+
});
|
|
204
214
|
});
|
|
205
215
|
describe('orboto_messages (ORB-1742 self-echo exclusion)', () => {
|
|
206
216
|
it('fetches with excludeRef = the session ref by default; includeOwnSends drops the filter', async () => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** ORB-2465 - the live notice reaches an MCP-only agent through the escape hatch; every call is labelled in the metrics. */
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { metricsToolName } from '../tail-labels.js';
|
|
4
|
+
const ID = '6f1c2b1e-3d4a-4b5c-8d9e-0a1b2c3d4e5f';
|
|
5
|
+
const call = (method, path) => metricsToolName('orboto_api_call', { method, path });
|
|
6
|
+
describe('ORB-2465 - live notice tail labels', () => {
|
|
7
|
+
it('labels the send, the status and the active list', () => {
|
|
8
|
+
expect(call('POST', '/v1/agent/broadcast/live')).toBe('orboto_agent_notice');
|
|
9
|
+
expect(call('GET', `/v1/agent/broadcasts/live/${ID}`)).toBe('orboto_agent_notice_status');
|
|
10
|
+
expect(call('GET', '/v1/agent/broadcasts/live')).toBe('orboto_agent_notice_status');
|
|
11
|
+
});
|
|
12
|
+
it('leaves the scope broadcasts and the prompt-hook delivery unlabelled', () => {
|
|
13
|
+
expect(call('POST', '/v1/agent/broadcast')).toBe('orboto_api_call');
|
|
14
|
+
expect(call('POST', '/v1/agent/broadcasts/live/deliver')).toBe('orboto_api_call');
|
|
15
|
+
});
|
|
16
|
+
});
|
package/dist/tools/agent-who.js
CHANGED
|
@@ -16,7 +16,8 @@ export function whoLine(s) {
|
|
|
16
16
|
const address = s.addresses.join(', ') || (s.role ? `${s.role} (no project)` : 'no scope');
|
|
17
17
|
const channel = s.channel.announced ? `${s.channel.announced}${s.channel.online ? ' online' : ' offline'}` : 'no channel';
|
|
18
18
|
const ticket = s.currentTicket ? `; on ${s.currentTicket.key ?? s.currentTicket.title}` : '';
|
|
19
|
-
|
|
19
|
+
const host = s.host ? `, host ${s.host}` : '';
|
|
20
|
+
return `- ${address} - ${s.email} session ${s.shortId}${host}, ${s.status}, ${channel}, last seen ${s.lastSeenAt}${ticket}`;
|
|
20
21
|
}
|
|
21
22
|
export function makeAgentWhoHandler(client) {
|
|
22
23
|
return async (args = {}) => {
|
|
@@ -28,4 +28,7 @@ describe('orboto_who', () => {
|
|
|
28
28
|
expect(agentWhoToolConfig.annotations.readOnlyHint).toBe(true);
|
|
29
29
|
expect(whoLine({ ...entry, addresses: [], role: null, channel: { announced: null, online: false }, currentTicket: null })).toContain('no scope');
|
|
30
30
|
});
|
|
31
|
+
it('ORB-2465: names the host a live notice filters on', () => {
|
|
32
|
+
expect(whoLine({ ...entry, host: 'build-host-a' })).toContain('session f072e1c5, host build-host-a, working');
|
|
33
|
+
});
|
|
31
34
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @see ORB-1605, ORB-1607, ORB-1818
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import { resolveTicketByKey, applyAgentProfile, mcpInstanceToken } from './shared.js';
|
|
12
|
+
import { resolveTicketByKey, applyAgentProfile, mcpInstanceToken, localSessionHost } from './shared.js';
|
|
13
13
|
import { AgentSessionScopeSchema } from './agent-session-scope.js';
|
|
14
14
|
import { PROCESS_OWNER, PROTECT_TEXT_META, storePayload } from '../response-budget.js';
|
|
15
15
|
import { loadRequiredRules } from '../required-rules.js';
|
|
@@ -202,7 +202,8 @@ export function makeSessionStartHandler(client, opts = {}) {
|
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
204
|
const toRegistration = (r) => (r && typeof r.sessionId === 'string' ? { sessionId: r.sessionId, scope: r.scope ?? null, reconnects: r.reconnects, lastReconnectAt: r.lastReconnectAt } : null);
|
|
205
|
-
|
|
205
|
+
const host = localSessionHost(extra);
|
|
206
|
+
let registration = await client.post('/v1/agent/heartbeat', { ...(input.scope !== undefined ? { scope: input.scope } : {}), ...(host ? { clientInfo: { host } } : {}) }, { instanceToken })
|
|
206
207
|
.then(toRegistration)
|
|
207
208
|
.catch(() => null);
|
|
208
209
|
if (input.scope !== undefined && registration)
|
|
@@ -14,6 +14,8 @@ import { CHANNEL_START_COMMANDS, CHANNEL_START_COMMANDS_WITHOUT_CLI } from '../i
|
|
|
14
14
|
import { readFileSync } from 'node:fs';
|
|
15
15
|
import { makeResponseExpandHandler } from './response-expand.js';
|
|
16
16
|
import { applyResponseBudget, budgetFor, DEFAULT_BUDGET_CHARS } from '../response-budget.js';
|
|
17
|
+
import { hostname } from 'node:os';
|
|
18
|
+
import { localSessionHost } from './shared.js';
|
|
17
19
|
beforeEach(() => { vi.restoreAllMocks(); });
|
|
18
20
|
afterEach(() => { vi.restoreAllMocks(); });
|
|
19
21
|
function stubByPath(map) {
|
|
@@ -572,6 +574,21 @@ describe('ORB-2136 - session scope and ref', () => {
|
|
|
572
574
|
await makeSessionStartHandler(client)({ scope: { role: 'worker', projectKeys: ['orb'] } }, { sessionId: 'keep1' });
|
|
573
575
|
expect(rememberedScope('mcp-keep1')).toEqual({ role: 'worker', projectKeys: ['ORB'] });
|
|
574
576
|
});
|
|
577
|
+
it('ORB-2465 - a stdio session reports its host on the heartbeat; an HTTP connection reports none', async () => {
|
|
578
|
+
const seen = [];
|
|
579
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
580
|
+
const u = new URL(url.toString());
|
|
581
|
+
seen.push({ path: u.pathname, body: init?.body ? JSON.parse(init.body) : undefined });
|
|
582
|
+
const body = u.pathname === '/v1/agent/heartbeat'
|
|
583
|
+
? { sessionToken: 'x', sessionId: 'cbb52195-0000-4000-8000-000000000000', scope: null }
|
|
584
|
+
: u.pathname === '/agent-instructions' ? { instructions: 'rules here', rulesHash: 'fixture' } : {};
|
|
585
|
+
return { ok: true, status: 200, statusText: 'OK', json: async () => body, text: async () => '' };
|
|
586
|
+
});
|
|
587
|
+
await makeSessionStartHandler(client)({});
|
|
588
|
+
expect(seen.find((c) => c.path === '/v1/agent/heartbeat')?.body).toEqual({ clientInfo: { host: hostname().trim().toLowerCase() } });
|
|
589
|
+
expect(localSessionHost({ sessionId: 'http-1' }, 'server')).toBeUndefined();
|
|
590
|
+
expect(localSessionHost(undefined, ' Mac-A ')).toBe('mac-a');
|
|
591
|
+
});
|
|
575
592
|
it('without a scope the heartbeat body stays empty and the digest says no scope is declared', async () => {
|
|
576
593
|
const seen = [];
|
|
577
594
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
package/dist/tools/shared.js
CHANGED
|
@@ -114,6 +114,13 @@ export function checkoutInstanceToken(env = process.env, host = hostname(), cwd
|
|
|
114
114
|
return explicit || `agent-${createHash('sha256').update(`${host}:${cwd}`).digest('hex').slice(0, 12)}`;
|
|
115
115
|
}
|
|
116
116
|
const MCP_PROCESS_INSTANCE = checkoutInstanceToken();
|
|
117
|
+
/** ORB-2465 - the machine a stdio proxy runs on, lower-cased, as `agent-broadcast live --host` matches it; an HTTP connection (per-connection session id) runs on the server, so it reports none. */
|
|
118
|
+
export function localSessionHost(extra, host = hostname()) {
|
|
119
|
+
if (extra?.sessionId)
|
|
120
|
+
return undefined;
|
|
121
|
+
const value = host.trim().toLowerCase().slice(0, 200);
|
|
122
|
+
return value || undefined;
|
|
123
|
+
}
|
|
117
124
|
/** Precedence: explicit caller-supplied token > per-connection MCP session id
|
|
118
125
|
* (distinct per client even on a shared HTTP server) > per-process id (stdio). */
|
|
119
126
|
export function mcpInstanceToken(explicit, extra) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.216.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",
|