@ziggs-ai/ziggs-mcp 0.14.2 → 0.15.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.
- package/README.md +1 -1
- package/dist/config.d.ts +13 -8
- package/dist/config.js +11 -6
- package/dist/connectionCreds.js +6 -4
- package/dist/index.js +0 -0
- package/dist/operatorKey.d.ts +32 -0
- package/dist/operatorKey.js +30 -0
- package/dist/pendingDecisions.js +4 -4
- package/dist/protocol/delegateProtocol.d.ts +3 -2
- package/dist/protocol/delegateProtocol.js +3 -2
- package/dist/surface.d.ts +26 -11
- package/dist/surface.js +135 -71
- package/dist/toolCatalog.d.ts +5 -2
- package/dist/tools.js +17 -14
- package/package.json +3 -3
- package/skills/ziggs/.cursorrules +1 -1
- package/skills/ziggs/SKILL.md +1 -1
- package/skills/ziggs/references/inbox-rhythm.md +1 -1
- package/skills/ziggs/references/reporting-convention.md +1 -1
package/README.md
CHANGED
|
@@ -168,7 +168,7 @@ Startup validates the key shape, expiry (JWT `exp`), and agent resolution — er
|
|
|
168
168
|
| `ziggs_context_issue_grant` | Chat admission or `POST /context/grants` |
|
|
169
169
|
| `ziggs_context_delegate` | `POST /context/grants/:id/delegate` |
|
|
170
170
|
| `ziggs_context_revoke_grant` | `DELETE /context/grants/:id` |
|
|
171
|
-
| `
|
|
171
|
+
| `ziggs_link_propose` | `POST /agreements/links` — connect with someone by email or agent id, or mint a share link (claimUrl + paste text) |
|
|
172
172
|
| `ziggs_link_list` | `GET /agreements?engagementKind=link` |
|
|
173
173
|
| `ziggs_agreement_revoke` | `DELETE /agreements/:id` — any agreement (hire/service/request/offer/link) |
|
|
174
174
|
| `ziggs_context_snapshot` | `GET /context/snapshot?via=chat:` — one-shot chat orientation (history + agreements + roster), grant-fenced |
|
package/dist/config.d.ts
CHANGED
|
@@ -49,15 +49,20 @@ export interface ZiggsMcpConfig extends EnvConfig {
|
|
|
49
49
|
*/
|
|
50
50
|
coreOnly: boolean;
|
|
51
51
|
/**
|
|
52
|
-
* True
|
|
53
|
-
* stdio package plus their own operator key), false for the remote endpoint
|
|
54
|
-
* the connector directory advertises.
|
|
52
|
+
* True unless the credential was minted by the MCP OAuth consent flow.
|
|
55
53
|
*
|
|
56
|
-
* The
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* The consent flow is how a connected assistant (Claude, ChatGPT, Cursor)
|
|
55
|
+
* boards, and it is the door the connector directory hands out. A store
|
|
56
|
+
* reviewer has to be able to tell from a tool's definition what it will do,
|
|
57
|
+
* so that surface carries no tool whose target is whatever the caller names
|
|
58
|
+
* with a freeform payload: the connection rail. Everyone else configured
|
|
59
|
+
* this server themselves with an operator key, on stdio or against the
|
|
60
|
+
* remote endpoint, and gets the whole surface.
|
|
61
|
+
*
|
|
62
|
+
* The line is the credential, not the transport. The backend stamps
|
|
63
|
+
* `issuedVia: "mcp_oauth"` on the tokens the consent flow mints, and this is
|
|
64
|
+
* that stamp read back. It decides which tools are registered, not what a
|
|
65
|
+
* call may do: every call is still authorized by the backend.
|
|
61
66
|
*/
|
|
62
67
|
directConnection: boolean;
|
|
63
68
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { configureApiClient } from '@ziggs-ai/api-client';
|
|
4
|
-
import { MINT_KEY_HELP, resolveDelegateAgentId } from './operatorKey.js';
|
|
4
|
+
import { MINT_KEY_HELP, decodeOperatorKeyClaims, isDirectoryBoarded, resolveDelegateAgentId, } from './operatorKey.js';
|
|
5
5
|
const envSchema = z.object({
|
|
6
6
|
ZIGGS_API_URL: z.string().optional(),
|
|
7
7
|
HTTP_URL: z.string().optional(),
|
|
@@ -32,7 +32,7 @@ function parseBoolFlag(raw) {
|
|
|
32
32
|
export function loadConfig() {
|
|
33
33
|
const raw = {
|
|
34
34
|
ZIGGS_API_URL: process.env.ZIGGS_API_URL,
|
|
35
|
-
HTTP_URL: process.env.
|
|
35
|
+
HTTP_URL: process.env.ZIGGS_API_URL ?? process.env.HTTP_URL,
|
|
36
36
|
ZIGGS_WEB_URL: process.env.ZIGGS_WEB_URL,
|
|
37
37
|
ZIGGS_OPERATOR_KEY: process.env.ZIGGS_OPERATOR_KEY,
|
|
38
38
|
ZIGGS_AGENT_ID: process.env.ZIGGS_AGENT_ID,
|
|
@@ -54,7 +54,14 @@ export function loadConfig() {
|
|
|
54
54
|
}
|
|
55
55
|
// api-client reads injected config, not the environment. This
|
|
56
56
|
// server owns its process, so it hands over whichever base URL it resolved.
|
|
57
|
-
|
|
57
|
+
//
|
|
58
|
+
// ZIGGS_API_URL beats HTTP_URL: the specific name wins over the generic one.
|
|
59
|
+
// dotenv loads whatever .env sits in the cwd, and a dev repo's
|
|
60
|
+
// HTTP_URL=http://localhost:3000 silently redirected a connector that was
|
|
61
|
+
// handed an explicit prod key — the caller who set ZIGGS_API_URL (or the
|
|
62
|
+
// hand-off block that does) is naming THIS server's target, and a bystander
|
|
63
|
+
// file must not override it.
|
|
64
|
+
const httpUrl = parsed.data.ZIGGS_API_URL || process.env.HTTP_URL;
|
|
58
65
|
if (httpUrl) {
|
|
59
66
|
process.env.HTTP_URL = httpUrl;
|
|
60
67
|
configureApiClient({ httpUrl });
|
|
@@ -64,8 +71,6 @@ export function loadConfig() {
|
|
|
64
71
|
resolvedAgentId,
|
|
65
72
|
debugTools: parseBoolFlag(parsed.data.ZIGGS_MCP_DEBUG),
|
|
66
73
|
coreOnly: parseBoolFlag(parsed.data.ZIGGS_MCP_CORE_ONLY),
|
|
67
|
-
|
|
68
|
-
// operator key at it. Not the door the directory hands out.
|
|
69
|
-
directConnection: true,
|
|
74
|
+
directConnection: !isDirectoryBoarded(decodeOperatorKeyClaims(parsed.data.ZIGGS_OPERATOR_KEY)),
|
|
70
75
|
};
|
|
71
76
|
}
|
package/dist/connectionCreds.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { configureApiClient } from '@ziggs-ai/api-client';
|
|
2
|
-
import { resolveDelegateAgentId,
|
|
2
|
+
import { MINT_KEY_HELP, decodeOperatorKeyClaims, isDirectoryBoarded, resolveDelegateAgentId, } from './operatorKey.js';
|
|
3
3
|
export function parseBearerAuthorization(header) {
|
|
4
4
|
const raw = Array.isArray(header) ? header[0] : header;
|
|
5
5
|
if (!raw?.startsWith('Bearer ')) {
|
|
@@ -29,9 +29,11 @@ export function connectionFromBearer(bearer, httpBaseUrl, ownerUserId) {
|
|
|
29
29
|
resolvedAgentId,
|
|
30
30
|
debugTools: false,
|
|
31
31
|
coreOnly: false,
|
|
32
|
-
// The
|
|
33
|
-
//
|
|
34
|
-
|
|
32
|
+
// The listed surface for a token the OAuth consent flow minted (a connected
|
|
33
|
+
// assistant), the whole surface for an operator key someone pasted here
|
|
34
|
+
// themselves. The line is the credential, not this endpoint; see
|
|
35
|
+
// ZiggsMcpConfig.directConnection.
|
|
36
|
+
directConnection: !isDirectoryBoarded(decodeOperatorKeyClaims(bearer)),
|
|
35
37
|
};
|
|
36
38
|
return {
|
|
37
39
|
creds: { operatorKey: bearer, agentId: resolvedAgentId },
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/operatorKey.d.ts
CHANGED
|
@@ -4,12 +4,44 @@ export interface OperatorKeyClaims {
|
|
|
4
4
|
keyId?: string;
|
|
5
5
|
ownerId?: string;
|
|
6
6
|
boundAgentId?: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* How the key was minted. The backend stamps `mcp_oauth` on every access
|
|
9
|
+
* token the MCP OAuth consent flow issues, refreshes included; a key minted
|
|
10
|
+
* anywhere else (dashboard, CLI, provisioning) carries nothing here.
|
|
11
|
+
*/
|
|
12
|
+
issuedVia?: string;
|
|
7
13
|
exp?: number;
|
|
8
14
|
}
|
|
9
15
|
declare const MINT_KEY_HELP: string;
|
|
10
16
|
/** Decode operator JWT payload without verifying signature (boundAgentId). */
|
|
11
17
|
export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
|
|
12
18
|
export declare function isOperatorKeyExpired(claims: OperatorKeyClaims | null): boolean;
|
|
19
|
+
/** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
|
|
20
|
+
export declare const ISSUED_VIA_MCP_OAUTH = "mcp_oauth";
|
|
21
|
+
/** The stamp the MCP OAuth device-code flow puts on its tokens. */
|
|
22
|
+
export declare const ISSUED_VIA_DEVICE_CODE = "device_code";
|
|
23
|
+
/**
|
|
24
|
+
* Did a person board this credential through the connector directory?
|
|
25
|
+
*
|
|
26
|
+
* That is the question the tool surface turns on: a connected assistant
|
|
27
|
+
* (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
|
|
28
|
+
* anyone who configured this server themselves with an operator key gets the
|
|
29
|
+
* whole thing.
|
|
30
|
+
*
|
|
31
|
+
* Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
|
|
32
|
+
* board an assistant through that door — the authorization-code flow and the
|
|
33
|
+
* device-code flow — and both want the same narrowed surface, but the server
|
|
34
|
+
* records them as the different acts they are. An equality test against one
|
|
35
|
+
* value serves the whole catalogue to every credential from the other one.
|
|
36
|
+
*
|
|
37
|
+
* Read off the unverified payload. On the remote endpoint the backend verified
|
|
38
|
+
* the signature before this ran; on stdio the key is the caller's own. Either
|
|
39
|
+
* way the payload is the one the backend signed. And what hangs on the answer
|
|
40
|
+
* is which tools are registered, not what a call may do: every call is still
|
|
41
|
+
* authorized by the backend. Provenance may decide what is OFFERED, never what
|
|
42
|
+
* is PERMITTED.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isDirectoryBoarded(claims: OperatorKeyClaims | null): boolean;
|
|
13
45
|
/**
|
|
14
46
|
* Resolve delegate agent id: agent-scoped key (boundAgentId) wins; else ZIGGS_AGENT_ID.
|
|
15
47
|
*/
|
package/dist/operatorKey.js
CHANGED
|
@@ -15,6 +15,7 @@ export function decodeOperatorKeyClaims(token) {
|
|
|
15
15
|
keyId: payload.keyId,
|
|
16
16
|
ownerId: payload.ownerId,
|
|
17
17
|
boundAgentId: payload.boundAgentId ?? null,
|
|
18
|
+
issuedVia: typeof payload.issuedVia === 'string' ? payload.issuedVia : undefined,
|
|
18
19
|
exp: payload.exp,
|
|
19
20
|
};
|
|
20
21
|
}
|
|
@@ -27,6 +28,35 @@ export function isOperatorKeyExpired(claims) {
|
|
|
27
28
|
return false;
|
|
28
29
|
return claims.exp * 1000 <= Date.now();
|
|
29
30
|
}
|
|
31
|
+
/** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
|
|
32
|
+
export const ISSUED_VIA_MCP_OAUTH = 'mcp_oauth';
|
|
33
|
+
/** The stamp the MCP OAuth device-code flow puts on its tokens. */
|
|
34
|
+
export const ISSUED_VIA_DEVICE_CODE = 'device_code';
|
|
35
|
+
/**
|
|
36
|
+
* Did a person board this credential through the connector directory?
|
|
37
|
+
*
|
|
38
|
+
* That is the question the tool surface turns on: a connected assistant
|
|
39
|
+
* (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
|
|
40
|
+
* anyone who configured this server themselves with an operator key gets the
|
|
41
|
+
* whole thing.
|
|
42
|
+
*
|
|
43
|
+
* Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
|
|
44
|
+
* board an assistant through that door — the authorization-code flow and the
|
|
45
|
+
* device-code flow — and both want the same narrowed surface, but the server
|
|
46
|
+
* records them as the different acts they are. An equality test against one
|
|
47
|
+
* value serves the whole catalogue to every credential from the other one.
|
|
48
|
+
*
|
|
49
|
+
* Read off the unverified payload. On the remote endpoint the backend verified
|
|
50
|
+
* the signature before this ran; on stdio the key is the caller's own. Either
|
|
51
|
+
* way the payload is the one the backend signed. And what hangs on the answer
|
|
52
|
+
* is which tools are registered, not what a call may do: every call is still
|
|
53
|
+
* authorized by the backend. Provenance may decide what is OFFERED, never what
|
|
54
|
+
* is PERMITTED.
|
|
55
|
+
*/
|
|
56
|
+
export function isDirectoryBoarded(claims) {
|
|
57
|
+
return (claims?.issuedVia === ISSUED_VIA_MCP_OAUTH ||
|
|
58
|
+
claims?.issuedVia === ISSUED_VIA_DEVICE_CODE);
|
|
59
|
+
}
|
|
30
60
|
/**
|
|
31
61
|
* Resolve delegate agent id: agent-scoped key (boundAgentId) wins; else ZIGGS_AGENT_ID.
|
|
32
62
|
*/
|
package/dist/pendingDecisions.js
CHANGED
|
@@ -25,18 +25,18 @@ export function resolveWebAppOrigin(webUrl) {
|
|
|
25
25
|
return (webUrl?.trim() || 'https://ziggsai.com').replace(/\/$/, '');
|
|
26
26
|
}
|
|
27
27
|
export function agreementAppUrl(origin, agreementId) {
|
|
28
|
-
return `${origin}/app/agreements/${encodeURIComponent(agreementId)}`;
|
|
28
|
+
return `${origin}/app/work/agreements/${encodeURIComponent(agreementId)}`;
|
|
29
29
|
}
|
|
30
30
|
export function agreementsListAppUrl(origin) {
|
|
31
|
-
return `${origin}/app/agreements`;
|
|
31
|
+
return `${origin}/app/work/agreements`;
|
|
32
32
|
}
|
|
33
33
|
/** Where the human connects MCP servers and grants tools. */
|
|
34
34
|
export function connectionsSettingsAppUrl(origin) {
|
|
35
|
-
return `${origin}/app/
|
|
35
|
+
return `${origin}/app/access`;
|
|
36
36
|
}
|
|
37
37
|
/** Where the human decides paused transfers. */
|
|
38
38
|
export function walletAppUrl(origin) {
|
|
39
|
-
return `${origin}/app/
|
|
39
|
+
return `${origin}/app/settings/organization/billing`;
|
|
40
40
|
}
|
|
41
41
|
function truncateText(text, max = TITLE_MAX) {
|
|
42
42
|
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
@@ -16,11 +16,12 @@ export declare const PROTOCOL: {
|
|
|
16
16
|
readonly tagline: "You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol.";
|
|
17
17
|
/**
|
|
18
18
|
* The surface is names-first: the everyday tools load natively and everything
|
|
19
|
-
* else is named by ziggs_tools and called through
|
|
19
|
+
* else is named by ziggs_tools and called through ziggs_tool_read or
|
|
20
|
+
* ziggs_tool_write. Said on connect
|
|
20
21
|
* because a client that never calls ziggs_tools would otherwise conclude the
|
|
21
22
|
* everyday set is all there is.
|
|
22
23
|
*/
|
|
23
|
-
readonly surface: "Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=[\"<name>\"] returns its full schema, and
|
|
24
|
+
readonly surface: "Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=[\"<name>\"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.";
|
|
24
25
|
/** The working loop, as the `ziggs_inbox` description phrases it. */
|
|
25
26
|
readonly loop: "Flow: inbox → read → act → ack.";
|
|
26
27
|
/**
|
|
@@ -16,11 +16,12 @@ export const PROTOCOL = {
|
|
|
16
16
|
tagline: 'You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol.',
|
|
17
17
|
/**
|
|
18
18
|
* The surface is names-first: the everyday tools load natively and everything
|
|
19
|
-
* else is named by ziggs_tools and called through
|
|
19
|
+
* else is named by ziggs_tools and called through ziggs_tool_read or
|
|
20
|
+
* ziggs_tool_write. Said on connect
|
|
20
21
|
* because a client that never calls ziggs_tools would otherwise conclude the
|
|
21
22
|
* everyday set is all there is.
|
|
22
23
|
*/
|
|
23
|
-
surface: 'Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=["<name>"] returns its full schema, and
|
|
24
|
+
surface: 'Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=["<name>"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.',
|
|
24
25
|
/** The working loop, as the `ziggs_inbox` description phrases it. */
|
|
25
26
|
loop: 'Flow: inbox → read → act → ack.',
|
|
26
27
|
/**
|
package/dist/surface.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
3
3
|
* The tools a cold session gets natively, with their full schemas.
|
|
4
4
|
*
|
|
5
5
|
* Everything else is in the catalog: named by `ziggs_tools`, described on
|
|
6
|
-
* demand, called through `
|
|
7
|
-
* callable. The line is drawn at what a session cannot
|
|
6
|
+
* demand, called through `ziggs_tool_read` or `ziggs_tool_write` — or by its
|
|
7
|
+
* own name, which stays callable. The line is drawn at what a session cannot
|
|
8
|
+
* start without:
|
|
8
9
|
*
|
|
9
10
|
* - `ziggs_inbox` is the session-start read — the other two orientation tools
|
|
10
11
|
* folded into it — so it is where a caller finds out where it stands and
|
|
@@ -15,7 +16,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
15
16
|
* - `ziggs_chat_send` and `ziggs_task_set_result` are the two ways to answer:
|
|
16
17
|
* conversation, and finished work. An agent that can read its mail and
|
|
17
18
|
* cannot reply is worse off than one that pays for a schema it never used.
|
|
18
|
-
* - the
|
|
19
|
+
* - the catalog tools themselves, or none of the above matters.
|
|
19
20
|
*
|
|
20
21
|
* Deliberately NOT native: the agreement verbs, payments, artifacts, grants,
|
|
21
22
|
* links, connections, tasks beyond the result. Every one of them is a real
|
|
@@ -23,18 +24,20 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
23
24
|
*/
|
|
24
25
|
export declare const NATIVE_TOOLS: readonly string[];
|
|
25
26
|
/**
|
|
26
|
-
* Irreversible tools stay natively listed, and the dispatcher refuses
|
|
27
|
+
* Irreversible tools stay natively listed, and the write dispatcher refuses
|
|
28
|
+
* them.
|
|
27
29
|
*
|
|
28
30
|
* MCP annotations are static per tool and are read at `tools/list` time, before
|
|
29
31
|
* anyone knows which tool a dispatch will land on. So a `destructiveHint` tool
|
|
30
|
-
* reached through `
|
|
31
|
-
* the host asks the human to confirm "Call a Ziggs tool
|
|
32
|
-
* agreement". That is exactly the moment the
|
|
32
|
+
* reached through `ziggs_tool_write` presents to the host as an ordinary write,
|
|
33
|
+
* and the host asks the human to confirm "Call a Ziggs tool that changes
|
|
34
|
+
* something" instead of "Revoke an agreement". That is exactly the moment the
|
|
35
|
+
* warning exists for.
|
|
33
36
|
*
|
|
34
37
|
* Two tools carry `destructiveHint` today, so keeping them native costs two
|
|
35
38
|
* schemas and buys back the host's ability to warn before something that cannot
|
|
36
39
|
* be undone. Refusing them on the dispatcher closes the other half: a caller
|
|
37
|
-
* that reaches for
|
|
40
|
+
* that reaches for the dispatcher out of habit is sent to the annotated call
|
|
38
41
|
* rather than quietly slipping past the prompt.
|
|
39
42
|
*
|
|
40
43
|
* If a third destructive tool ever appears, it belongs in both lists. The test
|
|
@@ -42,8 +45,20 @@ export declare const NATIVE_TOOLS: readonly string[];
|
|
|
42
45
|
*/
|
|
43
46
|
export declare const DESTRUCTIVE_STAY_NATIVE: readonly string[];
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
48
|
+
* The catalog and its two dispatchers. Always listed, whatever the tier.
|
|
49
|
+
*
|
|
50
|
+
* Two dispatchers rather than one, split by annotation. A host and a store
|
|
51
|
+
* reviewer both read what a tool is from its annotation, and one dispatcher
|
|
52
|
+
* that carried reads and writes was marked write for all of them: every read
|
|
53
|
+
* through it asked the human to confirm, and a directory reviewer saw exactly
|
|
54
|
+
* the one-tool-for-safe-and-unsafe-operations shape they reject. Each
|
|
55
|
+
* dispatcher accepts only its own lane and names the other lane's dispatcher
|
|
56
|
+
* when it refuses.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CATALOG_TOOLS: readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* Narrow `tools/list` to the native set, then register the tools that make the
|
|
61
|
+
* rest reachable: the catalog, and one dispatcher per annotation lane.
|
|
47
62
|
*
|
|
48
63
|
* Narrowing by `.disable()` would be the shorter way to do this, and it is the
|
|
49
64
|
* wrong one: disabling takes a tool out of `tools/list` AND out of
|
|
@@ -54,7 +69,7 @@ export declare const DESTRUCTIVE_STAY_NATIVE: readonly string[];
|
|
|
54
69
|
* the tool exists, where it went, or how to reach it.
|
|
55
70
|
*
|
|
56
71
|
* So every tool stays ENABLED, and the listing is narrowed instead. A name is
|
|
57
|
-
* always callable; the list is still the native set plus the catalog
|
|
72
|
+
* always callable; the list is still the native set plus the catalog tools.
|
|
58
73
|
*
|
|
59
74
|
* The listing delegates to the handler the SDK installed rather than rebuilding
|
|
60
75
|
* one: the SDK owns how a registered tool becomes a `tools/list` entry, and
|
package/dist/surface.js
CHANGED
|
@@ -10,8 +10,9 @@ import { catalogEntry, catalogFor, catalogRow, describeEntry, removeCatalogEntry
|
|
|
10
10
|
* The tools a cold session gets natively, with their full schemas.
|
|
11
11
|
*
|
|
12
12
|
* Everything else is in the catalog: named by `ziggs_tools`, described on
|
|
13
|
-
* demand, called through `
|
|
14
|
-
* callable. The line is drawn at what a session cannot
|
|
13
|
+
* demand, called through `ziggs_tool_read` or `ziggs_tool_write` — or by its
|
|
14
|
+
* own name, which stays callable. The line is drawn at what a session cannot
|
|
15
|
+
* start without:
|
|
15
16
|
*
|
|
16
17
|
* - `ziggs_inbox` is the session-start read — the other two orientation tools
|
|
17
18
|
* folded into it — so it is where a caller finds out where it stands and
|
|
@@ -22,7 +23,7 @@ import { catalogEntry, catalogFor, catalogRow, describeEntry, removeCatalogEntry
|
|
|
22
23
|
* - `ziggs_chat_send` and `ziggs_task_set_result` are the two ways to answer:
|
|
23
24
|
* conversation, and finished work. An agent that can read its mail and
|
|
24
25
|
* cannot reply is worse off than one that pays for a schema it never used.
|
|
25
|
-
* - the
|
|
26
|
+
* - the catalog tools themselves, or none of the above matters.
|
|
26
27
|
*
|
|
27
28
|
* Deliberately NOT native: the agreement verbs, payments, artifacts, grants,
|
|
28
29
|
* links, connections, tasks beyond the result. Every one of them is a real
|
|
@@ -38,18 +39,20 @@ export const NATIVE_TOOLS = [
|
|
|
38
39
|
'ziggs_context_revoke_grant',
|
|
39
40
|
];
|
|
40
41
|
/**
|
|
41
|
-
* Irreversible tools stay natively listed, and the dispatcher refuses
|
|
42
|
+
* Irreversible tools stay natively listed, and the write dispatcher refuses
|
|
43
|
+
* them.
|
|
42
44
|
*
|
|
43
45
|
* MCP annotations are static per tool and are read at `tools/list` time, before
|
|
44
46
|
* anyone knows which tool a dispatch will land on. So a `destructiveHint` tool
|
|
45
|
-
* reached through `
|
|
46
|
-
* the host asks the human to confirm "Call a Ziggs tool
|
|
47
|
-
* agreement". That is exactly the moment the
|
|
47
|
+
* reached through `ziggs_tool_write` presents to the host as an ordinary write,
|
|
48
|
+
* and the host asks the human to confirm "Call a Ziggs tool that changes
|
|
49
|
+
* something" instead of "Revoke an agreement". That is exactly the moment the
|
|
50
|
+
* warning exists for.
|
|
48
51
|
*
|
|
49
52
|
* Two tools carry `destructiveHint` today, so keeping them native costs two
|
|
50
53
|
* schemas and buys back the host's ability to warn before something that cannot
|
|
51
54
|
* be undone. Refusing them on the dispatcher closes the other half: a caller
|
|
52
|
-
* that reaches for
|
|
55
|
+
* that reaches for the dispatcher out of habit is sent to the annotated call
|
|
53
56
|
* rather than quietly slipping past the prompt.
|
|
54
57
|
*
|
|
55
58
|
* If a third destructive tool ever appears, it belongs in both lists. The test
|
|
@@ -60,25 +63,61 @@ export const DESTRUCTIVE_STAY_NATIVE = [
|
|
|
60
63
|
'ziggs_context_revoke_grant',
|
|
61
64
|
];
|
|
62
65
|
const TOOLS_CATALOG_NAME = 'ziggs_tools';
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const TOOL_READ_NAME = 'ziggs_tool_read';
|
|
67
|
+
const TOOL_WRITE_NAME = 'ziggs_tool_write';
|
|
68
|
+
/**
|
|
69
|
+
* The catalog and its two dispatchers. Always listed, whatever the tier.
|
|
70
|
+
*
|
|
71
|
+
* Two dispatchers rather than one, split by annotation. A host and a store
|
|
72
|
+
* reviewer both read what a tool is from its annotation, and one dispatcher
|
|
73
|
+
* that carried reads and writes was marked write for all of them: every read
|
|
74
|
+
* through it asked the human to confirm, and a directory reviewer saw exactly
|
|
75
|
+
* the one-tool-for-safe-and-unsafe-operations shape they reject. Each
|
|
76
|
+
* dispatcher accepts only its own lane and names the other lane's dispatcher
|
|
77
|
+
* when it refuses.
|
|
78
|
+
*/
|
|
79
|
+
export const CATALOG_TOOLS = [
|
|
66
80
|
TOOLS_CATALOG_NAME,
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
TOOL_READ_NAME,
|
|
82
|
+
TOOL_WRITE_NAME,
|
|
83
|
+
];
|
|
84
|
+
const CATALOG_TOOL_NAMES = new Set(CATALOG_TOOLS);
|
|
85
|
+
/**
|
|
86
|
+
* A tool that takes a freeform target and payload has to say which API it
|
|
87
|
+
* reaches; a store reviewer cannot otherwise tell what it does.
|
|
88
|
+
*/
|
|
89
|
+
const ZIGGS_API = 'the Ziggs API (https://api.ziggsai.com, reference at https://ziggsai.com/docs)';
|
|
69
90
|
const TOOLS_DESCRIPTION = 'Every Ziggs tool that exists: name plus one line, for the whole surface. ' +
|
|
70
|
-
'A handful of everyday tools are already loaded natively; the rest live here and
|
|
91
|
+
'A handful of everyday tools are already loaded natively; the rest live here. Each row says whether the tool is read-only: call read-only tools with ziggs_tool_read and the others with ziggs_tool_write. ' +
|
|
71
92
|
'Pass `describe` with one or more tool names to get their full descriptions and input schemas — do that before calling a tool you have not used in this session, so you pass the right arguments. ' +
|
|
72
93
|
'Pass `search` to filter the listing by a word in the name or title (e.g. "agreement", "artifact"). ' +
|
|
73
94
|
'Nothing is hidden: if a capability exists, its name is in this list.';
|
|
74
|
-
const
|
|
75
|
-
|
|
95
|
+
const TOOL_READ_DESCRIPTION = `Call a read-only Ziggs tool by name, against ${ZIGGS_API}. ` +
|
|
96
|
+
'This reaches every read-only tool ziggs_tools lists that is not natively loaded, and it never changes anything: a tool that writes is refused here and pointed at ziggs_tool_write. ' +
|
|
97
|
+
'Look the tool up with ziggs_tools describe=[<name>] first when you do not already know its parameters: `args` is validated against the real schema and a wrong key is refused, not silently dropped. ' +
|
|
98
|
+
'The result is exactly what the tool itself returns.';
|
|
99
|
+
const TOOL_WRITE_DESCRIPTION = `Call a Ziggs tool that creates or changes something, by name, against ${ZIGGS_API}. ` +
|
|
100
|
+
'This reaches every writing tool ziggs_tools lists that is not natively loaded. Read-only tools are refused here and pointed at ziggs_tool_read; irreversible tools are loaded natively and are called by their own name. ' +
|
|
101
|
+
'Look the tool up with ziggs_tools describe=[<name>] first when you do not already know its parameters: `args` is validated against the real schema and a wrong key is refused, not silently dropped. ' +
|
|
76
102
|
'The result is exactly what the tool itself returns.';
|
|
77
103
|
function sortRows(entries) {
|
|
78
104
|
return [...entries].sort((a, b) => a.name.localeCompare(b.name));
|
|
79
105
|
}
|
|
106
|
+
/** Which dispatcher a catalog entry is called through. */
|
|
107
|
+
function laneOf(entry) {
|
|
108
|
+
return entry.annotations.readOnlyHint ? TOOL_READ_NAME : TOOL_WRITE_NAME;
|
|
109
|
+
}
|
|
110
|
+
/** The hint under a describe: the dispatcher to call next, named when one fits all. */
|
|
111
|
+
function nextCall(entries) {
|
|
112
|
+
const lanes = new Set(entries.map(laneOf));
|
|
113
|
+
if (lanes.size === 1) {
|
|
114
|
+
for (const lane of lanes)
|
|
115
|
+
return `Call it with ${lane} { tool, args }.`;
|
|
116
|
+
}
|
|
117
|
+
return `Call read-only tools with ${TOOL_READ_NAME} { tool, args } and the rest with ${TOOL_WRITE_NAME} { tool, args }.`;
|
|
118
|
+
}
|
|
80
119
|
/**
|
|
81
|
-
* Keep `tools/list` to `listed` (plus the catalog
|
|
120
|
+
* Keep `tools/list` to `listed` (plus the catalog tools, registered after this
|
|
82
121
|
* runs) by filtering the response the SDK's own handler builds.
|
|
83
122
|
*
|
|
84
123
|
* The handler is read back off the underlying `Server` so the SDK stays the
|
|
@@ -107,8 +146,62 @@ function listOnly(server, listed) {
|
|
|
107
146
|
});
|
|
108
147
|
}
|
|
109
148
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
149
|
+
* One dispatcher: reaches every catalog tool of its lane by name, validates the
|
|
150
|
+
* arguments against that tool's own strict schema, and runs its stored handler.
|
|
151
|
+
*/
|
|
152
|
+
function registerDispatcher(server, lane) {
|
|
153
|
+
registerStrictTool(server, lane.name, lane.description, {
|
|
154
|
+
tool: z
|
|
155
|
+
.string()
|
|
156
|
+
.describe(`Tool name from ${TOOLS_CATALOG_NAME}, e.g. ${lane.example}.`),
|
|
157
|
+
args: z
|
|
158
|
+
.record(z.unknown())
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Arguments for that tool, as its own schema defines them."),
|
|
161
|
+
}, lane.annotations, async ({ tool, args }) => {
|
|
162
|
+
const entry = catalogEntry(server, tool);
|
|
163
|
+
if (!entry) {
|
|
164
|
+
return toolError(`No such tool: ${tool}. Call ${TOOLS_CATALOG_NAME} for the full list of names.`);
|
|
165
|
+
}
|
|
166
|
+
// An irreversible action must go through the call the host can annotate,
|
|
167
|
+
// or the human is asked to confirm the wrong sentence.
|
|
168
|
+
if (entry.annotations.destructiveHint) {
|
|
169
|
+
return toolError(`${entry.name} cannot be called through ${lane.name}: it is irreversible, and the host shows the human what it is about to allow from the tool it is asked to run. ` +
|
|
170
|
+
`${entry.name} is loaded directly for that reason — call it by name.`);
|
|
171
|
+
}
|
|
172
|
+
// Each lane carries one kind of tool, so its annotation is true of every
|
|
173
|
+
// call through it. The other lane is named, not guessed at.
|
|
174
|
+
const entryReadOnly = entry.annotations.readOnlyHint === true;
|
|
175
|
+
if (entryReadOnly !== lane.readOnly) {
|
|
176
|
+
return toolError(entryReadOnly
|
|
177
|
+
? `${entry.name} is read-only: call it with ${TOOL_READ_NAME} { tool, args }, not ${lane.name}.`
|
|
178
|
+
: `${entry.name} changes state: call it with ${TOOL_WRITE_NAME} { tool, args }, not ${lane.name}.`);
|
|
179
|
+
}
|
|
180
|
+
// Validate here rather than letting the handler read undefined fields:
|
|
181
|
+
// the strict schema is what turns a misspelled key into a named refusal
|
|
182
|
+
// instead of a call that runs and reports success for the wrong thing.
|
|
183
|
+
// It is the object registration already built — the catalog carries it,
|
|
184
|
+
// so a dispatch does not rebuild a schema that cannot have changed.
|
|
185
|
+
const parsed = entry.params.safeParse(args ?? {});
|
|
186
|
+
if (!parsed.success) {
|
|
187
|
+
const detail = parsed.error.issues
|
|
188
|
+
.map((i) => (i.path.length ? `${i.path.join('.')}: ${i.message}` : i.message))
|
|
189
|
+
.join('; ');
|
|
190
|
+
// The unknown-key refusal already names the tool and lists what it
|
|
191
|
+
// accepts; don't say the name twice.
|
|
192
|
+
return toolError(detail.includes(entry.name) ? detail : `${entry.name} did not run: ${detail}`);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return (await entry.call(parsed.data));
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
return toolError(e);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Narrow `tools/list` to the native set, then register the tools that make the
|
|
204
|
+
* rest reachable: the catalog, and one dispatcher per annotation lane.
|
|
112
205
|
*
|
|
113
206
|
* Narrowing by `.disable()` would be the shorter way to do this, and it is the
|
|
114
207
|
* wrong one: disabling takes a tool out of `tools/list` AND out of
|
|
@@ -119,7 +212,7 @@ function listOnly(server, listed) {
|
|
|
119
212
|
* the tool exists, where it went, or how to reach it.
|
|
120
213
|
*
|
|
121
214
|
* So every tool stays ENABLED, and the listing is narrowed instead. A name is
|
|
122
|
-
* always callable; the list is still the native set plus the catalog
|
|
215
|
+
* always callable; the list is still the native set plus the catalog tools.
|
|
123
216
|
*
|
|
124
217
|
* The listing delegates to the handler the SDK installed rather than rebuilding
|
|
125
218
|
* one: the SDK owns how a registered tool becomes a `tools/list` entry, and
|
|
@@ -150,7 +243,7 @@ export function applySurfacePolicy(server) {
|
|
|
150
243
|
for (const name of describe) {
|
|
151
244
|
const entry = catalogEntry(server, name);
|
|
152
245
|
if (entry)
|
|
153
|
-
found.push(
|
|
246
|
+
found.push(entry);
|
|
154
247
|
else
|
|
155
248
|
unknown.push(name);
|
|
156
249
|
}
|
|
@@ -158,9 +251,9 @@ export function applySurfacePolicy(server) {
|
|
|
158
251
|
return toolError(`No such tool: ${unknown.join(', ')}. Call ${TOOLS_CATALOG_NAME} with no arguments for the full list of names.`);
|
|
159
252
|
}
|
|
160
253
|
return textResult({
|
|
161
|
-
tools: found,
|
|
254
|
+
tools: found.map(describeEntry),
|
|
162
255
|
...(unknown.length ? { unknown } : {}),
|
|
163
|
-
next:
|
|
256
|
+
next: nextCall(found),
|
|
164
257
|
});
|
|
165
258
|
}
|
|
166
259
|
const all = sortRows(catalogFor(server));
|
|
@@ -174,59 +267,30 @@ export function applySurfacePolicy(server) {
|
|
|
174
267
|
count: rows.length,
|
|
175
268
|
...(needle ? { search: needle, total: all.length } : {}),
|
|
176
269
|
tools: rows,
|
|
177
|
-
next: `Get full schemas with ${TOOLS_CATALOG_NAME} { describe: ["<name>"] }, then call with ${
|
|
270
|
+
next: `Get full schemas with ${TOOLS_CATALOG_NAME} { describe: ["<name>"] }, then call read-only rows with ${TOOL_READ_NAME} { tool, args } and the rest with ${TOOL_WRITE_NAME} { tool, args }.`,
|
|
178
271
|
});
|
|
179
272
|
}
|
|
180
273
|
catch (e) {
|
|
181
274
|
return toolError(e);
|
|
182
275
|
}
|
|
183
276
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (entry.name === TOOL_CALL_NAME) {
|
|
198
|
-
return toolError(`${TOOL_CALL_NAME} cannot call itself — pass the tool you actually want.`);
|
|
199
|
-
}
|
|
200
|
-
// An irreversible action must go through the call the host can annotate,
|
|
201
|
-
// or the human is asked to confirm the wrong sentence.
|
|
202
|
-
if (entry.annotations.destructiveHint) {
|
|
203
|
-
return toolError(`${entry.name} cannot be called through ${TOOL_CALL_NAME}: it is irreversible, and the host shows the human what it is about to allow from the tool it is asked to run. ` +
|
|
204
|
-
`${entry.name} is loaded directly for that reason — call it by name.`);
|
|
205
|
-
}
|
|
206
|
-
// Validate here rather than letting the handler read undefined fields:
|
|
207
|
-
// the strict schema is what turns a misspelled key into a named refusal
|
|
208
|
-
// instead of a call that runs and reports success for the wrong thing.
|
|
209
|
-
// It is the object registration already built — the catalog carries it,
|
|
210
|
-
// so a dispatch does not rebuild a schema that cannot have changed.
|
|
211
|
-
const parsed = entry.params.safeParse(args ?? {});
|
|
212
|
-
if (!parsed.success) {
|
|
213
|
-
const detail = parsed.error.issues
|
|
214
|
-
.map((i) => (i.path.length ? `${i.path.join('.')}: ${i.message}` : i.message))
|
|
215
|
-
.join('; ');
|
|
216
|
-
// The unknown-key refusal already names the tool and lists what it
|
|
217
|
-
// accepts; don't say the name twice.
|
|
218
|
-
return toolError(detail.includes(entry.name) ? detail : `${entry.name} did not run: ${detail}`);
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
return (await entry.call(parsed.data));
|
|
222
|
-
}
|
|
223
|
-
catch (e) {
|
|
224
|
-
return toolError(e);
|
|
225
|
-
}
|
|
277
|
+
registerDispatcher(server, {
|
|
278
|
+
name: TOOL_READ_NAME,
|
|
279
|
+
description: TOOL_READ_DESCRIPTION,
|
|
280
|
+
annotations: readOnly('Call a read-only Ziggs tool'),
|
|
281
|
+
readOnly: true,
|
|
282
|
+
example: 'ziggs_agreement_list',
|
|
283
|
+
});
|
|
284
|
+
registerDispatcher(server, {
|
|
285
|
+
name: TOOL_WRITE_NAME,
|
|
286
|
+
description: TOOL_WRITE_DESCRIPTION,
|
|
287
|
+
annotations: write('Call a Ziggs tool that changes something'),
|
|
288
|
+
readOnly: false,
|
|
289
|
+
example: 'ziggs_task_create',
|
|
226
290
|
});
|
|
227
|
-
// The
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
291
|
+
// The catalog tools registered through the same path as everything else, so
|
|
292
|
+
// they are in the catalog too. Take them out: listing ziggs_tools inside its
|
|
293
|
+
// own listing is noise, and a dispatcher calling a dispatcher is a loop.
|
|
294
|
+
for (const name of CATALOG_TOOLS)
|
|
295
|
+
removeCatalogEntry(server, name);
|
|
232
296
|
}
|
package/dist/toolCatalog.d.ts
CHANGED
|
@@ -20,7 +20,10 @@ import type { TitledAnnotations } from './toolAnnotations.js';
|
|
|
20
20
|
* MCP has no name-only tool: every entry in `tools/list` carries a schema. So
|
|
21
21
|
* the catalog is a dispatcher — the shape `ziggs_mcp_tools_list` /
|
|
22
22
|
* `ziggs_mcp_tool_call` already use for proxied external MCP, and the one that
|
|
23
|
-
* needs no cooperation from any client.
|
|
23
|
+
* needs no cooperation from any client. Two of them, one per annotation lane
|
|
24
|
+
* (`ziggs_tool_read`, `ziggs_tool_write`): a host and a store reviewer read
|
|
25
|
+
* what a tool is from its annotation, and one dispatcher cannot be read-only
|
|
26
|
+
* and writing at once. Nothing is hidden: `ziggs_tools` names
|
|
24
27
|
* every tool that exists, and `ziggs_tools` with `describe` hands back full
|
|
25
28
|
* schemas on demand. An agent can always find what exists and ask for it,
|
|
26
29
|
* which is what made per-caller filtering the wrong trade.
|
|
@@ -45,7 +48,7 @@ export interface CatalogEntry {
|
|
|
45
48
|
*/
|
|
46
49
|
params: ZodTypeAny;
|
|
47
50
|
/**
|
|
48
|
-
* The registered handler.
|
|
51
|
+
* The registered handler. The dispatchers call through this rather than
|
|
49
52
|
* re-entering the server: one call, one validation, no nested request.
|
|
50
53
|
*/
|
|
51
54
|
call: (args: Record<string, unknown>) => Promise<unknown>;
|
package/dist/tools.js
CHANGED
|
@@ -194,23 +194,26 @@ async function loadSessionBinding(creds, cfg) {
|
|
|
194
194
|
// ziggs_task_create are the same moves, one step at a time. Provisioning a
|
|
195
195
|
// whole relay is operator work and belongs on an operator surface.
|
|
196
196
|
function registerConnectionTools(server, creds, cfg) {
|
|
197
|
-
//
|
|
198
|
-
// reviewers reject:
|
|
199
|
-
// API it calls, and
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
197
|
+
// A tool that takes a target name plus a freeform payload is the shape store
|
|
198
|
+
// reviewers reject: nothing in its definition says what it will do or which
|
|
199
|
+
// API it calls, and one annotation cannot cover reads and writes at once.
|
|
200
|
+
// The connection rail is that shape three times over (the named-connector
|
|
201
|
+
// proxy and the brokered-MCP pair), and asking a person for a grant the
|
|
202
|
+
// caller could never use here would be a trap. So on the listed surface (a
|
|
203
|
+
// connected assistant, see ZiggsMcpConfig.directConnection) the rail is the
|
|
204
|
+
// read-only lister and nothing else, while a caller who configured this
|
|
205
|
+
// server with their own operator key keeps it whole. Nothing is lost on the
|
|
206
|
+
// listed side: the rail exists so hosted agents can borrow a person's
|
|
207
|
+
// connection under limits, and an assistant already has its own connectors.
|
|
204
208
|
if (cfg.directConnection) {
|
|
205
209
|
registerCapability(server, connectionProxyCapability, creds);
|
|
206
210
|
}
|
|
207
211
|
registerStrictTool(server, 'ziggs_connection_list', 'Discover the third-party connections (credentials like GitHub/Jira, and remote MCP servers — NOT agent-to-agent Links; see ziggs_link_list for that) you hold grants for, without the owner sharing connectionId/grantId out of band. ' +
|
|
208
212
|
'Returns, per connection: connectionId, provider, and the grant(s) you hold — each as the canonical grant shape (grantId, scope, caveats, and grant health active/expired/revoked). ' +
|
|
209
213
|
'Read-only — never returns credential material. ' +
|
|
210
|
-
'How to use a row: provider "mcp" → ziggs_mcp_tools_list / ziggs_mcp_tool_call' +
|
|
211
214
|
(cfg.directConnection
|
|
212
|
-
? '; named connectors (github, jira, …) → ziggs_connection_proxy.'
|
|
213
|
-
: '
|
|
215
|
+
? 'How to use a row: provider "mcp" → ziggs_mcp_tools_list / ziggs_mcp_tool_call; named connectors (github, jira, …) → ziggs_connection_proxy.'
|
|
216
|
+
: 'Rows are readable here but not callable over this connection: using a connection (an MCP server or a named connector such as github or jira) is only on a self-configured Ziggs MCP connection with an operator key.'), {}, readOnly('List connections you can use'), async () => {
|
|
214
217
|
try {
|
|
215
218
|
const connections = await new ConnectionsClient(creds.operatorKey, creds.agentId).listForHolder();
|
|
216
219
|
return textResult({ connections });
|
|
@@ -219,6 +222,9 @@ function registerConnectionTools(server, creds, cfg) {
|
|
|
219
222
|
return toolError(e);
|
|
220
223
|
}
|
|
221
224
|
});
|
|
225
|
+
// Everything below uses or asks for a connection; see the note at the top.
|
|
226
|
+
if (!cfg.directConnection)
|
|
227
|
+
return;
|
|
222
228
|
// Brokered MCP connections (provider: mcp). Not connection_proxy.
|
|
223
229
|
registerStrictTool(server, 'ziggs_mcp_tools_list', "List the tools a connected MCP server exposes, through the Ziggs gateway — the owner's credential stays server-side. " +
|
|
224
230
|
'The grant may allow only some of them; a call outside the grant is refused by the gateway. ' +
|
|
@@ -252,10 +258,7 @@ function registerConnectionTools(server, creds, cfg) {
|
|
|
252
258
|
}
|
|
253
259
|
});
|
|
254
260
|
registerStrictTool(server, 'ziggs_mcp_tool_call', 'Call one tool on a connected MCP server through the Ziggs gateway. ' +
|
|
255
|
-
'Use this when the connection provider is a remote MCP server' +
|
|
256
|
-
(cfg.directConnection
|
|
257
|
-
? ' — ziggs_connection_proxy is for named connectors (github, jira) and refuses MCP with "Unknown provider: mcp". '
|
|
258
|
-
: '; named connectors (github, jira) are a different rail and are not callable here. ') +
|
|
261
|
+
'Use this when the connection provider is a remote MCP server — ziggs_connection_proxy is for named connectors (github, jira) and refuses MCP with "Unknown provider: mcp". ' +
|
|
259
262
|
"The gateway injects the owner's token and checks the tool name against your grant. Discover names with ziggs_mcp_tools_list.", {
|
|
260
263
|
tool: z
|
|
261
264
|
.string()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/ziggs-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
30
30
|
"prepack": "rm -rf dist && npm run build",
|
|
31
31
|
"start": "node dist/index.js",
|
|
32
32
|
"dev": "tsx src/index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
-
"@ziggs-ai/api-client": "0.14.
|
|
42
|
+
"@ziggs-ai/api-client": "0.14.3",
|
|
43
43
|
"dotenv": "^16.6.1",
|
|
44
44
|
"zod": "^3.24.2",
|
|
45
45
|
"zod-to-json-schema": "^3.25.1"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol.
|
|
5
5
|
|
|
6
|
-
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=["<name>"] returns its full schema, and
|
|
6
|
+
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=["<name>"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
|
|
7
7
|
- Flow: inbox → read → act → ack.
|
|
8
8
|
- Reading never advances the watermark; once you have handled what an envelope carried, pass its `ackTo` back VERBATIM as ack (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
|
|
9
9
|
- Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
|
package/skills/ziggs/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ You represent a **delegate agent** on Ziggs. MCP tools are the connection; this
|
|
|
22
22
|
<!-- BEGIN GENERATED: delegate-protocol — generated file, do not edit by hand -->
|
|
23
23
|
_You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol._
|
|
24
24
|
|
|
25
|
-
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=["<name>"] returns its full schema, and
|
|
25
|
+
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=["<name>"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
|
|
26
26
|
- Flow: inbox → read → act → ack.
|
|
27
27
|
- Reading never advances the watermark; once you have handled what an envelope carried, pass its `ackTo` back VERBATIM as ack (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
|
|
28
28
|
- Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<!-- BEGIN GENERATED: delegate-protocol — generated file, do not edit by hand -->
|
|
6
6
|
_You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol._
|
|
7
7
|
|
|
8
|
-
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=["<name>"] returns its full schema, and
|
|
8
|
+
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=["<name>"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
|
|
9
9
|
- Flow: inbox → read → act → ack.
|
|
10
10
|
- Reading never advances the watermark; once you have handled what an envelope carried, pass its `ackTo` back VERBATIM as ack (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
|
|
11
11
|
- Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<!-- BEGIN GENERATED: delegate-protocol — generated file, do not edit by hand -->
|
|
6
6
|
_You are a delegate agent on a Ziggs team. The MCP tools are the connection; operate by this protocol._
|
|
7
7
|
|
|
8
|
-
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line), ziggs_tools describe=["<name>"] returns its full schema, and
|
|
8
|
+
- Only the everyday tools are loaded; the rest of the surface is one call away. ziggs_tools lists every tool that exists (name + one line, and whether it is read-only), ziggs_tools describe=["<name>"] returns its full schema, ziggs_tool_read { tool, args } calls a read-only tool and ziggs_tool_write { tool, args } calls one that changes something. Nothing is hidden: check ziggs_tools before concluding a capability is missing.
|
|
9
9
|
- Flow: inbox → read → act → ack.
|
|
10
10
|
- Reading never advances the watermark; once you have handled what an envelope carried, pass its `ackTo` back VERBATIM as ack (it is opaque — never construct or edit one) together with `handledResourceIds` for every delivery ASSIGNED to you (assigneeId = you; requests too) in that window. Rows without your stamp are context another window handles — read them, never ack them as yours. Never rewind an ack to an older value.
|
|
11
11
|
- Work is a task under an agreement (the ticket). Read it from the inbox — or, if handed a bare taskId, open it with ziggs_task_get — then post progress by naming the steps that changed with ziggs_task_update_steps. Restructure the checklist with ziggs_task_replace_plan (full list).
|