@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.
@@ -1,227 +1,18 @@
1
1
  import { z } from 'zod';
2
- import { AgentSearchClient, ContextGrantsClient, createAgreement, listAgreements, revokeAgreement, claimAgreement, addChatMember, grantCaveat, } from '@ziggs-ai/api-client';
3
- import { fetchMyOrgs, resolveOrgSelector } from './orgs.js';
4
- import { READ_ONLY, WRITE, DESTRUCTIVE } from './toolAnnotations.js';
2
+ import { ContextGrantsClient, addChatMember, contextBounds, resolveOrgScopeId, LINK_CAPABILITIES, DISCOVERY_CAPABILITIES, contextDelegateCapability, } from '@ziggs-ai/api-client';
3
+ import { WRITE, DESTRUCTIVE } from './toolAnnotations.js';
5
4
  import { toolError } from './toolError.js';
6
- function textResult(data) {
7
- return {
8
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
9
- };
10
- }
11
- /**
12
- * Human/LLM-readable bounds for a context grant. ZIG-646 folded a context
13
- * grant's temporal mode + read watermark into the canonical `caveats` array,
14
- * so pull them back out here to keep this tool's `bounds` summary stable.
15
- */
16
- function contextBounds(grant) {
17
- return {
18
- temporal: grantCaveat(grant, 'temporal'),
19
- watermarkAt: grantCaveat(grant, 'watermark_at'),
20
- expiresAt: grant.expiresAt,
21
- };
22
- }
5
+ import { registerCapabilities, registerCapability, textResult } from './capabilityAdapter.js';
23
6
  const grantScopeKindSchema = z.enum(['chat', 'agreement', 'org']);
24
7
  const contextTemporalSchema = z.enum(['from-now', 'from-start']);
25
8
  const DEFAULT_WEB_URL = 'https://ziggsai.com';
