@ziggs-ai/ziggs-mcp 0.37.3 → 0.37.5

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
@@ -165,7 +165,6 @@ Startup validates the key shape, expiry (JWT `exp`), and agent resolution — er
165
165
  | `ziggs_artifact_download` | `GET /artifacts/:id/download` |
166
166
  | `ziggs_agent_search` | Agent search |
167
167
  | `ziggs_agent_get` | `GET /agents/:id` — full profile of one agent by exact id |
168
- | `ziggs_context_issue_grant` | Chat admission or `POST /context/grants` |
169
168
  | `ziggs_context_delegate` | `POST /context/grants/:id/delegate` |
170
169
  | `ziggs_context_revoke_grant` | `DELETE /context/grants/:id` |
171
170
  | `ziggs_link_propose` | `POST /agreements/links` — connect with someone by email or agent id, or mint a share link (claimUrl + paste text) |
@@ -15,7 +15,7 @@ ziggs_artifact_attach ziggs_artifact_upload_url ziggs_artifact_complete_file zig
15
15
  ziggs_task_create ziggs_task_set_result ziggs_task_replace_plan ziggs_task_update_steps
16
16
  ziggs_task_acknowledge_control ziggs_task_list ziggs_task_cancel ziggs_task_get
17
17
  ziggs_connection_list ziggs_agent_search ziggs_agent_get
18
- ziggs_context_issue_grant ziggs_context_delegate ziggs_context_request ziggs_access_request
18
+ ziggs_context_delegate ziggs_access_request
19
19
  ziggs_link_propose ziggs_link_list ziggs_introduction_mint ziggs_introduction_redeem
20
20
  ziggs_introduction_list ziggs_introduction_revoke ziggs_context_revoke_grant ziggs_payment_balance
21
21
  `.trim().split(/\s+/));
@@ -32,7 +32,7 @@ ziggs_agent_search ziggs_agent_get ziggs_marketplace_view
32
32
  ziggs_agreement_buy ziggs_agreement_bid ziggs_agreement_broker ziggs_agreement_request
33
33
  ziggs_agreement_handoff ziggs_agreement_claim ziggs_agreement_subcontract
34
34
  ziggs_chat_open ziggs_chat_send ziggs_artifact_share ziggs_artifact_attach
35
- ziggs_context_issue_grant ziggs_context_delegate ziggs_context_request ziggs_access_request
35
+ ziggs_context_delegate ziggs_access_request
36
36
  ziggs_link_propose ziggs_introduction_mint ziggs_introduction_redeem
37
37
  `.trim().split(/\s+/));
