@orboto/mcp 0.219.1 → 0.221.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/manifest-size.test.js +2 -0
- package/dist/tail-labels.js +11 -0
- package/dist/tools/agent-messages-compact.test.js +51 -0
- package/dist/tools/agent-messages.js +26 -8
- package/dist/tools/lexware-tail.test.js +38 -0
- package/dist/tools/update-project.js +31 -10
- package/dist/tools/update-project.test.js +26 -0
- package/package.json +2 -2
|
@@ -49,6 +49,8 @@ afterEach(() => { vi.restoreAllMocks(); });
|
|
|
49
49
|
* the resource.
|
|
50
50
|
* ORB-2465 added no named tool: the live notice rides the escape hatch
|
|
51
51
|
* (full = 171,194 chars, 6 free).
|
|
52
|
+
* ORB-2502 gave `orboto_messages` a `since` parameter inside the ceiling: the
|
|
53
|
+
* limit lost its schema default and the project hint shrank (full = 171,197).
|
|
52
54
|
*
|
|
53
55
|
* @see ORB-1805, ORB-1669, ORB-1910
|
|
54
56
|
*/
|
package/dist/tail-labels.js
CHANGED
|
@@ -20,6 +20,17 @@ 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: 'GET', path: /^\/admin\/integrations\/lexware(\/(readiness|events|deliveries))?$/, label: 'orboto_lexware_connection' },
|
|
24
|
+
{ method: 'POST', path: /^\/admin\/integrations\/lexware\/test$/, label: 'orboto_lexware_connection' },
|
|
25
|
+
{ method: 'POST', path: /^\/admin\/integrations\/lexware(\/(rotate-key|subscriptions))?$/, label: 'orboto_lexware_admin' },
|
|
26
|
+
{ method: 'PATCH', path: /^\/admin\/integrations\/lexware$/, label: 'orboto_lexware_admin' },
|
|
27
|
+
{ method: 'DELETE', path: /^\/admin\/integrations\/lexware(\/subscriptions)?$/, label: 'orboto_lexware_admin' },
|
|
28
|
+
{ method: 'GET', path: new RegExp(`^/customers/${SEG}/lexware-contact$`), label: 'orboto_lexware_contact' },
|
|
29
|
+
{ method: 'PUT', path: new RegExp(`^/customers/${SEG}/lexware-contact$`), label: 'orboto_lexware_contact' },
|
|
30
|
+
{ method: 'DELETE', path: new RegExp(`^/customers/${SEG}/lexware-contact$`), label: 'orboto_lexware_contact' },
|
|
31
|
+
{ method: 'POST', path: new RegExp(`^/customers/${SEG}/lexware-billing-runs(/preview|/${SEG}/refresh)?$`), label: 'orboto_lexware_billing_run' },
|
|
32
|
+
{ method: 'GET', path: new RegExp(`^/customers/${SEG}/lexware-billing-runs(/summary)?$`), label: 'orboto_lexware_billing_run' },
|
|
33
|
+
{ method: 'GET', path: /^\/admin\/integrations\/lexware\/billing-runs$/, label: 'orboto_lexware_billing_run' },
|
|
23
34
|
{ method: 'POST', path: /^\/v1\/agent\/broadcast\/live$/, label: 'orboto_agent_notice' },
|
|
24
35
|
{ method: 'GET', path: new RegExp(`^/v1/agent/broadcasts/live(/${SEG})?$`), label: 'orboto_agent_notice_status' },
|
|
25
36
|
{ method: 'GET', path: new RegExp(`^/v1/agent/messages/${SEG}/attachments(/${SEG}/url)?$`), label: 'orboto_agent_message_attachments' },
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ORB-2502 - orboto_messages lists the newest 10 unread compactly with the exact count; all:true keeps every message in full.
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { OrbotoClient } from '../orboto-client.js';
|
|
6
|
+
import { makeAgentMessagesHandler } from './agent-messages.js';
|
|
7
|
+
afterEach(() => { vi.restoreAllMocks(); });
|
|
8
|
+
const client = new OrbotoClient({ baseUrl: 'https://orboto.example.com', apiKey: 'orb_x' });
|
|
9
|
+
const party = (label) => ({ userId: 'u', email: 'e', sessionId: null, role: null, label });
|
|
10
|
+
const message = {
|
|
11
|
+
id: 'm1', fromUserId: 'u1', toUserId: 'u2', fromSessionId: null, toSessionId: null, from: party('Ada'), to: party('Bot'),
|
|
12
|
+
kind: 'request', subject: 'review', payload: { message: 'x'.repeat(1_000) }, threadId: null, projectKey: 'ORB',
|
|
13
|
+
createdAt: '2026-10-01T10:00:00Z', deliveredAt: null, readAt: null, dismissed: null,
|
|
14
|
+
excerpt: `message: ${'x'.repeat(291)}`, payloadChars: 1_009,
|
|
15
|
+
};
|
|
16
|
+
function stubInbox(urls) {
|
|
17
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
|
|
18
|
+
urls.push(String(url));
|
|
19
|
+
return {
|
|
20
|
+
ok: true, status: 200, statusText: 'OK', text: async () => '',
|
|
21
|
+
json: async () => ({ messages: [message], sessionId: 'cbb52195-0000-4000-8000-000000000000', counts: { unread: 14, olderThan7Days: 12 } }),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
describe('ORB-2502 - the compact inbox on MCP', () => {
|
|
26
|
+
it('asks for the newest 10 compactly, prints the excerpt and the count, and leaves the payload out of the structured answer', async () => {
|
|
27
|
+
const urls = [];
|
|
28
|
+
stubInbox(urls);
|
|
29
|
+
const result = await makeAgentMessagesHandler(client)({ since: '2026-09-30T00:00:00Z' }, { sessionId: 'abc' });
|
|
30
|
+
expect(urls[0]).toContain('limit=10');
|
|
31
|
+
expect(urls[0]).toContain('compact=true');
|
|
32
|
+
expect(urls[0]).toContain('since=2026-09-30T00%3A00%3A00Z');
|
|
33
|
+
const text = result.content[0].text;
|
|
34
|
+
expect(text).toContain(`\n message: ${'x'.repeat(291)}\n ... 709 more characters (all:true prints everything)`);
|
|
35
|
+
expect(text).toContain('14 unread, 12 older than 7 days (all, limit N, since)');
|
|
36
|
+
expect(text).not.toContain('payload:');
|
|
37
|
+
expect(result.structuredContent).toMatchObject({ messages: [{ id: 'm1', payload: null }] });
|
|
38
|
+
});
|
|
39
|
+
it('all:true prints every message in full as before, with the limit of 50', async () => {
|
|
40
|
+
const urls = [];
|
|
41
|
+
stubInbox(urls);
|
|
42
|
+
const result = await makeAgentMessagesHandler(client)({ all: true }, { sessionId: 'abc' });
|
|
43
|
+
expect(urls[0]).toContain('all=true');
|
|
44
|
+
expect(urls[0]).toContain('limit=50');
|
|
45
|
+
expect(urls[0]).not.toContain('compact');
|
|
46
|
+
const text = result.content[0].text;
|
|
47
|
+
expect(text).toContain(`payload: {"message":"${'x'.repeat(1_000)}"}`);
|
|
48
|
+
expect(text).not.toContain('older than 7 days');
|
|
49
|
+
expect(result.structuredContent).toMatchObject({ messages: [{ payload: { message: 'x'.repeat(1_000) } }] });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -14,13 +14,14 @@ const PartySchema = z.object({ userId: z.string(), email: z.string(), sessionId:
|
|
|
14
14
|
const UNREGISTERED_INBOX_HINT = 'Not registered: only account mail is listed. Register with orboto_session_start to see the mail addressed to your session.';
|
|
15
15
|
export const agentMessagesToolConfig = {
|
|
16
16
|
title: 'Agent inbox and message work',
|
|
17
|
-
description: 'Your inbox as THIS session sees it. It holds mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out, all:true is the account view (ORB-2136). Before orboto_session_start only account mail is listed; register to see mail addressed to your session (ORB-2347). 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
|
+
description: 'Your inbox as THIS session sees it. It holds mail to this session, account mail inside your declared scope, broadcasts; sibling-session mail and own sends stay out, all:true is the account view with every message in full (ORB-2136). By default it lists the newest 10 unread, newest first, each as a header plus the first 300 payload characters, then the line `N unread, M older than 7 days`; limit and since (ISO time or message id) narrow it (ORB-2502). Before orboto_session_start only account mail is listed; register to see mail addressed to your session (ORB-2347). 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).',
|
|
18
18
|
inputSchema: z.object({
|
|
19
19
|
messageWork: z.record(z.unknown()).optional()
|
|
20
20
|
.describe('Work envelope {messageId, mutation, cursor, limit, openOnly}; schema: orboto_api_search.'),
|
|
21
21
|
all: z.boolean().default(false),
|
|
22
|
-
limit: z.number().int().min(1).max(200).
|
|
23
|
-
|
|
22
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
23
|
+
since: z.string().optional(),
|
|
24
|
+
project: z.string().min(1).max(64).optional().describe('Key/UUID; includes unscoped mail.'),
|
|
24
25
|
includeOwnSends: z.boolean().default(false),
|
|
25
26
|
ackIds: z.array(z.string().uuid()).max(200).optional().describe('Message ids to mark read.'),
|
|
26
27
|
dismiss: z.object({
|
|
@@ -55,6 +56,16 @@ export const agentMessagesToolConfig = {
|
|
|
55
56
|
}).shape,
|
|
56
57
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
57
58
|
};
|
|
59
|
+
/** ORB-2502 - the compact view: the header, the first 300 payload characters indented, and how much was left out. */
|
|
60
|
+
function compactLines(m, header) {
|
|
61
|
+
const excerpt = m.excerpt ?? '';
|
|
62
|
+
const rest = (m.payloadChars ?? 0) - Array.from(excerpt).length;
|
|
63
|
+
return [
|
|
64
|
+
header,
|
|
65
|
+
...(excerpt ? excerpt.replace(/\n+$/, '').split('\n').map((line) => ` ${line}`) : []),
|
|
66
|
+
...(rest > 0 ? [` ... ${rest} more characters (all:true prints everything)`] : []),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
58
69
|
export function makeAgentMessagesHandler(client) {
|
|
59
70
|
return async (args, extra) => {
|
|
60
71
|
if (args.messageWork) {
|
|
@@ -80,23 +91,30 @@ export function makeAgentMessagesHandler(client) {
|
|
|
80
91
|
const res = await client.post('/v1/agent/messages/ack', { ids: args.ackIds, instanceToken: mcpInstanceToken(undefined, extra) });
|
|
81
92
|
acked = res.acked;
|
|
82
93
|
}
|
|
94
|
+
const compact = !args.all;
|
|
83
95
|
const q = new URLSearchParams();
|
|
84
96
|
if (args.all)
|
|
85
97
|
q.set('all', 'true');
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
q.set('limit', String(args.limit ?? (compact ? 10 : 50)));
|
|
99
|
+
if (compact)
|
|
100
|
+
q.set('compact', 'true');
|
|
101
|
+
if (args.since)
|
|
102
|
+
q.set('since', args.since);
|
|
88
103
|
if (args.project)
|
|
89
104
|
q.set('project', args.project);
|
|
90
105
|
const instanceToken = mcpInstanceToken(undefined, extra);
|
|
91
106
|
if (!args.includeOwnSends)
|
|
92
107
|
q.set('excludeRef', instanceToken);
|
|
93
|
-
const { messages, sessionId } = await client.get(`/v1/agent/messages
|
|
108
|
+
const { messages, sessionId, counts } = await client.get(`/v1/agent/messages?${q.toString()}`, { instanceToken });
|
|
109
|
+
const header = (m) => `[${m.kind}]${m.projectKey ? ` [${m.projectKey}]` : ''} ${m.subject} (from ${m.from?.label ?? m.fromUserId} to ${m.to?.label ?? m.toUserId}, ${m.createdAt}, id ${m.id}${m.threadId ? `, thread ${m.threadId}` : ''})`;
|
|
94
110
|
const lines = messages.length === 0
|
|
95
111
|
? [acked > 0 ? `Acknowledged ${acked} message(s). Inbox empty.` : 'Inbox empty.']
|
|
96
|
-
: messages.
|
|
112
|
+
: messages.flatMap((m) => (compact ? compactLines(m, header(m)) : [`${header(m)}${m.payload ? ` payload: ${JSON.stringify(m.payload)}` : ''}`]));
|
|
97
113
|
if (messages.length > 0) {
|
|
98
114
|
lines.push(`Acknowledge with ackIds once handled; reply via orboto_agent_notify with threadId (toSessionRef = the sender's instance short id reaches that session only).`);
|
|
99
115
|
}
|
|
116
|
+
if (compact && counts)
|
|
117
|
+
lines.push(`${counts.unread} unread, ${counts.olderThan7Days} older than 7 days (all, limit N, since)`);
|
|
100
118
|
if (args.dismiss) {
|
|
101
119
|
lines.unshift(`Dismissed ${dismissedCount} message(s) as ${args.dismiss.reason}${closedCount > 0 ? `, ${closedCount} closed for the account` : ''}${feedbackSent > 0 ? `, ${feedbackSent} sender(s) told about the misroute` : ''}.`);
|
|
102
120
|
}
|
|
@@ -110,7 +128,7 @@ export function makeAgentMessagesHandler(client) {
|
|
|
110
128
|
sessionId: sessionId ?? null,
|
|
111
129
|
messages: messages.map((m) => ({
|
|
112
130
|
id: m.id, fromUserId: m.fromUserId, from: m.from, to: m.to, kind: m.kind, subject: m.subject,
|
|
113
|
-
payload: m.payload, threadId: m.threadId, projectKey: m.projectKey ?? null,
|
|
131
|
+
payload: compact ? null : m.payload, threadId: m.threadId, projectKey: m.projectKey ?? null,
|
|
114
132
|
createdAt: m.createdAt, readAt: m.readAt, dismissed: m.dismissed ?? null,
|
|
115
133
|
})),
|
|
116
134
|
acked,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** ORB-2508 - the Lexware connection 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 call = (method, path) => metricsToolName('orboto_api_call', { method, path });
|
|
5
|
+
describe('ORB-2508 - Lexware tail labels', () => {
|
|
6
|
+
it('labels status, readiness, logs and the profile test as orboto_lexware_connection', () => {
|
|
7
|
+
for (const path of ['/admin/integrations/lexware', '/admin/integrations/lexware/readiness', '/admin/integrations/lexware/events?limit=20', '/admin/integrations/lexware/deliveries']) {
|
|
8
|
+
expect(call('GET', path), path).toBe('orboto_lexware_connection');
|
|
9
|
+
}
|
|
10
|
+
expect(call('POST', '/admin/integrations/lexware/test')).toBe('orboto_lexware_connection');
|
|
11
|
+
});
|
|
12
|
+
it('labels the operator writes as orboto_lexware_admin', () => {
|
|
13
|
+
expect(call('POST', '/admin/integrations/lexware')).toBe('orboto_lexware_admin');
|
|
14
|
+
expect(call('POST', '/admin/integrations/lexware/rotate-key')).toBe('orboto_lexware_admin');
|
|
15
|
+
expect(call('POST', '/admin/integrations/lexware/subscriptions')).toBe('orboto_lexware_admin');
|
|
16
|
+
expect(call('PATCH', '/admin/integrations/lexware')).toBe('orboto_lexware_admin');
|
|
17
|
+
expect(call('DELETE', '/admin/integrations/lexware/subscriptions')).toBe('orboto_lexware_admin');
|
|
18
|
+
expect(call('DELETE', '/admin/integrations/lexware')).toBe('orboto_lexware_admin');
|
|
19
|
+
});
|
|
20
|
+
it('labels the customer contact mapping as orboto_lexware_contact (ORB-2511)', () => {
|
|
21
|
+
const path = '/customers/6f1c2b1e-3d4a-4b5c-8d9e-0a1b2c3d4e5f/lexware-contact';
|
|
22
|
+
for (const method of ['GET', 'PUT', 'DELETE'])
|
|
23
|
+
expect(call(method, path), method).toBe('orboto_lexware_contact');
|
|
24
|
+
expect(call('GET', `${path}?name=Acme&page=1`)).toBe('orboto_lexware_contact');
|
|
25
|
+
});
|
|
26
|
+
it('labels the billing run preview, create, list and refresh as orboto_lexware_billing_run (ORB-2512)', () => {
|
|
27
|
+
const base = '/customers/6f1c2b1e-3d4a-4b5c-8d9e-0a1b2c3d4e5f/lexware-billing-runs';
|
|
28
|
+
for (const path of [`${base}/preview`, base, `${base}/7a1c2b1e-3d4a-4b5c-8d9e-0a1b2c3d4e5f/refresh`])
|
|
29
|
+
expect(call('POST', path), path).toBe('orboto_lexware_billing_run');
|
|
30
|
+
expect(call('GET', `${base}?limit=20`)).toBe('orboto_lexware_billing_run');
|
|
31
|
+
expect(call('GET', `${base}/summary?from=2026-10-01&to=2026-10-31`)).toBe('orboto_lexware_billing_run');
|
|
32
|
+
expect(call('GET', '/admin/integrations/lexware/billing-runs')).toBe('orboto_lexware_billing_run');
|
|
33
|
+
expect(call('DELETE', base)).toBe('orboto_api_call');
|
|
34
|
+
});
|
|
35
|
+
it('leaves the public receiver unlabelled', () => {
|
|
36
|
+
expect(call('POST', '/inbound/lexware/6f1c2b1e-3d4a-4b5c-8d9e-0a1b2c3d4e5f')).toBe('orboto_api_call');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -54,23 +54,38 @@ export function makeUpdateProjectHandler(client) {
|
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
/** ORB-2503 - a customer by UUID or by case-insensitive exact name; an ambiguous or unknown name is refused with the candidates. */
|
|
58
|
+
export async function resolveCustomerRef(client, ref) {
|
|
59
|
+
const wanted = ref.trim();
|
|
60
|
+
if (UUID_RE.test(wanted))
|
|
61
|
+
return wanted;
|
|
62
|
+
const rows = await client.get(`/customers?includeInactive=true&search=${encodeURIComponent(wanted)}`);
|
|
63
|
+
const exact = rows.filter((c) => c.name.trim().toLowerCase() === wanted.toLowerCase());
|
|
64
|
+
if (exact.length === 1)
|
|
65
|
+
return exact[0].id;
|
|
66
|
+
if (exact.length > 1)
|
|
67
|
+
throw new Error(`Customer name "${wanted}" is ambiguous: ${exact.map((c) => `${c.name} (${c.id})`).join(', ')}. Pass the id.`);
|
|
68
|
+
if (rows.length === 0)
|
|
69
|
+
throw new Error(`No customer named "${wanted}".`);
|
|
70
|
+
throw new Error(`No customer named exactly "${wanted}"; candidates: ${rows.map((c) => c.name).join(', ')}.`);
|
|
71
|
+
}
|
|
57
72
|
export const createProjectToolConfig = {
|
|
58
|
-
title: 'Create a
|
|
59
|
-
description: 'Create a project
|
|
73
|
+
title: 'Create a project',
|
|
74
|
+
description: 'Create a project (key auto-derived if omitted). Agents pass customer or internal. Needs project:create. Returns readiness with fix actions; its doc space is auto-created.',
|
|
60
75
|
inputSchema: z.object({
|
|
61
|
-
name: z.string().min(1).max(255)
|
|
62
|
-
key: z.string().min(2).max(10).regex(PROJECT_KEY_RE).optional()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
name: z.string().min(1).max(255),
|
|
77
|
+
key: z.string().min(2).max(10).regex(PROJECT_KEY_RE).optional(),
|
|
78
|
+
description: z.string().nullable().optional(),
|
|
79
|
+
customerId: z.string().regex(UUID_RE).nullable().optional(),
|
|
80
|
+
customer: z.string().min(1).optional().describe('Customer name or UUID.'),
|
|
81
|
+
internal: z.string().min(3).max(200).optional().describe('Reason for no customer.'),
|
|
67
82
|
language: z.enum(LOCALE_CODES).nullable().optional()
|
|
68
|
-
.describe('
|
|
83
|
+
.describe('Null inherits workspace.'),
|
|
69
84
|
}).shape,
|
|
70
85
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
71
86
|
};
|
|
72
87
|
export function makeCreateProjectHandler(client) {
|
|
73
|
-
return async ({ name, key, description, customerId, language }) => {
|
|
88
|
+
return async ({ name, key, description, customerId, customer, internal, language }) => {
|
|
74
89
|
const body = { name };
|
|
75
90
|
if (key !== undefined)
|
|
76
91
|
body.key = key;
|
|
@@ -78,6 +93,10 @@ export function makeCreateProjectHandler(client) {
|
|
|
78
93
|
body.description = description;
|
|
79
94
|
if (customerId !== undefined)
|
|
80
95
|
body.customerId = customerId;
|
|
96
|
+
if (customer !== undefined)
|
|
97
|
+
body.customerId = await resolveCustomerRef(client, customer);
|
|
98
|
+
if (internal !== undefined)
|
|
99
|
+
Object.assign(body, { internal: true, internalReason: internal });
|
|
81
100
|
if (language !== undefined)
|
|
82
101
|
body.language = language;
|
|
83
102
|
const created = await client.post('/projects', body);
|
|
@@ -96,6 +115,8 @@ export function makeCreateProjectHandler(client) {
|
|
|
96
115
|
description: created.description,
|
|
97
116
|
status: created.status,
|
|
98
117
|
language: created.language ?? null,
|
|
118
|
+
customerId: created.customerId ?? null,
|
|
119
|
+
internal: created.internal ?? false,
|
|
99
120
|
},
|
|
100
121
|
};
|
|
101
122
|
};
|
|
@@ -128,6 +128,32 @@ describe('orboto_create_project', () => {
|
|
|
128
128
|
customerId: '11111111-2222-3333-4444-555555555555',
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
|
+
it('ORB-2503 - resolves customer by case-insensitive exact name before the POST', async () => {
|
|
132
|
+
const calls = stub([
|
|
133
|
+
{ json: [{ id: 'c-2', name: 'Acme Labs' }, { id: 'c-1', name: 'ACME' }] },
|
|
134
|
+
{ json: { readiness, id: 'new4', key: 'AC', name: 'Acme app', description: null, status: 'active', customerId: 'c-1', internal: false } },
|
|
135
|
+
]);
|
|
136
|
+
const res = await makeCreateProjectHandler(client)({ name: 'Acme app', customer: 'acme' });
|
|
137
|
+
expect(calls[0].url).toBe('https://orboto.example.com/customers?includeInactive=true&search=acme');
|
|
138
|
+
expect(calls[1].body).toEqual({ name: 'Acme app', customerId: 'c-1' });
|
|
139
|
+
expect(res.structuredContent).toMatchObject({ customerId: 'c-1', internal: false });
|
|
140
|
+
});
|
|
141
|
+
it('ORB-2503 - refuses an ambiguous customer name with the candidates and never POSTs', async () => {
|
|
142
|
+
const calls = stub([{ json: [{ id: 'c-1', name: 'Acme' }, { id: 'c-2', name: 'acme' }] }]);
|
|
143
|
+
await expect(makeCreateProjectHandler(client)({ name: 'X', customer: 'ACME' }))
|
|
144
|
+
.rejects.toThrow(/ambiguous: Acme \(c-1\), acme \(c-2\)/);
|
|
145
|
+
expect(calls).toHaveLength(1);
|
|
146
|
+
});
|
|
147
|
+
it('ORB-2503 - refuses an unknown customer name and lists near matches', async () => {
|
|
148
|
+
stub([{ json: [{ id: 'c-1', name: 'Acme Labs' }] }]);
|
|
149
|
+
await expect(makeCreateProjectHandler(client)({ name: 'X', customer: 'Acme' }))
|
|
150
|
+
.rejects.toThrow(/candidates: Acme Labs/);
|
|
151
|
+
});
|
|
152
|
+
it('ORB-2503 - sends internal with its reason', async () => {
|
|
153
|
+
const calls = stub([{ json: { readiness, id: 'new5', key: 'OPS', name: 'Ops', description: null, status: 'active', customerId: null, internal: true } }]);
|
|
154
|
+
await makeCreateProjectHandler(client)({ name: 'Ops', internal: 'Our own infrastructure' });
|
|
155
|
+
expect(calls[0].body).toEqual({ name: 'Ops', internal: true, internalReason: 'Our own infrastructure' });
|
|
156
|
+
});
|
|
131
157
|
it('surfaces a 409 (duplicate key) as OrbotoApiError', async () => {
|
|
132
158
|
stub([
|
|
133
159
|
{ ok: false, status: 409, json: { error: 'project key already exists' } },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orboto/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.221.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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test:watch": "vitest"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
27
27
|
"zod": "^3.25.76"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|