@ziggs-ai/ziggs-mcp 0.1.34 → 0.1.35

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
@@ -2,8 +2,8 @@
2
2
 
3
3
  MCP (stdio) server for **Claude Code**, **Cursor**, and other MCP hosts.
4
4
 
5
- **In scope:** chat, agreements (service and hire, direct or published), scope, context discovery/reads, artifacts.
6
- **Out of scope (by design):** agent `transfer`, payment grants / bounded spend.
5
+ **In scope:** chat, agreements (service and hire, direct or published), scope, context discovery/reads, artifacts, payments (`ziggs_payment_*` — transfers, escrow holds, payment grants; ZIG-896).
6
+ Transfers above the wallet owner's policy pause as `approval_required` — the human decides on the wallet page (surfaced by `ziggs_pending_decisions`); there is no agent-side approve tool.
7
7
 
8
8
  ---
9
9
 
@@ -41,7 +41,7 @@ Skill only (no plugin): `skills/ziggs/SKILL.md` ships in the package for org pro
41
41
 
42
42
  | Step | Tool |
43
43
  |------|------|
44
- | List chats / discover reach | `ziggs_list_chats` or `ziggs_discover_context` |
44
+ | List chats / discover reach | `ziggs_list_chats` or `ziggs_list_grants` |
45
45
  | Send message | `ziggs_send_message` |
46
46
  | Propose + respond | `ziggs_propose_agreement`, `ziggs_respond_to_agreement` |
47
47
 
