@ziggs-ai/ziggs-mcp 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilityAdapter.d.ts +25 -0
- package/dist/capabilityAdapter.js +75 -0
- package/dist/inboxToolResult.d.ts +8 -8
- package/dist/inboxToolResult.js +35 -78
- package/dist/paymentTools.d.ts +9 -8
- package/dist/paymentTools.js +12 -248
- package/dist/tools.js +54 -279
- package/dist/trustTools.d.ts +4 -1
- package/dist/trustTools.js +14 -272
- package/package.json +3 -3
- package/dist/orgs.d.ts +0 -28
- package/dist/orgs.js +0 -44
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type ZodRawShape } from 'zod';
|
|
3
|
+
import type { CapabilityDefinition, CapabilityParam, Creds } from '@ziggs-ai/api-client';
|
|
4
|
+
/** One JSON text-content result shape for every MCP tool (was copied 3×). */
|
|
5
|
+
export declare function textResult(data: unknown): {
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
};
|
|
11
|
+
export declare function toZodShape(params: Record<string, CapabilityParam>): ZodRawShape;
|
|
12
|
+
export interface RegisterCapabilityOptions {
|
|
13
|
+
/** Web-app origin for human-facing URLs (claim links etc.). */
|
|
14
|
+
webUrl?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Description override for wording assembled from MCP-local shared consts
|
|
17
|
+
* (e.g. the delegate-protocol reporting rule, ZIG-557) — schema and handler
|
|
18
|
+
* still come from the shared definition.
|
|
19
|
+
*/
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Surface-local response decoration (e.g. the read-plan on ziggs_read_context). */
|
|
22
|
+
transformResult?: (result: unknown, args: Record<string, unknown>) => unknown;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerCapability(server: McpServer, cap: CapabilityDefinition, creds: Creds, opts?: RegisterCapabilityOptions): void;
|
|
25
|
+
export declare function registerCapabilities(server: McpServer, caps: CapabilityDefinition[], creds: Creds, opts?: RegisterCapabilityOptions): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
|
|
3
|
+
import { toolError } from './toolError.js';
|
|
4
|
+
/** One JSON text-content result shape for every MCP tool (was copied 3×). */
|
|
5
|
+
export function textResult(data) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* ZIG-956 — the MCP surface adapter for shared capability definitions
|
|
12
|
+
* (api-client `capabilities/`). Lowers the neutral param DSL to zod v3 (the
|
|
13
|
+
* MCP SDK's ZodRawShape; api-client itself carries no zod, and agent-sdk is on
|
|
14
|
+
* zod v4, so the schema is defined once and lowered per surface).
|
|
15
|
+
*/
|
|
16
|
+
function zodParam(param) {
|
|
17
|
+
let t;
|
|
18
|
+
switch (param.type) {
|
|
19
|
+
case 'string':
|
|
20
|
+
t = param.enum ? z.enum(param.enum) : z.string();
|
|
21
|
+
break;
|
|
22
|
+
case 'number':
|
|
23
|
+
t = z.number();
|
|
24
|
+
break;
|
|
25
|
+
case 'boolean':
|
|
26
|
+
t = z.boolean();
|
|
27
|
+
break;
|
|
28
|
+
case 'object':
|
|
29
|
+
t = z.record(z.unknown());
|
|
30
|
+
break;
|
|
31
|
+
case 'array': {
|
|
32
|
+
const items = param.items;
|
|
33
|
+
const inner = items?.type === 'string'
|
|
34
|
+
? items.enum
|
|
35
|
+
? z.enum(items.enum)
|
|
36
|
+
: z.string()
|
|
37
|
+
: z.record(z.unknown());
|
|
38
|
+
t = z.array(inner);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (param.description)
|
|
43
|
+
t = t.describe(param.description);
|
|
44
|
+
if (!param.required)
|
|
45
|
+
t = t.optional();
|
|
46
|
+
return t;
|
|
47
|
+
}
|
|
48
|
+
export function toZodShape(params) {
|
|
49
|
+
const shape = {};
|
|
50
|
+
for (const [name, param] of Object.entries(params)) {
|
|
51
|
+
shape[name] = zodParam(param);
|
|
52
|
+
}
|
|
53
|
+
return shape;
|
|
54
|
+
}
|
|
55
|
+
export function registerCapability(server, cap, creds, opts = {}) {
|
|
56
|
+
const annotations = cap.annotation === 'read-only'
|
|
57
|
+
? READ_ONLY
|
|
58
|
+
: cap.annotation === 'destructive'
|
|
59
|
+
? DESTRUCTIVE
|
|
60
|
+
: WRITE;
|
|
61
|
+
server.tool(cap.names.mcp, opts.description ?? cap.descriptions.mcp, toZodShape(cap.params), annotations, async (args) => {
|
|
62
|
+
try {
|
|
63
|
+
const env = { creds, webUrl: opts.webUrl, surface: 'mcp' };
|
|
64
|
+
const result = await cap.handler(args, env);
|
|
65
|
+
return textResult(opts.transformResult ? opts.transformResult(result, args) : result);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return toolError(e.message);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function registerCapabilities(server, caps, creds, opts = {}) {
|
|
73
|
+
for (const cap of caps)
|
|
74
|
+
registerCapability(server, cap, creds, opts);
|
|
75
|
+
}
|
|
@@ -21,16 +21,15 @@ export interface ReadPlanResult {
|
|
|
21
21
|
/**
|
|
22
22
|
* ZIG-634: replace the free-text `nextActions` hints with typed `readPlan`
|
|
23
23
|
* call objects — tool name + pre-filled args — so the most common loop
|
|
24
|
-
* (inbox → read
|
|
24
|
+
* (inbox → read what was addressed to you → ack) needs no guesswork.
|
|
25
25
|
*
|
|
26
26
|
* Safe by construction: every entry is synthesized purely from fields already
|
|
27
|
-
* on the envelope (
|
|
28
|
-
* is read and no new permission check runs — this is the same information
|
|
29
|
-
* caller already received, restated as runnable calls.
|
|
27
|
+
* on the envelope (delivery kinds and ids, the per-chat fold, ackTo). No new
|
|
28
|
+
* data is read and no new permission check runs — this is the same information
|
|
29
|
+
* the caller already received, restated as runnable calls.
|
|
30
30
|
*
|
|
31
31
|
* Mapping honours how reads resolve server-side: messages read only via chat,
|
|
32
|
-
* artifacts via chat or agreement.
|
|
33
|
-
* use the per-chat breakdown (ZIG-543) to name the chatIds.
|
|
32
|
+
* artifacts via chat or agreement.
|
|
34
33
|
*/
|
|
35
34
|
export declare function buildReadPlan(inbox: InboxEnvelope, grantsByScope?: Map<string, ScopeGrantTag>): ReadPlanResult;
|
|
36
35
|
/**
|
|
@@ -70,7 +69,8 @@ export declare function indexReachByScope(reach: GrantView[]): Map<string, Scope
|
|
|
70
69
|
* and append readPlan last so each inbox call self-narrates the follow-up
|
|
71
70
|
* calls (ZIG-634) without disturbing the leading humanAttention key.
|
|
72
71
|
*
|
|
73
|
-
* When `reach` (the caller's own live grants) is passed,
|
|
74
|
-
*
|
|
72
|
+
* When `reach` (the caller's own live grants) is passed, the read plan pins
|
|
73
|
+
* each read's covering grant (ZIG-635) so the agent can present
|
|
74
|
+
* X-Context-Grant-Id without a separate discover round-trip.
|
|
75
75
|
*/
|
|
76
76
|
export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: GrantView[], activeTasksError?: string): Record<string, unknown>;
|
package/dist/inboxToolResult.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { grantCaveat } from '@ziggs-ai/api-client';
|
|
2
2
|
import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
|
|
3
|
-
/** Keep the plan bounded; the full
|
|
3
|
+
/** Keep the plan bounded; the full deliveries array still carries everything. */
|
|
4
4
|
const MAX_READ_PLAN = 12;
|
|
5
5
|
function readContextCall(type, kind, id, grantId) {
|
|
6
6
|
// ZIG-660: pin the covering grant so the read presents the right
|
|
@@ -15,25 +15,24 @@ function readContextCall(type, kind, id, grantId) {
|
|
|
15
15
|
/**
|
|
16
16
|
* ZIG-634: replace the free-text `nextActions` hints with typed `readPlan`
|
|
17
17
|
* call objects — tool name + pre-filled args — so the most common loop
|
|
18
|
-
* (inbox → read
|
|
18
|
+
* (inbox → read what was addressed to you → ack) needs no guesswork.
|
|
19
19
|
*
|
|
20
20
|
* Safe by construction: every entry is synthesized purely from fields already
|
|
21
|
-
* on the envelope (
|
|
22
|
-
* is read and no new permission check runs — this is the same information
|
|
23
|
-
* caller already received, restated as runnable calls.
|
|
21
|
+
* on the envelope (delivery kinds and ids, the per-chat fold, ackTo). No new
|
|
22
|
+
* data is read and no new permission check runs — this is the same information
|
|
23
|
+
* the caller already received, restated as runnable calls.
|
|
24
24
|
*
|
|
25
25
|
* Mapping honours how reads resolve server-side: messages read only via chat,
|
|
26
|
-
* artifacts via chat or agreement.
|
|
27
|
-
* use the per-chat breakdown (ZIG-543) to name the chatIds.
|
|
26
|
+
* artifacts via chat or agreement.
|
|
28
27
|
*/
|
|
29
28
|
export function buildReadPlan(inbox, grantsByScope) {
|
|
30
29
|
const proposals = inbox.proposalsAwaitingMe ?? [];
|
|
31
30
|
const connectionRequests = inbox.connectionRequestsAwaitingMe ?? [];
|
|
32
|
-
const
|
|
31
|
+
const deliveries = inbox.deliveries ?? [];
|
|
33
32
|
// ZIG-660: dedup by call signature so the same (type, via) can't appear
|
|
34
|
-
// twice when
|
|
35
|
-
//
|
|
36
|
-
//
|
|
33
|
+
// twice when several deliveries land in one chat. Collect candidates
|
|
34
|
+
// uncapped; the cap is applied once, after the ack is reserved, so the ack
|
|
35
|
+
// step always survives.
|
|
37
36
|
const candidates = [];
|
|
38
37
|
const seen = new Set();
|
|
39
38
|
const add = (key, call) => {
|
|
@@ -58,57 +57,35 @@ export function buildReadPlan(inbox, grantsByScope) {
|
|
|
58
57
|
why: 'connection request awaiting your response — wait for the human to approve/reject',
|
|
59
58
|
});
|
|
60
59
|
}
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (kind === '
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
read('artifacts', 'chat',
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Messages resolve only via chat — name the chats from the breakdown.
|
|
77
|
-
for (const c of s.chats ?? []) {
|
|
78
|
-
if (c.newMessages)
|
|
79
|
-
read('messages', 'chat', c.chatId);
|
|
80
|
-
}
|
|
81
|
-
// Artifacts (incl. task-result artifacts) read directly via the agreement.
|
|
82
|
-
if (s.newArtifacts)
|
|
83
|
-
read('artifacts', 'agreement', id);
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
// org: both messages and artifacts resolve per chat only.
|
|
87
|
-
for (const c of s.chats ?? []) {
|
|
88
|
-
if (c.newMessages)
|
|
89
|
-
read('messages', 'chat', c.chatId);
|
|
90
|
-
if (c.newArtifacts)
|
|
91
|
-
read('artifacts', 'chat', c.chatId);
|
|
92
|
-
}
|
|
60
|
+
// ZIG-635: pin the covering grant for a chat/agreement read when the caller
|
|
61
|
+
// holds one, so the read presents the right X-Context-Grant-Id without a
|
|
62
|
+
// separate discover round-trip. Untagged reads still work by id.
|
|
63
|
+
const grantFor = (kind, id) => grantsByScope?.get(`${kind}:${id}`)?.grantId;
|
|
64
|
+
const read = (type, viaKind, viaId) => add(`read:${type}:${viaKind}:${viaId}`, readContextCall(type, viaKind, viaId, grantFor(viaKind, viaId)));
|
|
65
|
+
// Reads — one call per place mail actually landed. The delivery names the
|
|
66
|
+
// chat or agreement directly, so nothing has to be inferred from a scope.
|
|
67
|
+
for (const d of deliveries) {
|
|
68
|
+
if (d.kind === 'message' && d.chatId)
|
|
69
|
+
read('messages', 'chat', d.chatId);
|
|
70
|
+
else if (d.kind === 'artifact') {
|
|
71
|
+
if (d.chatId)
|
|
72
|
+
read('artifacts', 'chat', d.chatId);
|
|
73
|
+
else if (d.agreementId)
|
|
74
|
+
read('artifacts', 'agreement', d.agreementId);
|
|
93
75
|
}
|
|
94
76
|
}
|
|
95
|
-
// Close the loop: reading never clears the inbox — pre-fill the ack call with
|
|
96
|
-
// each scope's own latestAt (only scopes that actually have a high-water mark).
|
|
97
|
-
const ackTargets = scopes
|
|
98
|
-
.filter((s) => s.latestAt)
|
|
99
|
-
.map((s) => ({ kind: s.scope.kind, id: s.scope.id, upTo: s.latestAt }));
|
|
100
77
|
// ZIG-660: reserve a slot for the ack before capping, so the pre-filled ack
|
|
101
78
|
// never gets squeezed out exactly when there's the most news. Report how many
|
|
102
79
|
// read/decision candidates the cap dropped as an explicit count.
|
|
103
|
-
const reserve =
|
|
80
|
+
const reserve = inbox.ackTo ? 1 : 0;
|
|
104
81
|
const budget = Math.max(0, MAX_READ_PLAN - reserve);
|
|
105
82
|
const truncated = Math.max(0, candidates.length - budget);
|
|
106
83
|
const plan = candidates.slice(0, budget);
|
|
107
|
-
if (
|
|
84
|
+
if (inbox.ackTo) {
|
|
108
85
|
plan.push({
|
|
109
86
|
tool: 'ziggs_inbox',
|
|
110
|
-
args: { ack:
|
|
111
|
-
why: 'reading does not clear the inbox — ack
|
|
87
|
+
args: { ack: inbox.ackTo },
|
|
88
|
+
why: 'reading does not clear the inbox — ack once you have handled everything above',
|
|
112
89
|
});
|
|
113
90
|
}
|
|
114
91
|
return { plan, truncated };
|
|
@@ -186,34 +163,18 @@ export function indexReachByScope(reach) {
|
|
|
186
163
|
}
|
|
187
164
|
return byScope;
|
|
188
165
|
}
|
|
189
|
-
/**
|
|
190
|
-
* ZIG-635: tag each inbox scope with its covering grant. Scopes with no
|
|
191
|
-
* matching live grant (e.g. reachable via membership, not a grant) are left
|
|
192
|
-
* untagged — the agent keeps navigating by id, never a fabricated grant.
|
|
193
|
-
*/
|
|
194
|
-
function tagScopesWithGrants(scopes, byScope) {
|
|
195
|
-
if (!byScope?.size)
|
|
196
|
-
return scopes;
|
|
197
|
-
return scopes.map((s) => {
|
|
198
|
-
const tag = byScope.get(`${s.scope.kind}:${s.scope.id}`);
|
|
199
|
-
return tag ? { ...s, grant: tag } : s;
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
166
|
/**
|
|
203
167
|
* Put humanAttention first so MCP hosts surface it before counts (ZIG-482),
|
|
204
168
|
* and append readPlan last so each inbox call self-narrates the follow-up
|
|
205
169
|
* calls (ZIG-634) without disturbing the leading humanAttention key.
|
|
206
170
|
*
|
|
207
|
-
* When `reach` (the caller's own live grants) is passed,
|
|
208
|
-
*
|
|
171
|
+
* When `reach` (the caller's own live grants) is passed, the read plan pins
|
|
172
|
+
* each read's covering grant (ZIG-635) so the agent can present
|
|
173
|
+
* X-Context-Grant-Id without a separate discover round-trip.
|
|
209
174
|
*/
|
|
210
175
|
export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach, activeTasksError) {
|
|
211
|
-
// ZIG-660: build the grant index once and feed both the read plan (grant
|
|
212
|
-
// pinning) and the scope tags from it — buildReadPlan no longer runs before
|
|
213
|
-
// the grants are available.
|
|
214
176
|
const byScope = reach?.length ? indexReachByScope(reach) : undefined;
|
|
215
177
|
const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope);
|
|
216
|
-
const scopes = tagScopesWithGrants(inbox.scopes ?? [], byScope);
|
|
217
178
|
const origin = resolveWebAppOrigin(webOrigin);
|
|
218
179
|
// ZIG-659: the inbox reports session-start counts and points to
|
|
219
180
|
// ziggs_pending_decisions for the sessionChatCard — it no longer re-emits the
|
|
@@ -248,11 +209,7 @@ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach,
|
|
|
248
209
|
};
|
|
249
210
|
const { humanAttention, ...rest } = inbox;
|
|
250
211
|
const payload = ack
|
|
251
|
-
? {
|
|
252
|
-
: { ...rest,
|
|
253
|
-
return humanAttention
|
|
254
|
-
? { humanAttention, ...payload }
|
|
255
|
-
: ack
|
|
256
|
-
? payload
|
|
257
|
-
: { ...inbox, scopes, ...tail };
|
|
212
|
+
? { ackedUpTo: ack.ackedUpTo, ...rest, ...tail }
|
|
213
|
+
: { ...rest, ...tail };
|
|
214
|
+
return humanAttention ? { humanAttention, ...payload } : payload;
|
|
258
215
|
}
|
package/dist/paymentTools.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { type Creds } from '@ziggs-ai/api-client';
|
|
3
3
|
/**
|
|
4
|
-
* ZIG-896 — the wallet toolset on the MCP surface, same base names
|
|
5
|
-
* PAYMENT_TOOLS (payment_* → ziggs_payment_*),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* ZIG-896 / ZIG-956 — the wallet toolset on the MCP surface, same base names
|
|
5
|
+
* as the SDK's PAYMENT_TOOLS (payment_* → ziggs_payment_*), now registered
|
|
6
|
+
* from the shared capability definitions in api-client (one schema + handler,
|
|
7
|
+
* two thin surface adapters). Safety is unchanged: money-moving calls are
|
|
8
|
+
* policy-gated server-side (transfers above threshold return
|
|
9
|
+
* `approval_required`; the human decides on the wallet page — surfaced by
|
|
10
|
+
* ziggs_pending_decisions), and rails stay gated by the operator key's
|
|
11
|
+
* payments scopes. There is deliberately NO approve/decide tool on either
|
|
12
|
+
* agent surface — an agent must not approve its own spend.
|
|
12
13
|
*/
|
|
13
14
|
export declare function registerPaymentTools(server: McpServer, creds: Creds): void;
|
package/dist/paymentTools.js
CHANGED
|
@@ -1,252 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
}
|
|
1
|
+
import { PAYMENT_CAPABILITIES } from '@ziggs-ai/api-client';
|
|
2
|
+
import { registerCapabilities } from './capabilityAdapter.js';
|
|
22
3
|
/**
|
|
23
|
-
* ZIG-896 — the wallet toolset on the MCP surface, same base names
|
|
24
|
-
* PAYMENT_TOOLS (payment_* → ziggs_payment_*),
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
4
|
+
* ZIG-896 / ZIG-956 — the wallet toolset on the MCP surface, same base names
|
|
5
|
+
* as the SDK's PAYMENT_TOOLS (payment_* → ziggs_payment_*), now registered
|
|
6
|
+
* from the shared capability definitions in api-client (one schema + handler,
|
|
7
|
+
* two thin surface adapters). Safety is unchanged: money-moving calls are
|
|
8
|
+
* policy-gated server-side (transfers above threshold return
|
|
9
|
+
* `approval_required`; the human decides on the wallet page — surfaced by
|
|
10
|
+
* ziggs_pending_decisions), and rails stay gated by the operator key's
|
|
11
|
+
* payments scopes. There is deliberately NO approve/decide tool on either
|
|
12
|
+
* agent surface — an agent must not approve its own spend.
|
|
31
13
|
*/
|
|
32
14
|
export function registerPaymentTools(server, creds) {
|
|
33
|
-
|
|
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
|
-
});
|
|
15
|
+
registerCapabilities(server, PAYMENT_CAPABILITIES, creds);
|
|
252
16
|
}
|