26
- /**
27
- * ZIG-941 #6 — org-scoped grants may name the org instead of pasting its opaque
28
- * org_... id. Resolve the selector against the operator's memberships via the
29
- * (previously unused) resolveOrgSelector: exact id or case-insensitive name.
30
- * Ambiguous names return the candidate list rather than guessing; a name that
31
- * matched nothing errors with a pointer to ziggs_list_my_orgs. An org_... id
32
- * that is not a membership passes through unchanged — you may hold a grant on
33
- * an org you do not belong to, so the server stays the authority on the id.
34
- */
35
- async function resolveOrgScope(creds, scopeId) {
36
- const resolution = resolveOrgSelector(await fetchMyOrgs(creds), scopeId);
37
- if (resolution.status === 'ok')
38
- return { scopeId: resolution.orgId };
39
- if (resolution.status === 'ambiguous') {
40
- return {
41
- error: toolError(`Org name "${scopeId}" matches ${resolution.matches.length} of your orgs — pass the org id: ` +
42
- resolution.matches.map((m) => `${m.name} (${m.orgId})`).join(', ')),
43
- };
44
- }
45
- if (scopeId.startsWith('org_'))
46
- return { scopeId };
47
- return {
48
- error: toolError(`No org named "${scopeId}" in your memberships — use ziggs_list_my_orgs to see them, or pass the org id.`),
49
- };
50
- }
51
- // ZIG-941 #7 — the link tool group, pulled out so the lean session-start
52
- // tier (ZIGGS_MCP_CORE_ONLY) can skip it. Registration is unchanged.
53
- function registerLinkTools(server, creds, webUrl) {
54
- server.tool('ziggs_request_link', 'Request a bilateral trust link with another agent before cross-org reach (party-to-party, NOT a third-party service connection — see ziggs_list_my_connections for that). A link is just an agreement (POST /agreements {engagementKind:"link"}). The target OWNER must approve it (via ziggs_respond_to_agreement) before unpublished delegates can message each other.', {
55
- providerId: z
56
- .string()
57
- .describe('Bare agent id to link with (the target delegate). Use ziggs_search_agents or a known delegate id — do not guess.'),
58
- message: z
59
- .string()
60
- .optional()
61
- .describe('Optional note shown to the counterparty human on approval (agreement description)'),
62
- }, WRITE, async ({ providerId, message }) => {
63
- try {
64
- const { agreement } = await createAgreement({ engagementKind: 'link', providerId, description: message }, creds);
65
- return textResult({
66
- status: 'pending',
67
- message: 'Link agreement created — the counterparty owner must approve (ziggs_respond_to_agreement) before cross-org reach. Surface pending state to the human.',
68
- agreement,
69
- });
70
- }
71
- catch (e) {
72
- return toolError(e.message);
73
- }
74
- });
75
- server.tool('ziggs_create_link_invite', 'Create a shareable OPEN link invite (bilateral agent-to-agent trust, NOT a third-party service connection — see ziggs_list_my_connections for that) when you do NOT have the counterparty\'s agent id (e.g. connecting across orgs). Creates an open link agreement (POST /agreements {engagementKind:"link"}, proposedTo:"everyone"). Share the returned inviteId (agreementId) out-of-band; the recipient forms the link by calling ziggs_claim_link_invite — neither side pastes an agent id. Revoke via ziggs_revoke_link to disable.', {
76
- message: z
77
- .string()
78
- .optional()
79
- .describe('Optional note shown to whoever opens the invite (agreement description)'),
80
- }, WRITE, async ({ message }) => {
81
- try {
82
- const { agreement } = await createAgreement({ engagementKind: 'link', description: message }, creds);
83
- return textResult({
84
- status: 'open',
85
- inviteId: agreement.agreementId,
86
- claimUrl: `${webUrl}/app/link-invites/${agreement.agreementId}`,
87
- message: 'Open link invite created. Share claimUrl with the counterparty — they open it in the Ziggs web app to claim and activate the link. No agent id needed on either side.',
88
- agreement,
89
- });
90
- }
91
- catch (e) {
92
- return toolError(e.message);
93
- }
94
- });
95
- server.tool('ziggs_claim_link_invite', 'Claim an open link invite by its id to form a bilateral link (agent-to-agent trust, NOT a third-party service connection — see ziggs_list_my_connections for that) (POST /agreements/:id/claim). You become the counterparty and the link activates immediately (cross-org reach + bilateral context grants). You cannot claim your own invite.', {
96
- agreementId: z
97
- .string()
98
- .describe('The invite id (agreementId) shared by the issuer'),
99
- }, WRITE, async ({ agreementId }) => {
100
- try {
101
- const { agreement } = await claimAgreement(agreementId, creds);
102
- return textResult({
103
- status: 'linked',
104
- message: 'Link invite claimed — you are now linked. A link is reach-only: open a chat with the peer (ziggs_open_conversation) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you already hold with ziggs_delegate_grant, before reading context.',
105
- agreement,
106
- });
107
- }
108
- catch (e) {
109
- return toolError(e.message);
110
- }
111
- });
112
- 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.', {
113
- status: z
114
- .enum(['active', 'open', 'cancelled', 'all'])
115
- .optional()
116
- .describe('active (default) = established links; open = pending proposals/invites awaiting approval or claim; cancelled = revoked; all = every link regardless of status'),
117
- }, READ_ONLY, async ({ status }) => {
118
- try {
119
- const resolvedStatus = status ?? 'active';
120
- const links = await listAgreements({
121
- engagementKind: 'link',
122
- ...(resolvedStatus === 'all' ? {} : { status: resolvedStatus }),
123
- }, creds);
124
- // ZIG-670: link-shaped summaries, not raw agreement documents — the
125
- // money block, approvals array, and Mongo internals are noise here.
126
- const summaries = links.map((a) => ({
127
- agreementId: a.agreementId,
128
- status: a.status,
129
- proposalStatus: a.proposalStatus,
130
- parties: {
131
- creatorAgent: a.parties?.creatorAgent ?? null,
132
- providerAgent: a.parties?.providerAgent ?? null,
133
- creator: a.parties?.creator ?? null,
134
- proposedTo: a.parties?.proposedTo ?? null,
135
- },
136
- ...(a.description ? { description: a.description } : {}),
137
- createdAt: a.createdAt,
138
- }));
139
- const hasActive = links.some((a) => a.status === 'active');
140
- return textResult({
141
- count: summaries.length,
142
- status: resolvedStatus,
143
- links: summaries,
144
- ...(hasActive
145
- ? {
146
- nextSteps: 'A link is reach-only. Open a chat with the peer (ziggs_open_conversation, participantId = peer agent id) and grant it chat access with ziggs_issue_grant, or share a slice of a grant you hold with ziggs_delegate_grant, before reading context.',
147
- }
148
- : {}),
149
- });
150
- }
151
- catch (e) {
152
- return toolError(e.message);
153
- }
154
- });
155
- server.tool('ziggs_revoke_link', 'Revoke a bilateral link agreement — agent-to-agent trust, NOT a third-party service connection (DELETE /agreements/:agreementId). Either party may revoke; cross-org reach ends immediately. For non-link agreements (hire/service/quest), use ziggs_revoke_agreement — same endpoint, different messaging.', {
156
- agreementId: z
157
- .string()
158
- .describe('agreementId of the link agreement (from ziggs_list_links)'),
159
- }, DESTRUCTIVE, async ({ agreementId }) => {
160
- try {
161
- const result = await revokeAgreement(agreementId, creds);
162
- return textResult({
163
- status: 'revoked',
164
- message: 'Link revoked — unpublished cross-org reach to this peer is blocked again.',
165
- agreementId,
166
- agreement: result.agreement,
167
- });
168
- }
169
- catch (e) {
170
- return toolError(e.message);
171
- }
172
- });
173
- }
174
- /** ZIG-433 — agent search + context grant management through MCP. */
9
+ /** ZIG-433 / ZIG-956 — agent search + context grant management through MCP.
10
+ * The SDK-twin tools (search/get, links, delegate) come from the shared
11
+ * capability layer; only the human-authority grant tools (issue/revoke) stay
12
+ * MCP-local. */
175
13
  export function registerTrustTools(server, creds, cfg) {
176
14
  const webUrl = cfg?.ZIGGS_WEB_URL?.replace(/\/$/, '') ?? DEFAULT_WEB_URL;
177
- server.tool('ziggs_search_agents', 'Find agents (AgentSearchClient). A keyword/natural-language query searches the published store AND, scoped to your authority, your own org-mates and any delegate you have an active link with — so you can find a teammate or another user\'s delegate by name and ziggs_open_conversation with it directly, even if it is unpublished/offline and has never been in a chat with you. Passing an EXACT agent id resolves that one agent even if unpublished/private — use this for a delegate someone shared an id for, then ziggs_request_link if not yet linked. Each result carries a per-row `reachability` field derived from HOW you can reach it — `published` (store directory), `same-org`, `linked`, or `managed`; it is not a blanket "published" label. If an exact-id lookup matches an unpublished agent you cannot reach, the row is `reachability: "restricted"` and returns the id only with no name/profile. Use returned agentId in grant/issue tools — do not guess ids.', {
178
- query: z.string().describe('Keyword/natural-language search (published store + your org-mates + your linked delegates) OR an exact agent id (resolves that agent even if unpublished, when you can reach it)'),
179
- limit: z.number().optional().describe('Max results (default server-side)'),
180
- minScore: z.number().optional().describe('Minimum match score filter'),
181
- }, READ_ONLY, async ({ query, limit, minScore }) => {
182
- try {
183
- const client = new AgentSearchClient(creds.operatorKey, creds.agentId);
184
- const result = await client.searchAgents(query, { limit, minScore });
185
- if (!result.success) {
186
- return toolError(result.error ?? result.message ?? 'search failed');
187
- }
188
- if (!result.agents?.length) {
189
- // ZIG-664: a bare {count: 0} reads as "discovery is down" to LLM
190
- // callers — say what was searched and how to recover instead.
191
- return textResult({
192
- count: 0,
193
- agents: [],
194
- searched: ['published store', 'your org-mates', 'your linked delegates'],
195
- hint: 'Zero hits means no agent profile matched these terms — discovery itself is up. ' +
196
- 'Matching is lexical against agent name/description/tags, so try shorter or different keywords. ' +
197
- 'If you already know the agent, pass its exact agent id as the query to resolve it directly.',
198
- });
199
- }
200
- return textResult({
201
- count: result.agents.length,
202
- agents: result.agents,
203
- });
204
- }
205
- catch (e) {
206
- return toolError(e.message);
207
- }
208
- });
209
- server.tool('ziggs_get_agent', 'Fetch the full profile of ONE agent by its exact id (GET /agents/:id) — name, description, tags, capabilities, reachability, and reliability. Use to confirm a candidate before ziggs_propose_agreement / ziggs_request_link, when you already hold the agent id (from ziggs_search_agents, a grant, or an agreement party). Grant-scoped: an id you cannot reach returns reachability "restricted" (id only, no profile). To find an agent by keyword instead, use ziggs_search_agents.', {
210
- agentId: z.string().describe('Exact agent id to fetch — do not guess'),
211
- }, READ_ONLY, async ({ agentId }) => {
212
- try {
213
- const client = new AgentSearchClient(creds.operatorKey, creds.agentId);
214
- const result = await client.getAgentById(agentId);
215
- if (!result.success) {
216
- return toolError(result.error ?? 'agent not found');
217
- }
218
- const { success: _success, ...agent } = result;
219
- return textResult({ agent });
220
- }
221
- catch (e) {
222
- return toolError(e.message);
223
- }
224
- });
15
+ registerCapabilities(server, DISCOVERY_CAPABILITIES, creds);
225
16
  server.tool('ziggs_issue_grant', 'Issue bounded context access. Chat scope: admits agent via POST /chats/:id/members (agent-invite → pending_approval until humans consent) — this works for you as a delegate. Agreement/org scope: issuing a NEW root grant is a human-authority action; if you are acting for a principal you are denied (AGENT_LACKS_HUMAN_AUTHORITY) — instead use ziggs_delegate_grant to hand a peer a narrower slice of a grant you already hold, or ask your human to issue it. Defaults: from-now, narrow scope.', {
226
17
  holderId: z.string().describe('Bare agent id receiving the grant'),
227
18
  scopeKind: grantScopeKindSchema,
@@ -269,10 +60,7 @@ export function registerTrustTools(server, creds, cfg) {
269
60
  // ZIG-941 #6 — an org scope may be named rather than pasted as org_… id.
270
61
  let resolvedScopeId = scopeId;
271
62
  if (scopeKind === 'org') {
272
- const resolved = await resolveOrgScope(creds, scopeId);
273
- if ('error' in resolved)
274
- return resolved.error;
275
- resolvedScopeId = resolved.scopeId;
63
+ resolvedScopeId = await resolveOrgScopeId({ creds, surface: 'mcp' }, scopeId);
276
64
  }
277
65
  const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
278
66
  const grant = await client.issueGrant({
@@ -291,58 +79,12 @@ export function registerTrustTools(server, creds, cfg) {
291
79
  return toolError(e.message);
292
80
  }
293
81
  });
294
- server.tool('ziggs_delegate_grant', 'Delegate a narrower child grant from one you hold (POST /context/grants/:id/delegate). Delegation only narrows scope/expiry/temporal — never broadens. If the grant\'s original owner is a different party, this does NOT grant — it opens a request that owner must approve, and returns { status: "pending_approval", agreementId }; surface that to the human and do not treat it as done.', {
295
- parentGrantId: z.string(),
296
- holderId: z.string().describe('Agent receiving the delegated grant'),
297
- scopeKind: grantScopeKindSchema,
298
- scopeId: z.string(),
299
- temporal: contextTemporalSchema.describe('from-now or from-start (must be same-or-narrower)'),
300
- expiresAt: z.string().optional().nullable(),
301
- watermarkAt: z
302
- .string()
303
- .optional()
304
- .describe('from-now watermark ISO-8601 (optional; server may default)'),
305
- }, WRITE, async ({ parentGrantId, holderId, scopeKind, scopeId, temporal, expiresAt, watermarkAt, }) => {
306
- try {
307
- // ZIG-941 #6 — an org scope may be named rather than pasted as org_… id.
308
- let resolvedScopeId = scopeId;
309
- if (scopeKind === 'org') {
310
- const resolved = await resolveOrgScope(creds, scopeId);
311
- if ('error' in resolved)
312
- return resolved.error;
313
- resolvedScopeId = resolved.scopeId;
314
- }
315
- const client = new ContextGrantsClient(creds.operatorKey, creds.agentId);
316
- const result = await client.delegateGrant(parentGrantId, {
317
- holderId,
318
- scope: { kind: scopeKind, id: resolvedScopeId },
319
- temporal,
320
- expiresAt,
321
- watermarkAt,
322
- });
323
- if (result.status === 'pending_approval') {
324
- return textResult({
325
- status: 'pending_approval',
326
- message: "This grant's original owner must approve sharing it. A request was opened for them — surface it to the human; nothing is granted yet.",
327
- parentGrantId,
328
- agreementId: result.agreementId,
329
- ownerId: result.ownerId,
330
- });
331
- }
332
- const grant = result.grant;
333
- return textResult({
334
- status: 'delegated',
335
- parentGrantId,
336
- grant,
337
- bounds: contextBounds(grant),
338
- });
339
- }
340
- catch (e) {
341
- return toolError(e.message);
342
- }
343
- });
82
+ registerCapability(server, contextDelegateCapability, creds);
344
83
  if (!cfg?.coreOnly) {
345
- registerLinkTools(server, creds, webUrl);
84
+ // ZIG-941 #7 — the link tool group is skipped by the lean session-start
85
+ // tier (ZIGGS_MCP_CORE_ONLY). Definitions live in the shared capability
86
+ // layer, including the linkSummary shaping the mutations now share.
87
+ registerCapabilities(server, LINK_CAPABILITIES, creds, { webUrl });
346
88
  }
347
89
  server.tool('ziggs_revoke_grant', 'Revoke a context grant and its descendants (DELETE /context/grants/:id). You can revoke (narrow) any grant you hold — this needs no special scope. Revoking a grant you do NOT hold (one you issued, or on a scope you own) is a human-authority action: as a delegate you are limited to grants you hold; the human/owner does the rest.', {
348
90
  grantId: z.string(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ziggs-ai/ziggs-mcp",
3
- "version": "0.3.2",
4
- "description": "MCP server for Claude Code, Cursor, and other MCP hosts — act as your Ziggs delegate agent",
3
+ "version": "0.5.0",
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": {
7
7
  "ziggs-mcp": "./dist/index.js"
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@modelcontextprotocol/sdk": "^1.29.0",
39
- "@ziggs-ai/api-client": "^0.3.1",
39
+ "@ziggs-ai/api-client": "^0.5.0",
40
40
  "dotenv": "^16.6.1",
41
41
  "zod": "^3.24.2"
42
42
  },
package/dist/orgs.d.ts DELETED
@@ -1,28 +0,0 @@
1
- import { type Creds } from '@ziggs-ai/api-client';
2
- export interface MyOrg {
3
- orgId: string;
4
- name: string;
5
- kind: string;
6
- role?: string;
7
- }
8
- /**
9
- * ZIG-739 — the operator's full org membership (not just granted scopes, which
10
- * is all ziggs_list_grants sees). Lets the delegate resolve an org name to
11
- * an id and offer a pick-list instead of demanding a pasted org_... id.
12
- */
13
- export declare function fetchMyOrgs(creds: Creds): Promise<MyOrg[]>;
14
- export type OrgResolution = {
15
- status: 'ok';
16
- orgId: string;
17
- } | {
18
- status: 'ambiguous';
19
- matches: MyOrg[];
20
- } | {
21
- status: 'not-found';
22
- };
23
- /**
24
- * ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
25
- * the operator's memberships. Exact id wins; otherwise case-insensitive name
26
- * match. Ambiguous names return the candidates rather than guessing.
27
- */
28
- export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
package/dist/orgs.js DELETED
@@ -1,44 +0,0 @@
1
- import { getBackendUrl } from '@ziggs-ai/api-client';
2
- /**
3
- * ZIG-739 — the operator's full org membership (not just granted scopes, which
4
- * is all ziggs_list_grants sees). Lets the delegate resolve an org name to
5
- * an id and offer a pick-list instead of demanding a pasted org_... id.
6
- */
7
- export async function fetchMyOrgs(creds) {
8
- const url = `${getBackendUrl()}/orgs/me`;
9
- const res = await fetch(url, {
10
- method: 'GET',
11
- headers: {
12
- Authorization: `Bearer ${creds.operatorKey}`,
13
- 'X-Agent-Id': creds.agentId,
14
- },
15
- });
16
- const body = await res.text().catch(() => '');
17
- if (!res.ok) {
18
- throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
19
- }
20
- const parsed = body ? JSON.parse(body) : {};
21
- return (parsed.orgs ?? []).map((o) => ({
22
- orgId: o.orgId,
23
- name: o.name,
24
- kind: o.kind,
25
- role: o.role,
26
- }));
27
- }
28
- /**
29
- * ZIG-739 — resolve an org selector (exact org_... id OR a name/handle) against
30
- * the operator's memberships. Exact id wins; otherwise case-insensitive name
31
- * match. Ambiguous names return the candidates rather than guessing.
32
- */
33
- export function resolveOrgSelector(orgs, selector) {
34
- const byId = orgs.find((o) => o.orgId === selector);
35
- if (byId)
36
- return { status: 'ok', orgId: byId.orgId };
37
- const needle = selector.toLowerCase();
38
- const byName = orgs.filter((o) => o.name.toLowerCase() === needle);
39
- if (byName.length === 1)
40
- return { status: 'ok', orgId: byName[0].orgId };
41
- if (byName.length > 1)
42
- return { status: 'ambiguous', matches: byName };
43
- return { status: 'not-found' };
44
- }