@ziggs-ai/ziggs-mcp 0.3.1 → 0.4.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/tools.js CHANGED
@@ -1,6 +1,6 @@
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, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, assertNoLeakedConnectionSecret, ContextDiscoveryClient, ContextReadClient, ContextGrantsClient, GrantsClient, unreadableGrantRails, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getTask, getBackendUrl, ArtifactsClient, } from '@ziggs-ai/api-client';
3
+ import { getAgreement, getMyAgreements, listMyChats, proposeDirectTo, proposeBroadcast, publishOffer, claimOffer, provisionRelayWorkers, respondToAgreement, revokeAgreement, counterAgreement, fulfillAgreement, sendChatMessage, ConnectionsClient, PaymentsClient, ContextReadClient, GrantsClient, InboxClient, createTask, updateTaskState, replaceTaskPlan, listTasks, getTask, getBackendUrl, fetchMyOrgs, fetchDelegateAccess, GRANTS_CAPABILITIES, contextReadCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, recordArtifactCapability, openConversationCapability, connectionProxyCapability, requestConnectionCapability, } from '@ziggs-ai/api-client';
4
4
  import { decodeOperatorKeyClaims } from './operatorKey.js';
5
5
  import { registerTrustTools } from './trustTools.js';
6
6
  import { registerPaymentTools } from './paymentTools.js';
@@ -18,6 +18,7 @@ function buildRelayCoordinatorTaskBody(opts) {
18
18
  }
19
19
  import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
20
20
  import { toolError } from './toolError.js';
21
+ import { registerCapability, registerCapabilities, textResult, } from './capabilityAdapter.js';
21
22
  // ZIG-557: the protocol sentences (loop / ack / humanAttention) are sourced
22
23
  // from the shared const so this description can't drift from SKILL / server
23
24
  // instructions / .cursorrules.
@@ -46,118 +47,12 @@ const ZIGGS_SEND_MESSAGE_DESCRIPTION = 'Send a chat message as the delegate agen
46
47
  const ZIGGS_RECORD_ARTIFACT_DESCRIPTION = 'Write an artifact to a chat or agreement scope. Set visibility explicitly. ' +
47
48
  'For a finished deliverable, set content_type=result and pass taskId to bind it to the task. ' +
48
49
  PROTOCOL.reporting;
49
- /** Teach the result slot on the record_artifact success path (ZIG-560). */
50
- function recordArtifactReportingHint(contentType, taskId) {
51
- if (contentType === 'result') {
52
- return taskId
53
- ? 'Recorded as a task-bound result artifact. Close the task by setting its terminal result with ziggs_set_task_result ({ summary, status, links }).'
54
- : 'Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with ziggs_set_task_result ({ summary, status, links }).';
55
- }
56
- return 'Reporting finished work? Record it with content_type=result bound to the task (taskId), then ziggs_set_task_result — chat messages are conversation only.';
57
- }
58
- function textResult(data) {
59
- return {
60
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
61
- };
62
- }
63
- const contextReadTypeSchema = z.enum([
64
- 'messages',
65
- 'artifacts',
66
- 'agreements',
67
- 'tasks',
68
- ]);
69
- const artifactVisibilitySchema = z.enum(['chat', 'agent-private']);
70
50
  // ZIG-899 — the strict artifact write (fail loudly, return the artifactId)
71
51
  // 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).
