@ziggs-ai/ziggs-mcp 0.1.25 → 0.1.26

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
 
@@ -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;
@@ -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')
@@ -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,6 +246,20 @@ 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);
@@ -256,7 +267,7 @@ async function loadSessionActionsPayload(creds, cfg) {
256
267
  let activeTasks = [];
257
268
  try {
258
269
  const listed = await listTasks({ state: 'active', limit: 20 }, creds);
259
- activeTasks = listed.tasks ?? [];
270
+ activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
260
271
  }
261
272
  catch {
262
273
  // Inbox is still useful when task listing fails.
@@ -477,7 +488,7 @@ export function registerZiggsTools(server, creds, cfg) {
477
488
  return toolError(e.message);
478
489
  }
479
490
  });
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.', {
491
+ 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
492
  proposedTo: z.string(),
482
493
  chatId: z.string(),
483
494
  description: z.string(),
@@ -486,7 +497,11 @@ export function registerZiggsTools(server, creds, cfg) {
486
497
  .optional()
487
498
  .describe('Human user id = payer (your userId)'),
488
499
  price: z.number().optional().describe('Optional; does not trigger transfer by itself'),
489
- }, WRITE, async ({ proposedTo, chatId, description, payerId, price }) => {
500
+ engagementKind: z
501
+ .enum(['hire', 'service'])
502
+ .optional()
503
+ .describe("'service' (default) = one-off deliverable; 'hire' = ongoing engagement"),
504
+ }, WRITE, async ({ proposedTo, chatId, description, payerId, price, engagementKind }) => {
490
505
  try {
491
506
  const resolvedPayer = payerId ?? cfg.ZIGGS_OWNER_USER_ID;
492
507
  if (!resolvedPayer) {
@@ -498,7 +513,7 @@ export function registerZiggsTools(server, creds, cfg) {
498
513
  description,
499
514
  payerId: resolvedPayer,
500
515
  price,
501
- engagementKind: 'service',
516
+ engagementKind: engagementKind ?? 'service',
502
517
  }, creds);
503
518
  return textResult({ agreement });
504
519
  }
@@ -606,7 +621,7 @@ export function registerZiggsTools(server, creds, cfg) {
606
621
  let activeTasks = [];
607
622
  try {
608
623
  const listed = await listTasks({ state: 'active', limit: 20 }, creds);
609
- activeTasks = listed.tasks ?? [];
624
+ activeTasks = filterTasksForDelegate(listed.tasks ?? [], delegateSelfIds(creds, cfg));
610
625
  }
611
626
  catch {
612
627
  // omit work card when tasks fail
@@ -693,7 +708,7 @@ export function registerZiggsTools(server, creds, cfg) {
693
708
  if ((chatId && agreementId) || (!chatId && !agreementId)) {
694
709
  return toolError('Pass exactly one of chatId or agreementId');
695
710
  }
696
- await writeArtifactStrict(creds, {
711
+ const { artifactId } = await writeArtifactStrict(creds, {
697
712
  text,
698
713
  visibility,
699
714
  chatId,
@@ -703,6 +718,7 @@ export function registerZiggsTools(server, creds, cfg) {
703
718
  });
704
719
  return textResult({
705
720
  ok: true,
721
+ artifactId,
706
722
  visibility,
707
723
  chatId,
708
724
  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.26",
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": {