@ziggs-ai/ziggs-mcp 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  MCP (stdio) server for **Claude Code**, **Cursor**, and other MCP hosts.
4
4
 
5
- **In scope:** chat, agreements, scope, context discovery/reads, artifacts.
6
- **Out of scope (by design):** agent `transfer`, hire agreements, capability tokens / bounded spend.
5
+ **In scope:** chat, agreements (service and hire, direct or published), scope, context discovery/reads, artifacts.
6
+ **Out of scope (by design):** agent `transfer`, payment grants / bounded spend.
7
7
 
8
8
  ---
9
9
 
@@ -73,4 +73,4 @@ export declare function indexReachByScope(reach: ContextReachDescriptor[]): Map<
73
73
  * When `reach` (the caller's own live grants) is passed, each scope is tagged
74
74
  * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
75
75
  */
76
- export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: ContextReachDescriptor[]): Record<string, unknown>;
76
+ export declare function formatInboxToolResult(inbox: InboxEnvelope, ack?: InboxAckResult | null, webOrigin?: string, activeTasks?: Task[], reach?: ContextReachDescriptor[], activeTasksError?: string): Record<string, unknown>;
@@ -205,7 +205,7 @@ function tagScopesWithGrants(scopes, byScope) {
205
205
  * When `reach` (the caller's own live grants) is passed, each scope is tagged
206
206
  * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
207
207
  */
208
- export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach) {
208
+ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach, activeTasksError) {
209
209
  // ZIG-660: build the grant index once and feed both the read plan (grant
210
210
  // pinning) and the scope tags from it — buildReadPlan no longer runs before
211
211
  // the grants are available.
@@ -213,7 +213,7 @@ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach)
213
213
  const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope);
214
214
  const scopes = tagScopesWithGrants(inbox.scopes ?? [], byScope);
215
215
  const origin = resolveWebAppOrigin(webOrigin);
216
- const pending = formatPendingDecisionsPayload(inbox, origin, { activeTasks });
216
+ const pending = formatPendingDecisionsPayload(inbox, origin, { activeTasks, activeTasksError });
217
217
  const pendingTail = pending.hasActionable === true
218
218
  ? {
219
219
  pendingCount: pending.pendingCount,
@@ -226,6 +226,14 @@ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach)
226
226
  : {};
227
227
  const tail = {
228
228
  ...pendingTail,
229
+ // ZIG-700 — always surface a task-fetch failure, even when there is nothing
230
+ // else actionable, so hasActiveWork:false is not read as "no tasks".
231
+ ...(pending.activeTasksFetchError
232
+ ? {
233
+ activeTasksFetchError: pending.activeTasksFetchError,
234
+ workCardOmitted: true,
235
+ }
236
+ : {}),
229
237
  ...(readPlan.length ? { readPlan } : {}),
230
238
  ...(readPlanTruncated ? { readPlanTruncated } : {}),
231
239
  };
@@ -34,6 +34,20 @@ export declare function agreementsListAppUrl(origin: string): string;
34
34
  /** Where the human connects MCP servers and grants tools (ZIG-686). */
35
35
  export declare function connectionsSettingsAppUrl(origin: string): string;
36
36
  export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
37
+ /**
38
+ * ZIG-658: keep only tasks this delegate (or its principal) is expected to
39
+ * execute. The backend's GET /tasks returns every task reachable in the org,
40
+ * so the session card must not present all of them as "your work".
41
+ *
42
+ * A task is "for me" when:
43
+ * - it is explicitly assigned (`assigneeId`) to one of my ids, or
44
+ * - it has no explicit assignee and one of my ids sits on the executing side
45
+ * of the task/agreement (executor, agent, provider, providerAgent,
46
+ * proposedTo).
47
+ * Tasks with no assignee and no readable agreement parties are excluded —
48
+ * "can't tell" must not render as "yours".
49
+ */
50
+ export declare function filterTasksForDelegate(tasks: Task[], selfIds: ReadonlySet<string>): Task[];
37
51
  export declare function buildActiveWorkItems(tasks: Task[], webOrigin: string): ActiveWorkItem[];