38
38
  export function installDirectorySurface(server) {
@@ -4,9 +4,9 @@
4
4
  * Every assistant start-up pays for one inbox read, and it was several times
5
5
  * the size of the connect text before any content was opened. This changes how
6
6
  * the result is written, never what it says: which deliveries are visible,
7
- * their order, their reach, the acknowledgement tokens, the counters and the
8
- * lease all come from the server unchanged. What goes is only what can be read
9
- * back from what stays:
7
+ * their order within a thread, their reach, the acknowledgement tokens, the
8
+ * counters and the lease all come from the server unchanged. What goes is only
9
+ * what can be read back from what stays:
10
10
  *
11
11
  * - a delivery field that is null, false or empty (absent reads the same);
12
12
  * - `partyKind` on a delivery, which `sources` already gives for its `partyId`,
@@ -24,17 +24,61 @@
24
24
  *
25
25
  * Acknowledgement stays exact: every `ackTo` — the envelope's, each row's and
26
26
  * the ack step's — is passed through byte for byte.
27
+ *
28
+ * The rows are written grouped rather than as one flat list: by who they are
29
+ * for, then by who sent them, then by the chat, task or agreement they are
30
+ * about. A flat list mixed the assistant's own work with mail it
31
+ * only passes along to its person and mail for the person's other
32
+ * assistants, and the split lived in one sentence of counts. Every row is
33
+ * still there, oldest first inside its thread; see {@link groupDeliveries}.
27
34
  */
28
35
  type Json = Record<string, unknown>;
29
36
  /** One delivery row, without what it would say by default or elsewhere. */
30
37
  export declare function compactDelivery(row: Json, kindOfParty: Map<string, unknown>, usualPartyId?: unknown): Json;
38
+ /** Who an inbox read belongs to: the connection's assistants and their person. */
39
+ export interface InboxReader {
40
+ agentIds: readonly string[];
41
+ /** The person the assistants work for; read off the human source when absent. */
42
+ personId?: string | null;
43
+ }
44
+ /** The sections of a grouped inbox, in the order they are written. */
45
+ export declare const INBOX_SECTIONS: readonly ["forYou", "forYourPerson", "forOtherAssistants", "context"];
46
+ export type InboxSection = (typeof INBOX_SECTIONS)[number];
47
+ /**
48
+ * Who a row is for. One rule for the grouping and for the counts in the
49
+ * notice, so the two cannot disagree: assigned to one of the reader's
50
+ * assistants is theirs to handle; unassigned and flagged for a human is the
51
+ * person's, which the assistant passes along; assigned to anyone else is
52
+ * another assistant's; the rest nobody is asked to act on.
53
+ */
54
+ export declare function sectionOf(row: Json, agentIds: readonly string[]): InboxSection;
55
+ /**
56
+ * The window's rows, grouped: section, then sender, then thread.
57
+ *
58
+ * Nothing is hidden and nothing is reordered within a thread. Senders and
59
+ * threads are written in the order their oldest row arrived, which is the
60
+ * order the server sends (oldest first), except that the reader's own actions
61
+ * and their person's come last in a section: they are a record of what this
62
+ * side did, not news from anyone.
63
+ *
64
+ * A row drops only what its place in the grouping says: the sender (the
65
+ * group's `from`), the thread's own id, the assignee in `forYou` when the
66
+ * connection has one assistant, and `needsHuman` in `forYourPerson`. A thread
67
+ * names the first agreement its rows sit under; a row under another one keeps
68
+ * its own, and a row under none says `agreementId: null`, so every row reads
69
+ * back exactly.
70
+ */
71
+ export declare function groupDeliveries(rows: readonly Json[], reader: InboxReader, compactRow?: (row: Json) => Json): Partial<Record<InboxSection, Json[]>>;
31
72
  /**
32
73
  * The compact inbox result. Takes the result exactly as it would otherwise be
33
74
  * returned and gives back the same information in fewer characters.
75
+ *
76
+ * With a reader, `deliveries` is written grouped ({@link groupDeliveries});
77
+ * without one there is no telling whose a row is, so it stays one list.
34
78
  */
35
- export declare function compactInboxResult(result: Json): Json;
79
+ export declare function compactInboxResult(result: Json, reader?: InboxReader): Json;
36
80
  /** The inbox result as one line of JSON: indentation is most of nothing. */
37
- export declare function compactTextResult(data: Json): {
81
+ export declare function compactTextResult(data: Json, reader?: InboxReader): {
38
82
  content: {
39
83
  type: "text";
40
84
  text: string;
@@ -4,9 +4,9 @@
4
4
  * Every assistant start-up pays for one inbox read, and it was several times
5
5
  * the size of the connect text before any content was opened. This changes how
6
6
  * the result is written, never what it says: which deliveries are visible,
7
- * their order, their reach, the acknowledgement tokens, the counters and the
8
- * lease all come from the server unchanged. What goes is only what can be read
9
- * back from what stays:
7
+ * their order within a thread, their reach, the acknowledgement tokens, the
8
+ * counters and the lease all come from the server unchanged. What goes is only
9
+ * what can be read back from what stays:
10
10
  *
11
11
  * - a delivery field that is null, false or empty (absent reads the same);
12
12
  * - `partyKind` on a delivery, which `sources` already gives for its `partyId`,
@@ -24,6 +24,13 @@
24
24
  *
25
25
  * Acknowledgement stays exact: every `ackTo` — the envelope's, each row's and
26
26
  * the ack step's — is passed through byte for byte.
27
+ *
28
+ * The rows are written grouped rather than as one flat list: by who they are
29
+ * for, then by who sent them, then by the chat, task or agreement they are
30
+ * about. A flat list mixed the assistant's own work with mail it
31
+ * only passes along to its person and mail for the person's other
32
+ * assistants, and the split lived in one sentence of counts. Every row is
33
+ * still there, oldest first inside its thread; see {@link groupDeliveries}.
27
34
  */
28
35
  const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
29
36
  /** Session fields `ziggs_auth_status` carries; the inbox does not repeat them. */
@@ -84,11 +91,140 @@ function usualPartyOf(rows) {
84
91
  [best, most] = [id, n];
85
92
  return best;
86
93
  }
94
+ /** The sections of a grouped inbox, in the order they are written. */
95
+ export const INBOX_SECTIONS = ['forYou', 'forYourPerson', 'forOtherAssistants', 'context'];
96
+ /**
97
+ * Who a row is for. One rule for the grouping and for the counts in the
98
+ * notice, so the two cannot disagree: assigned to one of the reader's
99
+ * assistants is theirs to handle; unassigned and flagged for a human is the
100
+ * person's, which the assistant passes along; assigned to anyone else is
101
+ * another assistant's; the rest nobody is asked to act on.
102
+ */
103
+ export function sectionOf(row, agentIds) {
104
+ const assignee = row.assigneeId ? String(row.assigneeId) : '';
105
+ if (assignee && agentIds.includes(assignee))
106
+ return 'forYou';
107
+ if (!assignee && row.needsHuman === true)
108
+ return 'forYourPerson';
109
+ if (assignee)
110
+ return 'forOtherAssistants';
111
+ return 'context';
112
+ }
113
+ /** What a row is about: its chat, else its task, else its agreement, else itself. */
114
+ function anchorOf(row) {
115
+ if (row.chatId)
116
+ return ['chatId', String(row.chatId)];
117
+ if (row.taskId)
118
+ return ['taskId', String(row.taskId)];
119
+ if (row.agreementId)
120
+ return ['agreementId', String(row.agreementId)];
121
+ return [row.kind === 'artifact' ? 'artifactId' : 'resourceId', String(row.resourceId)];
122
+ }
123
+ /**
124
+ * The window's rows, grouped: section, then sender, then thread.
125
+ *
126
+ * Nothing is hidden and nothing is reordered within a thread. Senders and
127
+ * threads are written in the order their oldest row arrived, which is the
128
+ * order the server sends (oldest first), except that the reader's own actions
129
+ * and their person's come last in a section: they are a record of what this
130
+ * side did, not news from anyone.
131
+ *
132
+ * A row drops only what its place in the grouping says: the sender (the
133
+ * group's `from`), the thread's own id, the assignee in `forYou` when the
134
+ * connection has one assistant, and `needsHuman` in `forYourPerson`. A thread
135
+ * names the first agreement its rows sit under; a row under another one keeps
136
+ * its own, and a row under none says `agreementId: null`, so every row reads
137
+ * back exactly.
138
+ */
139
+ export function groupDeliveries(rows, reader, compactRow = (row) => ({ ...row })) {
140
+ const sections = new Map();
141
+ for (const row of rows) {
142
+ const section = sectionOf(row, reader.agentIds);
143
+ const from = row.actorId ? String(row.actorId) : null;
144
+ const anchor = anchorOf(row);
145
+ let senders = sections.get(section);
146
+ if (!senders)
147
+ sections.set(section, (senders = new Map()));
148
+ let group = senders.get(String(from));
149
+ if (!group)
150
+ senders.set(String(from), (group = { from, threads: new Map() }));
151
+ const key = anchor.join(':');
152
+ let thread = group.threads.get(key);
153
+ if (!thread)
154
+ group.threads.set(key, (thread = { anchor, rows: [] }));
155
+ thread.rows.push(row);
156
+ }
157
+ const own = (from) => from !== null && (reader.agentIds.includes(from) || from === reader.personId);
158
+ const out = {};
159
+ for (const section of INBOX_SECTIONS) {
160
+ const senders = sections.get(section);
161
+ if (!senders)
162
+ continue;
163
+ const ordered = [...senders.values()];
164
+ out[section] = [
165
+ ...ordered.filter((g) => !own(g.from)),
166
+ ...ordered.filter((g) => own(g.from)),
167
+ ].map((g) => {
168
+ const threads = [...g.threads.values()].map((t) => writeThread(t, section, reader, compactRow));
169
+ const count = [...g.threads.values()].reduce((n, t) => n + t.rows.length, 0);
170
+ return {
171
+ from: g.from,
172
+ ...(g.from !== null && reader.agentIds.includes(g.from) ? { you: true } : {}),
173
+ ...(g.from !== null && g.from === reader.personId ? { yourPerson: true } : {}),
174
+ count,
175
+ threads,
176
+ };
177
+ });
178
+ }
179
+ return out;
180
+ }
181
+ function writeThread(thread, section, reader, compactRow) {
182
+ const [field, id] = thread.anchor;
183
+ const lifted = field === 'agreementId'
184
+ ? null
185
+ : thread.rows.map((r) => r.agreementId).find(Boolean) ?? null;
186
+ const rows = thread.rows.map((original) => {
187
+ const row = compactRow(original);
188
+ delete row.actorId;
189
+ if (field !== 'resourceId' && field !== 'artifactId')
190
+ delete row[field];
191
+ if (lifted) {
192
+ if (row.agreementId === lifted)
193
+ delete row.agreementId;
194
+ else if (!row.agreementId)
195
+ row.agreementId = null;
196
+ }
197
+ if (section === 'forYou') {
198
+ if (reader.agentIds.length === 1)
199
+ delete row.assigneeId;
200
+ if (row.assignedToConnection === true)
201
+ delete row.assignedToConnection;
202
+ }
203
+ if (section === 'forYourPerson')
204
+ delete row.needsHuman;
205
+ return row;
206
+ });
207
+ return { [field]: id, ...(lifted ? { agreementId: lifted } : {}), rows };
208
+ }
209
+ /** The reader's assistants, without empty ids, and their person. */
210
+ function readerOf(result, reader) {
211
+ const agentIds = (reader?.agentIds ?? []).filter(Boolean);
212
+ if (!agentIds.length)
213
+ return null;
214
+ const human = Array.isArray(result.sources)
215
+ ? result.sources.find((s) => isRecord(s) && s.partyKind === 'human')
216
+ : undefined;
217
+ const personId = reader?.personId ?? (isRecord(human) ? String(human.partyId) : null);
218
+ return { agentIds, personId };
219
+ }
87
220
  /**
88
221
  * The compact inbox result. Takes the result exactly as it would otherwise be
89
222
  * returned and gives back the same information in fewer characters.
223
+ *
224
+ * With a reader, `deliveries` is written grouped ({@link groupDeliveries});
225
+ * without one there is no telling whose a row is, so it stays one list.
90
226
  */
91
- export function compactInboxResult(result) {
227
+ export function compactInboxResult(result, reader) {
92
228
  const kindOfParty = new Map();
93
229
  if (Array.isArray(result.sources))
94
230
  for (const s of result.sources)
@@ -101,7 +237,10 @@ export function compactInboxResult(result) {
101
237
  const usual = usualPartyOf(result.deliveries);
102
238
  if (usual !== undefined)
103
239
  out.deliveriesPartyId = usual;
104
- out.deliveries = result.deliveries.map((d) => isRecord(d) ? compactDelivery(d, kindOfParty, usual) : d);
240
+ const whose = readerOf(result, reader);
241
+ out.deliveries = whose
242
+ ? groupDeliveries(result.deliveries.filter(isRecord), whose, (d) => compactDelivery(d, kindOfParty, usual))
243
+ : result.deliveries.map((d) => isRecord(d) ? compactDelivery(d, kindOfParty, usual) : d);
105
244
  }
106
245
  if (isRecord(result.session))
107
246
  out.session = compactSession(result.session, result.connectionNotices);
@@ -135,9 +274,9 @@ export function compactInboxResult(result) {
135
274
  return out;
136
275
  }
137
276
  /** The inbox result as one line of JSON: indentation is most of nothing. */
138
- export function compactTextResult(data) {
277
+ export function compactTextResult(data, reader) {
139
278
  return {
140
- content: [{ type: 'text', text: JSON.stringify(compactInboxResult(data)) }],
279
+ content: [{ type: 'text', text: JSON.stringify(compactInboxResult(data, reader)) }],
141
280
  };
142
281
  }
143
282
  /**
@@ -152,19 +291,17 @@ export function windowNote(deliveries, selfAgentIds, outOfReach) {
152
291
  const rows = Array.isArray(deliveries) ? deliveries.filter(isRecord) : [];
153
292
  if (!rows.length)
154
293
  return '';
155
- const mine = rows.filter((d) => selfAgentIds.includes(String(d.assigneeId))).length;
156
- const person = rows.filter((d) => !d.assigneeId && d.needsHuman === true).length;
157
- const others = rows.filter((d) => d.assigneeId && !selfAgentIds.includes(String(d.assigneeId))).length;
158
- // Rows nobody is asked to handle (no assignee, not the person's) are context.
159
- const context = rows.length - mine - person - others;
160
- const n = (k, one, many) => `${k} ${k === 1 ? one : many}`;
161
- const parts = [n(mine, 'is assigned to you', 'are assigned to you')];
162
- if (person)
163
- parts.push(`${person} ${person === 1 ? 'needs' : 'need'} your person`);
164
- if (others)
165
- parts.push(`${others} ${others === 1 ? 'is' : 'are'} for other assistants`);
166
- if (context)
167
- parts.push(`${context} ${context === 1 ? 'is' : 'are'} context`);
294
+ // The same rule the grouping uses, so each count is its section's size.
295
+ const count = (s) => rows.filter((d) => sectionOf(d, selfAgentIds) === s).length;
296
+ const words = {
297
+ forYou: 'yours to handle',
298
+ forYourPerson: 'pass these along to your person',
299
+ forOtherAssistants: 'your person’s other assistants',
300
+ context: 'nobody is asked to act',
301
+ };
302
+ // forYou is always named, so "nothing for you" is said rather than implied.
303
+ const parts = INBOX_SECTIONS.filter((s) => s === 'forYou' || count(s) > 0).map((s) => `${s} ${count(s)} (${words[s]})`);
168
304
  const reach = outOfReach ? ` ${outOfReach} of them ${outOfReach === 1 ? 'is' : 'are'} out of reach.` : '';
169
- return `Those counts are decisions and tasks, not messages. This window has ${n(rows.length, 'delivery', 'deliveries')}: ${parts.join(', ')}.${reach}`;
305
+ const total = `${rows.length} ${rows.length === 1 ? 'delivery' : 'deliveries'}`;
306
+ return `Those counts are decisions and tasks, not messages. This window has ${total}, grouped by who they are for, then by sender: ${parts.join(', ')}.${reach}`;
170
307
  }
package/dist/server.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { 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
+ export type { ZiggsMcpConfig };
4
5
  /** Shared MCP server factory — stdio (local) and remote HTTP (backend) reuse this. */
5
6
  export declare function createZiggsMcpServer(creds: Creds, cfg: ZiggsMcpConfig): McpServer;
6
7
  /** Drop this process's inbox host. Best-effort — shutdown must not fail on it. */
@@ -38,8 +38,7 @@ export const SEARCH_ALIASES = {
38
38
  'add people',
39
39
  ],
40
40
  ziggs_agent_search: ['teammate', 'colleague', 'directory', 'find person'],
41
- ziggs_access_request: ['access', 'permission', 'request access', 'chat', 'approval', 'grant'],
42
- ziggs_context_request: ['grant', 'permission', 'restricted', 'request access', 'unlock document'],
41
+ ziggs_access_request: ['access', 'permission', 'request access', 'chat', 'approval', 'grant', 'restricted', 'unlock document'],
43
42
  ziggs_agreement_claim: ['hire'],
44
43
  ziggs_agreement_buy: [
45
44
  'hire',
package/dist/tools.js CHANGED
@@ -692,12 +692,14 @@ export function registerZiggsTools(server, creds, cfg) {
692
692
  }
693
693
  }
694
694
  const news = formatInboxToolResult(inbox, null, cfg.ZIGGS_WEB_URL, undefined, reach, undefined, { agentId: creds.agentId, ownerUserId: ownerPrincipalId(creds, cfg) });
695
+ // Whose rows are whose, so the deliveries come back grouped.
696
+ const reader = { agentIds: [creds.agentId], personId: ownerPrincipalId(creds, cfg) };
695
697
  // A long-poll is a continuation of a session that already oriented
696
698
  // itself, so it returns news only. A cold call is the session start, and
697
699
  // carries what the other two orientation tools used to each be called
698
700
  // for: the binding, the decisions, and the card.
699
701
  if (waitSeconds != null)
700
- return compactTextResult(news);
702
+ return compactTextResult(news, reader);
701
703
  const [bindingSettled, readsSettled] = await sessionStart;
702
704
  const session = bindingSettled.status === 'fulfilled'
703
705
  ? bindingSettled.value
@@ -712,7 +714,7 @@ export function registerZiggsTools(server, creds, cfg) {
712
714
  ...news,
713
715
  session,
714
716
  sessionActionsFetchError: readsSettled.reason.message,
715
- });
717
+ }, reader);
716
718
  }
717
719
  const actions = buildSessionActions(inbox, readsSettled.value, creds, cfg);
718
720
  // No prose list of next actions rides here: the calls are in
@@ -729,7 +731,7 @@ export function registerZiggsTools(server, creds, cfg) {
729
731
  ...(note && typeof actions.notice === 'string'
730
732
  ? { notice: `${actions.notice} ${note}` }
731
733
  : {}),
732
- });
734
+ }, reader);
733
735
  }
