@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/tools.js CHANGED
@@ -1,8 +1,7 @@
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
- import { fetchMyOrgs } from './orgs.js';
6
5
  import { registerTrustTools } from './trustTools.js';
7
6
  import { registerPaymentTools } from './paymentTools.js';
8
7
  import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
@@ -19,6 +18,7 @@ function buildRelayCoordinatorTaskBody(opts) {
19
18
  }
20
19
  import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
21
20
  import { toolError } from './toolError.js';
21
+ import { registerCapability, registerCapabilities, textResult, } from './capabilityAdapter.js';
22
22
  // ZIG-557: the protocol sentences (loop / ack / humanAttention) are sourced
23
23
  // from the shared const so this description can't drift from SKILL / server
24
24
  // instructions / .cursorrules.
@@ -47,75 +47,12 @@ const ZIGGS_SEND_MESSAGE_DESCRIPTION = 'Send a chat message as the delegate agen
47
47
  const ZIGGS_RECORD_ARTIFACT_DESCRIPTION = 'Write an artifact to a chat or agreement scope. Set visibility explicitly. ' +
48
48
  'For a finished deliverable, set content_type=result and pass taskId to bind it to the task. ' +
49
49
  PROTOCOL.reporting;
50
- /** Teach the result slot on the record_artifact success path (ZIG-560). */
51
- function recordArtifactReportingHint(contentType, taskId) {
52
- if (contentType === 'result') {
53
- return taskId
54
- ? 'Recorded as a task-bound result artifact. Close the task by setting its terminal result with ziggs_set_task_result ({ summary, status, links }).'
55
- : '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 }).';
56
- }
57
- 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.';
58
- }
59
- function textResult(data) {
60
- return {
61
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
62
- };
63
- }
64
- const contextReadTypeSchema = z.enum([
65
- 'messages',
66
- 'artifacts',
67
- 'agreements',
68
- 'tasks',
69
- ]);
70
- const artifactVisibilitySchema = z.enum(['chat', 'agent-private']);
71
50
  // ZIG-899 — the strict artifact write (fail loudly, return the artifactId)
72
51
  // moved into ArtifactsClient.writeStrict, shared with the SDK's record_artifact.