@@ -194,10 +194,11 @@ 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 /grants` (context scopes) |
197
+ | `ziggs_list_grants` | `GET /grants` (all rails) |
198
198
  | `ziggs_read_context` | `GET /context/read/:type` |
199
199
  | `ziggs_record_artifact` | `POST /artifacts` |
200
200
  | `ziggs_search_agents` | Agent search |
201
+ | `ziggs_get_agent` | `GET /agents/:id` — full profile of one agent by exact id |
201
202
  | `ziggs_issue_grant` | Chat admission or `POST /context/grants` |
202
203
  | `ziggs_delegate_grant` | `POST /context/grants/:id/delegate` |
203
204
  | `ziggs_revoke_grant` | `DELETE /context/grants/:id` |
@@ -214,6 +215,8 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
214
215
  | `ziggs_send_message` | `POST /chats/:id/messages` |
215
216
  | `ziggs_propose_agreement` | `POST /agreements/proposals` |
216
217
  | `ziggs_respond_to_agreement` | `PUT /agreements/:id/approvals/:partyId` (owner principal; approves hire, service, and `link` proposals) |
218
+ | `ziggs_counter_agreement` | `POST /agreements/:id/counter` — counter a pending proposal with revised terms |
219
+ | `ziggs_fulfill_agreement` | `POST /agreements/:id/fulfill` — provider marks its agreement complete |
217
220
 
218
221
  ---
219
222
 
@@ -0,0 +1,13 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { type Creds } from '@ziggs-ai/api-client';
3
+ /**
4
+ * ZIG-896 — the wallet toolset on the MCP surface, same base names as the SDK's
5
+ * PAYMENT_TOOLS (payment_* → ziggs_payment_*), riding the shared PaymentsClient
6
+ * (ZIG-894). Safety is unchanged: money-moving calls are policy-gated
7
+ * server-side (transfers above threshold return `approval_required`; the human
8
+ * decides on the wallet page — surfaced by ziggs_pending_decisions), and rails
9
+ * stay gated by the operator key's payments scopes. There is deliberately NO
10
+ * approve/decide tool on either agent surface — an agent must not approve its
11
+ * own spend.
12
+ */
13
+ export declare function registerPaymentTools(server: McpServer, creds: Creds): void;
@@ -0,0 +1,252 @@
1
+ import { z } from 'zod';
2
+ import { PaymentsClient } from '@ziggs-ai/api-client';
3
+ import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
4
+ import { toolError } from './toolError.js';
5
+ function textResult(data) {
6
+ return {
7
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
8
+ };
9
+ }
10
+ function buildCaveats(args) {
11
+ const caveats = [];
12
+ if (args.maxAmount != null)
13
+ caveats.push({ type: 'max_amount', value: args.maxAmount });
14
+ if (args.dailyBudget != null)
15
+ caveats.push({ type: 'daily_budget', value: args.dailyBudget });
16
+ if (args.allowedRecipients != null)
17
+ caveats.push({ type: 'allowed_recipients', value: args.allowedRecipients });
18
+ if (args.expiresInSeconds != null)
19
+ caveats.push({ type: 'expires_at', value: Date.now() + args.expiresInSeconds * 1000 });
20
+ return caveats;
21
+ }
22
+ /**
23
+ * ZIG-896 — the wallet toolset on the MCP surface, same base names as the SDK's
24
+ * PAYMENT_TOOLS (payment_* → ziggs_payment_*), riding the shared PaymentsClient
25
+ * (ZIG-894). Safety is unchanged: money-moving calls are policy-gated
26
+ * server-side (transfers above threshold return `approval_required`; the human
27
+ * decides on the wallet page — surfaced by ziggs_pending_decisions), and rails
28
+ * stay gated by the operator key's payments scopes. There is deliberately NO
29
+ * approve/decide tool on either agent surface — an agent must not approve its
30
+ * own spend.
31
+ */
32
+ export function registerPaymentTools(server, creds) {
33
+ const client = () => new PaymentsClient(creds.operatorKey, creds.agentId);
34
+ server.tool('ziggs_payment_balance', "Check the caller's current wallet balance and available balance (total minus active holds). Use before a transfer to confirm sufficient funds.", {}, READ_ONLY, async () => {
35
+ try {
36
+ return textResult(await client().balance());
37
+ }
38
+ catch (e) {
39
+ return toolError(e.message);
40
+ }
41
+ });
42
+ server.tool('ziggs_payment_resolve_wallet', 'Look up a walletId by userId or agentId. Use before a transfer when you only know the recipient by their platform ID.', {
43
+ userId: z.string().optional().describe('User to resolve'),
44
+ agentId: z.string().optional().describe('Agent to resolve'),
45
+ }, READ_ONLY, async ({ userId, agentId }) => {
46
+ if (!userId && !agentId) {
47
+ return toolError('Provide userId or agentId to resolve a wallet');
48
+ }
49
+ try {
50
+ const wallet = (await client().resolve({ userId, agentId }));
51
+ return textResult({
52
+ walletId: wallet?.['walletId'] || null,
53
+ ownerId: wallet?.['ownerId'] || null,
54
+ currency: wallet?.['currency'] || 'pez',
55
+ status: wallet?.['status'] || null,
56
+ });
57
+ }
58
+ catch (e) {
59
+ return toolError(e.message);
60
+ }
61
+ });
62
+ server.tool('ziggs_payment_transfer', 'Transfer funds to another wallet. Amounts are integer cents. As a delegate you spend under a payment grant the wallet owner issued (paymentGrantId — find yours via ziggs_list_grants scopeKind=wallet). Transfers above the owner\'s policy pause with status "approval_required": the human approves on the wallet page (it also shows in ziggs_pending_decisions) — you can wait inline with ziggs_payment_wait_for_approval, and you must NEVER approve your own transfer.', {
63
+ toWalletId: z
64
+ .string()
65
+ .describe('Destination wal_... id — or a userId/agentId to auto-resolve'),
66
+ amount: z.number().describe('Amount in integer cents, > 0'),
67
+ description: z.string().optional().describe('Human-readable transfer memo'),
68
+ idempotencyKey: z
69
+ .string()
70
+ .optional()
71
+ .describe('Client-supplied key to make retries safe (auto-generated when omitted)'),
72
+ paymentGrantId: z
73
+ .string()
74
+ .optional()
75
+ .describe('Payment grant to spend under (required for agent-impersonated transfers)'),
76
+ }, WRITE, async ({ toWalletId, amount, description, idempotencyKey, paymentGrantId }) => {
77
+ if (!amount || amount <= 0)
78
+ return toolError('amount must be positive');
79
+ try {
80
+ const result = (await client().transfer({
81
+ to: toWalletId,
82
+ amount: Math.round(amount),
83
+ description: description || 'Agent-initiated transfer',
84
+ idempotencyKey,
85
+ paymentGrantId,
86
+ }));
87
+ if (result['status'] === 'approval_required') {
88
+ return textResult({
89
+ status: 'approval_required',
90
+ approvalId: result['approvalId'] || null,
91
+ expiresAt: result['expiresAt'] || null,
92
+ reason: result['reason'] || null,
93
+ amount,
94
+ toWalletId: result['toWalletId'],
95
+ note: 'Transfer paused: the wallet owner must approve this amount on the wallet page (also listed by ziggs_pending_decisions). Tell the human now (pull-only MCP has no push). ' +
96
+ 'Wait inline with ziggs_payment_wait_for_approval when you expect a quick decision (≤2 min).',
97
+ });
98
+ }
99
+ return textResult({
100
+ status: 'transferred',
101
+ transactionId: result['transactionId'] || null,
102
+ amount,
103
+ toWalletId: result['toWalletId'],
104
+ });
105
+ }
106
+ catch (e) {
107
+ return toolError(e.message);
108
+ }
109
+ });
110
+ server.tool('ziggs_payment_wait_for_approval', 'Poll a paused transfer (status "approval_required") until the human decides or the timeout passes. Returns executed | rejected | expired | timeout | gone. Use for quick decisions (≤2 min); for longer waits, stop and check again next session.', {
111
+ approvalId: z.string().describe('Approval to wait on (from ziggs_payment_transfer)'),
112
+ timeoutMs: z.number().optional().describe('Max wait, default 120000'),
113
+ pollMs: z.number().optional().describe('Poll interval, default 3000 (min 500)'),
114
+ }, READ_ONLY, async ({ approvalId, timeoutMs, pollMs }) => {
115
+ try {
116
+ const result = (await client().waitForApproval(approvalId, {
117
+ timeoutMs,
118
+ pollMs,
119
+ }));
120
+ return textResult({
121
+ status: result['status'],
122
+ approvalId,
123
+ transactionId: result['transactionId'] || null,
124
+ approval: result['approval'] || null,
125
+ });
126
+ }
127
+ catch (e) {
128
+ return toolError(e.message);
129
+ }
130
+ });
131
+ server.tool('ziggs_payment_hold', "Pre-authorize (escrow) funds without moving them. Use to reserve payment at agreement formation; release with ziggs_payment_release once work is complete, or refund if it's cancelled.", {
132
+ amount: z.number().describe('Amount in integer cents, > 0'),
133
+ description: z.string().optional().describe('Human-readable hold memo'),
134
+ idempotencyKey: z.string().optional().describe('Client-supplied retry-safety key'),
135
+ }, WRITE, async ({ amount, description, idempotencyKey }) => {
136
+ if (!amount || amount <= 0)
137
+ return toolError('amount must be positive');
138
+ try {
139
+ const result = (await client().hold({
140
+ amount: Math.round(amount),
141
+ description: description || 'Agent escrow hold',
142
+ idempotencyKey,
143
+ }));
144
+ return textResult({
145
+ status: 'held',
146
+ transactionId: result['transaction']?.['transactionId'] || null,
147
+ amount,
148
+ });
149
+ }
150
+ catch (e) {
151
+ return toolError(e.message);
152
+ }
153
+ });
154
+ server.tool('ziggs_payment_release', "Settle or refund an escrow hold. action='complete' transfers held funds to toWalletId (work done); action='refund' returns funds to the sender (work cancelled).", {
155
+ holdId: z.string().describe('Hold to settle (transactionId from ziggs_payment_hold)'),
156
+ action: z.enum(['complete', 'refund']).describe('complete = pay out, refund = return'),
157
+ toWalletId: z
158
+ .string()
159
+ .optional()
160
+ .describe('Destination wallet — required when action=complete'),
161
+ idempotencyKey: z.string().optional().describe('Client-supplied retry-safety key'),
162
+ }, WRITE, async ({ holdId, action, toWalletId, idempotencyKey }) => {
163
+ if (action === 'complete' && !toWalletId) {
164
+ return toolError('toWalletId is required when action=complete');
165
+ }
166
+ try {
167
+ const result = (await client().release({
168
+ holdId,
169
+ action,
170
+ toWalletId,
171
+ idempotencyKey,
172
+ }));
173
+ return textResult({
174
+ status: action === 'complete' ? 'settled' : 'refunded',
175
+ transactionId: result['transaction']?.['transactionId'] || null,
176
+ holdId,
177
+ action,
178
+ });
179
+ }
180
+ catch (e) {
181
+ return toolError(e.message);
182
+ }
183
+ });
184
+ const grantCaveatArgs = {
185
+ maxAmount: z.number().optional().describe('Per-transfer ceiling in cents'),
186
+ dailyBudget: z.number().optional().describe('Rolling daily budget in cents'),
187
+ allowedRecipients: z
188
+ .array(z.string())
189
+ .optional()
190
+ .describe('Wallet ids the holder may pay'),
191
+ expiresInSeconds: z.number().optional().describe('Grant lifetime from now'),
192
+ };
193
+ server.tool('ziggs_payment_issue_grant', "Issue a payment grant delegating bounded spend from the operator's wallet to an agent holder. Caveats bound what the holder can do (max_amount, daily_budget, allowed_recipients, expiry). The holder spends by passing the grantId as paymentGrantId on transfers.", {
194
+ holderId: z.string().describe('Agent that will hold the grant'),
195
+ ...grantCaveatArgs,
196
+ }, WRITE, async ({ holderId, ...caveatArgs }) => {
197
+ try {
198
+ const caveats = buildCaveats(caveatArgs);
199
+ const result = (await client().issueGrant({ holderId, caveats }));
200
+ const grant = result['grant'];
201
+ return textResult({
202
+ grantId: grant?.['grantId'] || null,
203
+ holderId: grant?.['holderId'] || holderId,
204
+ caveats: grant?.['caveats'] || caveats,
205
+ expiresAt: grant?.['expiresAt'] || null,
206
+ });
207
+ }
208
+ catch (e) {
209
+ return toolError(e.message);
210
+ }
211
+ });
212
+ server.tool('ziggs_payment_attenuate_grant', 'Re-delegate a payment grant you hold to another agent with TIGHTER caveats (narrowing only — the child can never exceed the parent). Use to pass a bounded spend slice to a sub-agent.', {
213
+ grantId: z.string().describe('Parent grant to attenuate'),
214
+ holderId: z.string().describe('Agent that will hold the narrowed grant'),
215
+ ...grantCaveatArgs,
216
+ }, WRITE, async ({ grantId, holderId, ...caveatArgs }) => {
217
+ try {
218
+ const caveats = buildCaveats(caveatArgs);
219
+ const result = (await client().attenuateGrant({
220
+ grantId,
221
+ holderId,
222
+ caveats,
223
+ }));
224
+ const grant = result['grant'];
225
+ return textResult({
226
+ grantId: grant?.['grantId'] || null,
227
+ parentGrantId: grant?.['parentGrantId'] || grantId,
228
+ holderId: grant?.['holderId'] || holderId,
229
+ caveats: grant?.['caveats'] || caveats,
230
+ expiresAt: grant?.['expiresAt'] || null,
231
+ });
232
+ }
233
+ catch (e) {
234
+ return toolError(e.message);
235
+ }
236
+ });
237
+ server.tool('ziggs_payment_revoke_grant', 'Revoke a payment grant (and its attenuated children). The holder can no longer spend under it.', {
238
+ grantId: z.string().describe('Grant to revoke'),
239
+ }, DESTRUCTIVE, async ({ grantId }) => {
240
+ try {
241
+ const result = (await client().revokeGrant(grantId));
242
+ return textResult({
243
+ status: 'revoked',
244
+ grantId,
245
+ revoked: result?.['revoked'] ?? null,
246
+ });
247
+ }
248
+ catch (e) {
249
+ return toolError(e.message);
250
+ }
251
+ });
252
+ }
@@ -25,6 +25,26 @@ export interface ActiveWorkItem {
25
25
  appUrl: string | null;
26
26
  sayWork: string;
27
27
  }
28
+ /**
29
+ * ZIG-896 — a paused transfer awaiting the wallet owner's decision
30
+ * (GET /payments/approvals?status=pending). Deliberately NOT a
31
+ * PendingDecisionItem: there is no agent-side approve tool on any surface —
32
+ * the human decides on the wallet page. The agent may only poll with
33
+ * ziggs_payment_wait_for_approval.
34
+ */
35
+ export interface PaymentApprovalItem {
36
+ approvalId: string;
37
+ amount: number | null;
38
+ toWalletId: string | null;
39
+ description: string | null;
40
+ reason: string | null;
41
+ initiatorId: string | null;
42
+ requestedAt: string | null;
43
+ requestedAtLabel: string | null;
44
+ expiresAt: string | null;
45
+ appUrl: string;
46
+ waitTool: string;
47
+ }
28
48
  /**
29
49
  * ZIG-659: pointer emitted by the tools that do NOT own the session card
30
50
  * (ziggs_auth_status, ziggs_inbox). They report the counts and send the caller
@@ -37,7 +57,11 @@ export declare function agreementAppUrl(origin: string, agreementId: string): st
37
57
  export declare function agreementsListAppUrl(origin: string): string;
38
58
  /** Where the human connects MCP servers and grants tools (ZIG-686). */
39
59
  export declare function connectionsSettingsAppUrl(origin: string): string;
60
+ /** Where the human decides paused transfers (ZIG-896). */
61
+ export declare function walletAppUrl(origin: string): string;
40
62
  export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
63
+ /** ZIG-896 — shape pending payment approvals for the session payload/card. */
64
+ export declare function buildPaymentApprovalItems(approvals: Array<Record<string, unknown>>, webOrigin: string): PaymentApprovalItem[];
41
65
  /**
42
66
  * ZIG-658: keep only tasks this delegate (or its principal) is expected to
43
67
  * execute. The backend's GET /tasks returns every task reachable in the org,
@@ -63,6 +87,7 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
63
87
  truncatedProposals?: number;
64
88
  truncatedConnectionRequests?: number;
65
89
  agreementsListAppUrl?: string;
90
+ paymentApprovals?: PaymentApprovalItem[];
66
91
  }): string;
67
92
  /** Structured session payload for MCP tools (ZIG-625 + active work). */
68
93
  export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, opts?: {
@@ -75,5 +100,8 @@ export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webO
75
100
  * the card is not duplicated across every session-start tool.
76
101
  */
77
102
  withSessionCard?: boolean;
103
+ /** ZIG-896 — pending payment approvals (GET /payments/approvals rows). */
104
+ paymentApprovals?: Array<Record<string, unknown>>;
105
+ paymentApprovalsError?: string;
78
106
  }): Record<string, unknown>;
79
107
  export declare function buildPendingNextActions(decisions: PendingDecisionItem[], work?: ActiveWorkItem[]): string[];
@@ -20,6 +20,10 @@ export function agreementsListAppUrl(origin) {
20
20
  export function connectionsSettingsAppUrl(origin) {
21
21
  return `${origin}/app/settings/connections`;
22
22
  }
23
+ /** Where the human decides paused transfers (ZIG-896). */
24
+ export function walletAppUrl(origin) {
25
+ return `${origin}/app/wallet`;
26
+ }
23
27
  function truncateText(text, max = TITLE_MAX) {
24
28
  const oneLine = text.replace(/\s+/g, ' ').trim();
25
29
  if (oneLine.length <= max)
@@ -96,6 +100,27 @@ export function buildPendingDecisionItems(inbox, webOrigin) {
96
100
  }
97
101
  return items;
98
102
  }
103
+ /** ZIG-896 — shape pending payment approvals for the session payload/card. */
104
+ export function buildPaymentApprovalItems(approvals, webOrigin) {
105
+ return approvals.map((a) => {
106
+ const payload = a['payload'] ?? {};
107
+ const approvalId = String(a['approvalId'] ?? '');
108
+ const requestedAt = a['createdAt'] ? String(a['createdAt']) : null;
109
+ return {
110
+ approvalId,
111
+ amount: typeof payload['amount'] === 'number' ? payload['amount'] : null,
112
+ toWalletId: payload['toWalletId'] || null,
113
+ description: payload['description'] || null,
114
+ reason: a['reason'] || null,
115
+ initiatorId: a['initiatorId'] || null,
116
+ requestedAt,
117
+ requestedAtLabel: formatWhen(requestedAt),
118
+ expiresAt: a['expiresAt'] ? String(a['expiresAt']) : null,
119
+ appUrl: walletAppUrl(webOrigin),
120
+ waitTool: `ziggs_payment_wait_for_approval approvalId=${approvalId}`,
121
+ };
122
+ });
123
+ }
99
124
  /**
100
125
  * ZIG-658: keep only tasks this delegate (or its principal) is expected to
101
126
  * execute. The backend's GET /tasks returns every task reachable in the org,
@@ -194,6 +219,40 @@ function buildDecisionSection(items, opts) {
194
219
  }
195
220
  return lines;
196
221
  }
222
+ /** ZIG-896 — paused transfers section: the human decides on the wallet page. */
223
+ function buildPaymentApprovalSection(items, startIndex = 1) {
224
+ if (!items.length)
225
+ return [];
226
+ const lines = [];
227
+ let n = startIndex;
228
+ for (const item of items) {
229
+ lines.push('---');
230
+ lines.push('');
231
+ lines.push(`#### ${n}. 💸 Payment approval`);
232
+ n += 1;
233
+ lines.push('');
234
+ const what = item.amount != null
235
+ ? `**Transfer ${item.amount} cents${item.toWalletId ? ` → \`${item.toWalletId}\`` : ''}**`
236
+ : '**Paused transfer**';
237
+ lines.push(what);
238
+ lines.push('');
239
+ lines.push(`\`${item.approvalId}\`${item.requestedAtLabel ? ` · ${item.requestedAtLabel}` : ''}`);
240
+ lines.push('');
241
+ lines.push("_A transfer above your spending policy is paused — approve or reject it on the wallet page. The agent cannot decide this for you (there is no agent-side approve tool). It expires if you don't decide._");
242
+ if (item.reason || item.description) {
243
+ lines.push('');
244
+ lines.push(`> ${item.reason || item.description}`);
245
+ }
246
+ lines.push('');
247
+ lines.push(`[Decide in Ziggs wallet →](${item.appUrl})`);
248
+ lines.push('');
249
+ lines.push('| You say in chat | What the agent runs |');
250
+ lines.push('|:----------------|:--------------------|');
251
+ lines.push(`| \`wait for ${item.approvalId}\` | \`${item.waitTool}\` |`);
252
+ lines.push('');
253
+ }
254
+ return lines;
255
+ }
197
256
  function buildWorkSection(work, startIndex = 1) {
198
257
  if (!work.length)
199
258
  return [];
@@ -237,7 +296,8 @@ export function buildSessionChatCard(decisions, work, opts) {
237
296
  const truncatedProposals = opts.truncatedProposals ?? 0;
238
297
  const truncatedConnectionRequests = opts.truncatedConnectionRequests ?? 0;
239
298
  const truncated = truncatedProposals + truncatedConnectionRequests;
240
- const decisionListed = decisions.length + truncated;
299
+ const paymentApprovals = opts.paymentApprovals ?? [];
300
+ const decisionListed = decisions.length + truncated + paymentApprovals.length;
241
301
  const workCount = work.length;
242
302
  const total = decisionListed + workCount;
243
303
  if (total === 0)
@@ -252,6 +312,9 @@ export function buildSessionChatCard(decisions, work, opts) {
252
312
  `| Approve / reject | **${decisionListed}** |`,
253
313
  `| — proposals | ${decisions.filter((d) => d.kind === 'proposal').length}${truncatedProposals ? ` (+${truncatedProposals} hidden)` : ''} |`,
254
314
  `| — link requests | ${decisions.filter((d) => d.kind === 'link_request').length}${truncatedConnectionRequests ? ` (+${truncatedConnectionRequests} hidden)` : ''} |`,
315
+ ...(paymentApprovals.length > 0
316
+ ? [`| — payment approvals | ${paymentApprovals.length} |`]
317
+ : []),
255
318
  ]
256
319
  : []),
257
320
  ...(workCount > 0 ? [`| Active tasks (your work) | **${workCount}** |`] : []),
@@ -264,6 +327,10 @@ export function buildSessionChatCard(decisions, work, opts) {
264
327
  lines.push(...buildDecisionSection(decisions, { ...opts, startIndex: sectionIndex }));
265
328
  sectionIndex += decisions.length;
266
329
  }
330
+ if (paymentApprovals.length) {
331
+ lines.push(...buildPaymentApprovalSection(paymentApprovals, sectionIndex));
332
+ sectionIndex += paymentApprovals.length;
333
+ }
267
334
  if (work.length) {
268
335
  lines.push(...buildWorkSection(work, sectionIndex));
269
336
  }
@@ -278,9 +345,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
278
345
  const withSessionCard = opts?.withSessionCard !== false;
279
346
  const decisions = buildPendingDecisionItems(inbox, webOrigin);
280
347
  const activeWork = buildActiveWorkItems(opts?.activeTasks ?? [], webOrigin);
348
+ const paymentApprovals = buildPaymentApprovalItems(opts?.paymentApprovals ?? [], webOrigin);
281
349
  const truncatedProposals = inbox.truncatedProposals ?? 0;
282
350
  const truncatedConnectionRequests = inbox.truncatedConnectionRequests ?? 0;
283
- const pendingCount = decisions.length + truncatedProposals + truncatedConnectionRequests;
351
+ const pendingCount = decisions.length +
352
+ truncatedProposals +
353
+ truncatedConnectionRequests +
354
+ paymentApprovals.length;
284
355
  const activeWorkCount = activeWork.length;
285
356
  const actionCount = pendingCount + activeWorkCount;
286
357
  const listUrl = agreementsListAppUrl(webOrigin);
@@ -288,6 +359,7 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
288
359
  truncatedProposals,
289
360
  truncatedConnectionRequests,
290
361
  agreementsListAppUrl: listUrl,
362
+ paymentApprovals,
291
363
  };
