@ziggs-ai/ziggs-mcp 0.1.31 → 0.1.34

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/README.md CHANGED
@@ -194,7 +194,7 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
194
194
  | Tool | Maps to |
195
195
  |------|---------|
196
196
  | `ziggs_inbox` | `GET /inbox` + `POST /inbox/ack` |
197
- | `ziggs_discover_context` | `GET /context/discovery` |
197
+ | `ziggs_discover_context` | `GET /grants` (context scopes) |
198
198
  | `ziggs_read_context` | `GET /context/read/:type` |
199
199
  | `ziggs_record_artifact` | `POST /artifacts` |
200
200
  | `ziggs_search_agents` | Agent search |
@@ -206,7 +206,7 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
206
206
  | `ziggs_revoke_link` | `DELETE /agreements/:agreementId` (see also `ziggs_revoke_agreement`) |
207
207
  | `ziggs_revoke_agreement` | `DELETE /agreements/:id` — any agreement (hire/service/quest/link) |
208
208
  | `ziggs_smoke_impersonation` | [Internal/debug] connectivity check — only when `ZIGGS_MCP_DEBUG=1`; not part of normal delegate workflow |
209
- | `ziggs_get_scope` | `GET /scope?via=` |
209
+ | `ziggs_context_snapshot` | `GET /context/snapshot?via=chat:` — one-shot chat orientation (history + agreements + roster), grant-fenced |
210
210
  | `ziggs_list_my_agreements` | `GET /agreements?scope=mine` |
211
211
  | `ziggs_get_agreement` | `GET /agreements/:id` |
212
212
  | `ziggs_list_chats` | `GET /chats/mine` |
@@ -1,4 +1,4 @@
1
- import type { ContextReachDescriptor, ContextReadType, InboxAckResult, InboxEnvelope, Task } from '@ziggs-ai/api-client';
1
+ import type { GrantView, ContextReadType, InboxAckResult, InboxEnvelope, Task } from '@ziggs-ai/api-client';
2
2
  /**
3
3
  * ZIG-634 (Step 1): a pre-filled next call. The agent can run it verbatim
4
4
  * instead of assembling args from the ids scattered through the response.
@@ -64,7 +64,7 @@ export interface ScopeGrantTag {
64
64
  * list is already the caller's non-expired grants (holderId == principalId),
65
65
  * so this is grant metadata the caller already holds — no protected content.
66
66
  */
67
- export declare function indexReachByScope(reach: ContextReachDescriptor[]): Map<string, ScopeGrantTag>;
67
+ export declare function indexReachByScope(reach: GrantView[]): Map<string, ScopeGrantTag>;
68
68
  /**
69
69
  * Put humanAttention first so MCP hosts surface it before counts (ZIG-482),
70
70
  * and append readPlan last so each inbox call self-narrates the follow-up
@@ -73,4 +73,4 @@ export declare function indexReachByScope(reach: ContextReachDescriptor[]): Map<
73
73
  * When `reach` (the caller's own live grants) is passed, each scope is tagged
74
74
  * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
75
75
  */
76
- export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: ContextReachDescriptor[], activeTasksError?: string): Record<string, unknown>;
76
+ export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string): Record<string, unknown>;
@@ -1,3 +1,4 @@
1
+ import { grantCaveat } from '@ziggs-ai/api-client';
1
2
  import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
2
3
  /** Keep the plan bounded; the full scopes array still carries everything. */
3
4
  const MAX_READ_PLAN = 12;
@@ -147,13 +148,14 @@ export function buildReadContextReadPlan(page, type, via, presentedGrantId) {
147
148
  }
148
149
  return plan;
149
150
  }
