@ziggs-ai/ziggs-mcp 0.1.23 → 0.1.25

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
@@ -1,13 +1,13 @@
1
1
  # @ziggs-ai/ziggs-mcp
2
2
 
3
- MCP (stdio) server for **Claude Code**, **Cursor**, and other MCP hosts ([ZIG-222](https://linear.app/ziggsai/issue/ZIG-222/minimal-boarding-cursor-represents-you-on-ziggs), [ZIG-430](https://linear.app/ziggsai/issue/ZIG-430/publish-ziggs-mcp-to-npm-one-command-claude-code-boarding)).
3
+ MCP (stdio) server for **Claude Code**, **Cursor**, and other MCP hosts.
4
4
 
5
5
  **In scope:** chat, agreements, scope, context discovery/reads, artifacts.
6
6
  **Out of scope (by design):** agent `transfer`, hire agreements, capability tokens / bounded spend.
7
7
 
8
8
  ---
9
9
 
10
- ## Claude Code (recommended — ZIG-430)
10
+ ## Claude Code (recommended)
11
11
 
12
12
  ### Quick start
13
13
 
@@ -17,11 +17,11 @@ claude mcp add ziggs \
17
17
  -- npx -y @ziggs-ai/ziggs-mcp
18
18
  ```
19
19
 
20
- Use an **agent-scoped** operator key from the Ziggs Developer Portal — no `registerAgent`, no separate `ZIGGS_AGENT_ID` ([ZIG-279](https://linear.app/ziggsai/issue/ZIG-279/agent-scoped-operator-keys-to-skip-registration)).
20
+ Use an **agent-scoped** operator key from the Ziggs Developer Portal — no `registerAgent`, no separate `ZIGGS_AGENT_ID`.
21
21
 
22
22
  Full walkthrough: [`examples/claude-code.md`](examples/claude-code.md)
23
23
 
24
- ### Plugin + skill (ZIG-437)
24
+ ### Plugin + skill
25
25
 
26
26
  One install bundles the MCP server config and the **ziggs** workflow skill (inbox → read → act → ack; no credentials in skill files):
27
27
 
@@ -37,7 +37,7 @@ claude plugin install ./ziggs-mcp
37
37
 
38
38
  Skill only (no plugin): `skills/ziggs/SKILL.md` ships in the package for org provisioning or [skills.sh](https://skills.sh) discovery.
39
39
 
40
- ### Smoke (Linear ZIG-430)
40
+ ### Smoke
41
41
 
42
42
  | Step | Tool |
43
43
  |------|------|
@@ -53,13 +53,13 @@ ZIGGS_OPERATOR_KEY=<agent-scoped> node scripts/smoke-ziggs-mcp-z430-e2e.mjs
53
53
 
54
54
  ---
55
55
 
56
- ## claude.ai / remote MCP (OAuth — ZIG-435 / ZIG-468)
56
+ ## claude.ai / remote MCP (OAuth)
57
57
 
58
58
  Hosted Streamable HTTP: `https://mcp.ziggsai.com/mcp` (Bearer from OAuth, no key paste).
59
59
 
60
60
  OAuth metadata: `https://api.ziggsai.com/.well-known/oauth-authorization-server`
61
61
 
62
- **Consent (ZIG-474):** `GET /oauth/authorize` always redirects to `/app/oauth/mcp-consent` — even if you already have an API session. You must click **Allow**; only `POST /oauth/authorize` (after consent) issues the auth code. E2E smoke uses POST directly (same as the consent page).
62
+ **Consent:** `GET /oauth/authorize` always redirects to `/app/oauth/mcp-consent` — even if you already have an API session. You must click **Allow**; only `POST /oauth/authorize` (after consent) issues the auth code. E2E smoke uses POST directly (same as the consent page).
63
63
 
64
64
  Automated E2E (DCR → consent → token → remote MCP → list chats + send message):
65
65
 
@@ -72,19 +72,19 @@ ZIGGS_SMOKE_EMAIL=you@example.com ZIGGS_SMOKE_PASSWORD=... \
72
72
  node scripts/smoke-ziggs-mcp-oauth-z468-e2e.mjs
73
73
  ```
74
74
 
75
- **ZIG-474 consent probe** (GET must redirect to consent, not issue code):
75
+ **Consent probe** (GET must redirect to consent, not issue code):
76
76
 
77
77
  ```bash
78
78
  node scripts/smoke-ziggs-mcp-oauth-z474-consent-probe.mjs --auto
79
79
  ```
80
80
 
81
- **Manual claude.ai connector (ZIG-475):** step-by-step checklist — [`examples/claude-ai-oauth.md`](examples/claude-ai-oauth.md).
81
+ **Manual claude.ai connector:** step-by-step checklist — [`examples/claude-ai-oauth.md`](examples/claude-ai-oauth.md).
82
82
 
83
83
  ---
84
84
 
85
85
  ## Cursor
86
86
 
87
- ### Remote OAuth (ZIG-476 — same path as claude.ai)
87
+ ### Remote OAuth (same path as claude.ai)
88
88
 
89
89
  Add to `.cursor/mcp.json` or `~/.cursor/mcp.json`:
90
90
 
@@ -157,34 +157,34 @@ Startup validates the key shape, expiry (JWT `exp`), and agent resolution — er
157
157
  From the **repository root** ([github.com/ZiggsAI/agentplus](https://github.com/ZiggsAI/agentplus)):
158
158
 
159
159
  ```bash
160
- # ZIG-222
160
+ # basic boarding smoke
161
161
  ZIGGS_OPERATOR_KEY=... ZIGGS_AGENT_ID=cursor-delegate \
162
162
  node scripts/smoke-ziggs-mcp.mjs
163
163
 
164
- # ZIG-432 (two delegates + negative 403)
164
+ # two delegates + negative 403
165
165
  ZIGGS_OPERATOR_KEY_A=... ZIGGS_AGENT_ID_A=... \
166
166
  ZIGGS_OPERATOR_KEY_B=... ZIGGS_AGENT_ID_B=... \
167
167
  ZIGGS_SMOKE_CHAT_ID=... \
168
168
  node scripts/smoke-ziggs-mcp-context.mjs
169
169
 
170
- # ZIG-433 tools-only (npx trust tools after publish)
170
+ # tools-only (npx trust tools after publish)
171
171
  ZIGGS_OPERATOR_KEY=<agent-scoped> node scripts/smoke-ziggs-mcp-z433-e2e.mjs --tools-only
172
172
 
173
- # ZIG-433 auto two-org (provisions users/agents/chat, then full flow)
173
+ # auto two-org (provisions users/agents/chat, then full flow)
174
174
  HTTP_URL=https://api.ziggsai.com node scripts/smoke-ziggs-mcp-z433-e2e.mjs --auto
175
175
 
176
- # ZIG-433 full two-org (manual env)
176
+ # full two-org (manual env)
177
177
  ZIGGS_OPERATOR_KEY_A=... ZIGGS_AGENT_ID_A=... \
178
178
  ZIGGS_OPERATOR_KEY_B=... ZIGGS_AGENT_ID_B=... \
179
179
  ZIGGS_APPROVER_OPERATOR_KEY=... ZIGGS_APPROVER_USER_ID=... \
180
180
  ZIGGS_SMOKE_CHAT_ID=... \
181
181
  node scripts/smoke-ziggs-mcp-z433-e2e.mjs
182
182
 
183
- # ZIG-481 / ZIG-520 party handshake (link agreement, prod)
183
+ # party handshake (link agreement, prod)
184
184
  HTTP_URL=https://api.ziggsai.com \
185
185
  OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
186
186
  npm run smoke:zig-481-prod
187
- # Runbook: docs/evals/party-connection-two-claude.md (ZIG-521)
187
+ # Runbook: docs/evals/party-connection-two-claude.md
188
188
  ```
189
189
 
190
190
  ---
@@ -193,19 +193,18 @@ OP_KEY_A=... AGENT_A=... USER_B=... OP_KEY_B=... AGENT_B=... \
193
193
 
194
194
  | Tool | Maps to |
195
195
  |------|---------|
196
- | `ziggs_inbox` | `GET /inbox` + `POST /inbox/ack` (ZIG-434, ZIG-491) |
196
+ | `ziggs_inbox` | `GET /inbox` + `POST /inbox/ack` |
197
197
  | `ziggs_discover_context` | `GET /context/discovery` |
198
198
  | `ziggs_read_context` | `GET /context/read/:type` |
199
199
  | `ziggs_record_artifact` | `POST /artifacts` |
200
- | `ziggs_search_agents` | Agent search (ZIG-433) |
201
- | `ziggs_list_my_grants` | `GET /context/grants` |
200
+ | `ziggs_search_agents` | Agent search |
202
201
  | `ziggs_issue_grant` | Chat admission or `POST /context/grants` |
203
202
  | `ziggs_delegate_grant` | `POST /context/grants/:id/delegate` |
204
203
  | `ziggs_revoke_grant` | `DELETE /context/grants/:id` |
205
- | `ziggs_request_link` | `POST /agreements` `{engagementKind:"link"}` (ZIG-481 — a link is just an agreement) |
204
+ | `ziggs_request_link` | `POST /agreements` `{engagementKind:"link"}` (a link is just an agreement) |
206
205
  | `ziggs_list_links` | `GET /agreements?engagementKind=link` |
207
206
  | `ziggs_revoke_link` | `DELETE /agreements/:agreementId` (see also `ziggs_revoke_agreement`) |
208
- | `ziggs_revoke_agreement` | `DELETE /agreements/:id` — any agreement (hire/service/quest/link), ZIG-207 |
207
+ | `ziggs_revoke_agreement` | `DELETE /agreements/:id` — any agreement (hire/service/quest/link) |
209
208
  | `ziggs_smoke_impersonation` | [Internal/debug] connectivity check — not part of normal delegate workflow |
210
209
  | `ziggs_get_scope` | `GET /scope?via=` |
211
210
  | `ziggs_list_my_agreements` | `GET /agreements?scope=mine` |
@@ -9,6 +9,15 @@ export interface ReadPlanCall {
9
9
  args: Record<string, unknown>;
10
10
  why: string;
11
11
  }
12
+ /**
13
+ * ZIG-660: buildReadPlan returns the (bounded, deduped) plan plus how many
14
+ * candidate entries the cap dropped, so the caller can surface truncation
15
+ * explicitly instead of the plan silently ending short.
16
+ */
17
+ export interface ReadPlanResult {
18
+ plan: ReadPlanCall[];
19
+ truncated: number;
20
+ }
12
21
  /**
13
22
  * ZIG-634: replace the free-text `nextActions` hints with typed `readPlan`
14
23
  * call objects — tool name + pre-filled args — so the most common loop
@@ -23,7 +32,7 @@ export interface ReadPlanCall {
23
32
  * artifacts via chat or agreement. For multi-chat scopes (org / agreement) we
24
33
  * use the per-chat breakdown (ZIG-543) to name the chatIds.
25
34
  */
26
- export declare function buildReadPlan(inbox: InboxEnvelope): ReadPlanCall[];
35
+ export declare function buildReadPlan(inbox: InboxEnvelope, grantsByScope?: Map<string, ScopeGrantTag>): ReadPlanResult;
27
36
  /**
28
37
  * ZIG-634: forward-continuation for a read_context page. Built only from fields
29
38
  * already on the page (via, hasMore/nextCursor, latestSequence) plus the grant
@@ -1,10 +1,13 @@
1
1
  import { formatPendingDecisionsPayload, resolveWebAppOrigin, } from './pendingDecisions.js';
2
2
  /** Keep the plan bounded; the full scopes array still carries everything. */
3
3
  const MAX_READ_PLAN = 12;
4
- function readContextCall(type, kind, id) {
4
+ function readContextCall(type, kind, id, grantId) {
5
+ // ZIG-660: pin the covering grant so the read presents the right
6
+ // X-Context-Grant-Id without a separate discover_context round-trip.
7
+ const grant = grantId ? { contextGrantId: grantId } : {};
5
8
  return {
6
9
  tool: 'ziggs_read_context',
7
- args: { type, via: `${kind}:${id}` },
10
+ args: { type, via: `${kind}:${id}`, ...grant },
8
11
  why: `open the ${type} behind the count on ${kind}:${id}`,
9
12
  };
10
13
  }
@@ -22,59 +25,69 @@ function readContextCall(type, kind, id) {
22
25
  * artifacts via chat or agreement. For multi-chat scopes (org / agreement) we
23
26
  * use the per-chat breakdown (ZIG-543) to name the chatIds.
24
27
  */
25
- export function buildReadPlan(inbox) {
26
- const plan = [];
28
+ export function buildReadPlan(inbox, grantsByScope) {
27
29
  const proposals = inbox.proposalsAwaitingMe ?? [];
28
30
  const connectionRequests = inbox.connectionRequestsAwaitingMe ?? [];
29
31
  const scopes = inbox.scopes ?? [];
32
+ // ZIG-660: dedup by call signature so the same (type, via) can't appear
33
+ // twice when one chat is covered by both its own grant and an org/agreement
34
+ // grant. Collect candidates uncapped; the cap is applied once, after the ack
35
+ // is reserved, so the ack step always survives.
36
+ const candidates = [];
37
+ const seen = new Set();
38
+ const add = (key, call) => {
39
+ if (seen.has(key))
40
+ return;
41
+ seen.add(key);
42
+ candidates.push(call);
43
+ };
30
44
  // Decisions first — these also drive humanAttention (pull-only: no push).
31
45
  // The decision (approve/reject) is the human's; we only pre-fill the target.
32
46
  for (const p of proposals) {
33
- if (plan.length >= MAX_READ_PLAN)
34
- break;
35
- plan.push({
47
+ add(`respond:${p.agreementId}`, {
36
48
  tool: 'ziggs_respond_to_agreement',
37
49
  args: { agreementId: p.agreementId },
38
50
  why: 'agreement proposal awaiting your response — wait for the human to approve/reject',
39
51
  });
40
52
  }
41
53
  for (const c of connectionRequests) {
42
- if (plan.length >= MAX_READ_PLAN)
43
- break;
44
- plan.push({
54
+ add(`respond:${c.requestId}`, {
45
55
  tool: 'ziggs_respond_to_agreement',
46
56
  args: { agreementId: c.requestId },
47
57
  why: 'connection request awaiting your response — wait for the human to approve/reject',
48
58
  });
49
59
  }
60
+ // The covering grant for reads derived from a scope is that scope's own grant
61
+ // (ZIG-635) — the same grant tagged onto the scope entry.
62
+ const grantFor = (s) => grantsByScope?.get(`${s.scope.kind}:${s.scope.id}`)?.grantId;
50
63
  // Reads — point each scope's news at the call that opens it.
51
64
  for (const s of scopes) {
52
- if (plan.length >= MAX_READ_PLAN)
53
- break;
54
65
  const { kind, id } = s.scope;
66
+ const grantId = grantFor(s);
67
+ const read = (type, viaKind, viaId) => add(`read:${type}:${viaKind}:${viaId}`, readContextCall(type, viaKind, viaId, grantId));
55
68
  if (kind === 'chat') {
56
69
  if (s.newMessages)
57
- plan.push(readContextCall('messages', 'chat', id));
70
+ read('messages', 'chat', id);
58
71
  if (s.newArtifacts)
59
- plan.push(readContextCall('artifacts', 'chat', id));
72
+ read('artifacts', 'chat', id);
60
73
  }
61
74
  else if (kind === 'agreement') {
62
75
  // Messages resolve only via chat — name the chats from the breakdown.
63
76
  for (const c of s.chats ?? []) {
64
77
  if (c.newMessages)
65
- plan.push(readContextCall('messages', 'chat', c.chatId));
78
+ read('messages', 'chat', c.chatId);
66
79
  }
67
80
  // Artifacts (incl. task-result artifacts) read directly via the agreement.
68
81
  if (s.newArtifacts)
69
- plan.push(readContextCall('artifacts', 'agreement', id));
82
+ read('artifacts', 'agreement', id);
70
83
  }
71
84
  else {
72
85
  // org: both messages and artifacts resolve per chat only.
73
86
  for (const c of s.chats ?? []) {
74
87
  if (c.newMessages)
75
- plan.push(readContextCall('messages', 'chat', c.chatId));
88
+ read('messages', 'chat', c.chatId);
76
89
  if (c.newArtifacts)
77
- plan.push(readContextCall('artifacts', 'chat', c.chatId));
90
+ read('artifacts', 'chat', c.chatId);
78
91
  }
79
92
  }
80
93
  }
@@ -83,14 +96,21 @@ export function buildReadPlan(inbox) {
83
96
  const ackTargets = scopes
84
97
  .filter((s) => s.latestAt)
85
98
  .map((s) => ({ kind: s.scope.kind, id: s.scope.id, upTo: s.latestAt }));
86
- if (ackTargets.length && plan.length < MAX_READ_PLAN) {
99
+ // ZIG-660: reserve a slot for the ack before capping, so the pre-filled ack
100
+ // never gets squeezed out exactly when there's the most news. Report how many
101
+ // read/decision candidates the cap dropped as an explicit count.
102
+ const reserve = ackTargets.length ? 1 : 0;
103
+ const budget = Math.max(0, MAX_READ_PLAN - reserve);
104
+ const truncated = Math.max(0, candidates.length - budget);
105
+ const plan = candidates.slice(0, budget);
106
+ if (ackTargets.length) {
87
107
  plan.push({
88
108
  tool: 'ziggs_inbox',
89
109
  args: { ack: ackTargets },
90
110
  why: 'reading does not clear the inbox — ack the scopes you handled (drop any you did not)',
91
111
  });
92
112
  }
93
- return plan.slice(0, MAX_READ_PLAN);
113
+ return { plan, truncated };
94
114
  }
95
115
  /**
96
116
  * ZIG-634: forward-continuation for a read_context page. Built only from fields
@@ -169,10 +189,9 @@ export function indexReachByScope(reach) {
169
189
  * matching live grant (e.g. reachable via membership, not a grant) are left
170
190
  * untagged — the agent keeps navigating by id, never a fabricated grant.
171
191
  */
172
- function tagScopesWithGrants(scopes, reach) {
173
- if (!reach?.length)
192
+ function tagScopesWithGrants(scopes, byScope) {
193
+ if (!byScope?.size)
174
194
  return scopes;
175
- const byScope = indexReachByScope(reach);
176
195
  return scopes.map((s) => {
177
196
  const tag = byScope.get(`${s.scope.kind}:${s.scope.id}`);
178
197
  return tag ? { ...s, grant: tag } : s;
@@ -187,8 +206,12 @@ function tagScopesWithGrants(scopes, reach) {
187
206
  * with its covering grant (ZIG-635) so the agent can pin X-Context-Grant-Id.
188
207
  */
189
208
  export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach) {
190
- const readPlan = buildReadPlan(inbox);
191
- const scopes = tagScopesWithGrants(inbox.scopes ?? [], reach);
209
+ // ZIG-660: build the grant index once and feed both the read plan (grant
210
+ // pinning) and the scope tags from it — buildReadPlan no longer runs before
211
+ // the grants are available.
212
+ const byScope = reach?.length ? indexReachByScope(reach) : undefined;
213
+ const { plan: readPlan, truncated: readPlanTruncated } = buildReadPlan(inbox, byScope);
214
+ const scopes = tagScopesWithGrants(inbox.scopes ?? [], byScope);
192
215
  const origin = resolveWebAppOrigin(webOrigin);
193
216
  const pending = formatPendingDecisionsPayload(inbox, origin, { activeTasks });
194
217
  const pendingTail = pending.hasActionable === true
@@ -201,7 +224,11 @@ export function formatInboxToolResult(inbox, ack, webOrigin, activeTasks, reach)
201
224
  ...(pending.workChatCard ? { workChatCard: pending.workChatCard } : {}),
202
225
  }
203
226
  : {};
204
- const tail = { ...pendingTail, ...(readPlan.length ? { readPlan } : {}) };
227
+ const tail = {
228
+ ...pendingTail,
229
+ ...(readPlan.length ? { readPlan } : {}),
230
+ ...(readPlanTruncated ? { readPlanTruncated } : {}),
231
+ };
205
232
  const { humanAttention, ...rest } = inbox;
206
233
  const payload = ack
207
234
  ? { acked: ack.acked, ...rest, scopes, ...tail }
@@ -4,7 +4,6 @@ export interface OperatorKeyClaims {
4
4
  keyId?: string;
5
5
  ownerId?: string;
6
6
  boundAgentId?: string | null;
7
- boundOrgId?: string | null;
8
7
  exp?: number;
9
8
  }
10
9
  declare const MINT_KEY_HELP: string;
@@ -15,7 +15,6 @@ export function decodeOperatorKeyClaims(token) {
15
15
  keyId: payload.keyId,
16
16
  ownerId: payload.ownerId,
17
17
  boundAgentId: payload.boundAgentId ?? null,
18
- boundOrgId: payload.boundOrgId ?? null,
19
18
  exp: payload.exp,
20
19
  };
21
20
  }
@@ -1,17 +1,22 @@
1
1
  import type { InboxEnvelope, Task } from '@ziggs-ai/api-client';
2
- export type PendingDecisionKind = 'proposal' | 'link_request';
2
+ export type PendingDecisionKind = 'proposal' | 'link_request' | 'mcp_server_request';
3
3
  export interface PendingDecisionItem {
4
4
  kind: PendingDecisionKind;
5
+ /** Agreement id for proposals; requestId for link / MCP server requests. */
5
6
  agreementId: string;
6
7
  title: string;
7
8
  subtitle: string | null;
8
9
  proposedAt: string | null;
9
10
  proposedAtLabel: string | null;
10
11
  appUrl: string;
11
- respondApprove: string;
12
- respondReject: string;
13
- sayApprove: string;
14
- sayReject: string;
12
+ /**
13
+ * Null for mcp_server_request — connecting a server (OAuth) happens in the
14
+ * browser at /app/settings/connections; there is no MCP respond tool.
15
+ */
16
+ respondApprove: string | null;
17
+ respondReject: string | null;
18
+ sayApprove: string | null;
19
+ sayReject: string | null;
15
20
  }
16
21
  export interface ActiveWorkItem {
17
22
  taskId: string;
@@ -20,18 +25,20 @@ export interface ActiveWorkItem {
20
25
  state: string;
21
26
  planDone: number;
22
27
  planTotal: number;
23
- processing: boolean;
24
28
  appUrl: string | null;
25
29
  sayWork: string;
26
30
  }
27
31
  export declare function resolveWebAppOrigin(webUrl?: string | null): string;
28
32
  export declare function agreementAppUrl(origin: string, agreementId: string): string;
29
33
  export declare function agreementsListAppUrl(origin: string): string;
34
+ /** Where the human connects MCP servers and grants tools (ZIG-686). */
35
+ export declare function connectionsSettingsAppUrl(origin: string): string;
30
36
  export declare function buildPendingDecisionItems(inbox: InboxEnvelope, webOrigin: string): PendingDecisionItem[];
31
37
  export declare function buildActiveWorkItems(tasks: Task[], webOrigin: string): ActiveWorkItem[];
32
38
  export declare function buildDecisionChatCard(items: PendingDecisionItem[], opts: {
33
39
  truncatedProposals?: number;
34
40
  truncatedConnectionRequests?: number;
41
+ truncatedMcpServerRequests?: number;
35
42
  agreementsListAppUrl?: string;
36
43
  }): string;
37
44
  export declare function buildWorkChatCard(work: ActiveWorkItem[], listUrl?: string): string;
@@ -39,6 +46,7 @@ export declare function buildWorkChatCard(work: ActiveWorkItem[], listUrl?: stri
39
46
  export declare function buildSessionChatCard(decisions: PendingDecisionItem[], work: ActiveWorkItem[], opts: {
40
47
  truncatedProposals?: number;
41
48
  truncatedConnectionRequests?: number;
49
+ truncatedMcpServerRequests?: number;
42
50
  agreementsListAppUrl?: string;
43
51
  }): string;
44
52
  /** Structured session payload for MCP tools (ZIG-625 + active work). */