292
364
  const sessionChatCard = buildSessionChatCard(decisions, activeWork, cardOpts);
293
365
  const proposalCount = decisions.filter((d) => d.kind === 'proposal').length;
@@ -307,11 +379,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
307
379
  summary: {
308
380
  proposals: proposalCount + truncatedProposals,
309
381
  linkRequests: linkCount + truncatedConnectionRequests,
382
+ paymentApprovals: paymentApprovals.length,
310
383
  activeTasks: activeWorkCount,
311
- listed: decisions.length,
384
+ listed: decisions.length + paymentApprovals.length,
312
385
  truncated: truncatedProposals + truncatedConnectionRequests,
313
386
  },
314
387
  decisions,
388
+ paymentApprovals,
315
389
  activeWork,
316
390
  truncatedProposals,
317
391
  truncatedConnectionRequests,
@@ -336,6 +410,13 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
336
410
  workCardOmitted: true,
337
411
  }
338
412
  : {}),
413
+ // ZIG-896 — same signal for the approvals fetch: hasPending must not read
414
+ // as "no paused transfers" when the approvals call failed.
415
+ ...(opts?.paymentApprovalsError
416
+ ? {
417
+ paymentApprovalsFetchError: `Could not load payment approvals: ${opts.paymentApprovalsError}`,
418
+ }
419
+ : {}),
339
420
  instruction,