150
- function toScopeGrantTag(d) {
151
+ function toScopeGrantTag(g) {
151
152
  return {
152
- grantId: d.grantId,
153
- temporal: d.temporal,
154
- watermarkAt: d.watermarkAt,
155
- expiresAt: d.expiresAt,
156
- parentGrantId: d.parentGrantId,
153
+ grantId: g.grantId,
154
+ // Context grants carry temporal/watermark as caveats (ZIG-646).
155
+ temporal: grantCaveat(g, 'temporal') ?? 'from-now',
156
+ watermarkAt: grantCaveat(g, 'watermark_at') ?? '',
157
+ expiresAt: g.expiresAt,
158
+ parentGrantId: g.parentGrantId,
157
159
  };
158
160
  }
159
161
  /**
@@ -173,11 +175,11 @@ function isBroaderGrant(a, b) {
173
175
  */
174
176
  export function indexReachByScope(reach) {
175
177
  const byScope = new Map();
176
- for (const d of reach) {
177
- if (!d?.scope)
178
+ for (const g of reach) {
179
+ if (!g?.scope)
178
180
  continue;
179
- const key = `${d.scope.kind}:${d.scope.id}`;
180
- const tag = toScopeGrantTag(d);
181
+ const key = `${g.scope.kind}:${g.scope.id}`;
182
+ const tag = toScopeGrantTag(g);
181
183
  const existing = byScope.get(key);
182
184
  if (!existing || isBroaderGrant(tag, existing))
183
185
  byScope.set(key, tag);
package/dist/toolError.js CHANGED
@@ -13,7 +13,7 @@ const HTTP_STATUS = /(?:^|\s)([1-5]\d{2})(?=\s|$)/;
13
13
  const SCOPE_DENIED_HINT = 'You are not authorized for this scope. To get access: ask the counterparty ' +
14
14
  'to issue you a context grant (they run ziggs_issue_grant), or request a ' +
15
15
  'bilateral link first (ziggs_request_link). Check what you can already ' +
16
- 'reach with ziggs_discover_context / ziggs_get_scope.';
16
+ 'reach with ziggs_discover_context / ziggs_context_snapshot.';
17
17
  function codeForStatus(status) {
18
18
  if (status === 401)
19
19
  return 'NOT_AUTHENTICATED';
package/dist/tools.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
- import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDirectTo, proposeBroadcast, publishOffer, respondToAgreement, revokeAgreement, ScopeClient, sendChatMessage, ContextDiscoveryClient, ContextReadClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getBackendUrl, } from '@ziggs-ai/api-client';
3
+ import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDirectTo, proposeBroadcast, publishOffer, claimOffer, provisionRelayWorkers, respondToAgreement, revokeAgreement, sendChatMessage, ContextDiscoveryClient, ContextReadClient, GrantsClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getBackendUrl, } from '@ziggs-ai/api-client';
4
4
  import { decodeOperatorKeyClaims } from './operatorKey.js';
5
5
  import { registerTrustTools } from './trustTools.js';
6
6
  import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
7
7
  import { filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
8
8
  import { PROTOCOL } from './protocol/delegateProtocol.js';
9
+ const RELAY_COORDINATOR_AGENT_ID = 'relay-coordinator';
10
+ function buildRelayCoordinatorTaskBody(opts) {
11
+ const title = opts.title?.trim() || 'Relay coordinator job';
12
+ return {
13
+ agreementId: opts.hireAgreementId,
14
+ assigneeId: RELAY_COORDINATOR_AGENT_ID,
15
+ description: `${title}\nrelay:v1\n${JSON.stringify(opts.payload)}`,
16
+ };
17
+ }
9
18
  import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
10
19
  import { toolError } from './toolError.js';
11
20
  // ZIG-557: the protocol sentences (loop / ack / humanAttention) are sourced
@@ -50,7 +59,6 @@ function textResult(data) {
50
59
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
51
60
  };
52
61
  }
53
- const scopeKindSchema = z.enum(['chat', 'agreement', 'task', 'counterparty']);
54
62
  const contextReadTypeSchema = z.enum([
55
63
  'messages',
56
64
  'artifacts',
@@ -138,24 +146,6 @@ async function proxyConnection(creds, input) {
138
146
  const result = parsed?.['result'];
139
147
  return result ?? parsed;
140
148
  }
141
- /** ZIG-640 — server-side org rebind; existing OAuth Bearer keeps working. */
142
- async function rebindDelegateOrg(creds, orgId) {
143
- const url = `${getBackendUrl()}/agents/claude-delegate/rebind`;
144
- const res = await fetch(url, {
145
- method: 'POST',
146
- headers: {
147
- 'content-type': 'application/json',
148
- Authorization: `Bearer ${creds.operatorKey}`,
149
- 'X-Agent-Id': creds.agentId,
150
- },
151
- body: JSON.stringify({ orgId }),
152
- });
153
- const body = await res.text().catch(() => '');
154
- if (!res.ok) {
155
- throw new Error(`POST /agents/claude-delegate/rebind ${res.status} ${body.slice(0, 200)}`);
156
- }
157
- return body ? JSON.parse(body) : {};
158
- }
159
149
  /** ZIG-640 — runtime acting org from server (self-hire / agent row). */
160
150
  async function fetchDelegateAccess(creds) {
161
151
  const url = `${getBackendUrl()}/agents/claude-delegate/access`;
@@ -216,35 +206,32 @@ function resolveOrgSelector(orgs, selector) {
216
206
  return { status: 'not-found' };
217
207
  }
218
208
  /**
219
- * ZIG-641 — cross-connection discovery: every connection this agent holds a
220
- * grant for, joined with provider + health, so ziggs_connection_proxy's
221
- * connectionId/grantId no longer has to arrive out of band. Same leak guard
222
- * doesn't apply here (the backend never returns credential material for this
223
- * route), but the response is still scanned defensively for consistency
224
- * with proxyConnection.
209
+ * ZIG-641 / ZIG-648 — cross-connection discovery over the unified GET /grants:
210
+ * every connection grant this agent holds, grouped by connection so
211
+ * ziggs_connection_proxy's connectionId/grantId no longer has to arrive out of
212
+ * band. `provider` comes from the grant's resolved scope label. The response is
213
+ * scanned defensively for leaked secrets, as proxyConnection does.
225
214
  */
226
215
  async function listConnectionsForHolder(creds) {
227
- const url = `${getBackendUrl()}/connections/grants`;
228
- const res = await fetch(url, {
229
- method: 'GET',
230
- headers: {
231
- Authorization: `Bearer ${creds.operatorKey}`,
232
- 'X-Agent-Id': creds.agentId,
233
- },
216
+ const client = new GrantsClient(creds.operatorKey, creds.agentId);
217
+ // All pages of the agent's live connection grants (not just the first page).
218
+ const items = await client.listAllGrants({
219
+ scopeKind: 'connection',
220
+ health: 'active',
234
221
  });
235
- const body = await res.text().catch(() => '');
236
- if (!res.ok) {
237
- throw new Error(`GET /connections/grants ${res.status} ${body.slice(0, 200)}`);
238
- }
239
- assertNoLeakedSecret(body);
240
- let parsed = body;
241
- try {
242
- parsed = body ? JSON.parse(body) : null;
243
- }
244
- catch {
245
- parsed = body;
222
+ const byConnection = new Map();
223
+ for (const g of items) {
224
+ const connectionId = g.scope.id;
225
+ let group = byConnection.get(connectionId);
226
+ if (!group) {
227
+ group = { connectionId, provider: g.scope.label ?? null, grants: [] };
228
+ byConnection.set(connectionId, group);
229
+ }
230
+ group.grants.push(g);
246
231
  }
247
- return parsed?.['connections'] ?? [];
232
+ const result = [...byConnection.values()];
233
+ assertNoLeakedSecret(JSON.stringify(result));
234
+ return result;
248
235
  }
249
236
  /**
250
237
  * ZIG-686 — agent-initiated MCP connection request: ask the principal to
@@ -370,9 +357,9 @@ export function registerZiggsTools(server, creds, cfg) {
370
357
  ? 'Not connected — complete MCP OAuth consent first, then re-check actingOrgId here.'
371
358
  : actingOrgName
372
359
  ? `This MCP session acts in **${actingOrgName}** (${actingOrgId}). Runtime org comes from server-side delegate + self-hire — not the OAuth JWT.`
373
- : 'Connected but could not resolve acting org name — call ziggs_switch_org or reconnect OAuth.',
360
+ : 'Connected but could not resolve acting org name — reconnect MCP OAuth if needed.',
374
361
  switchOrgHint: switchOrgHint ??
375
- 'To change org without reconnecting: call ziggs_switch_org with confirm=true.',
362
+ 'Org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screen to act elsewhere.',
376
363
  apiBase: getBackendUrl(),
377
364
  webAppOrigin: webOrigin,
378
365
  docs: 'https://ziggsai.com/docs',
@@ -382,7 +369,7 @@ export function registerZiggsTools(server, creds, cfg) {
382
369
  : 'Next: ziggs_inbox or ziggs_pending_decisions at session start.',
383
370
  });
384
371
  });
385
- 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 () => {
372
+ 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 useful before OAuth reconnect when the human wants to pick a target org.', {}, READ_ONLY, async () => {
386
373
  try {
387
374
  const orgs = await fetchMyOrgs(creds);
388
375
  return textResult({ count: orgs.length, orgs });
@@ -391,40 +378,24 @@ export function registerZiggsTools(server, creds, cfg) {
391
378
  return toolError(e.message);
392
379
  }
393
380
  });
394
- 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.', {
381
+ server.tool('ziggs_switch_org', 'Deprecated: org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screenthis tool no longer switches org server-side.', {
395
382
  org: z
396
383
  .string()
397
- .describe('Org id (org_...) or org name to act in — must be one you belong to'),
398
- confirm: z
399
- .literal(true)
400
- .describe('Must be true confirms the human approved switching acting org'),
401
- }, WRITE, async ({ org, confirm }) => {
402
- if (confirm !== true) {
403
- return toolError('confirm must be true — org switch changes which workspace you act in');
404
- }
405
- const selector = org.trim();
406
- try {
407
- const orgs = await fetchMyOrgs(creds);
408
- const resolved = resolveOrgSelector(orgs, selector);
409
- if (resolved.status === 'ambiguous') {
410
- return toolError(`"${selector}" matches ${resolved.matches.length} orgs — switch by exact orgId. Candidates: ${JSON.stringify(resolved.matches)}`);
411
- }
412
- if (resolved.status === 'not-found') {
413
- return toolError(`No org matches "${selector}". You belong to: ${JSON.stringify(orgs)}`);
414
- }
415
- const result = await rebindDelegateOrg(creds, resolved.orgId);
416
- return textResult({
417
- ok: true,
418
- resolvedOrgId: resolved.orgId,
419
- ...result,
420
- note: result.unchanged
421
- ? 'Already acting in this org — no changes made.'
422
- : 'Org rebind complete. Existing OAuth token unchanged; call ziggs_auth_status to verify actingOrgId.',
423
- });
424
- }
425
- catch (e) {
426
- return toolError(e.message);
427
- }
384
+ .optional()
385
+ .describe('Ignored — kept for backward compatibility with older prompts'),
386
+ confirm: z.literal(true).optional(),
387
+ }, WRITE, async () => {
388
+ return textResult({
389
+ ok: false,
390
+ deprecated: true,
391
+ message: 'ziggs_switch_org no longer switches org. Org is bound at MCP OAuth consent. Reconnect the Ziggs MCP server in your client, pick the desired org on the consent screen, then call ziggs_auth_status to verify actingOrgId.',
392
+ reconnectSteps: [
393
+ 'Remove or disable the Ziggs MCP connection in your client',
394
+ 'Re-add https://mcp.ziggsai.com/mcp (or your .mcp.json entry)',
395
+ 'On the Ziggs consent screen, select the org you want',
396
+ 'Call ziggs_auth_status and confirm actingOrgId',
397
+ ],
398
+ });
428
399
  });
429
400
  server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
430
401
  try {
@@ -444,21 +415,21 @@ export function registerZiggsTools(server, creds, cfg) {
444
415
  }
445
416
  });
446
417
  if (cfg.debugTools) {
447
- server.tool('ziggs_smoke_impersonation', '[Internal/debug] Connectivity check for the operator-key impersonation path — lists agreements and resolves scope from the first chat. Not part of normal delegate workflow; use ziggs_list_my_agreements / ziggs_get_scope instead.', {}, READ_ONLY, async () => {
418
+ server.tool('ziggs_smoke_impersonation', '[Internal/debug] Connectivity check for the operator-key impersonation path — lists agreements and snapshots the first chat. Not part of normal delegate workflow; use ziggs_list_my_agreements / ziggs_context_snapshot instead.', {}, READ_ONLY, async () => {
448
419
  try {
449
420
  const agreements = await getMyAgreements({}, creds);
450
421
  const chats = await listMyChats(creds);
451
- let scope = null;
422
+ let snapshot = null;
452
423
  const firstChatId = chats[0]?.chatId;
453
424
  if (firstChatId) {
454
- const client = new ScopeClient(creds.operatorKey, creds.agentId);
455
- scope = await client.get('chat', firstChatId);
425
+ const client = new ContextReadClient(creds.operatorKey, creds.agentId);
426
+ snapshot = await client.snapshot(firstChatId, { maxMessages: 5 });
456
427
  }
457
428
  return textResult({
458
429
  ok: true,
459
430
  agreementsCount: agreements.length,
460
431
  chatsCount: chats.length,
461
- scope,
432
+ snapshot,
462
433
  });
463
434
  }
464
435
  catch (e) {
@@ -466,13 +437,20 @@ export function registerZiggsTools(server, creds, cfg) {
466
437
  }
467
438
  });
468
439
  }
469
- server.tool('ziggs_get_scope', 'Resolve the access graph for the delegate agent from a chat, agreement, task, or counterparty entry point.', {
470
- scopeKind: scopeKindSchema.describe('Entry kind'),
471
- scopeId: z.string().describe('Entry id'),
472
- }, READ_ONLY, async ({ scopeKind, scopeId }) => {
440
+ server.tool('ziggs_context_snapshot', 'One-shot orientation for a chat: history, agreements (with which party is you), and the roster of agents/users — grant-fenced. Use when entering a chat you have not read yet; follow up with ziggs_read_context forward deltas from the returned latestSequence.', {
441
+ chatId: z.string().describe('Chat id to snapshot'),
442
+ maxMessages: z.number().optional().describe('Optional message history cap'),
443
+ contextGrantId: z
444
+ .string()
445
+ .optional()
446
+ .describe('Optional grant id when reading under a context grant'),
447
+ }, READ_ONLY, async ({ chatId, maxMessages, contextGrantId }) => {
473
448
  try {
474
- const client = new ScopeClient(creds.operatorKey, creds.agentId);
475
- const result = await client.get(scopeKind, scopeId);
449
+ const client = new ContextReadClient(creds.operatorKey, creds.agentId);
450
+ const result = await client.snapshot(chatId, {
451
+ maxMessages,
452
+ contextGrantId,
453
+ });
476
454
  return textResult(result);
477
455
  }
478
456
  catch (e) {
@@ -560,24 +538,26 @@ export function registerZiggsTools(server, creds, cfg) {
560
538
  return toolError(e.message);
561
539
  }
562
540
  });
563
- server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty in a chat, as the payer-side delegate: the counterparty (proposedTo) does the work, your side pays. engagementKind "service" (default) = one deliverable; "hire" = an ongoing engagement (same kinds ziggs_publish_offer accepts). price is recorded on the agreement but does not itself trigger a transfer — V1 has no real payment rail yet.', {
541
+ server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side there is no payer input. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays). Set providerId to your own agent id to offer (you work, proposedTo pays). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. price is recorded on the agreement but does not itself trigger a transfer.', {
564
542
  proposedTo: z.string(),
565
543
  chatId: z.string(),
566
544
  description: z.string(),
545
+ providerId: z
546
+ .string()
547
+ .optional()
548
+ .describe('Who does the work. Omitted = proposedTo (commission). Your agent id = offer. Another agent id = broker/matchmaking.'),
567
549
  price: z.number().optional().describe('Optional; does not trigger transfer by itself'),
568
550
  engagementKind: z
569
551
  .enum(['hire', 'service'])
570
552
  .optional()
571
553
  .describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
572
- }, WRITE, async ({ proposedTo, chatId, description, price, engagementKind }) => {
554
+ }, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind }) => {
573
555
  try {
574
556
  const agreement = await proposeDirectTo({
575
557
  proposedTo,
576
558
  chatId,
577
559
  description,
578
- // This tool is always payer-side: the recipient is the provider,
579
- // and the payer is derived server-side as the creator's side.
580
- providerId: proposedTo,
560
+ providerId: providerId?.trim() || proposedTo,
581
561
  price,
582
562
  engagementKind: engagementKind ?? 'service',
583
563
  }, creds);
@@ -637,6 +617,70 @@ export function registerZiggsTools(server, creds, cfg) {
637
617
  return toolError(e.message);
638
618
  }
639
619
  });
620
+ server.tool('ziggs_claim_offer', 'Claim a published standing offer (POST /marketplace/offers/claim). Use for relay worker provisioning when the worker has a marketplace offer — no worker-side approval needed.', {
621
+ agreementId: z.string().describe('Open offer agreementId to claim'),
622
+ }, WRITE, async ({ agreementId }) => {
623
+ try {
624
+ const offer = await claimOffer(agreementId, creds);
625
+ return textResult({ offer });
626
+ }
627
+ catch (e) {
628
+ return toolError(e.message);
629
+ }
630
+ });
631
+ server.tool('ziggs_provision_relay_workers', 'Initiator path: provision per-step worker agreements before relay kickoff. Reuses active delegations under the hire, claims standing offers when available, otherwise proposes delegations (worker must approve — never impersonated). Returns relay:v1 payload and POST /tasks body when all steps are active.', {
632
+ hireAgreementId: z.string(),
633
+ chatId: z
634
+ .string()
635
+ .optional()
636
+ .describe('Required when a step has no standing offer and needs delegation under the hire'),
637
+ inputArtifactIds: z.array(z.string()).optional(),
638
+ steps: z.array(z.object({
639
+ stepId: z.string(),
640
+ order: z.number(),
641
+ assigneeId: z.string(),
642
+ description: z.string(),
643
+ offerAgreementId: z
644
+ .string()
645
+ .optional()
646
+ .describe('Explicit open offer to claim for this worker'),
647
+ })),
648
+ kickoff: z
649
+ .boolean()
650
+ .optional()
651
+ .describe('When true and readyForKickoff, also POST /tasks on the hire for relay-coordinator'),
652
+ }, WRITE, async ({ hireAgreementId, chatId, inputArtifactIds, steps, kickoff }) => {
653
+ try {
654
+ const result = await provisionRelayWorkers({
655
+ creds,
656
+ hireAgreementId,
657
+ chatId,
658
+ inputArtifactIds,
659
+ steps,
660
+ });
661
+ const relayTaskBody = buildRelayCoordinatorTaskBody({
662
+ hireAgreementId,
663
+ payload: result.payload,
664
+ });
665
+ let task;
666
+ if (kickoff && result.readyForKickoff) {
667
+ task = await createTask(relayTaskBody, creds);
668
+ }
669
+ return textResult({
670
+ ...result,
671
+ relayTaskBody,
672
+ task,
673
+ nextSteps: result.readyForKickoff
674
+ ? kickoff && task
675
+ ? 'Relay coordinator task created — watch Execution for step progress.'
676
+ : 'All worker agreements active — POST relayTaskBody via createTask or set kickoff=true.'
677
+ : `Worker approval pending on: ${result.pendingApprovals.join(', ')}. Call ziggs_respond_to_agreement after workers approve, then re-run with kickoff=true.`,
678
+ });
679
+ }
680
+ catch (e) {
681
+ return toolError(e.message);
682
+ }
683
+ });
640
684
  server.tool('ziggs_respond_to_agreement', 'Approve or reject a pending agreement. Uses PUT /approvals/:partyId or POST /claim for an open broadcast (public or org-scoped; org-scoped quests are claimable only by members of the agreement\'s org).', {
641
685
  agreementId: z.string(),
642
686
  action: z.enum(['approve', 'reject']),
@@ -689,13 +733,18 @@ export function registerZiggsTools(server, creds, cfg) {
689
733
  activeTasksError = e.message;
690
734
  }
691
735
  // ZIG-635: tag each scope with the covering grant from the caller's own
692
- // live reach descriptors. Best-effort — the inbox still works untagged.
736
+ // held context grants. Best-effort — the inbox still works untagged.
737
+ // All pages of live grants only (GET /grants also returns expired/revoked
738
+ // and paginates), so a scope isn't left untagged behind the first page.
693
739
  let reach = [];
694
740
  try {
695
- reach = await new ContextDiscoveryClient(creds.operatorKey, creds.agentId).discover();
741
+ reach = await new GrantsClient(creds.operatorKey, creds.agentId).listAllGrants({
742
+ scopeKind: ['chat', 'agreement', 'org'],
743
+ health: 'active',
744
+ });
696
745
  }
697
746
  catch {
698
- // omit grant tags when discovery fails
747
+ // omit grant tags when the grants read fails
699
748
  }
700
749
  return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError));
701
750
  }
@@ -703,11 +752,23 @@ export function registerZiggsTools(server, creds, cfg) {
703
752
  return toolError(e.message);
704
753
  }
705
754
  });
706
- server.tool('ziggs_discover_context', 'List every context grant this delegate holds, as scope descriptors — grantId, scope, temporal, watermarkAt, expiresAt (no content). The single "what is my reach" tool; pass a grantId to ziggs_read_context to pin a specific grant.', {}, READ_ONLY, async () => {
755
+ server.tool('ziggs_discover_context', 'List every context grant this delegate holds, as canonical grants — grantId, scope, caveats (temporal + watermark_at), expiresAt, health (no content). The single "what is my reach" tool; pass a grantId to ziggs_read_context to pin a specific grant. Cursor-paginated: pass cursor from a prior nextCursor to page.', {
756
+ cursor: z
757
+ .string()
758
+ .optional()
759
+ .describe('Opaque cursor from a prior nextCursor'),
760
+ limit: z.number().optional().describe('Page size (default server-side)'),
761
+ }, READ_ONLY, async ({ cursor, limit }) => {
707
762
  try {
708
- const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
709
- const reach = await client.discover();
710
- return textResult({ count: reach.length, reach });
763
+ const client = new GrantsClient(creds.operatorKey, creds.agentId);
764
+ const { items, nextCursor } = await client.listGrants({
765
+ scopeKind: ['chat', 'agreement', 'org'],
766
+ // Reach = live grants only; revoked/expired are not reachable.
767
+ health: 'active',
768
+ cursor,
769
+ limit,
770
+ });
771
+ return textResult({ count: items.length, grants: items, nextCursor });
711
772
  }
712
773
  catch (e) {
713
774
  return toolError(e.message);
@@ -922,7 +983,7 @@ export function registerZiggsTools(server, creds, cfg) {
922
983
  }
923
984
  });
924
985
  server.tool('ziggs_list_my_connections', 'Discover the third-party connections (credentials like GitHub/Jira, NOT agent-to-agent Links — see ziggs_list_links for that) you hold grants for (e.g. "is GitHub connected?") without the owner sharing connectionId/grantId out of band. ' +
925
- 'Returns, per connection: connectionId, provider, connection health (linked/expired/revoked), and the grant(s) you hold — each as the canonical grant shape (grantId, scope, caveats, and grant health active/expired/revoked). ' +
986
+ 'Returns, per connection: connectionId, provider, and the grant(s) you hold — each as the canonical grant shape (grantId, scope, caveats, and grant health active/expired/revoked). ' +
926
987
  'Read-only — never returns credential material. Feed the connectionId + a grantId with health "active" into ziggs_connection_proxy to actually use it.', {}, READ_ONLY, async () => {
927
988
  try {
928
989
  const connections = await listConnectionsForHolder(creds);
@@ -57,7 +57,7 @@ export function registerTrustTools(server, creds, cfg) {
57
57
  return toolError(e.message);
58
58
  }
59
59
  });
60
- server.tool('ziggs_issue_grant', 'Issue bounded context access. Chat scope: admits agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent). Agreement/org scope: POST /context/grants (requires context:admin on operator key). Defaults: from-now, narrow scope.', {
60
+ server.tool('ziggs_issue_grant', 'Issue bounded context access. Chat scope: admits agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent) — this works for you as a delegate. Agreement/org scope: issuing a NEW root grant is a human-authority action; if you are acting for a principal you are denied (AGENT_LACKS_HUMAN_AUTHORITY) instead use ziggs_delegate_grant to hand a peer a narrower slice of a grant you already hold, or ask your human to issue it. Defaults: from-now, narrow scope.', {
61
61
  holderId: z.string().describe('Bare agent id receiving the grant'),
62
62
  scopeKind: grantScopeKindSchema,
63
63
  scopeId: z.string().describe('chatId, agreementId, or orgId'),
@@ -210,7 +210,7 @@ export function registerTrustTools(server, creds, cfg) {
210
210
  const { agreement } = await claimAgreement(agreementId, creds);
211
211
  return textResult({
212
212
  status: 'linked',
213
- message: 'Link invite claimed — you are now linked. A link is reach-only: use ziggs_open_conversation and/or ziggs_issue_grant before reading context.',
213
+ message: 'Link invite claimed — you are now linked. A link is reach-only: open a chat with the peer (ziggs_open_conversation) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you already hold with ziggs_delegate_grant, before reading context.',
214
214
  agreement,
215
215
  });
216
216
  }
@@ -252,7 +252,7 @@ export function registerTrustTools(server, creds, cfg) {
252
252
  links: summaries,
253
253
  ...(hasActive
254
254
  ? {
255
- nextSteps: 'A link is reach-only. Use ziggs_open_conversation (participantId = peer agent id) and/or ziggs_issue_grant before reading context.',
255
+ nextSteps: 'A link is reach-only. Open a chat with the peer (ziggs_open_conversation, participantId = peer agent id) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you hold with ziggs_delegate_grant, before reading context.',
256
256
  }
257
257
  : {}),
258
258
  });
@@ -279,7 +279,7 @@ export function registerTrustTools(server, creds, cfg) {
279
279
  return toolError(e.message);
280
280
  }
281
281
  });
282
- 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.', {
282
+ 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 do NOT hold (one you issued, or on a scope you own) is a human-authority action: as a delegate you are limited to grants you hold; the human/owner does the rest.', {
283
283
  grantId: z.string(),
284
284
  }, DESTRUCTIVE, async ({ grantId }) => {
285
285
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.1.31",
3
+ "version": "0.1.34",
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.25",
39
+ "@ziggs-ai/api-client": "^0.1.29",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },
@@ -38,8 +38,8 @@ The sections below elaborate this protocol with tools, examples, and edge cases.
38
38
 
39
39
  ## Session start — pending decisions + inbox
40
40
 
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`** (`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.
41
+ 1. Call **`ziggs_auth_status`** after OAuth connect — check **`actingOrgId`** / **`actingOrgName`** (runtime org, not JWT). Org is fixed at consent (ZIG-852).
42
+ 2. To act in another org: **reconnect MCP OAuth** and pick that org on the consent screen, then re-check **`ziggs_auth_status`**. Use **`ziggs_list_my_orgs`** to help the human choose a target org name before reconnecting.
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.
@@ -100,7 +100,7 @@ When coordinating with another org’s delegate:
100
100
 
101
101
  ## Boarding checklist (cold session)
102
102
 
103
- 1. Confirm MCP tools are available (e.g. `ziggs_list_chats` or `ziggs_get_scope`).
103
+ 1. Confirm MCP tools are available (e.g. `ziggs_list_chats` or `ziggs_context_snapshot`).
104
104
  2. Run **`ziggs_inbox`** — empty inbox is fine.
105
105
  3. Ask the human what they want to do on Ziggs before issuing grants or opening new agreements.
106
106