73
- // ZIG-894 — the leak-guard and the connections proxy/request calls moved into
74
- // @ziggs-ai/api-client's ConnectionsClient (shared with the agent SDK).
75
- /** ZIG-640 — runtime acting org from server (self-hire / agent row). */
76
- async function fetchDelegateAccess(creds) {
77
- const url = `${getBackendUrl()}/agents/claude-delegate/access`;
78
- const res = await fetch(url, {
79
- method: 'GET',
80
- headers: {
81
- Authorization: `Bearer ${creds.operatorKey}`,
82
- 'X-Agent-Id': creds.agentId,
83
- },
84
- });
85
- const body = await res.text().catch(() => '');
86
- if (!res.ok) {
87
- throw new Error(`GET /agents/claude-delegate/access ${res.status} ${body.slice(0, 200)}`);
88
- }
89
- return body ? JSON.parse(body) : {};
90
- }
91
- /**
92
- * ZIG-641 / ZIG-648 — cross-connection discovery over the unified GET /grants:
93
- * every connection grant this agent holds, grouped by connection so
94
- * ziggs_connection_proxy's connectionId/grantId no longer has to arrive out of
95
- * band. `provider` comes from the grant's resolved scope label. The response is
96
- * scanned defensively for leaked secrets, as ConnectionsClient.proxy does.
97
- */
98
- async function listConnectionsForHolder(creds) {
99
- const client = new GrantsClient(creds.operatorKey, creds.agentId);
100
- // All pages of the agent's live connection grants (not just the first page).
101
- const items = await client.listAllGrants({
102
- scopeKind: 'connection',
103
- health: 'active',
104
- });
105
- const byConnection = new Map();
106
- for (const g of items) {
107
- const connectionId = g.scope.id;
108
- let group = byConnection.get(connectionId);
109
- if (!group) {
110
- group = { connectionId, provider: g.scope.label ?? null, grants: [] };
111
- byConnection.set(connectionId, group);
112
- }
113
- group.grants.push(g);
114
- }
115
- const result = [...byConnection.values()];
116
- assertNoLeakedConnectionSecret(JSON.stringify(result));
117
- return result;
118
- }
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).
119
56
  /**
120
57
  * Ids this delegate answers for: its own agent id plus its principal's user
121
58
  * id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
@@ -283,67 +220,19 @@ function registerMarketplaceTools(server, creds) {
283
220
  });
284
221
  }
285
222
  function registerConnectionTools(server, creds) {
286
- 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. " +
287
- '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). ' +
288
- 'Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. ' +
289
- "Don't know connectionId/grantId yet? Call ziggs_list_my_connections first.", {
290
- connectionId: z.string().describe('Connection to act on'),
291
- grantId: z
292
- .string()
293
- .describe('Grant the owner issued to this agent for the connection'),
294
- action: z.string().describe('Provider action, e.g. repo:read'),
295
- payload: z
296
- .record(z.unknown())
297
- .optional()
298
- .describe('Action-specific arguments (provider-defined)'),
299
- }, WRITE, async ({ connectionId, grantId, action, payload }) => {
300
- try {
301
- const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).proxy({ connectionId, grantId, action, payload });
302
- return textResult({ ok: true, action, result });
303
- }
304
- catch (e) {
305
- return toolError(e.message);
306
- }
307
- });
223
+ registerCapability(server, connectionProxyCapability, creds);
308
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. ' +
309
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). ' +
310
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 () => {
311
227
  try {
312
- const connections = await listConnectionsForHolder(creds);
228
+ const connections = await new ConnectionsClient(creds.operatorKey, creds.agentId).listForHolder();
313
229
  return textResult({ connections });
314
230
  }
315
231
  catch (e) {
316
232
  return toolError(e.message);
317
233
  }
318
234
  });
319
- server.tool('ziggs_request_connection', 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
320
- '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). ' +
321
- '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.', {
322
- chatId: z
323
- .string()
324
- .describe('The chat you are working in — the consent card is opened there'),
325
- serverUrl: z.string().describe('Remote MCP server URL (https)'),
326
- tools: z
327
- .array(z.string())
328
- .describe("Tool names you want — become the grant's allowed_actions caveats"),
329
- reason: z
330
- .string()
331
- .optional()
332
- .describe('Plain-language reason shown to the human deciding'),
333
- }, WRITE, async ({ chatId, serverUrl, tools, reason }) => {
334
- try {
335
- const result = await new ConnectionsClient(creds.operatorKey, creds.agentId).requestMcpConnection({ chatId, serverUrl, tools, reason });
336
- return textResult({
337
- ok: true,
338
- ...result,
339
- 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. ' +
340
- 'Once approved, the connection + grant appear in ziggs_list_my_connections for ziggs_connection_proxy.',
341
- });
342
- }
343
- catch (e) {
344
- return toolError(e.message);
345
- }
346
- });
235
+ registerCapability(server, requestConnectionCapability, creds);
347
236
  }
348
237
  export function registerZiggsTools(server, creds, cfg) {
349
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 () => {
@@ -527,17 +416,7 @@ export function registerZiggsTools(server, creds, cfg) {
527
416
  return toolError(e.message);
528
417
  }
529
418
  });
530
- 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.', {
531
- participantId: z.string().describe('User or agent id to converse with'),
532
- }, WRITE, async ({ participantId }) => {
533
- try {
534
- const out = await openConversation(participantId, creds);
535
- return textResult(out);
536
- }
537
- catch (e) {
538
- return toolError(e.message);
539
- }
540
- });
419
+ registerCapability(server, openConversationCapability, creds);
541
420
  server.tool('ziggs_send_message', ZIGGS_SEND_MESSAGE_DESCRIPTION, {
542
421
  chatId: z.string(),
543
422
  receiverId: z
@@ -574,7 +453,7 @@ export function registerZiggsTools(server, creds, cfg) {
574
453
  return toolError(e.message);
575
454
  }
576
455
  });
577
- server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side — there is no payer input. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays). Set providerId to your own agent id to offer (you work, proposedTo pays). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. price is recorded on the agreement but does not itself trigger a transfer.', {
456
+ server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty (proposedTo) in a chat. The payer is always derived server-side as the non-providing side — there is no payer input. Omit providerId (or set it to proposedTo) to commission the recipient (they work, your side pays). Set providerId to your own agent id to offer (you work, proposedTo pays). Set providerId to a third-party agent id to broker (they work, proposedTo pays) — that provider must have an active published offer whose terms match this proposal (price, lifecycle, engagementKind, etc.) or the call fails naming the mismatched field. engagementKind "service" (default) = one deliverable; "hire" = ongoing engagement. Agreements are STANDING by default (lifecycle "open": no expiry, unlimited tasks) — hire once, then keep spawning tasks under the same agreement; set expiresAt (time-bound) or maxExecutions (count-bound) only when the engagement should end on its own. price is recorded on the agreement but does not itself trigger a transfer.', {
578
457
  proposedTo: z.string(),
579
458
  chatId: z.string(),
580
459
  description: z.string(),
@@ -587,7 +466,25 @@ export function registerZiggsTools(server, creds, cfg) {
587
466
  .enum(['hire', 'service'])
588
467
  .optional()
589
468
  .describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
590
- }, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind }) => {
469
+ expiresAt: z
470
+ .string()
471
+ .optional()
472
+ .describe('ISO date: the agreement ends (is cancelled, tasks and all) at this time. Omit for a standing agreement.'),
473
+ maxExecutions: z
474
+ .number()
475
+ .int()
476
+ .positive()
477
+ .optional()
478
+ .describe('The agreement auto-fulfills after this many completed tasks. Omit for unlimited tasks.'),
479
+ lifecycle: z
480
+ .enum(['open', 'time-bound', 'count-bound'])
481
+ .optional()
482
+ .describe("Usually inferred: expiresAt → 'time-bound', maxExecutions → 'count-bound', neither → 'open' (standing)."),
483
+ billing: z
484
+ .enum(['total', 'per_task'])
485
+ .optional()
486
+ .describe("How price reads. 'total' (default) = one price for the whole engagement, escrowed now and paid at the end. 'per_task' = a RATE charged for each completed task, paid as work lands — requires a standing (open) agreement, and is the default for a hire. Never send 'per_task' for a one-off price or the payer is charged it once per task."),
487
+ }, WRITE, async ({ proposedTo, chatId, description, providerId, price, engagementKind, expiresAt, maxExecutions, lifecycle, billing, }) => {
591
488
  try {
592
489
  const agreement = await proposeDirectTo({
593
490
  proposedTo,
@@ -596,6 +493,10 @@ export function registerZiggsTools(server, creds, cfg) {
596
493
  providerId: providerId?.trim() || proposedTo,
597
494
  price,
598
495
  engagementKind: engagementKind ?? 'service',
496
+ expiresAt,
497
+ maxExecutions,
498
+ lifecycle,
499
+ billing,
599
500
  }, creds);
600
501
  return textResult({ agreement });
601
502
  }
@@ -665,7 +566,7 @@ export function registerZiggsTools(server, creds, cfg) {
665
566
  return toolError(e.message);
666
567
  }
667
568
  });
668
- 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.', {
569
+ server.tool('ziggs_fulfill_agreement', 'END an agreement you PROVIDE — permanently (POST /agreements/:id/fulfill). Fulfilling terminates the whole relationship, not one deliverable: every grant the agreement conferred (context, connection, payment) is revoked, its shared space is torn down, and it cannot be reopened — the counterparty would have to re-hire you from scratch. Finished WORK is reported with ziggs_set_task_result, which closes the task and leaves the agreement standing for the next one. Only fulfill a count/time-bound engagement whose full scope is delivered and where nothing more is expected — never a standing hire that just finished a task. Party-gated server-side: only the providing side can fulfill.', {
669
570
  agreementId: z.string().describe('The agreement you provide, to mark fulfilled'),
670
571
  }, WRITE, async ({ agreementId }) => {
671
572
  try {
@@ -678,13 +579,9 @@ export function registerZiggsTools(server, creds, cfg) {
678
579
  });
679
580
  server.tool('ziggs_inbox', ZIGGS_INBOX_DESCRIPTION, {
680
581
  ack: z
681
- .array(z.object({
682
- kind: z.enum(['chat', 'agreement', 'org']),
683
- id: z.string(),
684
- upTo: z.string().describe('ISO timestamp handled up to (inclusive)'),
685
- }))
582
+ .string()
686
583
  .optional()
687
- .describe('Scopes you finished handling — acked before fetching, monotonic'),
584
+ .describe("The envelope's `ackTo` from a previous call, once you have handled everything it carried — acked before fetching, monotonic (an older value is a no-op)."),
688
585
  waitSeconds: z
689
586
  .number()
690
587
  .optional()
@@ -692,7 +589,7 @@ export function registerZiggsTools(server, creds, cfg) {
692
589
  }, READ_ONLY, async ({ ack, waitSeconds }) => {
693
590
  try {
694
591
  const client = new InboxClient(creds.operatorKey, creds.agentId);
695
- const acked = ack?.length ? await client.ack(ack) : null;
592
+ const acked = ack ? await client.ack(ack) : null;
696
593
  const inbox = await client.getInbox(waitSeconds != null ? { waitSeconds } : {});
697
594
  let activeTasks = [];
698
595
  let activeTasksError;
@@ -724,145 +621,23 @@ export function registerZiggsTools(server, creds, cfg) {
724
621
  return toolError(e.message);
725
622
  }
726
623
  });
727
- 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.', {
728
- scopeKind: z
729
- .array(z.enum(['chat', 'agreement', 'org', 'connection', 'wallet']))
730
- .optional()
731
- .describe('Rail filter (repeatable). Omit for every rail you can read.'),
732
- health: z
733
- .enum(['active', 'expired', 'revoked'])
734
- .optional()
735
- .describe('Grant health filter. Defaults to active (live grants only).'),
736
- cursor: z
737
- .string()
738
- .optional()
739
- .describe('Opaque cursor from a prior nextCursor'),
740
- limit: z.number().optional().describe('Page size (default server-side)'),
741
- }, READ_ONLY, async ({ scopeKind, health, cursor, limit }) => {
742
- try {
743
- const client = new GrantsClient(creds.operatorKey, creds.agentId);
744
- const kinds = scopeKind && scopeKind.length
745
- ? scopeKind
746
- : undefined;
747
- const { items, nextCursor } = await client.listGrants({
748
- scopeKind: kinds,
749
- // Reach = live grants only by default; pass health for expired/revoked.
750
- health: health ?? 'active',
751
- cursor,
752
- limit,
753
- });
754
- const unreadable = unreadableGrantRails(creds.operatorKey, kinds);
755
- return textResult({
756
- count: items.length,
757
- grants: items,
758
- nextCursor,
759
- ...(unreadable && unreadable.length
760
- ? { unreadableRails: unreadable }
761
- : {}),
762
- });
763
- }
764
- catch (e) {
765
- return toolError(e.message);
766
- }
767
- });
768
- 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.', {
769
- grantId: z
770
- .string()
771
- .describe('A grant you hold (grantId from ziggs_list_grants) to expand'),
772
- }, READ_ONLY, async ({ grantId }) => {
773
- try {
774
- const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
775
- return textResult(await client.getReach(grantId));
776
- }
777
- catch (e) {
778
- return toolError(e.message);
779
- }
780
- });
781
- 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 () => {
782
- try {
783
- const client = new ContextDiscoveryClient(creds.operatorKey, creds.agentId);
784
- const items = await client.discoverGrantable();
785
- return textResult({ count: items.length, items });
786
- }
787
- catch (e) {
788
- return toolError(e.message);
789
- }
790
- });
791
- 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.', {
792
- type: contextReadTypeSchema.describe('Resource type to read'),
793
- via: z
794
- .string()
795
- .describe('Scope entry, e.g. chat:<id>, agreement:<id>, task:<id>'),
796
- cursor: z.string().optional().describe('Opaque cursor from prior nextCursor'),
797
- after: z
798
- .string()
799
- .optional()
800
- .describe('ISO timestamp for forward-delta (messages/artifacts)'),
801
- direction: z
802
- .enum(['forward'])
803
- .optional()
804
- .describe('Use forward with after for message forward-delta'),
805
- limit: z.number().optional().describe('Page size (default server-side)'),
806
- state: z.string().optional().describe('Task state filter (tasks only)'),
807
- contextGrantId: z
808
- .string()
809
- .optional()
810
- .describe('Pin a specific grant when holding several'),
811
- }, READ_ONLY, async ({ type, via, cursor, after, direction, limit, state, contextGrantId }) => {
812
- try {
813
- const client = new ContextReadClient(creds.operatorKey, creds.agentId);
814
- const result = await client.read(type, {
815
- via,
816
- cursor,
817
- after,
818
- direction,
819
- limit,
820
- state,
821
- contextGrantId,
822
- });
823
- const readPlan = buildReadContextReadPlan(result, type, via, contextGrantId);
824
- return textResult(readPlan.length ? { ...result, readPlan } : result);
825
- }
826
- catch (e) {
827
- return toolError(e.message);
828
- }
624
+ registerCapabilities(server, GRANTS_CAPABILITIES, creds);
625
+ registerCapability(server, contextExpandReachCapability, creds);
626
+ registerCapability(server, contextDiscoverGrantableCapability, creds);
627
+ // The read-plan is MCP-local decoration (its next-call tool names and the
628
+ // inbox loop it feeds are this surface's); schema + handler stay shared.
629
+ registerCapability(server, contextReadCapability, creds, {
630
+ transformResult: (result, args) => {
631
+ const readPlan = buildReadContextReadPlan(result, args['type'], args['via'], args['contextGrantId']);
632
+ return readPlan.length
633
+ ? { ...result, readPlan }
634
+ : result;
635
+ },
829
636
  });
830
- server.tool('ziggs_record_artifact', ZIGGS_RECORD_ARTIFACT_DESCRIPTION, {
831
- text: z.string().describe('Artifact body'),
832
- visibility: artifactVisibilitySchema.describe('chat = visible to scope parties; agent-private = delegate-only'),
833
- chatId: z.string().optional().describe('Target chat (xor agreementId)'),
834
- agreementId: z
835
- .string()
836
- .optional()
837
- .describe('Target agreement (xor chatId)'),
838
- taskId: z
839
- .string()
840
- .optional()
841
- .describe('Optional task — creates a TaskArtifactLink alongside the primary scope link'),
842
- content_type: z.string().optional().describe('Default text'),
843
- idempotencyKey: z
844
- .string()
845
- .optional()
846
- .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.'),
847
- }, WRITE, async ({ text, visibility, chatId, agreementId, taskId, content_type, idempotencyKey }) => {
848
- try {
849
- if ((chatId && agreementId) || (!chatId && !agreementId)) {
850
- return toolError('Pass exactly one of chatId or agreementId');
851
- }
852
- const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({ text, visibility, chatId, agreementId, taskId, content_type, idempotencyKey });
853
- return textResult({
854
- ok: true,
855
- artifactId,
856
- visibility,
857
- chatId,
858
- agreementId,
859
- taskId,
860
- reportingHint: recordArtifactReportingHint(content_type, taskId),
861
- });
862
- }
863
- catch (e) {
864
- return toolError(e.message);
865
- }
637
+ // Description override keeps the reporting rule sourced from the shared
638
+ // PROTOCOL const (ZIG-557) so it can't drift from SKILL/server instructions.
639
+ registerCapability(server, recordArtifactCapability, creds, {
640
+ description: ZIGGS_RECORD_ARTIFACT_DESCRIPTION,
866
641
  });
867
642
  // ---------------------------------------------------------------------------
868
643
  // Task mutation tools (ZIG-555)
@@ -1,5 +1,8 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { type Creds } from '@ziggs-ai/api-client';
3
3
  import type { ZiggsMcpConfig } from './config.js';
4
- /** ZIG-433 — agent search + context grant management through MCP. */
4
+ /** ZIG-433 / ZIG-956 — agent search + context grant management through MCP.
5
+ * The SDK-twin tools (search/get, links, delegate) come from the shared
6
+ * capability layer; only the human-authority grant tools (issue/revoke) stay
7
+ * MCP-local. */
5
8
  export declare function registerTrustTools(server: McpServer, creds: Creds, cfg?: ZiggsMcpConfig): void;