340
421
  };
341
422
  }
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_context_snapshot.';
16
+ 'reach with ziggs_list_grants / ziggs_context_snapshot.';
17
17
  function codeForStatus(status) {
18
18
  if (status === 401)
19
19
  return 'NOT_AUTHENTICATED';
package/dist/tools.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
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';
3
+ import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDirectTo, proposeBroadcast, publishOffer, claimOffer, provisionRelayWorkers, respondToAgreement, revokeAgreement, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, assertNoLeakedConnectionSecret, ContextDiscoveryClient, ContextReadClient, ContextGrantsClient, GrantsClient, unreadableGrantRails, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getBackendUrl, ArtifactsClient, } from '@ziggs-ai/api-client';
4
4
  import { decodeOperatorKeyClaims } from './operatorKey.js';
5
5
  import { registerTrustTools } from './trustTools.js';
6
+ import { registerPaymentTools } from './paymentTools.js';
6
7
  import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
7
8
  import { filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
8
9
  import { PROTOCOL } from './protocol/delegateProtocol.js';
@@ -26,7 +27,7 @@ const ZIGGS_INBOX_DESCRIPTION = "What's new since your last ack — references o
26
27
  `${PROTOCOL.humanAttention} ${PROTOCOL.pendingDecisions} ` +
27
28
  'When hasActionable the response carries the pending/active counts and points to ziggs_pending_decisions for the sessionChatCard to paste (that tool owns the card; it is not duplicated here). ' +
28
29
  'A `readPlan` array gives the exact next calls (tool + pre-filled args) for the news in this response — run them verbatim to read each scope and ack; when the plan overflows, `readPlanTruncated` counts the reads it dropped (the ack call is always kept). ' +
29
- 'Each scope also carries the covering `grant` (grantId, temporal, watermarkAt, expiresAt), and readPlan reads come pre-pinned with that contextGrantId, so no separate discover_context call is needed. ' +
30
+ 'Each scope also carries the covering `grant` (grantId, temporal, watermarkAt, expiresAt), and readPlan reads come pre-pinned with that contextGrantId, so no separate ziggs_list_grants call is needed. ' +
30
31
  `${PROTOCOL.loop} ${PROTOCOL.ack}`;
31
32
  const ZIGGS_PENDING_DECISIONS_DESCRIPTION = 'Session start summary: approve/reject decisions AND active tasks assigned to your delegate. ' +
32
33
  'Call at session start in Cursor/Claude — pull-only MCP has no notification tray. ' +
@@ -66,86 +67,10 @@ const contextReadTypeSchema = z.enum([
66
67
  'tasks',
67
68
  ]);
68
69
  const artifactVisibilitySchema = z.enum(['chat', 'agent-private']);
69
- async function writeArtifactStrict(creds, input) {
70
- const url = `${getBackendUrl()}/artifacts`;
71
- const res = await fetch(url, {
72
- method: 'POST',
73
- headers: {
74
- 'content-type': 'application/json',
75
- Authorization: `Bearer ${creds.operatorKey}`,
76
- 'X-Agent-Id': creds.agentId,
77
- },
78
- body: JSON.stringify({
79
- text: input.text.trim(),
80
- content_type: input.content_type ?? 'text',
81
- visibility: input.visibility,
82
- chatId: input.chatId,
83
- agreementId: input.agreementId,
84
- taskId: input.taskId,
85
- }),
86
- });
87
- if (!res.ok) {
88
- const body = await res.text().catch(() => '');
89
- throw new Error(`POST /artifacts ${res.status} ${body.slice(0, 200)}`);
90
- }
91
- const body = (await res.json().catch(() => ({})));
92
- return { artifactId: body.artifactId };
93
- }
94
- // ZIG-569 — defense-in-depth mirror of the backend leak-guard
95
- // (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
96
- // vault token from the response; the MCP layer never sees that token, so it
97
- // instead pattern-scans the proxied body for high-signal provider credential
98
- // shapes and refuses to hand a likely-leaked secret to the model.
99
- const LEAKED_SECRET_PATTERNS = [
100
- /\bgh[posru]_[A-Za-z0-9]{16,}\b/, // GitHub PAT / OAuth / user / server / refresh
101
- /\bgithub_pat_[A-Za-z0-9_]{20,}\b/, // fine-grained GitHub PAT
102
- /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack bot/user/app/refresh tokens
103
- /\bxapp-[A-Za-z0-9-]{10,}\b/, // Slack app-level token
104
- /"(?:access_token|refresh_token)"\s*:\s*"[^"]{8,}"/, // raw OAuth token JSON keys
105
- ];
106
- function assertNoLeakedSecret(serialized) {
107
- for (const re of LEAKED_SECRET_PATTERNS) {
108
- if (re.test(serialized)) {
109
- throw new Error('connection proxy response withheld: it appears to contain a credential (token-leak guard)');
110
- }
111
- }
112
- }
113
- /**
114
- * ZIG-569 — call the backend connections proxy so the agent can use a stored
115
- * connection without ever seeing the credential. Impersonates the grant-holder
116
- * agent (X-Agent-Id, required by the broker), returns the provider `result`, and
117
- * mirrors the backend leak-guard before the body reaches the model.
118
- */
119
- async function proxyConnection(creds, input) {
120
- const url = `${getBackendUrl()}/connections/${encodeURIComponent(input.connectionId)}/proxy`;
121
- const res = await fetch(url, {
122
- method: 'POST',
123
- headers: {
124
- 'content-type': 'application/json',
125
- Authorization: `Bearer ${creds.operatorKey}`,
126
- 'X-Agent-Id': creds.agentId,
127
- },
128
- body: JSON.stringify({
129
- grantId: input.grantId,
130
- action: input.action,
131
- payload: input.payload ?? {},
132
- }),
133
- });
134
- const body = await res.text().catch(() => '');
135
- if (!res.ok) {
136
- throw new Error(`POST /connections/${input.connectionId}/proxy ${res.status} ${body.slice(0, 200)}`);
137
- }
138
- assertNoLeakedSecret(body);
139
- let parsed = body;
140
- try {
141
- parsed = body ? JSON.parse(body) : null;
142
- }
143
- catch {
144
- parsed = body;
145
- }
146
- const result = parsed?.['result'];
147
- return result ?? parsed;
148
- }
70
+ // ZIG-899 — the strict artifact write (fail loudly, return the artifactId)
71
+ // moved into ArtifactsClient.writeStrict, shared with the SDK's record_artifact.
72
+ // ZIG-894 the leak-guard and the connections proxy/request calls moved into
73
+ // @ziggs-ai/api-client's ConnectionsClient (shared with the agent SDK).
149
74
  /** ZIG-640 — runtime acting org from server (self-hire / agent row). */