74
- /** ZIG-640 — runtime acting org from server (self-hire / agent row). */
75
- async function fetchDelegateAccess(creds) {
76
- const url = `${getBackendUrl()}/agents/claude-delegate/access`;
77
- const res = await fetch(url, {
78
- method: 'GET',
79
- headers: {
80
- Authorization: `Bearer ${creds.operatorKey}`,
81
- 'X-Agent-Id': creds.agentId,
82
- },
83
- });
84
- const body = await res.text().catch(() => '');
85
- if (!res.ok) {
86
- throw new Error(`GET /agents/claude-delegate/access ${res.status} ${body.slice(0, 200)}`);
87
- }
88
- return body ? JSON.parse(body) : {};
89
- }
90
- /**
91
- * ZIG-739 — the operator's full org membership (not just granted scopes, which
92
- * is all ziggs_list_grants sees). Lets the delegate resolve an org name to
93
- * an id and offer a pick-list instead of demanding a pasted org_... id.
94
- */
95
- async function fetchMyOrgs(creds) {
96
- const url = `${getBackendUrl()}/orgs/me`;
97
- const res = await fetch(url, {
98
- method: 'GET',
99
- headers: {
100
- Authorization: `Bearer ${creds.operatorKey}`,
101
- 'X-Agent-Id': creds.agentId,
102
- },
103
- });
104
- const body = await res.text().catch(() => '');
105
- if (!res.ok) {
106
- throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
107
- }
108
- const parsed = body ? JSON.parse(body) : {};
109
- return (parsed.orgs ?? []).map((o) => ({
110
- orgId: o.orgId,
111
- name: o.name,
112
- kind: o.kind,
113
- role: o.role,
114
- }));
115
- }
116
- /**
117
- * ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
118
- * the operator's memberships. Exact id wins; otherwise case-insensitive name
119
- * match. Ambiguous names return the candidates rather than guessing.
120
- */
121
- function resolveOrgSelector(orgs, selector) {
122
- const byId = orgs.find((o) => o.orgId === selector);
123
- if (byId)
124
- return { status: 'ok', orgId: byId.orgId };
125
- const needle = selector.toLowerCase();
126
- const byName = orgs.filter((o) => o.name.toLowerCase() === needle);
127
- if (byName.length === 1)
128
- return { status: 'ok', orgId: byName[0].orgId };
129
- if (byName.length > 1)
130
- return { status: 'ambiguous', matches: byName };
131
- return { status: 'not-found' };
132
- }
133
- /**
134
- * ZIG-641 / ZIG-648 — cross-connection discovery over the unified GET /grants:
135
- * every connection grant this agent holds, grouped by connection so
136
- * ziggs_connection_proxy's connectionId/grantId no longer has to arrive out of
137
- * band. `provider` comes from the grant's resolved scope label. The response is
138
- * scanned defensively for leaked secrets, as ConnectionsClient.proxy does.
139
- */
140
- async function listConnectionsForHolder(creds) {
141
- const client = new GrantsClient(creds.operatorKey, creds.agentId);
142
- // All pages of the agent's live connection grants (not just the first page).
143
- const items = await client.listAllGrants({
144
- scopeKind: 'connection',
145
- health: 'active',
146
- });
147
- const byConnection = new Map();
148
- for (const g of items) {
149
- const connectionId = g.scope.id;
150
- let group = byConnection.get(connectionId);
151
- if (!group) {
152
- group = { connectionId, provider: g.scope.label ?? null, grants: [] };
153
- byConnection.set(connectionId, group);
154
- }
155
- group.grants.push(g);
156
- }
157
- const result = [...byConnection.values()];
158
- assertNoLeakedConnectionSecret(JSON.stringify(result));
159
- return result;
160
- }
52
+ // ZIG-894 / ZIG-956 — the leak-guard, the connections proxy/request calls, the
53
+ // grouped connection lister (ConnectionsClient.listForHolder), the org lookups
54
+ // (fetchMyOrgs / fetchDelegateAccess), and the whole SDK-twin tool definitions
55
+ // all live in @ziggs-ai/api-client now (shared with the agent SDK).
161
56
  /**
162
57
  * Ids this delegate answers for: its own agent id plus its principal's user
163
58
  * id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
@@ -205,6 +100,140 @@ async function loadSessionActionsPayload(creds, cfg, opts) {
205
100
  withSessionCard: opts?.withSessionCard,
206
101
  });
207
102
  }
103
+ // ZIG-941 #7 — heavy tool groups pulled out of registerZiggsTools so the
104
+ // lean session-start tier (ZIGGS_MCP_CORE_ONLY) can skip registering them.
105
+ // Registration is otherwise identical to the previous inline definitions.
106
+ function registerMarketplaceTools(server, creds) {
107
+ server.tool('ziggs_publish_quest', 'Publish an open quest any agent can claim (buyer-broadcast): you are the buyer, and whoever claims it does the work. audience="everyone" (default) is fully public across all orgs; audience="org" scopes it to your active org — only agents in your org see it in marketplace feeds and may claim it. The payer is derived server-side as your side (the publisher); there is no payer input.', {
108
+ description: z.string(),
109
+ chatId: z.string().optional(),
110
+ price: z.number().optional(),
111
+ audience: z
112
+ .enum(['everyone', 'org'])
113
+ .optional()
114
+ .describe("'everyone' (default, public) or 'org' (visible/claimable only within your org)"),
115
+ }, WRITE, async ({ description, chatId, price, audience }) => {
116
+ try {
117
+ // Buyer-broadcast: the payer is derived server-side as the creating
118
+ // principal (your side), and providerId is forbidden on broadcasts —
119
+ // the claiming agent fills the open provider side. So we send neither.
120
+ // audience flows straight through; the api-client + backend map it to
121
+ // the proposedTo sentinel and scope on the publisher's org.
122
+ const agreement = await proposeBroadcast({
123
+ description,
124
+ chatId: chatId ?? '',
125
+ price,
126
+ engagementKind: 'service',
127
+ audience: audience ?? 'everyone',
128
+ }, creds);
129
+ return textResult({ agreement });
130
+ }
131
+ catch (e) {
132
+ return toolError(e.message);
133
+ }
134
+ });
135
+ server.tool('ziggs_publish_offer', 'Publish a standing offer buyers can claim (seller-broadcast). audience="everyone" (default) is public; audience="org" scopes it to your active org. Requires an active org when audience="org".', {
136
+ description: z.string(),
137
+ price: z.number().optional(),
138
+ engagementKind: z.enum(['hire', 'service']).optional(),
139
+ audience: z
140
+ .enum(['everyone', 'org'])
141
+ .optional()
142
+ .describe("'everyone' (default, public) or 'org' (visible/claimable only within your org)"),
143
+ }, WRITE, async ({ description, price, engagementKind, audience }) => {
144
+ try {
145
+ const agreement = await publishOffer({
146
+ description,
147
+ price,
148
+ engagementKind,
149
+ audience: audience ?? 'everyone',
150
+ }, creds);
151
+ return textResult({ offer: agreement });
152
+ }
153
+ catch (e) {
154
+ return toolError(e.message);
155
+ }
156
+ });
157
+ 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.', {
158
+ agreementId: z.string().describe('Open offer agreementId to claim'),
159
+ }, WRITE, async ({ agreementId }) => {
160
+ try {
161
+ const offer = await claimOffer(agreementId, creds);
162
+ return textResult({ offer });
163
+ }
164
+ catch (e) {
165
+ return toolError(e.message);
166
+ }
167
+ });
168
+ 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.', {
169
+ hireAgreementId: z.string(),
170
+ chatId: z
171
+ .string()
172
+ .optional()
173
+ .describe('Required when a step has no standing offer and needs delegation under the hire'),
174
+ inputArtifactIds: z.array(z.string()).optional(),
175
+ steps: z.array(z.object({
176
+ stepId: z.string(),
177
+ order: z.number(),
178
+ assigneeId: z.string(),
179
+ description: z.string(),
180
+ offerAgreementId: z
181
+ .string()
182
+ .optional()
183
+ .describe('Explicit open offer to claim for this worker'),
184
+ })),
185
+ kickoff: z
186
+ .boolean()
187
+ .optional()
188
+ .describe('When true and readyForKickoff, also POST /tasks on the hire for relay-coordinator'),
189
+ }, WRITE, async ({ hireAgreementId, chatId, inputArtifactIds, steps, kickoff }) => {
190
+ try {
191
+ const result = await provisionRelayWorkers({
192
+ creds,
193
+ hireAgreementId,
194
+ chatId,
195
+ inputArtifactIds,
196
+ steps,
197
+ });
198
+ const relayTaskBody = buildRelayCoordinatorTaskBody({
199
+ hireAgreementId,
200
+ payload: result.payload,
201
+ });
202
+ let task;
203
+ if (kickoff && result.readyForKickoff) {
204
+ task = await createTask(relayTaskBody, creds);
205
+ }
206
+ return textResult({
207
+ ...result,
208
+ relayTaskBody,
209
+ task,
210
+ nextSteps: result.readyForKickoff
211
+ ? kickoff && task
212
+ ? 'Relay coordinator task created — watch Execution for step progress.'
213
+ : 'All worker agreements active — POST relayTaskBody via createTask or set kickoff=true.'
214
+ : `Worker approval pending on: ${result.pendingApprovals.join(', ')}. Call ziggs_respond_to_agreement after workers approve, then re-run with kickoff=true.`,
215
+ });
216
+ }
217
+ catch (e) {
218
+ return toolError(e.message);
219
+ }
220
+ });
221
+ }
222
+ function registerConnectionTools(server, creds) {
223
+ registerCapability(server, connectionProxyCapability, creds);
224
+ 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. ' +
225
+ '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). ' +
226
+ '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 () => {
227
+ try {
228
+ const connections = await new ConnectionsClient(creds.operatorKey, creds.agentId).listForHolder();
229
+ return textResult({ connections });
230
+ }
231
+ catch (e) {
232
+ return toolError(e.message);
233
+ }
234
+ });
235
+ registerCapability(server, requestConnectionCapability, creds);
236
+ }
208
237
  export function registerZiggsTools(server, creds, cfg) {
209
238
  server.tool('ziggs_auth_status', 'Verify MCP OAuth binding: delegate agent id, owner user id, and org scope. Call after connect before inbox/chats. Includes pendingDecisions summary when approve/reject is waiting. (Renamed from ziggs_connection_status — "connection" now refers only to third-party credential connections, see ziggs_connection_proxy.)', {}, READ_ONLY, async () => {
210
239
  const claims = decodeOperatorKeyClaims(creds.operatorKey);
@@ -346,15 +375,22 @@ export function registerZiggsTools(server, creds, cfg) {
346
375
  return toolError(e.message);
347
376
  }
348
377
  });
349
- server.tool('ziggs_list_my_agreements', 'List agreements visible to the impersonated delegate agent (scope=mine).', {
378
+ server.tool('ziggs_list_my_agreements', 'List agreements you are a party to — your hires, proposals, and work (default scope "mine"). Pass scope "reachable" to list every agreement your grant can read in the org, including ones you are not a party to; the isYou flags on each row mark which party (if any) is you.', {
379
+ scope: z
380
+ .enum(['mine', 'reachable'])
381
+ .optional()
382
+ .describe('mine (default): only agreements where you are a party. reachable: all agreements your grant can read in the org.'),
350
383
  proposalStatus: z
351
384
  .string()
352
385
  .optional()
353
386
  .describe('Optional filter: pending, approved, rejected, …'),
354
- }, READ_ONLY, async ({ proposalStatus }) => {
387
+ }, READ_ONLY, async ({ scope, proposalStatus }) => {
355
388
  try {
356
- const agreements = await getMyAgreements(proposalStatus ? { proposalStatus } : {}, creds);
357
- return textResult({ count: agreements.length, agreements });
389
+ const agreements = await getMyAgreements({
390
+ ...(proposalStatus ? { proposalStatus } : {}),
391
+ partyOnly: scope !== 'reachable',
392
+ }, creds);
393
+ return textResult({ count: agreements.length, scope: scope ?? 'mine', agreements });
358
394
  }
359
395
  catch (e) {
360
396
  return toolError(e.message);
@@ -380,23 +416,13 @@ export function registerZiggsTools(server, creds, cfg) {
380
416
  return toolError(e.message);
381
417
  }
382
418
  });
383
- server.tool('ziggs_open_conversation', 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, an unpublished delegate must establish a link first — call ziggs_request_link (if you have its agent id) or ziggs_create_link_invite (if you do not) and have it approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.', {
384
- participantId: z.string().describe('User or agent id to converse with'),
385
- }, WRITE, async ({ participantId }) => {
386
- try {
387
- const out = await openConversation(participantId, creds);
388
- return textResult(out);
389
- }
390
- catch (e) {
391
- return toolError(e.message);
392
- }
393
- });
419
+ registerCapability(server, openConversationCapability, creds);
394
420
  server.tool('ziggs_send_message', ZIGGS_SEND_MESSAGE_DESCRIPTION, {
395
421
  chatId: z.string(),
396
422
  receiverId: z
397
423
  .string()
398
424
  .optional()
399
- .describe('User or agent id receiving the message. Optional when the chat has exactly one other member (the recipient is inferred server-side); required when multiple members could receive it.'),
425
+ .describe("User or agent id receiving the message. Optional: with exactly one other member the recipient is inferred server-side; in a room with several members an omitted receiver becomes a broadcast to the room's HUMAN members (agents are not woken by it). Pass 'human' to broadcast explicitly, or a specific agent id to address (and wake) that agent."),
400
426
  text: z.string(),
401
427
  entryType: z
402
428
  .string()
@@ -456,120 +482,9 @@ export function registerZiggsTools(server, creds, cfg) {
456
482
  return toolError(e.message);
457
483
  }
458
484
  });
459
- server.tool('ziggs_publish_quest', 'Publish an open quest any agent can claim (buyer-broadcast): you are the buyer, and whoever claims it does the work. audience="everyone" (default) is fully public across all orgs; audience="org" scopes it to your active org — only agents in your org see it in marketplace feeds and may claim it. The payer is derived server-side as your side (the publisher); there is no payer input.', {
460
- description: z.string(),
461
- chatId: z.string().optional(),
462
- price: z.number().optional(),
463
- audience: z
464
- .enum(['everyone', 'org'])
465
- .optional()
466
- .describe("'everyone' (default, public) or 'org' (visible/claimable only within your org)"),
467
- }, WRITE, async ({ description, chatId, price, audience }) => {
468
- try {
469
- // Buyer-broadcast: the payer is derived server-side as the creating
470
- // principal (your side), and providerId is forbidden on broadcasts —
471
- // the claiming agent fills the open provider side. So we send neither.
472
- // audience flows straight through; the api-client + backend map it to
473
- // the proposedTo sentinel and scope on the publisher's org.
474
- const agreement = await proposeBroadcast({
475
- description,
476
- chatId: chatId ?? '',
477
- price,
478
- engagementKind: 'service',
479
- audience: audience ?? 'everyone',
480
- }, creds);
481
- return textResult({ agreement });
482
- }
483
- catch (e) {
484
- return toolError(e.message);
485
- }
486
- });
487
- server.tool('ziggs_publish_offer', 'Publish a standing offer buyers can claim (seller-broadcast). audience="everyone" (default) is public; audience="org" scopes it to your active org. Requires an active org when audience="org".', {
488
- description: z.string(),
489
- price: z.number().optional(),
490
- engagementKind: z.enum(['hire', 'service']).optional(),
491
- audience: z
492
- .enum(['everyone', 'org'])
493
- .optional()
494
- .describe("'everyone' (default, public) or 'org' (visible/claimable only within your org)"),
495
- }, WRITE, async ({ description, price, engagementKind, audience }) => {
496
- try {
497
- const agreement = await publishOffer({
498
- description,
499
- price,
500
- engagementKind,
501
- audience: audience ?? 'everyone',
502
- }, creds);
503
- return textResult({ offer: agreement });
504
- }
505
- catch (e) {
506
- return toolError(e.message);
507
- }
508
- });
509
- 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.', {
510
- agreementId: z.string().describe('Open offer agreementId to claim'),
511
- }, WRITE, async ({ agreementId }) => {
512
- try {
513
- const offer = await claimOffer(agreementId, creds);
514
- return textResult({ offer });
515
- }
516
- catch (e) {
517
- return toolError(e.message);
518
- }
519
- });
520
- 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.', {
521
- hireAgreementId: z.string(),
522
- chatId: z
523
- .string()
524
- .optional()
525
- .describe('Required when a step has no standing offer and needs delegation under the hire'),
526
- inputArtifactIds: z.array(z.string()).optional(),
527
- steps: z.array(z.object({
528
- stepId: z.string(),
529
- order: z.number(),
530
- assigneeId: z.string(),
531
- description: z.string(),
532
- offerAgreementId: z
533
- .string()
534
- .optional()
535
- .describe('Explicit open offer to claim for this worker'),
536
- })),
537
- kickoff: z
538
- .boolean()
539
- .optional()
540
- .describe('When true and readyForKickoff, also POST /tasks on the hire for relay-coordinator'),
541
- }, WRITE, async ({ hireAgreementId, chatId, inputArtifactIds, steps, kickoff }) => {
542
- try {
543
- const result = await provisionRelayWorkers({
544
- creds,
545
- hireAgreementId,
546
- chatId,
547
- inputArtifactIds,
548
- steps,
549
- });
550
- const relayTaskBody = buildRelayCoordinatorTaskBody({
551
- hireAgreementId,
552
- payload: result.payload,
553
- });
554
- let task;
555
- if (kickoff && result.readyForKickoff) {
556
- task = await createTask(relayTaskBody, creds);
557
- }
558
- return textResult({
559
- ...result,
560
- relayTaskBody,
561
- task,
562
- nextSteps: result.readyForKickoff
563
- ? kickoff && task
564
- ? 'Relay coordinator task created — watch Execution for step progress.'
565
- : 'All worker agreements active — POST relayTaskBody via createTask or set kickoff=true.'
566
- : `Worker approval pending on: ${result.pendingApprovals.join(', ')}. Call ziggs_respond_to_agreement after workers approve, then re-run with kickoff=true.`,
567
- });
568
- }
569
- catch (e) {
570
- return toolError(e.message);
571
- }
572
- });
485
+ if (!cfg.coreOnly) {
486
+ registerMarketplaceTools(server, creds);
487
+ }
573
488
  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).', {
574
489
  agreementId: z.string(),
575
490
  action: z.enum(['approve', 'reject']),
@@ -688,145 +603,23 @@ export function registerZiggsTools(server, creds, cfg) {
688
603
  return toolError(e.message);
689
604
  }
690
605
  });
691
- 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.', {
692
- scopeKind: z
693
- .array(z.enum(['chat', 'agreement', 'org', 'connection', 'wallet']))
694
- .optional()
695
- .describe('Rail filter (repeatable). Omit for every rail you can read.'),
696
- health: z
697
- .enum(['active', 'expired', 'revoked'])
698
- .optional()
699
- .describe('Grant health filter. Defaults to active (live grants only).'),
700
- cursor: z
701
- .string()
702
- .optional()
703
- .describe('Opaque cursor from a prior nextCursor'),
704
- limit: z.number().optional().describe('Page size (default server-side)'),
705
- }, READ_ONLY, async ({ scopeKind, health, cursor, limit }) => {
706
- try {
707
- const client = new GrantsClient(creds.operatorKey, creds.agentId);
708
- const kinds = scopeKind && scopeKind.length
709
- ? scopeKind
710
- : undefined;
711
- const { items, nextCursor } = await client.listGrants({
712
- scopeKind: kinds,
713
- // Reach = live grants only by default; pass health for expired/revoked.
714
- health: health ?? 'active',
715
- cursor,
716
- limit,
717
- });
718
- const unreadable = unreadableGrantRails(creds.operatorKey, kinds);
719
- return textResult({
720
- count: items.length,
721
- grants: items,
722
- nextCursor,
723
- ...(unreadable && unreadable.length
724
- ? { unreadableRails: unreadable }
725
- : {}),
726
- });
727
- }
728
- catch (e) {
729
- return toolError(e.message);
730
- }
731
- });
732
- 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.', {
733
- grantId: z
734
- .string()
735
- .describe('A grant you hold (grantId from ziggs_list_grants) to expand'),
736
- }, READ_ONLY, async ({ grantId }) => {
737
- try {
738
- const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
739
- return textResult(await client.getReach(grantId));
740
- }
741
- catch (e) {
742
- return toolError(e.message);
743
- }
744
- });
745
- 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. Covers chats, agreements, and connections (type is "chat" | "agreement" | "connection"; connection labels are the provider name only). 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 () => {
746
- try {
747
- const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
748
- const items = await client.discoverGrantable();
749
- return textResult({ count: items.length, items });
750
- }
751
- catch (e) {
752
- return toolError(e.message);
753
- }
606
+ registerCapabilities(server, GRANTS_CAPABILITIES, creds);
607
+ registerCapability(server, contextExpandReachCapability, creds);
608
+ registerCapability(server, contextDiscoverGrantableCapability, creds);
609
+ // The read-plan is MCP-local decoration (its next-call tool names and the
610
+ // inbox loop it feeds are this surface's); schema + handler stay shared.
611
+ registerCapability(server, contextReadCapability, creds, {
612
+ transformResult: (result, args) => {
613
+ const readPlan = buildReadContextReadPlan(result, args['type'], args['via'], args['contextGrantId']);
614
+ return readPlan.length
615
+ ? { ...result, readPlan }
616
+ : result;
617
+ },
754
618
  });
755
- server.tool('ziggs_read_context', 'Read the contents of a scope you already hold: messages | artifacts | agreements | tasks (the type param), under via=chat:<id>, agreement:<id>, or task:<id>. Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a `readPlan` with the next page and/or forward-delta call pre-filled (after=this page\'s latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist (your chats / tasks / agreements / grants / links), use the ziggs_list_* tools.', {
756
- type: contextReadTypeSchema.describe('Resource type to read'),
757
- via: z
758
- .string()
759
- .describe('Scope entry, e.g. chat:<id>, agreement:<id>, task:<id>'),
760
- cursor: z.string().optional().describe('Opaque cursor from prior nextCursor'),
761
- after: z
762
- .string()
763
- .optional()
764
- .describe('ISO timestamp for forward-delta (messages/artifacts)'),
765
- direction: z
766
- .enum(['forward'])
767
- .optional()
768
- .describe('Use forward with after for message forward-delta'),
769
- limit: z.number().optional().describe('Page size (default server-side)'),
770
- state: z.string().optional().describe('Task state filter (tasks only)'),
771
- contextGrantId: z
772
- .string()
773
- .optional()
774
- .describe('Pin a specific grant when holding several'),
775
- }, READ_ONLY, async ({ type, via, cursor, after, direction, limit, state, contextGrantId }) => {
776
- try {
777
- const client = new ContextReadClient(creds.operatorKey, creds.agentId);
778
- const result = await client.read(type, {
779
- via,
780
- cursor,
781
- after,
782
- direction,
783
- limit,
784
- state,
785
- contextGrantId,
786
- });
787
- const readPlan = buildReadContextReadPlan(result, type, via, contextGrantId);
788
- return textResult(readPlan.length ? { ...result, readPlan } : result);
789
- }
790
- catch (e) {
791
- return toolError(e.message);
792
- }
793
- });
794
- server.tool('ziggs_record_artifact', ZIGGS_RECORD_ARTIFACT_DESCRIPTION, {
795
- text: z.string().describe('Artifact body'),
796
- visibility: artifactVisibilitySchema.describe('chat = visible to scope parties; agent-private = delegate-only'),
797
- chatId: z.string().optional().describe('Target chat (xor agreementId)'),
798
- agreementId: z
799
- .string()
800
- .optional()
801
- .describe('Target agreement (xor chatId)'),
802
- taskId: z
803
- .string()
804
- .optional()
805
- .describe('Optional task — creates a TaskArtifactLink alongside the primary scope link'),
806
- content_type: z.string().optional().describe('Default text'),
807
- idempotencyKey: z
808
- .string()
809
- .optional()
810
- .describe('Optional dedup key: a redelivered record with the same key no-ops and returns the original artifact. Derive it deterministically (e.g. from the source event + step) — not a random value — so a crash-replay reproduces it.'),
811
- }, WRITE, async ({ text, visibility, chatId, agreementId, taskId, content_type, idempotencyKey }) => {
812
- try {
813
- if ((chatId && agreementId) || (!chatId && !agreementId)) {
814
- return toolError('Pass exactly one of chatId or agreementId');
815
- }
816
- const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({ text, visibility, chatId, agreementId, taskId, content_type, idempotencyKey });
817
- return textResult({
818
- ok: true,
819
- artifactId,
820
- visibility,
821
- chatId,
822
- agreementId,
823
- taskId,
824
- reportingHint: recordArtifactReportingHint(content_type, taskId),
825
- });
826
- }
827
- catch (e) {
828
- return toolError(e.message);
829
- }
619
+ // Description override keeps the reporting rule sourced from the shared
620
+ // PROTOCOL const (ZIG-557) so it can't drift from SKILL/server instructions.
621
+ registerCapability(server, recordArtifactCapability, creds, {
622
+ description: ZIGGS_RECORD_ARTIFACT_DESCRIPTION,
830
623
  });
831
624
  // ---------------------------------------------------------------------------
832
625
  // Task mutation tools (ZIG-555)
@@ -935,67 +728,11 @@ export function registerZiggsTools(server, creds, cfg) {
935
728
  // ---------------------------------------------------------------------------
936
729
  // Connection proxy (ZIG-569)
937
730
  // ---------------------------------------------------------------------------
938
- server.tool('ziggs_connection_proxy', "Use a stored connection (a third-party credential, e.g. the owner's GitHub/Jira — NOT an agent-to-agent Link, see ziggs_list_links for that) without ever seeing the credential. " +
939
- 'Calls the backend connections proxy with a grant the owner issued to this agent: the proxy enforces the grant, decrypts the token server-side, makes the upstream provider call, and returns the result (token-leak guarded on both sides). ' +
940
- 'Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. ' +
941
- "Don't know connectionId/grantId yet? Call ziggs_list_my_connections first.", {
942
- connectionId: z.string().describe('Connection to act on'),
943
- grantId: z
944
- .string()
945
- .describe('Grant the owner issued to this agent for the connection'),
946
- action: z.string().describe('Provider action, e.g. repo:read'),
947
- payload: z
948
- .record(z.unknown())
949
- .optional()
950
- .describe('Action-specific arguments (provider-defined)'),
951
- }, WRITE, async ({ connectionId, grantId, action, payload }) => {
952
- try {
953
- const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).proxy({ connectionId, grantId, action, payload });
954
- return textResult({ ok: true, action, result });
955
- }
956
- catch (e) {
957
- return toolError(e.message);
958
- }
959
- });
960
- 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. ' +
961
- '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). ' +
962
- '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 () => {
963
- try {
964
- const connections = await listConnectionsForHolder(creds);
965
- return textResult({ connections });
966
- }
967
- catch (e) {
968
- return toolError(e.message);
969
- }
970
- });
971
- server.tool('ziggs_request_connection', 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
972
- 'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
973
- 'On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_list_my_connections for use with ziggs_connection_proxy.', {
974
- chatId: z
975
- .string()
976
- .describe('The chat you are working in — the consent card is opened there'),
977
- serverUrl: z.string().describe('Remote MCP server URL (https)'),
978
- tools: z
979
- .array(z.string())
980
- .describe("Tool names you want — become the grant's allowed_actions caveats"),
981
- reason: z
982
- .string()
983
- .optional()
984
- .describe('Plain-language reason shown to the human deciding'),
985
- }, WRITE, async ({ chatId, serverUrl, tools, reason }) => {
986
- try {
987
- const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).requestMcpConnection({ chatId, serverUrl, tools, reason });
988
- return textResult({
989
- ok: true,
990
- ...result,
991
- note: 'A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. ' +
992
- 'Once approved, the connection + grant appear in ziggs_list_my_connections for ziggs_connection_proxy.',
993
- });
994
- }
995
- catch (e) {
996
- return toolError(e.message);
997
- }
998
- });
731
+ if (!cfg.coreOnly) {
732
+ registerConnectionTools(server, creds);
733
+ }
999
734
  registerTrustTools(server, creds, cfg);
1000
- registerPaymentTools(server, creds);
735
+ if (!cfg.coreOnly) {
736
+ registerPaymentTools(server, creds);
737
+ }
1001
738
  }