@ziggs-ai/ziggs-mcp 0.1.28 → 0.1.30
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/tools.js +95 -8
- package/dist/trustTools.js +13 -3
- package/package.json +2 -2
- package/skills/ziggs/SKILL.md +1 -1
package/dist/tools.js
CHANGED
|
@@ -171,6 +171,49 @@ async function fetchDelegateAccess(creds) {
|
|
|
171
171
|
}
|
|
172
172
|
return body ? JSON.parse(body) : {};
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* ZIG-739 — the operator's full org membership (not just granted scopes, which
|
|
176
|
+
* is all ziggs_discover_context sees). Lets the delegate resolve an org name to
|
|
177
|
+
* an id and offer a pick-list instead of demanding a pasted org_... id.
|
|
178
|
+
*/
|
|
179
|
+
async function fetchMyOrgs(creds) {
|
|
180
|
+
const url = `${getBackendUrl()}/orgs/me`;
|
|
181
|
+
const res = await fetch(url, {
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${creds.operatorKey}`,
|
|
185
|
+
'X-Agent-Id': creds.agentId,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const body = await res.text().catch(() => '');
|
|
189
|
+
if (!res.ok) {
|
|
190
|
+
throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
|
|
191
|
+
}
|
|
192
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
193
|
+
return (parsed.orgs ?? []).map((o) => ({
|
|
194
|
+
orgId: o.orgId,
|
|
195
|
+
name: o.name,
|
|
196
|
+
kind: o.kind,
|
|
197
|
+
role: o.role,
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
|
|
202
|
+
* the operator's memberships. Exact id wins; otherwise case-insensitive name
|
|
203
|
+
* match. Ambiguous names return the candidates rather than guessing.
|
|
204
|
+
*/
|
|
205
|
+
function resolveOrgSelector(orgs, selector) {
|
|
206
|
+
const byId = orgs.find((o) => o.orgId === selector);
|
|
207
|
+
if (byId)
|
|
208
|
+
return { status: 'ok', orgId: byId.orgId };
|
|
209
|
+
const needle = selector.toLowerCase();
|
|
210
|
+
const byName = orgs.filter((o) => o.name.toLowerCase() === needle);
|
|
211
|
+
if (byName.length === 1)
|
|
212
|
+
return { status: 'ok', orgId: byName[0].orgId };
|
|
213
|
+
if (byName.length > 1)
|
|
214
|
+
return { status: 'ambiguous', matches: byName };
|
|
215
|
+
return { status: 'not-found' };
|
|
216
|
+
}
|
|
174
217
|
/**
|
|
175
218
|
* ZIG-641 — cross-connection discovery: every connection this agent holds a
|
|
176
219
|
* grant for, joined with provider + health, so ziggs_connection_proxy's
|
|
@@ -345,19 +388,40 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
345
388
|
: 'Next: ziggs_inbox or ziggs_pending_decisions at session start.',
|
|
346
389
|
});
|
|
347
390
|
});
|
|
348
|
-
server.tool('
|
|
349
|
-
|
|
391
|
+
server.tool('ziggs_list_my_orgs', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike ziggs_discover_context (granted scopes only), this is your full membership, so you can resolve an org name to an id and offer the human a pick-list before ziggs_switch_org.', {}, READ_ONLY, async () => {
|
|
392
|
+
try {
|
|
393
|
+
const orgs = await fetchMyOrgs(creds);
|
|
394
|
+
return textResult({ count: orgs.length, orgs });
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
return toolError(e.message);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
server.tool('ziggs_switch_org', 'Switch which org this MCP OAuth session acts in without reconnecting. Existing Bearer unchanged; runtime org flips server-side. Requires confirm=true (party-identity change). Accepts an org id (org_...) OR an org name — names resolve against your memberships (see ziggs_list_my_orgs); an ambiguous name returns the candidates. Call ziggs_auth_status after to verify actingOrgId.', {
|
|
401
|
+
org: z
|
|
402
|
+
.string()
|
|
403
|
+
.describe('Org id (org_...) or org name to act in — must be one you belong to'),
|
|
350
404
|
confirm: z
|
|
351
405
|
.literal(true)
|
|
352
406
|
.describe('Must be true — confirms the human approved switching acting org'),
|
|
353
|
-
}, WRITE, async ({
|
|
407
|
+
}, WRITE, async ({ org, confirm }) => {
|
|
354
408
|
if (confirm !== true) {
|
|
355
409
|
return toolError('confirm must be true — org switch changes which workspace you act in');
|
|
356
410
|
}
|
|
411
|
+
const selector = org.trim();
|
|
357
412
|
try {
|
|
358
|
-
const
|
|
413
|
+
const orgs = await fetchMyOrgs(creds);
|
|
414
|
+
const resolved = resolveOrgSelector(orgs, selector);
|
|
415
|
+
if (resolved.status === 'ambiguous') {
|
|
416
|
+
return toolError(`"${selector}" matches ${resolved.matches.length} orgs — switch by exact orgId. Candidates: ${JSON.stringify(resolved.matches)}`);
|
|
417
|
+
}
|
|
418
|
+
if (resolved.status === 'not-found') {
|
|
419
|
+
return toolError(`No org matches "${selector}". You belong to: ${JSON.stringify(orgs)}`);
|
|
420
|
+
}
|
|
421
|
+
const result = await rebindDelegateOrg(creds, resolved.orgId);
|
|
359
422
|
return textResult({
|
|
360
423
|
ok: true,
|
|
424
|
+
resolvedOrgId: resolved.orgId,
|
|
361
425
|
...result,
|
|
362
426
|
note: result.unchanged
|
|
363
427
|
? 'Already acting in this org — no changes made.'
|
|
@@ -475,13 +539,22 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
475
539
|
.string()
|
|
476
540
|
.optional()
|
|
477
541
|
.describe('Usually "message" for user-visible chat'),
|
|
478
|
-
|
|
542
|
+
idempotencyKey: z
|
|
543
|
+
.string()
|
|
544
|
+
.optional()
|
|
545
|
+
.describe('Retry-safe key. Reuse the SAME key when re-sending the SAME logical message (e.g. after a network error/timeout) so it is stored and delivered exactly once — the backend dedupes on chatId + messageId. Use a fresh key (or omit) for a genuinely new message.'),
|
|
546
|
+
}, WRITE, async ({ chatId, receiverId, text, entryType, idempotencyKey }) => {
|
|
479
547
|
try {
|
|
480
548
|
const result = await sendChatMessage({
|
|
481
549
|
chatId,
|
|
482
550
|
receiverId,
|
|
483
551
|
text,
|
|
484
|
-
messageId
|
|
552
|
+
// A stable idempotencyKey maps to a stable messageId so a retried
|
|
553
|
+
// logical send collapses to one row/one delivery (ZIG-718 backend
|
|
554
|
+
// dedupe); otherwise each call is a distinct message.
|
|
555
|
+
messageId: idempotencyKey
|
|
556
|
+
? `mcp_idem_${idempotencyKey}`
|
|
557
|
+
: `mcp_${randomUUID()}`,
|
|
485
558
|
entryType: entryType ?? 'message',
|
|
486
559
|
contentType: 'text',
|
|
487
560
|
}, creds);
|
|
@@ -656,6 +729,16 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
656
729
|
return toolError(e.message);
|
|
657
730
|
}
|
|
658
731
|
});
|
|
732
|
+
server.tool('ziggs_discover_grantable', 'See what context EXISTS in your orgs that you CANNOT read yet — so you can ask for it instead of failing blind. Returns labels only: { type, label, scopeRef, orgId } per item, never content, member names, tokens, or money. Bounded to orgs you have an active agreement in. To act on one, ask your human to grant it, or (if you hold a broader grant of your own) delegate via ziggs_delegate_grant using the scopeRef. Use ziggs_discover_context for what you already hold; this is what you lack.', {}, READ_ONLY, async () => {
|
|
733
|
+
try {
|
|
734
|
+
const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
|
|
735
|
+
const items = await client.discoverGrantable();
|
|
736
|
+
return textResult({ count: items.length, items });
|
|
737
|
+
}
|
|
738
|
+
catch (e) {
|
|
739
|
+
return toolError(e.message);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
659
742
|
server.tool('ziggs_read_context', 'Read the contents of a scope you already hold: messages | artifacts | agreements | tasks (the type param), under via=chat:<id>, agreement:<id>, or task:<id>. Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a `readPlan` with the next page and/or forward-delta call pre-filled (after=this page\'s latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist (your chats / tasks / agreements / grants / links), use the ziggs_list_* tools.', {
|
|
660
743
|
type: contextReadTypeSchema.describe('Resource type to read'),
|
|
661
744
|
via: z
|
|
@@ -746,9 +829,13 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
746
829
|
.string()
|
|
747
830
|
.optional()
|
|
748
831
|
.describe('Agent or user ID to explicitly assign this task to — must be a party to the agreement'),
|
|
749
|
-
|
|
832
|
+
inputArtifactIds: z
|
|
833
|
+
.array(z.string())
|
|
834
|
+
.optional()
|
|
835
|
+
.describe('Artifact ids this task consumes as structured inputs (ZIG-734) — pass prior-step output handles without embedding them in description'),
|
|
836
|
+
}, WRITE, async ({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds }) => {
|
|
750
837
|
try {
|
|
751
|
-
const task = await createTask({ agreementId, description, parentTaskId, assigneeId }, creds);
|
|
838
|
+
const task = await createTask({ agreementId, description, parentTaskId, assigneeId, inputArtifactIds }, creds);
|
|
752
839
|
return textResult({ ok: true, task });
|
|
753
840
|
}
|
|
754
841
|
catch (e) {
|
package/dist/trustTools.js
CHANGED
|
@@ -110,7 +110,7 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
110
110
|
return toolError(e.message);
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
|
-
server.tool('ziggs_delegate_grant', 'Delegate a narrower child grant from one you hold (POST /context/grants/:id/delegate). Delegation only narrows scope/expiry/temporal — never broadens.', {
|
|
113
|
+
server.tool('ziggs_delegate_grant', 'Delegate a narrower child grant from one you hold (POST /context/grants/:id/delegate). Delegation only narrows scope/expiry/temporal — never broadens. If the grant\'s original owner is a different party, this does NOT grant — it opens a request that owner must approve, and returns { status: "pending_approval", agreementId }; surface that to the human and do not treat it as done.', {
|
|
114
114
|
parentGrantId: z.string(),
|
|
115
115
|
holderId: z.string().describe('Agent receiving the delegated grant'),
|
|
116
116
|
scopeKind: grantScopeKindSchema,
|
|
@@ -124,13 +124,23 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
124
124
|
}, WRITE, async ({ parentGrantId, holderId, scopeKind, scopeId, temporal, expiresAt, watermarkAt, }) => {
|
|
125
125
|
try {
|
|
126
126
|
const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
|
|
127
|
-
const
|
|
127
|
+
const result = await client.delegateGrant(parentGrantId, {
|
|
128
128
|
holderId,
|
|
129
129
|
scope: { kind: scopeKind, id: scopeId },
|
|
130
130
|
temporal,
|
|
131
131
|
expiresAt,
|
|
132
132
|
watermarkAt,
|
|
133
133
|
});
|
|
134
|
+
if (result.status === 'pending_approval') {
|
|
135
|
+
return textResult({
|
|
136
|
+
status: 'pending_approval',
|
|
137
|
+
message: "This grant's original owner must approve sharing it. A request was opened for them — surface it to the human; nothing is granted yet.",
|
|
138
|
+
parentGrantId,
|
|
139
|
+
agreementId: result.agreementId,
|
|
140
|
+
ownerId: result.ownerId,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const grant = result.grant;
|
|
134
144
|
return textResult({
|
|
135
145
|
status: 'delegated',
|
|
136
146
|
parentGrantId,
|
|
@@ -265,7 +275,7 @@ export function registerTrustTools(server, creds, cfg) {
|
|
|
265
275
|
return toolError(e.message);
|
|
266
276
|
}
|
|
267
277
|
});
|
|
268
|
-
server.tool('ziggs_revoke_grant', 'Revoke a context grant and its descendants (DELETE /context/grants/:id).
|
|
278
|
+
server.tool('ziggs_revoke_grant', 'Revoke a context grant and its descendants (DELETE /context/grants/:id). You can revoke (narrow) any grant you hold — this needs no special scope. Revoking a grant you issued or whose scope you own, but do not hold, requires context:admin.', {
|
|
269
279
|
grantId: z.string(),
|
|
270
280
|
}, DESTRUCTIVE, async ({ grantId }) => {
|
|
271
281
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/ziggs-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "MCP server for Claude Code, Cursor, and other MCP hosts — act as your Ziggs delegate agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
-
"@ziggs-ai/api-client": "^0.1.
|
|
39
|
+
"@ziggs-ai/api-client": "^0.1.25",
|
|
40
40
|
"dotenv": "^16.6.1",
|
|
41
41
|
"zod": "^3.24.2"
|
|
42
42
|
},
|
package/skills/ziggs/SKILL.md
CHANGED
|
@@ -39,7 +39,7 @@ The sections below elaborate this protocol with tools, examples, and edge cases.
|
|
|
39
39
|
## Session start — pending decisions + inbox
|
|
40
40
|
|
|
41
41
|
1. Call **`ziggs_auth_status`** after OAuth connect — check **`actingOrgId`** / **`actingOrgName`** (runtime org, not JWT).
|
|
42
|
-
2. To switch org without reconnecting: **`ziggs_switch_org`**
|
|
42
|
+
2. To switch org without reconnecting: **`ziggs_switch_org`** (`org` = an org id **or** a name, `confirm: true`), then re-check **`ziggs_auth_status`**. Don't make the human paste an `org_...` id — call **`ziggs_list_my_orgs`** to resolve a name and offer a pick-list; an ambiguous name returns the candidates.
|
|
43
43
|
3. Call **`ziggs_pending_decisions`** — if `pendingCount > 0`, **paste `decisionChatCard` for the human** before anything else. Wait for explicit approve/reject; then `ziggs_respond_to_agreement`.
|
|
44
44
|
3. Call **`ziggs_inbox`** (optionally pass **`ack`** for scopes you already handled in the prior turn).
|
|
45
45
|
4. Read the envelope: scope news counts, `humanAttention`, and **`decisionChatCard`** when present.
|