150
75
  async function fetchDelegateAccess(creds) {
151
76
  const url = `${getBackendUrl()}/agents/claude-delegate/access`;
@@ -164,7 +89,7 @@ async function fetchDelegateAccess(creds) {
164
89
  }
165
90
  /**
166
91
  * ZIG-739 — the operator's full org membership (not just granted scopes, which
167
- * is all ziggs_discover_context sees). Lets the delegate resolve an org name to
92
+ * is all ziggs_list_grants sees). Lets the delegate resolve an org name to
168
93
  * an id and offer a pick-list instead of demanding a pasted org_... id.
169
94
  */
170
95
  async function fetchMyOrgs(creds) {
@@ -230,38 +155,9 @@ async function listConnectionsForHolder(creds) {
230
155
  group.grants.push(g);
231
156
  }
232
157
  const result = [...byConnection.values()];
233
- assertNoLeakedSecret(JSON.stringify(result));
158
+ assertNoLeakedConnectionSecret(JSON.stringify(result));
234
159
  return result;
235
160
  }
236
- /**
237
- * ZIG-686 — agent-initiated MCP connection request: ask the principal to
238
- * connect a remote MCP server and grant this agent the listed tools. Opens a
239
- * connection-consent agreement in the working chat (ZIG-798): the human
240
- * approves it there like any agreement, and on approval the server is connected
241
- * (if needed) and this agent is granted the tools. Returns the agreement id.
242
- */
243
- async function createMcpConnectionRequest(creds, input) {
244
- const url = `${getBackendUrl()}/connections/mcp/requests`;
245
- const res = await fetch(url, {
246
- method: 'POST',
247
- headers: {
248
- 'content-type': 'application/json',
249
- Authorization: `Bearer ${creds.operatorKey}`,
250
- 'X-Agent-Id': creds.agentId,
251
- },
252
- body: JSON.stringify({
253
- serverUrl: input.serverUrl,
254
- tools: input.tools,
255
- reason: input.reason,
256
- chatId: input.chatId,
257
- }),
258
- });
259
- const body = await res.text().catch(() => '');
260
- if (!res.ok) {
261
- throw new Error(`POST /connections/mcp/requests ${res.status} ${body.slice(0, 200)}`);
262
- }
263
- return body ? JSON.parse(body) : {};
264
- }
265
161
  /**
266
162
  * Ids this delegate answers for: its own agent id plus its principal's user
267
163
  * id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
@@ -291,9 +187,21 @@ async function loadSessionActionsPayload(creds, cfg, opts) {
291
187
  // failure so hasActiveWork:false is not mistaken for "no tasks".
292
188
  activeTasksError = e.message;
293
189
  }
190
+ // ZIG-896 — paused transfers awaiting the wallet owner. Same failure rule as
191
+ // tasks: a failed fetch is surfaced, not silently rendered as "none pending".
192
+ let paymentApprovals = [];
193
+ let paymentApprovalsError;
194
+ try {
195
+ paymentApprovals = (await new PaymentsClient(creds.operatorKey, creds.agentId).approvals({ status: 'pending' }));
196
+ }
197
+ catch (e) {
198
+ paymentApprovalsError = e.message;
199
+ }
294
200
  return formatPendingDecisionsPayload(inbox, webOrigin, {
295
201
  activeTasks,
296
202
  activeTasksError,
203
+ paymentApprovals,
204
+ paymentApprovalsError,
297
205
  withSessionCard: opts?.withSessionCard,
298
206
  });
299
207
  }
@@ -369,7 +277,7 @@ export function registerZiggsTools(server, creds, cfg) {
369
277
  : 'Next: ziggs_inbox or ziggs_pending_decisions at session start.',
370
278
  });
371
279
  });
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 () => {
280
+ server.tool('ziggs_list_my_orgs', 'List every org you (the operator) belong to — { orgId, name, kind, role }. Unlike ziggs_list_grants (granted scopes only), this is your full membership — useful before OAuth reconnect when the human wants to pick a target org.', {}, READ_ONLY, async () => {
373
281
  try {
374
282
  const orgs = await fetchMyOrgs(creds);
375
283
  return textResult({ count: orgs.length, orgs });
@@ -378,25 +286,6 @@ export function registerZiggsTools(server, creds, cfg) {
378
286
  return toolError(e.message);
379
287
  }
380
288
  });
381
- server.tool('ziggs_switch_org', 'Deprecated: org is fixed at OAuth consent. Reconnect MCP OAuth and pick the target org on the consent screen — this tool no longer switches org server-side.', {
382
- org: z
383
- .string()
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
- });
399
- });
400
289
  server.tool('ziggs_pending_decisions', ZIGGS_PENDING_DECISIONS_DESCRIPTION, {}, READ_ONLY, async () => {
401
290
  try {
402
291
  const payload = await loadSessionActionsPayload(creds, cfg);
@@ -708,6 +597,49 @@ export function registerZiggsTools(server, creds, cfg) {
708
597
  return toolError(e.message);
709
598
  }
710
599
  });
600
+ server.tool('ziggs_counter_agreement', 'Counter a pending proposal with revised terms instead of approving or rejecting (POST /agreements/:id/counter). Provide only the terms you want to change — price, description, expiry, lifecycle, or plan; omitted fields keep the original proposal\'s value. The counter goes back to the counterparty as a fresh pending proposal for them to approve/reject/counter. Read the current terms first with ziggs_get_agreement.', {
601
+ agreementId: z.string().describe('The pending agreement to counter'),
602
+ price: z.number().optional().describe('Revised price'),
603
+ agreementDescription: z
604
+ .string()
605
+ .optional()
606
+ .describe('Revised agreement description / terms'),
607
+ expiresAt: z.string().optional().describe('Revised expiry (ISO-8601)'),
608
+ lifecycle: z.string().optional().describe('Revised lifecycle'),
609
+ maxExecutions: z.number().optional().describe('Revised max executions'),
610
+ description: z
611
+ .string()
612
+ .optional()
613
+ .describe('Revised task description for the spawned work'),
614
+ plan: z
615
+ .record(z.unknown())
616
+ .optional()
617
+ .describe('Override plan { steps: [...] }; omit to keep the original'),
618
+ planReviewTiming: z
619
+ .enum(['with_proposal', 'before_execution'])
620
+ .optional()
621
+ .describe('When the buyer reviews the plan: with_proposal | before_execution'),
622
+ requireMidWorkPlanAck: z.boolean().optional(),
623
+ }, WRITE, async ({ agreementId, ...counter }) => {
624
+ try {
625
+ const agreement = await counterAgreement(agreementId, counter, creds);
626
+ return textResult({ status: 'countered', agreementId, agreement });
627
+ }
628
+ catch (e) {
629
+ return toolError(e.message);
630
+ }
631
+ });
632
+ server.tool('ziggs_fulfill_agreement', 'Mark an agreement you PROVIDE as fulfilled/complete once its work is delivered (POST /agreements/:id/fulfill) — closes the engagement so it no longer reads as in-progress. Party-gated server-side: only the providing side can fulfill. Use on your hire after the final deliverable is done and delivered.', {
633
+ agreementId: z.string().describe('The agreement you provide, to mark fulfilled'),
634
+ }, WRITE, async ({ agreementId }) => {
635
+ try {
636
+ const result = await fulfillAgreement(agreementId, creds);
637
+ return textResult({ status: 'fulfilled', agreementId, agreement: result.agreement });
638
+ }
639
+ catch (e) {
640
+ return toolError(e.message);
641
+ }
642
+ });
711
643
  server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
712
644
  ack: z
713
645
  .array(z.object({
@@ -752,29 +684,61 @@ export function registerZiggsTools(server, creds, cfg) {
752
684
  return toolError(e.message);
753
685
  }
754
686
  });
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.', {
687
+ server.tool('ziggs_list_grants', 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). The single answer to "what grants of mine do you hold?", holder-scoped and cross-session. Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. Pass a grantId to ziggs_read_context to pin a specific grant, or ziggs_expand_context to enumerate a scope.', {
688
+ scopeKind: z
689
+ .array(z.enum(['chat', 'agreement', 'org', 'connection', 'wallet']))
690
+ .optional()
691
+ .describe('Rail filter (repeatable). Omit for every rail you can read.'),
692
+ health: z
693
+ .enum(['active', 'expired', 'revoked'])
694
+ .optional()
695
+ .describe('Grant health filter. Defaults to active (live grants only).'),
756
696
  cursor: z
757
697
  .string()
758
698
  .optional()
759
699
  .describe('Opaque cursor from a prior nextCursor'),
760
700
  limit: z.number().optional().describe('Page size (default server-side)'),
761
- }, READ_ONLY, async ({ cursor, limit }) => {
701
+ }, READ_ONLY, async ({ scopeKind, health, cursor, limit }) => {
762
702
  try {
763
703
  const client = new GrantsClient(creds.operatorKey, creds.agentId);
704
+ const kinds = scopeKind && scopeKind.length
705
+ ? scopeKind
706
+ : undefined;
764
707
  const { items, nextCursor } = await client.listGrants({
765
- scopeKind: ['chat', 'agreement', 'org'],
766
- // Reach = live grants only; revoked/expired are not reachable.
767
- health: 'active',
708
+ scopeKind: kinds,
709
+ // Reach = live grants only by default; pass health for expired/revoked.
710
+ health: health ?? 'active',
768
711
  cursor,
769
712
  limit,
770
713
  });
771
- return textResult({ count: items.length, grants: items, nextCursor });
714
+ const unreadable = unreadableGrantRails(creds.operatorKey, kinds);
715
+ return textResult({
716
+ count: items.length,
717
+ grants: items,
718
+ nextCursor,
719
+ ...(unreadable && unreadable.length
720
+ ? { unreadableRails: unreadable }
721
+ : {}),
722
+ });
723
+ }
724
+ catch (e) {
725
+ return toolError(e.message);
726
+ }
727
+ });
728
+ server.tool('ziggs_expand_context', 'Expand a grant you hold into the chat/agreement ids inside its scope, so you can actually read through it. ziggs_list_grants tells you that you hold e.g. org:acme or agreement:x; this returns the { chats, agreements } (ids + labels only, never content) that scope covers — feed an id to ziggs_read_context (via=chat:<id> / agreement:<id>). Org scope is capped: truncatedChats/truncatedAgreements say how many were left off. Holder-only, grant-fenced.', {
729
+ grantId: z
730
+ .string()
731
+ .describe('A grant you hold (grantId from ziggs_list_grants) to expand'),
732
+ }, READ_ONLY, async ({ grantId }) => {
733
+ try {
734
+ const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
735
+ return textResult(await client.getReach(grantId));
772
736
  }
773
737
  catch (e) {
774
738
  return toolError(e.message);
775
739
  }
776
740
  });
777
- 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 () => {
741
+ 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_list_grants for what you already hold; this is what you lack.', {}, READ_ONLY, async () => {
778
742
  try {
779
743
  const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
780
744
  const items = await client.discoverGrantable();
@@ -841,14 +805,7 @@ export function registerZiggsTools(server, creds, cfg) {
841
805
  if ((chatId && agreementId) || (!chatId && !agreementId)) {
842
806
  return toolError('Pass exactly one of chatId or agreementId');
843
807
  }
844
- const { artifactId } = await writeArtifactStrict(creds, {
845
- text,
846
- visibility,
847
- chatId,
848
- agreementId,
849
- taskId,
850
- content_type,
851
- });
808
+ const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({ text, visibility, chatId, agreementId, taskId, content_type });
852
809
  return textResult({
853
810
  ok: true,
854
811
  artifactId,
@@ -970,12 +927,7 @@ export function registerZiggsTools(server, creds, cfg) {
970
927
  .describe('Action-specific arguments (provider-defined)'),
971
928
  }, WRITE, async ({ connectionId, grantId, action, payload }) => {
972
929
  try {
973
- const result = await proxyConnection(creds, {
974
- connectionId,
975
- grantId,
976
- action,
977
- payload,
978
- });
930
+ const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).proxy({ connectionId, grantId, action, payload });
979
931
  return textResult({ ok: true, action, result });
980
932
  }
981
933
  catch (e) {
@@ -1009,12 +961,7 @@ export function registerZiggsTools(server, creds, cfg) {
1009
961
  .describe('Plain-language reason shown to the human deciding'),
1010
962
  }, WRITE, async ({ chatId, serverUrl, tools, reason }) => {
1011
963
  try {
1012
- const result = await createMcpConnectionRequest(creds, {
1013
- chatId,
1014
- serverUrl,
1015
- tools,
1016
- reason,
1017
- });
964
+ const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).requestMcpConnection({ chatId, serverUrl, tools, reason });
1018
965
  return textResult({
1019
966
  ok: true,
1020
967
  ...result,
@@ -1027,4 +974,5 @@ export function registerZiggsTools(server, creds, cfg) {
1027
974
  }
1028
975
  });
1029
976
  registerTrustTools(server, creds, cfg);
977
+ registerPaymentTools(server, creds);
1030
978
  }
@@ -57,6 +57,22 @@ export function registerTrustTools(server, creds, cfg) {
57
57
  return toolError(e.message);
58
58
  }
59
59
  });
60
+ server.tool('ziggs_get_agent', 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, and reliability. Use to confirm a candidate before ziggs_propose_agreement / ziggs_request_link, when you already hold the agent id (from ziggs_search_agents, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_search_agents.', {
61
+ agentId: z.string().describe('Exact agent id to fetch — do not guess'),
62
+ }, READ_ONLY, async ({ agentId }) => {
63
+ try {
64
+ const client = new AgentSearchClient(creds.operatorKey, creds.agentId);
65
+ const result = await client.getAgentById(agentId);
66
+ if (!result.success) {
67
+ return toolError(result.error ?? 'agent not found');
68
+ }
69
+ const { success: _success, ...agent } = result;
70
+ return textResult({ agent });
71
+ }
72
+ catch (e) {
73
+ return toolError(e.message);
74
+ }
75
+ });
60
76
  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
77
  holderId: z.string().describe('Bare agent id receiving the grant'),
62
78
  scopeKind: grantScopeKindSchema,
@@ -51,7 +51,7 @@ Optional env:
51
51
 
52
52
  Ask Claude to call tools in order:
53
53
 
54
- 1. `ziggs_list_chats` or `ziggs_discover_context`
54
+ 1. `ziggs_list_chats` or `ziggs_list_grants`
55
55
  2. `ziggs_send_message` (chat you belong to)
56
56
  3. `ziggs_propose_agreement` + `ziggs_respond_to_agreement` (optional)
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
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,10 +36,13 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@ziggs-ai/api-client": "^0.1.29",
39
+ "@ziggs-ai/api-client": "^0.1.30",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },
43
+ "devDependencies": {
44
+ "@ziggs-ai/agent-sdk": "^0.2.2"
45
+ },
43
46
  "engines": {
44
47
  "node": ">=20"
45
48
  },
@@ -45,7 +45,7 @@ The sections below elaborate this protocol with tools, examples, and edge cases.
45
45
  4. Read the envelope: scope news counts, `humanAttention`, and **`decisionChatCard`** when present.
46
46
  5. Do **not** pull full scope history “just in case.” Only read scopes that show news or that you must act on.
47
47
 
48
- If `ziggs_inbox` is unavailable, fall back to **`ziggs_discover_context`** to list reachable scopes, then **`ziggs_read_context`** with **`after`** cursors — still inbox-first in spirit (delta reads only).
48
+ If `ziggs_inbox` is unavailable, fall back to **`ziggs_list_grants`** (scopeKind: chat/agreement/org) to list reachable scopes, then **`ziggs_read_context`** with **`after`** cursors — still inbox-first in spirit (delta reads only).
49
49
 
50
50
  ## The working loop
51
51
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  ## Reach is grant-gated
4
4
 
5
- You only read context your delegate **holds a grant for**. `ziggs_discover_context` lists scopes; `ziggs_read_context` enforces grants on every read.
5
+ You only read context your delegate **holds a grant for**. `ziggs_list_grants` lists every grant you hold across all rails (context / connection / wallet); `ziggs_read_context` enforces grants on every read.
6
+
7
+ To answer "what grants of mine do you hold?", call **`ziggs_list_grants`** — one call, all rails, cross-session. Filter by `scopeKind` for a single rail; rails you can't read are named in `unreadableRails`.
6
8
 
7
9
  ## Before issuing grants
8
10
 
@@ -14,7 +16,7 @@ Ask the human unless they already specified in this session:
14
16
  | `from-now` or `from-start`? | `from-start` exposes history — often needs counterparty approval |
15
17
  | Expiry / purpose? | Revocation and audit trail |
16
18
 
17
- Use **`ziggs_discover_context`** to see existing reach before adding more.
19
+ Use **`ziggs_list_grants`** (scopeKind: chat/agreement/org) to see existing reach before adding more.
18
20
 
19
21
  ## Approval gates
20
22
 
@@ -24,9 +26,14 @@ These commonly surface as **`pending_approval`** or blocked tool errors:
24
26
  - `from-start` history on a scope
25
27
  - Cross-org grant issue / delegate
26
28
  - Agreement steps that require a human principal
29
+ - A `ziggs_payment_transfer` above the wallet owner's spending policy (`approval_required` — decided on the wallet page, never by the agent)
27
30
 
28
31
  **Default:** present the pending item to the human with id, title, and recommended action — do not approve silently.
29
32
 
33
+ ## Payments (wallet rail)
34
+
35
+ Spending rides a **payment grant** the wallet owner issued (`ziggs_payment_issue_grant`; find held grants via `ziggs_list_grants` scopeKind=wallet, pass the grantId as `paymentGrantId` on `ziggs_payment_transfer`). Escrow: `ziggs_payment_hold` → `ziggs_payment_release`. A transfer above policy pauses as `approval_required`: it shows in `ziggs_pending_decisions`, the human decides on the wallet page, and you may poll briefly with `ziggs_payment_wait_for_approval`. There is deliberately **no agent-side approve tool** — never try to decide your own transfer.
36
+
30
37
  ## Trust tool sequence (cross-org)
31
38
 
32
39
  A **link** (bilateral agent-to-agent trust — not a `ziggs_connection_proxy` third-party