38
52
  export declare function buildDecisionChatCard(items: PendingDecisionItem[], opts: {
39
53
  truncatedProposals?: number;
@@ -52,5 +66,6 @@ export declare function buildSessionChatCard(decisions: PendingDecisionItem[], w
52
66
  /** Structured session payload for MCP tools (ZIG-625 + active work). */
53
67
  export declare function formatPendingDecisionsPayload(inbox: InboxEnvelope, webOrigin: string, opts?: {
54
68
  activeTasks?: Task[];
69
+ activeTasksError?: string;
55
70
  }): Record<string, unknown>;
56
71
  export declare function buildPendingNextActions(decisions: PendingDecisionItem[], work?: ActiveWorkItem[]): string[];
@@ -109,6 +109,32 @@ export function buildPendingDecisionItems(inbox, webOrigin) {
109
109
  }
110
110
  return items;
111
111
  }
112
+ /**
113
+ * ZIG-658: keep only tasks this delegate (or its principal) is expected to
114
+ * execute. The backend's GET /tasks returns every task reachable in the org,
115
+ * so the session card must not present all of them as "your work".
116
+ *
117
+ * A task is "for me" when:
118
+ * - it is explicitly assigned (`assigneeId`) to one of my ids, or
119
+ * - it has no explicit assignee and one of my ids sits on the executing side
120
+ * of the task/agreement (executor, agent, provider, providerAgent,
121
+ * proposedTo).
122
+ * Tasks with no assignee and no readable agreement parties are excluded —
123
+ * "can't tell" must not render as "yours".
124
+ */
125
+ export function filterTasksForDelegate(tasks, selfIds) {
126
+ const mine = (id) => typeof id === 'string' && id.length > 0 && selfIds.has(id);
127
+ return tasks.filter((t) => {
128
+ if (t.assigneeId)
129
+ return mine(t.assigneeId);
130
+ if (mine(t.executorId) || mine(t.agentId))
131
+ return true;
132
+ const p = t.agreement?.parties;
133
+ if (!p)
134
+ return false;
135
+ return mine(p.provider) || mine(p.providerAgent) || mine(p.proposedTo);
136
+ });
137
+ }
112
138
  export function buildActiveWorkItems(tasks, webOrigin) {
113
139
  return tasks
114
140
  .filter((t) => t.state === 'active')
@@ -381,6 +407,14 @@ export function formatPendingDecisionsPayload(inbox, webOrigin, opts) {
381
407
  ...(workChatCard ? { workChatCard } : {}),
382
408
  ...(sessionChatCard ? { sessionChatCard } : {}),
383
409
  ...(inbox.humanAttention ? { humanAttention: inbox.humanAttention } : {}),
410
+ // ZIG-700 — when the active-task fetch failed, say so instead of letting
411
+ // hasActiveWork:false read as "no tasks". Mirrors the inbox fetchError signal.
412
+ ...(opts?.activeTasksError
413
+ ? {
414
+ activeTasksFetchError: `Could not load active tasks: ${opts.activeTasksError}`,
415
+ workCardOmitted: true,
416
+ }
417
+ : {}),
384
418
  instruction,
385
419
  };
386
420
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * ZIG-667 — one error surface for every MCP tool.
3
+ *
4
+ * Raw client exceptions read like
5
+ * `ContextReadClient.read messages 403 {"error":"not authorized for this scope"}`
6
+ * — an internal class.method name, a raw HTTP status, and a raw backend body.
7
+ * LLM callers stall on stack-trace prose; they recover from errors they can
8
+ * parse. This module strips the internals, keeps a stable machine-readable
9
+ * code, and for authorization denials says what to do next.
10
+ */
11
+ export interface ToolErrorShape {
12
+ code: string;
13
+ message: string;
14
+ hint?: string;
15
+ }
16
+ /** Classify a raw client error message into a stable shape. */
17
+ export declare function classifyToolError(rawMessage: string): ToolErrorShape;
18
+ /** MCP tool error result: machine-readable code + cleaned message (+ hint). */
19
+ export declare function toolError(message: string): {
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ isError: boolean;
25
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * ZIG-667 — one error surface for every MCP tool.
3
+ *
4
+ * Raw client exceptions read like
5
+ * `ContextReadClient.read messages 403 {"error":"not authorized for this scope"}`
6
+ * — an internal class.method name, a raw HTTP status, and a raw backend body.
7
+ * LLM callers stall on stack-trace prose; they recover from errors they can
8
+ * parse. This module strips the internals, keeps a stable machine-readable
9
+ * code, and for authorization denials says what to do next.
10
+ */
11
+ const CLIENT_PREFIX = /^[A-Z][A-Za-z0-9]*Client\.[A-Za-z0-9_]+\s+/;
12
+ const HTTP_STATUS = /(?:^|\s)([1-5]\d{2})(?=\s|$)/;
13
+ const SCOPE_DENIED_HINT = 'You are not authorized for this scope. To get access: ask the counterparty ' +
14
+ 'to issue you a context grant (they run ziggs_issue_grant), or request a ' +
15
+ 'bilateral link first (ziggs_request_link). Check what you can already ' +
16
+ 'reach with ziggs_discover_context / ziggs_get_scope.';
17
+ function codeForStatus(status) {
18
+ if (status === 401)
19
+ return 'NOT_AUTHENTICATED';
20
+ if (status === 403)
21
+ return 'NOT_AUTHORIZED';
22
+ if (status === 404)
23
+ return 'NOT_FOUND';
24
+ if (status === 409)
25
+ return 'CONFLICT';
26
+ if (status === 429)
27
+ return 'RATE_LIMITED';
28
+ if (status >= 500)
29
+ return 'UPSTREAM_ERROR';
30
+ if (status >= 400)
31
+ return 'BAD_REQUEST';
32
+ return 'TOOL_ERROR';
33
+ }
34
+ /** Pull a human reason out of an embedded backend JSON body, if any. */
35
+ function extractBodyReason(raw) {
36
+ const start = raw.indexOf('{');
37
+ if (start === -1)
38
+ return null;
39
+ try {
40
+ const parsed = JSON.parse(raw.slice(start));
41
+ const reason = parsed['error'] ?? parsed['message'];
42
+ return typeof reason === 'string' && reason.trim() ? reason.trim() : null;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ /** Classify a raw client error message into a stable shape. */
49
+ export function classifyToolError(rawMessage) {
50
+ const cleaned = rawMessage.replace(CLIENT_PREFIX, '').trim();
51
+ const statusMatch = HTTP_STATUS.exec(cleaned);
52
+ const status = statusMatch ? Number(statusMatch[1]) : null;
53
+ const bodyReason = extractBodyReason(cleaned);
54
+ if (status === null) {
55
+ return { code: 'TOOL_ERROR', message: cleaned || rawMessage };
56
+ }
57
+ const code = codeForStatus(status);
58
+ // Prefer the backend's own reason over the transport framing.
59
+ const message = bodyReason ?? cleaned;
60
+ // Context-scope denials (the backend says "…for this scope") get the scope
61
+ // code and a recovery path. Other 403s (connection grants, party checks)
62
+ // keep the generic code — their fixes live in other domains.
63
+ if (code === 'NOT_AUTHORIZED' && /\bscope\b/i.test(message)) {
64
+ return {
65
+ code: 'NOT_AUTHORIZED_FOR_SCOPE',
66
+ message,
67
+ hint: SCOPE_DENIED_HINT,
68
+ };
69
+ }
70
+ return { code, message };
71
+ }
72
+ /** MCP tool error result: machine-readable code + cleaned message (+ hint). */
73
+ export function toolError(message) {
74
+ const shape = classifyToolError(message);
75
+ return {
76
+ content: [
77
+ { type: 'text', text: JSON.stringify({ error: shape }, null, 2) },
78
+ ],
79
+ isError: true,
80
+ };
81
+ }
package/dist/tools.js CHANGED
@@ -4,9 +4,10 @@ import { getAgreement, getMyAgreements, listMyChats, openConversation, proposeDi
4
4
  import { decodeOperatorKeyClaims } from './operatorKey.js';
5
5
  import { registerTrustTools } from './trustTools.js';
6
6
  import { formatInboxToolResult, buildReadContextReadPlan, } from './inboxToolResult.js';
7
- import { connectionsSettingsAppUrl, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
7
+ import { connectionsSettingsAppUrl, filterTasksForDelegate, formatPendingDecisionsPayload, resolveWebAppOrigin, buildPendingNextActions, } from './pendingDecisions.js';
8
8
  import { PROTOCOL } from './protocol/delegateProtocol.js';
9
9
  import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
10
+ import { toolError } from './toolError.js';
10
11
  // ZIG-557: the protocol sentences (loop / ack / humanAttention) are sourced
11
12
  // from the shared const so this description can't drift from SKILL / server
12
13
  // instructions / .cursorrules.
@@ -48,12 +49,6 @@ function textResult(data) {
48
49
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
49
50
  };
50
51
  }
51
- function toolError(message) {
52
- return {
53
- content: [{ type: 'text', text: message }],
54
- isError: true,
55
- };
56
- }
57
52
  const scopeKindSchema = z.enum(['chat', 'agreement', 'task', 'counterparty']);
58
53
  const contextReadTypeSchema = z.enum([
59
54
  'messages',
@@ -84,6 +79,8 @@ async function writeArtifactStrict(creds, input) {
84
79
  const body = await res.text().catch(() => '');
85
80
  throw new Error(`POST /artifacts ${res.status} ${body.slice(0, 200)}`);
86
81
  }
82
+ const body = (await res.json().catch(() => ({})));
83
+ return { artifactId: body.artifactId };
87
84
  }
88
85
  // ZIG-569 — defense-in-depth mirror of the backend leak-guard
89
86
  // (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
@@ -249,19 +246,36 @@ async function listMcpConnectionRequests(creds) {
249
246
  const parsed = body ? JSON.parse(body) : null;
250
247
  return parsed?.['requests'] ?? [];
251
248
  }
249
+ /**
250
+ * Ids this delegate answers for: its own agent id plus its principal's user
251
+ * id (operator-key ownerId / ZIGGS_OWNER_USER_ID). Used to decide which
252
+ * active tasks count as "your work" on session cards (ZIG-658).
253
+ */
254
+ function delegateSelfIds(creds, cfg) {
255
+ const ids = new Set([creds.agentId]);
256
+ const ownerId = decodeOperatorKeyClaims(creds.operatorKey)?.ownerId;
257
+ if (ownerId)
258
+ ids.add(ownerId);
259
+ if (cfg.ZIGGS_OWNER_USER_ID)
260
+ ids.add(cfg.ZIGGS_OWNER_USER_ID);
261
+ return ids;
262
+ }
252
263
  async function loadSessionActionsPayload(creds, cfg) {
253
264
  const webOrigin = resolveWebAppOrigin(cfg.ZIGGS_WEB_URL);
254
265
  const client = new InboxClient(creds.operatorKey, creds.agentId);
255
266
  const inbox = await client.getInbox();
256
267
  let activeTasks = [];
268
+ let activeTasksError;
257
269
  try {
258
270
  const listed = await listTasks({ state: 'active', limit: 20 }, creds);
259
- activeTasks = listed.tasks ?? [];
271
+ activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
260
272
  }
261
- catch {
262
- // Inbox is still useful when task listing fails.
273
+ catch (e) {
274
+ // ZIG-700 — inbox is still useful when task listing fails, but surface the
275
+ // failure so hasActiveWork:false is not mistaken for "no tasks".
276
+ activeTasksError = e.message;
263
277
  }
264
- return formatPendingDecisionsPayload(inbox, webOrigin, { activeTasks });
278
+ return formatPendingDecisionsPayload(inbox, webOrigin, { activeTasks, activeTasksError });
265
279
  }
266
280
  export function registerZiggsTools(server, creds, cfg) {
267
281
  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 () => {
@@ -477,7 +491,7 @@ export function registerZiggsTools(server, creds, cfg) {
477
491
  return toolError(e.message);
478
492
  }
479
493
  });
480
- server.tool('ziggs_propose_agreement', 'Propose a direct service agreement to one counterparty in a chat, as the payer-side delegate. Requires payerId (or ZIGGS_OWNER_USER_ID). price is recorded on the agreement but does not itself trigger a transfer — V1 has no real payment rail yet.', {
494
+ server.tool('ziggs_propose_agreement', 'Propose a direct agreement to one counterparty in a chat, as the payer-side delegate. engagementKind "service" (default) = one deliverable; "hire" = an ongoing engagement (same kinds ziggs_publish_offer accepts). Requires payerId (or ZIGGS_OWNER_USER_ID). price is recorded on the agreement but does not itself trigger a transfer — V1 has no real payment rail yet.', {
481
495
  proposedTo: z.string(),
482
496
  chatId: z.string(),
483
497
  description: z.string(),
@@ -486,7 +500,11 @@ export function registerZiggsTools(server, creds, cfg) {
486
500
  .optional()
487
501
  .describe('Human user id = payer (your userId)'),
488
502
  price: z.number().optional().describe('Optional; does not trigger transfer by itself'),
489
- }, WRITE, async ({ proposedTo, chatId, description, payerId, price }) => {
503
+ engagementKind: z
504
+ .enum(['hire', 'service'])
505
+ .optional()
506
+ .describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
507
+ }, WRITE, async ({ proposedTo, chatId, description, payerId, price, engagementKind }) => {
490
508
  try {
491
509
  const resolvedPayer = payerId ?? cfg.ZIGGS_OWNER_USER_ID;
492
510
  if (!resolvedPayer) {
@@ -498,7 +516,7 @@ export function registerZiggsTools(server, creds, cfg) {
498
516
  description,
499
517
  payerId: resolvedPayer,
500
518
  price,
501
- engagementKind: 'service',
519
+ engagementKind: engagementKind ?? 'service',
502
520
  }, creds);
503
521
  return textResult({ agreement });
504
522
  }
@@ -604,12 +622,14 @@ export function registerZiggsTools(server, creds, cfg) {
604
622
  const acked = ack?.length ? await client.ack(ack) : null;
605
623
  const inbox = await client.getInbox();
606
624
  let activeTasks = [];
625
+ let activeTasksError;
607
626
  try {
608
627
  const listed = await listTasks({ state: 'active', limit: 20 }, creds);
609
- activeTasks = listed.tasks ?? [];
628
+ activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
610
629
  }
611
- catch {
612
- // omit work card when tasks fail
630
+ catch (e) {
631
+ // ZIG-700 surface the failure instead of silently omitting the work card.
632
+ activeTasksError = e.message;
613
633
  }
614
634
  // ZIG-635: tag each scope with the covering grant from the caller's own
615
635
  // live reach descriptors. Best-effort — the inbox still works untagged.
@@ -620,7 +640,7 @@ export function registerZiggsTools(server, creds, cfg) {
620
640
  catch {
621
641
  // omit grant tags when discovery fails
622
642
  }
623
- return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach));
643
+ return textResult(formatInboxToolResult(inbox, acked, cfg.ZIGGS_WEB_URL, activeTasks, reach, activeTasksError));
624
644
  }
625
645
  catch (e) {
626
646
  return toolError(e.message);
@@ -693,7 +713,7 @@ export function registerZiggsTools(server, creds, cfg) {
693
713
  if ((chatId && agreementId) || (!chatId && !agreementId)) {
694
714
  return toolError('Pass exactly one of chatId or agreementId');
695
715
  }
696
- await writeArtifactStrict(creds, {
716
+ const { artifactId } = await writeArtifactStrict(creds, {
697
717
  text,
698
718
  visibility,
699
719
  chatId,
@@ -703,6 +723,7 @@ export function registerZiggsTools(server, creds, cfg) {
703
723
  });
704
724
  return textResult({
705
725
  ok: true,
726
+ artifactId,
706
727
  visibility,
707
728
  chatId,
708
729
  agreementId,
@@ -1,17 +1,12 @@
1
1
  import { z } from 'zod';
2
2
  import { AgentSearchClient, ContextGrantsClient, createAgreement, listAgreements, revokeAgreement, claimAgreement, addChatMember, } from '@ziggs-ai/api-client';
3
3
  import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
4
+ import { toolError } from './toolError.js';
4
5
  function textResult(data) {
5
6
  return {
6
7
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
7
8
  };
8
9
  }
9
- function toolError(message) {
10
- return {
11
- content: [{ type: 'text', text: message }],
12
- isError: true,
13
- };
14
- }
15
10
  const grantScopeKindSchema = z.enum(['chat', 'agreement', 'org']);
16
11
  const contextTemporalSchema = z.enum(['from-now', 'from-start']);
17
12
  const DEFAULT_WEB_URL = 'https://ziggsai.com';
@@ -29,9 +24,21 @@ export function registerTrustTools(server, creds, cfg) {
29
24
  if (!result.success) {
30
25
  return toolError(result.error ?? result.message ?? 'search failed');
31
26
  }
27
+ if (!result.agents?.length) {
28
+ // ZIG-664: a bare {count: 0} reads as "discovery is down" to LLM
29
+ // callers — say what was searched and how to recover instead.
30
+ return textResult({
31
+ count: 0,
32
+ agents: [],
33
+ searched: ['published store', 'your org-mates', 'your linked delegates'],
34
+ hint: 'Zero hits means no agent profile matched these terms — discovery itself is up. ' +
35
+ 'Matching is lexical against agent name/description/tags, so try shorter or different keywords. ' +
36
+ 'If you already know the agent, pass its exact agent id as the query to resolve it directly.',
37
+ });
38
+ }
32
39
  return textResult({
33
- count: result.agents?.length ?? 0,
34
- agents: result.agents ?? [],
40
+ count: result.agents.length,
41
+ agents: result.agents,
35
42
  });
36
43
  }
37
44
  catch (e) {
@@ -197,13 +204,38 @@ export function registerTrustTools(server, creds, cfg) {
197
204
  return toolError(e.message);
198
205
  }
199
206
  });
200
- server.tool('ziggs_list_links', 'List link agreements for this delegate — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_list_my_connections for those) (GET /agreements?engagementKind=link). Each item exposes parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner), proposal.status and status. Approve pending links via ziggs_respond_to_agreement.', {}, READ_ONLY, async () => {
207
+ server.tool('ziggs_list_links', 'List link agreements for this delegate — bilateral agent-to-agent trust relationships, NOT third-party service connections (see ziggs_list_my_connections for those) (GET /agreements?engagementKind=link). Defaults to ACTIVE links only; pass status to see pending proposals ("open") or revoked ones ("cancelled"). Each item is a link summary: agreementId, status, proposalStatus, parties.creatorAgent (requester), parties.providerAgent (target), parties.proposedTo (target owner). Approve pending links via ziggs_respond_to_agreement.', {
208
+ status: z
209
+ .enum(['active', 'open', 'cancelled', 'all'])
210
+ .optional()
211
+ .describe('active (default) = established links; open = pending proposals/invites awaiting approval or claim; cancelled = revoked; all = every link regardless of status'),
212
+ }, READ_ONLY, async ({ status }) => {
201
213
  try {
202
- const links = await listAgreements({ engagementKind: 'link' }, creds);
214
+ const resolvedStatus = status ?? 'active';
215
+ const links = await listAgreements({
216
+ engagementKind: 'link',
217
+ ...(resolvedStatus === 'all' ? {} : { status: resolvedStatus }),
218
+ }, creds);
219
+ // ZIG-670: link-shaped summaries, not raw agreement documents — the
220
+ // money block, approvals array, and Mongo internals are noise here.
221
+ const summaries = links.map((a) => ({
222
+ agreementId: a.agreementId,
223
+ status: a.status,
224
+ proposalStatus: a.proposalStatus,
225
+ parties: {
226
+ creatorAgent: a.parties?.creatorAgent ?? null,
227
+ providerAgent: a.parties?.providerAgent ?? null,
228
+ creator: a.parties?.creator ?? null,
229
+ proposedTo: a.parties?.proposedTo ?? null,
230
+ },
231
+ ...(a.description ? { description: a.description } : {}),
232
+ createdAt: a.createdAt,
233
+ }));
203
234
  const hasActive = links.some((a) => a.status === 'active');
204
235
  return textResult({
205
- count: links.length,
206
- links,
236
+ count: summaries.length,
237
+ status: resolvedStatus,
238
+ links: summaries,
207
239
  ...(hasActive
208
240
  ? {
209
241
  nextSteps: 'A link is reach-only. Use ziggs_open_conversation (participantId = peer agent id) and/or ziggs_issue_grant before reading context.',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "MCP server for Claude Code, Cursor, and other MCP hosts — act as your Ziggs delegate agent",
5
5
  "type": "module",
6
6
  "bin": {