734
736
  catch (e) {
735
737
  return toolError(e);
@@ -754,7 +756,10 @@ export function registerZiggsTools(server, creds, cfg) {
754
756
  registerStrictTool(server, 'ziggs_inbox_claim', {}, destructive('Claim the inbox for this worker'), async () => {
755
757
  try {
756
758
  const client = inboxFor(creds);
757
- return compactTextResult(await client.getInbox());
759
+ return compactTextResult(await client.getInbox(), {
760
+ agentIds: [creds.agentId],
761
+ personId: ownerPrincipalId(creds, cfg),
762
+ });
758
763
  }
759
764
  catch (e) {
760
765
  return toolError(e);
@@ -860,6 +865,12 @@ export function registerZiggsTools(server, creds, cfg) {
860
865
  assigneeId: z.string().optional(),
861
866
  waitsOn: z.array(z.string()).optional(),
862
867
  joinKind: z.enum(['all', 'any']).optional(),
868
+ runIf: z
869
+ .string()
870
+ .min(1)
871
+ .max(500)
872
+ .optional()
873
+ .describe('A yes/no question over the results of the nodes this one waits on, such as "Does the review report at least one bug?". On yes the node runs; on no it is skipped, and nodes waiting on it go ahead. When the answer is unclear it waits for the buyer. Needs waitsOn.'),
863
874
  }))
864
875
  .min(1)
865
876
  .max(100),
@@ -867,7 +878,7 @@ export function registerZiggsTools(server, creds, cfg) {
867
878
  review: z.literal('before_execution').optional(),
868
879
  })
869
880
  .optional()
870
- .describe('Native atomic graph. waitsOn values name nodeId values in this payload; all nodes are created or none are. Root nodes receive graph inputArtifactIds. review holds every node until the root acknowledgement.'),
881
+ .describe('Native atomic graph. waitsOn values name nodeId values in this payload; all nodes are created or none are. Root nodes receive graph inputArtifactIds. review holds every node until the root acknowledgement. runIf makes a node conditional on what the nodes before it found.'),
871
882
  // POST /tasks and the SDK's task_create have always taken
872
883
  // these three; only this surface hid them, so an agent on MCP had to
873
884
  // create-then-replace_plan even when it already knew the steps.
@@ -949,7 +960,8 @@ export function registerZiggsTools(server, creds, cfg) {
949
960
  });
950
961
  registerStrictTool(server, 'ziggs_task_set_result', {
951
962
  taskId: z.string(),
952
- state: z.enum(['completed', 'failed', 'cancelled']),
963
+ // Cancelling has its own tool, ziggs_task_cancel.
964
+ state: z.enum(['completed', 'failed']),
953
965
  result: z
954
966
  .object({
955
967
  summary: z.string().optional(),
@@ -3,6 +3,8 @@ import { type Creds } from '@ziggs-ai/api-client';
3
3
  import type { ZiggsMcpConfig } from './config.js';
4
4
  /** agent search + context grant management through MCP.
5
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. */
6
+ * capability layer; only revoking a grant stays MCP-local. Issuing one is a
7
+ * person's act (POST /context/grants takes a signed-in session), so there is
8
+ * no agent tool for it: an agent shares with artifact_share, a chat_open
9
+ * invite, or context_delegate. */
8
10
  export declare function registerTrustTools(server: McpServer, creds: Creds, cfg?: ZiggsMcpConfig): void;
@@ -1,103 +1,20 @@
1
1
  import { z } from 'zod';
2
- import { CONTEXT_GRANT_SCOPE_KINDS, ContextGrantsClient, addChatMember, contextBounds, resolveOrgScopeId, LINK_CAPABILITIES, INTRODUCTION_CAPABILITIES, DISCOVERY_CAPABILITIES, contextDelegateCapability, contextRequestCapability, accessRequestCapability, } from '@ziggs-ai/api-client';
3
- import { write, destructive } from './toolAnnotations.js';
2
+ import { ContextGrantsClient, LINK_CAPABILITIES, INTRODUCTION_CAPABILITIES, DISCOVERY_CAPABILITIES, contextDelegateCapability, accessRequestCapability, } from '@ziggs-ai/api-client';
3
+ import { destructive } from './toolAnnotations.js';
4
4
  import { registerStrictTool } from './strictParams.js';
5
5
  import { toolError } from './toolError.js';
6
6
  import { registerCapabilities, registerCapability, textResult } from './capabilityAdapter.js';
7
- // `artifact` is the narrowest context scope — one specific artifact, shared
8
- // without sharing any container it sits in. `task` is a branch of a work graph:
9
- // the named task and everything under it. Kept in step with the rail's own list
10
- // (`CONTEXT_GRANT_SCOPE_KINDS`) rather than written out again — a literal subset
11
- // still typechecks, which is how `task` was unaskable here after the server had
12
- // it.
13
- const grantScopeKindSchema = z.enum(CONTEXT_GRANT_SCOPE_KINDS);
14
- const contextTemporalSchema = z.enum(['from-now', 'from-start']);
15
7
  const DEFAULT_WEB_URL = 'https://ziggsai.com';
16
8
  /** agent search + context grant management through MCP.
17
9
  * The SDK-twin tools (search/get, links, delegate) come from the shared
18
- * capability layer; only the human-authority grant tools (issue/revoke) stay
19
- * MCP-local. */
10
+ * capability layer; only revoking a grant stays MCP-local. Issuing one is a
11
+ * person's act (POST /context/grants takes a signed-in session), so there is
12
+ * no agent tool for it: an agent shares with artifact_share, a chat_open
13
+ * invite, or context_delegate. */
20
14
  export function registerTrustTools(server, creds, cfg) {
21
15
  const webUrl = cfg?.ZIGGS_WEB_URL?.replace(/\/$/, '') ?? DEFAULT_WEB_URL;
22
16
  registerCapabilities(server, DISCOVERY_CAPABILITIES, creds);
23
- registerStrictTool(server, 'ziggs_context_issue_grant', {
24
- holderId: z.string().describe('Bare agent id receiving the grant'),
25
- scopeKind: grantScopeKindSchema,
26
- scopeId: z.string().describe('chatId, agreementId, orgId, artifactId, or taskId'),
27
- temporal: contextTemporalSchema
28
- .optional()
29
- .describe('from-now (default) or from-start (approval-gated on chat). Artifact ' +
30
- 'scope defaults to from-start and rejects from-now.'),
31
- expiresAt: z
32
- .string()
33
- .optional()
34
- .nullable()
35
- .describe('ISO-8601 expiry; omit for platform default TTL'),
36
- }, write('Give another agent access'), async ({ holderId, scopeKind, scopeId, temporal, expiresAt }) => {
37
- // An artifact predates any watermark you could set, so from-now would
38
- // validate and then read empty — the server refuses it outright. Defaulting
39
- // artifact scope to from-now here made the tool's own default invocation
40
- // fail, while the description promised it was always from-start. An
41
- // EXPLICIT from-now still goes through and is refused loudly: silently
42
- // rewriting what the caller asked for would hide the mistake.
43
- const resolvedTemporal = temporal ?? (scopeKind === 'artifact' ? 'from-start' : 'from-now');
44
- try {
45
- if (scopeKind === 'chat') {
46
- const result = await addChatMember({
47
- chatId: scopeId,
48
- memberId: holderId,
49
- memberType: 'agent',
50
- temporal: resolvedTemporal,
51
- }, creds);
52
- if ('status' in result && result.status === 'pending') {
53
- return textResult({
54
- status: 'pending_approval',
55
- outcome: 'pending',
56
- message: 'Human approval required before the grant is issued. Surface this to the user — do not treat as success.',
57
- scope: { kind: 'chat', id: scopeId },
58
- holderId,
59
- temporal: resolvedTemporal,
60
- pendingAdmission: result.pendingAdmission,
61
- });
62
- }
63
- return textResult({
64
- status: 'granted',
65
- scope: { kind: 'chat', id: scopeId },
66
- holderId,
67
- temporal: resolvedTemporal,
68
- bounds: {
69
- temporal: resolvedTemporal,
70
- note: 'from-now: readable content starts at grant watermark; from-start requires unanimous approval',
71
- },
72
- chat: 'chat' in result ? result.chat : undefined,
73
- });
74
- }
75
- // #6 — an org scope may be named rather than pasted as org_… id.
76
- let resolvedScopeId = scopeId;
77
- if (scopeKind === 'org') {
78
- resolvedScopeId = await resolveOrgScopeId({ creds, surface: 'mcp' }, scopeId);
79
- }
80
- const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
81
- const grant = await client.issueGrant({
82
- holderId,
83
- // This tool only ever hands access to another agent.
84
- holderKind: 'agent',
85
- scope: { kind: scopeKind, id: resolvedScopeId },
86
- temporal: resolvedTemporal,
87
- expiresAt,
88
- });
89
- return textResult({
90
- status: 'granted',
91
- grant,
92
- bounds: contextBounds(grant),
93
- });
94
- }
95
- catch (e) {
96
- return toolError(e);
97
- }
98
- });
99
17
  registerCapability(server, contextDelegateCapability, creds);
100
- registerCapability(server, contextRequestCapability, creds, { webUrl });
101
18
  registerCapability(server, accessRequestCapability, creds, { webUrl });
102
19
  if (!cfg?.coreOnly) {
103
20
  // #7 — the link tool group is skipped by the lean session-start
@@ -235,8 +235,8 @@ export class WorkspaceRouter {
235
235
  ...(name === 'ziggs_inbox' && args.waitSeconds == null ? { rules: sessionRules('directory') } : {}),
236
236
  ...(note && typeof pending.notice === 'string' ? { notice: `${pending.notice} ${note}` } : {}),
237
237
  session: { workspaces },
238
- instruction: `Assigned to any of agentIds is your work. ${ASK_YOUR_HUMAN}`,
239
- });
238
+ instruction: `deliveries.forYou is your work; deliveries.forYourPerson is for your person to see. ${ASK_YOUR_HUMAN}`,
239
+ }, { agentIds: self.agentIds ?? [] });
240
240
  }
241
241
  if (name === 'ziggs_inbox_peek') {
242
242
  return result(await this.client.request(`inbox/peek?wait=${Number(args.waitSeconds ?? 0)}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.37.3",
3
+ "version": "0.37.5",
4
4
  "description": "MCP server for Claude Code, Cursor, and other MCP hosts \u2014 act as your Ziggs delegate agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/sdk": "^1.29.0",
50
- "@ziggs-ai/api-client": "0.37.1",
50
+ "@ziggs-ai/api-client": "0.37.5",
51
51
  "dotenv": "^16.6.1",
52
52
  "zod": "^3.24.2",
53
53
  "zod-to-json-schema": "^3.25.1"
@@ -85,8 +85,8 @@ See [references/inbox-rhythm.md](references/inbox-rhythm.md) for a full catch-up
85
85
  - **`ziggs_inbox`** (cold, no `waitSeconds`): at session start, if anything awaits approve/reject, show **`sessionChatCard`** (includes app links + exact respond commands). Do not auto-approve.
86
86
  - **`pending_approval`** (grants, admissions, from-start history, agreement steps): **stop and show the human** — do not auto-approve on their behalf unless they explicitly asked for that action in this session.
87
87
  - **`humanAttention` on inbox**: when present, **tell the human immediately** — list each pending agreement proposal and ask approve/reject before other work.
88
- - Before **`ziggs_context_issue_grant`**, **`ziggs_context_delegate`**, or any grant that exposes **existing** org/chat/agreement context: **ask the human** what scope and temporal bound they want (`from-now` vs `from-start`).
89
- - Trust tools (`ziggs_agent_search`, grant issue/delegate/revoke): use for cross-org collaboration only when the human’s goal requires it.
88
+ - Before **`ziggs_context_delegate`**, a **`ziggs_chat_open`** invite into an existing room, or any grant that exposes **existing** org/chat/agreement context: **ask the human** what scope and temporal bound they want (`from-now` vs `from-start`).
89
+ - Trust tools (`ziggs_agent_search`, grant delegate/revoke): use for cross-org collaboration only when the human’s goal requires it.
90
90
 
91
91
  See [references/grants-and-approvals.md](references/grants-and-approvals.md).
92
92
 
@@ -51,7 +51,7 @@ counterparty owner approves it, and unpublished delegates can then reach each ot
51
51
  3. Target human approves via **`ziggs_agreement_respond`** (action `approve`) when the
52
52
  pending link agreement shows in their inbox / `ziggs_link_list`. This is the same generic
53
53
  approval tool used for hire and service proposals — there is no separate link-response tool.
54
- 4. **`ziggs_chat_open`** with the person to talk; **`ziggs_context_issue_grant`** only to let them read your context.
54
+ 4. **`ziggs_chat_open`** with the person to talk. To let them read existing context: **`ziggs_artifact_share`** for one artifact, a **`ziggs_chat_open`** invite (chatId plus participantIds) for a room, or **`ziggs_context_delegate`** for a slice of a grant you hold.
55
55
  5. To read their context, they grant it or you ask with **`ziggs_access_request`**.
56
56
  6. `ziggs_context_revoke_grant` when context access should end; **`ziggs_agreement_revoke`** when
57
57
  the agreement should end — any kind: hire, service, request, offer, or link.
@@ -61,4 +61,4 @@ counterparty owner approves it, and unpublished delegates can then reach each ot
61
61
  ## Web UI (Settings → Connections)
62
62
 
63
63
  Confusingly named after the *other* "connection" (this is the agent-link handshake UI, not
64
- `ziggs_connection_proxy` third-party connections). After an agent link appears under **Connected**, use your Claude delegate (MCP) for **`ziggs_chat_open`** or **`ziggs_context_issue_grant`**. The web UI manages handshake only — not chat or context grants.
64
+ `ziggs_connection_proxy` third-party connections). After an agent link appears under **Connected**, use your Claude delegate (MCP) for **`ziggs_chat_open`**, **`ziggs_artifact_share`** or **`ziggs_context_delegate`**. The web UI manages handshake only — not chat or context grants.