@ziggs-ai/ziggs-mcp 0.37.19 → 0.37.21
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/CHANGELOG.md +11 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +8 -0
- package/dist/inboxCard.d.ts +1 -1
- package/dist/inboxCard.js +34 -2
- package/dist/inboxChannel.d.ts +51 -0
- package/dist/inboxChannel.js +133 -0
- package/dist/protocol/connectText.d.ts +3 -2
- package/dist/protocol/connectText.js +7 -4
- package/dist/server.js +8 -1
- package/dist/surface.d.ts +14 -18
- package/dist/surface.js +34 -48
- package/dist/tools.js +5 -5
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.37.21
|
|
6
|
+
|
|
7
|
+
- The inbox card reports its size to the host. Without it a host left the frame at zero height, so the card was drawn but never seen.
|
|
8
|
+
- A key-based (operator key) connection now lists every tool by name, like the OAuth connection, instead of the everyday set behind a catalog. `ziggs_tools`, `ziggs_tool_read` and `ziggs_tool_write` stay listed and work as before, so a client that mounts only an allow-list still reaches the rest.
|
|
9
|
+
|
|
10
|
+
## 0.37.20
|
|
11
|
+
|
|
12
|
+
- Set `ZIGGS_MCP_CHANNEL=1` to make the local server a Claude Code channel: an open Claude Code session hears new mail within seconds. Mail already waiting becomes one event, and each new delivery one more. Events carry ids only, never what the mail says; read it with `ziggs_inbox`. Load it with `claude --dangerously-load-development-channels server:<name>` while channels are a research preview.
|
|
13
|
+
- `tools/list` no longer carries the `$schema` line in every input schema.
|
|
14
|
+
- Shorter parameter text on the agreement verbs, `ziggs_task_create` and `ziggs_artifact_record`. Every rule stays.
|
|
15
|
+
|
|
5
16
|
## 0.37.19
|
|
6
17
|
|
|
7
18
|
- New read-only `ziggs_inbox_show`: what is waiting on the person and their work in hand, as an MCP Apps card in hosts that draw one (Claude, ChatGPT, Cursor) and as the usual text everywhere else. The card's buttons only open the Ziggs website; it changes nothing itself. The view is served at `ui://ziggs/inbox-card`.
|
package/dist/config.d.ts
CHANGED
|
@@ -49,6 +49,12 @@ declare const envSchema: z.ZodObject<{
|
|
|
49
49
|
* cut cold-start deferred-loading (#7). Unset = all tools register.
|
|
50
50
|
*/
|
|
51
51
|
ZIGGS_MCP_CORE_ONLY: z.ZodOptional<z.ZodString>;
|
|
52
|
+
/**
|
|
53
|
+
* Set to 1/true/yes to ring an open Claude Code session when new mail
|
|
54
|
+
* arrives (a Claude Code channel). Claude Code must also load this server
|
|
55
|
+
* as a channel; see `inboxChannel.ts`.
|
|
56
|
+
*/
|
|
57
|
+
ZIGGS_MCP_CHANNEL: z.ZodOptional<z.ZodString>;
|
|
52
58
|
}, "strip", z.ZodTypeAny, {
|
|
53
59
|
ZIGGS_OPERATOR_KEY: string;
|
|
54
60
|
ZIGGS_API_URL?: string | undefined;
|
|
@@ -63,6 +69,7 @@ declare const envSchema: z.ZodObject<{
|
|
|
63
69
|
ZIGGS_SESSION_NAME?: string | undefined;
|
|
64
70
|
ZIGGS_MCP_DEBUG?: string | undefined;
|
|
65
71
|
ZIGGS_MCP_CORE_ONLY?: string | undefined;
|
|
72
|
+
ZIGGS_MCP_CHANNEL?: string | undefined;
|
|
66
73
|
}, {
|
|
67
74
|
ZIGGS_OPERATOR_KEY: string;
|
|
68
75
|
ZIGGS_API_URL?: string | undefined;
|
|
@@ -77,6 +84,7 @@ declare const envSchema: z.ZodObject<{
|
|
|
77
84
|
ZIGGS_SESSION_NAME?: string | undefined;
|
|
78
85
|
ZIGGS_MCP_DEBUG?: string | undefined;
|
|
79
86
|
ZIGGS_MCP_CORE_ONLY?: string | undefined;
|
|
87
|
+
ZIGGS_MCP_CHANNEL?: string | undefined;
|
|
80
88
|
}>;
|
|
81
89
|
type EnvConfig = z.infer<typeof envSchema>;
|
|
82
90
|
export interface ZiggsMcpConfig extends EnvConfig {
|
|
@@ -114,6 +122,11 @@ export interface ZiggsMcpConfig extends EnvConfig {
|
|
|
114
122
|
* connect text, inside that text's budget.
|
|
115
123
|
*/
|
|
116
124
|
eventsEnabled?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Local only: declare the Claude Code channel and ring the session for new
|
|
127
|
+
* mail. Adds one line to the compact door's connect text.
|
|
128
|
+
*/
|
|
129
|
+
channel?: boolean;
|
|
117
130
|
}
|
|
118
131
|
/** Load delegate credentials from the environment. */
|
|
119
132
|
export declare function loadConfig(): ZiggsMcpConfig;
|
package/dist/config.js
CHANGED
|
@@ -51,6 +51,12 @@ const envSchema = z.object({
|
|
|
51
51
|
* cut cold-start deferred-loading (#7). Unset = all tools register.
|
|
52
52
|
*/
|
|
53
53
|
ZIGGS_MCP_CORE_ONLY: z.string().optional(),
|
|
54
|
+
/**
|
|
55
|
+
* Set to 1/true/yes to ring an open Claude Code session when new mail
|
|
56
|
+
* arrives (a Claude Code channel). Claude Code must also load this server
|
|
57
|
+
* as a channel; see `inboxChannel.ts`.
|
|
58
|
+
*/
|
|
59
|
+
ZIGGS_MCP_CHANNEL: z.string().optional(),
|
|
54
60
|
});
|
|
55
61
|
/** Parse a truthy env flag (1/true/yes) — shared by ZIGGS_MCP_DEBUG and ZIGGS_MCP_CORE_ONLY. */
|
|
56
62
|
function parseBoolFlag(raw) {
|
|
@@ -75,6 +81,7 @@ export function loadConfig() {
|
|
|
75
81
|
ZIGGS_SESSION_NAME: process.env.ZIGGS_SESSION_NAME,
|
|
76
82
|
ZIGGS_MCP_DEBUG: process.env.ZIGGS_MCP_DEBUG,
|
|
77
83
|
ZIGGS_MCP_CORE_ONLY: process.env.ZIGGS_MCP_CORE_ONLY,
|
|
84
|
+
ZIGGS_MCP_CHANNEL: process.env.ZIGGS_MCP_CHANNEL,
|
|
78
85
|
};
|
|
79
86
|
const parsed = envSchema.safeParse(raw);
|
|
80
87
|
if (!parsed.success) {
|
|
@@ -108,6 +115,7 @@ export function loadConfig() {
|
|
|
108
115
|
resolvedAgentId,
|
|
109
116
|
debugTools: parseBoolFlag(parsed.data.ZIGGS_MCP_DEBUG),
|
|
110
117
|
coreOnly: parseBoolFlag(parsed.data.ZIGGS_MCP_CORE_ONLY),
|
|
118
|
+
channel: parseBoolFlag(parsed.data.ZIGGS_MCP_CHANNEL),
|
|
111
119
|
directConnection: !isDirectoryBoarded(decodeOperatorKeyClaims(parsed.data.ZIGGS_OPERATOR_KEY)),
|
|
112
120
|
};
|
|
113
121
|
}
|
package/dist/inboxCard.d.ts
CHANGED
|
@@ -83,5 +83,5 @@ export declare function registerInboxCardResource(server: McpServer): void;
|
|
|
83
83
|
* then it draws whatever `ui/notifications/tool-result` carries, and its
|
|
84
84
|
* buttons send `ui/open-link`.
|
|
85
85
|
*/
|
|
86
|
-
export declare const INBOX_CARD_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Ziggs</title>\n<style>\n :root { --fg:#1d1a20; --muted:#6b6470; --line:#e7e2ea; --bg:#ffffff; --brand:#7c3aed; --chip:#f4f1f6; }\n @media (prefers-color-scheme: dark) { :root { --fg:#f2eef4; --muted:#a59cab; --line:#332d38; --bg:#1b171e; --brand:#a78bfa; --chip:#29232d; } }\n * { box-sizing: border-box; }\n body { margin:0; padding:14px 16px; font:14px/1.45 system-ui,-apple-system,\"Segoe UI\",sans-serif; color:var(--fg); background:var(--bg); }\n h2 { margin:0 0 8px; font-size:12px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); }\n section + section { margin-top:16px; }\n ul { list-style:none; margin:0; padding:0; }\n li { display:flex; align-items:center; gap:12px; padding:9px 0; border-top:1px solid var(--line); }\n li:first-child { border-top:0; }\n .text { flex:1; min-width:0; }\n .title { font-weight:600; overflow-wrap:anywhere; }\n .meta { color:var(--muted); font-size:12px; }\n .chip { display:inline-block; padding:1px 7px; border-radius:999px; background:var(--chip); color:var(--muted); font-size:11px; margin-left:6px; font-weight:500; }\n button { flex:none; min-height:32px; padding:0 12px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--fg); font:inherit; font-size:13px; cursor:pointer; }\n button.primary { background:var(--brand); border-color:var(--brand); color:#fff; }\n .empty { color:var(--muted); }\n .all { margin-top:12px; }\n</style>\n</head>\n<body>\n<main id=\"root\"><p class=\"empty\">Loading your Ziggs inbox\u2026</p></main>\n<script>\n(function () {\n var nextId = 1, pending = {};\n function send(method, params) {\n var id = nextId++;\n window.parent.postMessage({ jsonrpc: \"2.0\", id: id, method: method, params: params || {} }, \"*\");\n return new Promise(function (resolve, reject) { pending[id] = { resolve: resolve, reject: reject }; });\n }\n function notify(method, params) {\n window.parent.postMessage({ jsonrpc: \"2.0\", method: method, params: params || {} }, \"*\");\n }\n function el(tag, cls, text) {\n var node = document.createElement(tag);\n if (cls) node.className = cls;\n if (text != null) node.textContent = String(text);\n return node;\n }\n function open(url) {\n if (!url) return;\n send(\"ui/open-link\", { url: url }).catch(function () {});\n }\n function row(title, chip, meta, label, url, primary) {\n var li = el(\"li\"), text = el(\"div\", \"text\"), head = el(\"div\", \"title\", title);\n if (chip) head.appendChild(el(\"span\", \"chip\", chip));\n text.appendChild(head);\n if (meta) text.appendChild(el(\"div\", \"meta\", meta));\n li.appendChild(text);\n if (url) {\n var button = el(\"button\", primary ? \"primary\" : \"\", label);\n button.type = \"button\";\n button.addEventListener(\"click\", function () { open(url); });\n li.appendChild(button);\n }\n return li;\n }\n function kindLabel(kind) {\n return kind === \"link_request\" ? \"Link request\" : kind === \"proposal\" ? \"Proposal\" : \"\";\n }\n function render(data) {\n var root = document.getElementById(\"root\");\n root.textContent = \"\";\n data = data || {};\n var waiting = data.waitingOnYou || [], work = data.yourWork || [], more = data.moreWaiting || 0;\n if (!waiting.length && !more && !work.length) {\n root.appendChild(el(\"p\", \"empty\", \"Nothing is waiting on you in Ziggs.\"));\n }\n if (waiting.length || more) {\n var s1 = el(\"section\"), l1 = el(\"ul\");\n s1.appendChild(el(\"h2\", \"\", \"Waiting on you\"));\n waiting.forEach(function (item) {\n var meta = [item.who, item.when].filter(Boolean).join(\" \u00B7 \");\n l1.appendChild(row(item.title, kindLabel(item.kind), meta, \"Review\", item.url, true));\n });\n s1.appendChild(l1);\n if (more) s1.appendChild(el(\"p\", \"meta\", more + \" more on the website\"));\n root.appendChild(s1);\n }\n if (work.length) {\n var s2 = el(\"section\"), l2 = el(\"ul\");\n s2.appendChild(el(\"h2\", \"\", \"Your work\"));\n work.forEach(function (item) {\n l2.appendChild(row(item.title, null, item.progress ? \"Plan \" + item.progress : \"In progress\", \"Open\", item.url, false));\n });\n s2.appendChild(l2);\n root.appendChild(s2);\n }\n if (data.allUrl) {\n var all = el(\"button\", \"all\", \"Open Ziggs\");\n all.type = \"button\";\n all.addEventListener(\"click\", function () { open(data.allUrl); });\n root.appendChild(all);\n }\n }\n window.addEventListener(\"message\", function (event) {\n if (event.source !== window.parent) return;\n var msg = event.data;\n if (!msg || msg.jsonrpc !== \"2.0\") return;\n if (msg.id != null && pending[msg.id] && !msg.method) {\n var waiter = pending[msg.id];\n delete pending[msg.id];\n if (msg.error) waiter.reject(msg.error); else waiter.resolve(msg.result);\n return;\n }\n if (msg.method === \"ui/notifications/tool-result\") {\n render((msg.params || {}).structuredContent);\n }\n });\n send(\"ui/initialize\", {\n protocolVersion: \"2026-01-26\",\n appInfo: { name: \"Ziggs inbox card\", version: \"1\" },\n appCapabilities: {}\n }).then(function () {
|
|
86
|
+
export declare const INBOX_CARD_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Ziggs</title>\n<style>\n :root { --fg:#1d1a20; --muted:#6b6470; --line:#e7e2ea; --bg:#ffffff; --brand:#7c3aed; --chip:#f4f1f6; }\n @media (prefers-color-scheme: dark) { :root { --fg:#f2eef4; --muted:#a59cab; --line:#332d38; --bg:#1b171e; --brand:#a78bfa; --chip:#29232d; } }\n * { box-sizing: border-box; }\n body { margin:0; padding:14px 16px; font:14px/1.45 system-ui,-apple-system,\"Segoe UI\",sans-serif; color:var(--fg); background:var(--bg); }\n h2 { margin:0 0 8px; font-size:12px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); }\n section + section { margin-top:16px; }\n ul { list-style:none; margin:0; padding:0; }\n li { display:flex; align-items:center; gap:12px; padding:9px 0; border-top:1px solid var(--line); }\n li:first-child { border-top:0; }\n .text { flex:1; min-width:0; }\n .title { font-weight:600; overflow-wrap:anywhere; }\n .meta { color:var(--muted); font-size:12px; }\n .chip { display:inline-block; padding:1px 7px; border-radius:999px; background:var(--chip); color:var(--muted); font-size:11px; margin-left:6px; font-weight:500; }\n button { flex:none; min-height:32px; padding:0 12px; border-radius:8px; border:1px solid var(--line); background:transparent; color:var(--fg); font:inherit; font-size:13px; cursor:pointer; }\n button.primary { background:var(--brand); border-color:var(--brand); color:#fff; }\n .empty { color:var(--muted); }\n .all { margin-top:12px; }\n</style>\n</head>\n<body>\n<main id=\"root\"><p class=\"empty\">Loading your Ziggs inbox\u2026</p></main>\n<script>\n(function () {\n var nextId = 1, pending = {}, remeasure = null;\n function send(method, params) {\n var id = nextId++;\n window.parent.postMessage({ jsonrpc: \"2.0\", id: id, method: method, params: params || {} }, \"*\");\n return new Promise(function (resolve, reject) { pending[id] = { resolve: resolve, reject: reject }; });\n }\n function notify(method, params) {\n window.parent.postMessage({ jsonrpc: \"2.0\", method: method, params: params || {} }, \"*\");\n }\n function el(tag, cls, text) {\n var node = document.createElement(tag);\n if (cls) node.className = cls;\n if (text != null) node.textContent = String(text);\n return node;\n }\n function open(url) {\n if (!url) return;\n send(\"ui/open-link\", { url: url }).catch(function () {});\n }\n function row(title, chip, meta, label, url, primary) {\n var li = el(\"li\"), text = el(\"div\", \"text\"), head = el(\"div\", \"title\", title);\n if (chip) head.appendChild(el(\"span\", \"chip\", chip));\n text.appendChild(head);\n if (meta) text.appendChild(el(\"div\", \"meta\", meta));\n li.appendChild(text);\n if (url) {\n var button = el(\"button\", primary ? \"primary\" : \"\", label);\n button.type = \"button\";\n button.addEventListener(\"click\", function () { open(url); });\n li.appendChild(button);\n }\n return li;\n }\n function kindLabel(kind) {\n return kind === \"link_request\" ? \"Link request\" : kind === \"proposal\" ? \"Proposal\" : \"\";\n }\n function render(data) {\n var root = document.getElementById(\"root\");\n root.textContent = \"\";\n data = data || {};\n var waiting = data.waitingOnYou || [], work = data.yourWork || [], more = data.moreWaiting || 0;\n if (!waiting.length && !more && !work.length) {\n root.appendChild(el(\"p\", \"empty\", \"Nothing is waiting on you in Ziggs.\"));\n }\n if (waiting.length || more) {\n var s1 = el(\"section\"), l1 = el(\"ul\");\n s1.appendChild(el(\"h2\", \"\", \"Waiting on you\"));\n waiting.forEach(function (item) {\n var meta = [item.who, item.when].filter(Boolean).join(\" \u00B7 \");\n l1.appendChild(row(item.title, kindLabel(item.kind), meta, \"Review\", item.url, true));\n });\n s1.appendChild(l1);\n if (more) s1.appendChild(el(\"p\", \"meta\", more + \" more on the website\"));\n root.appendChild(s1);\n }\n if (work.length) {\n var s2 = el(\"section\"), l2 = el(\"ul\");\n s2.appendChild(el(\"h2\", \"\", \"Your work\"));\n work.forEach(function (item) {\n l2.appendChild(row(item.title, null, item.progress ? \"Plan \" + item.progress : \"In progress\", \"Open\", item.url, false));\n });\n s2.appendChild(l2);\n root.appendChild(s2);\n }\n if (data.allUrl) {\n var all = el(\"button\", \"all\", \"Open Ziggs\");\n all.type = \"button\";\n all.addEventListener(\"click\", function () { open(data.allUrl); });\n root.appendChild(all);\n }\n if (remeasure) remeasure();\n }\n window.addEventListener(\"message\", function (event) {\n if (event.source !== window.parent) return;\n var msg = event.data;\n if (!msg || msg.jsonrpc !== \"2.0\") return;\n if (msg.id != null && pending[msg.id] && !msg.method) {\n var waiter = pending[msg.id];\n delete pending[msg.id];\n if (msg.error) waiter.reject(msg.error); else waiter.resolve(msg.result);\n return;\n }\n if (msg.method === \"ui/notifications/tool-result\") {\n render((msg.params || {}).structuredContent);\n }\n });\n // A host sizes the frame from what the view reports. Without a report the\n // frame stays at zero height and the card is drawn but never seen.\n function reportSize() {\n var lastWidth = 0, lastHeight = 0, queued = false;\n function measure() {\n if (queued) return;\n queued = true;\n var next = typeof requestAnimationFrame === \"function\" ? requestAnimationFrame : function (fn) { setTimeout(fn, 16); };\n next(function () {\n queued = false;\n var doc = document.documentElement, previous = doc.style.height;\n doc.style.height = \"max-content\";\n var height = Math.ceil(doc.getBoundingClientRect().height);\n doc.style.height = previous;\n var width = Math.ceil(window.innerWidth);\n if (width === lastWidth && height === lastHeight) return;\n lastWidth = width; lastHeight = height;\n notify(\"ui/notifications/size-changed\", { width: width, height: height });\n });\n }\n remeasure = measure;\n measure();\n if (typeof ResizeObserver === \"function\") {\n var observer = new ResizeObserver(measure);\n observer.observe(document.documentElement);\n observer.observe(document.body);\n }\n }\n send(\"ui/initialize\", {\n protocolVersion: \"2026-01-26\",\n appInfo: { name: \"Ziggs inbox card\", version: \"1\" },\n appCapabilities: {}\n }).then(function () {\n notify(\"ui/notifications/initialized\", {});\n reportSize();\n }, function () {});\n})();\n</script>\n</body>\n</html>\n";
|
|
87
87
|
export {};
|
package/dist/inboxCard.js
CHANGED
|
@@ -98,7 +98,7 @@ export const INBOX_CARD_HTML = `<!doctype html>
|
|
|
98
98
|
<main id="root"><p class="empty">Loading your Ziggs inbox…</p></main>
|
|
99
99
|
<script>
|
|
100
100
|
(function () {
|
|
101
|
-
var nextId = 1, pending = {};
|
|
101
|
+
var nextId = 1, pending = {}, remeasure = null;
|
|
102
102
|
function send(method, params) {
|
|
103
103
|
var id = nextId++;
|
|
104
104
|
window.parent.postMessage({ jsonrpc: "2.0", id: id, method: method, params: params || {} }, "*");
|
|
@@ -168,6 +168,7 @@ export const INBOX_CARD_HTML = `<!doctype html>
|
|
|
168
168
|
all.addEventListener("click", function () { open(data.allUrl); });
|
|
169
169
|
root.appendChild(all);
|
|
170
170
|
}
|
|
171
|
+
if (remeasure) remeasure();
|
|
171
172
|
}
|
|
172
173
|
window.addEventListener("message", function (event) {
|
|
173
174
|
if (event.source !== window.parent) return;
|
|
@@ -183,11 +184,42 @@ export const INBOX_CARD_HTML = `<!doctype html>
|
|
|
183
184
|
render((msg.params || {}).structuredContent);
|
|
184
185
|
}
|
|
185
186
|
});
|
|
187
|
+
// A host sizes the frame from what the view reports. Without a report the
|
|
188
|
+
// frame stays at zero height and the card is drawn but never seen.
|
|
189
|
+
function reportSize() {
|
|
190
|
+
var lastWidth = 0, lastHeight = 0, queued = false;
|
|
191
|
+
function measure() {
|
|
192
|
+
if (queued) return;
|
|
193
|
+
queued = true;
|
|
194
|
+
var next = typeof requestAnimationFrame === "function" ? requestAnimationFrame : function (fn) { setTimeout(fn, 16); };
|
|
195
|
+
next(function () {
|
|
196
|
+
queued = false;
|
|
197
|
+
var doc = document.documentElement, previous = doc.style.height;
|
|
198
|
+
doc.style.height = "max-content";
|
|
199
|
+
var height = Math.ceil(doc.getBoundingClientRect().height);
|
|
200
|
+
doc.style.height = previous;
|
|
201
|
+
var width = Math.ceil(window.innerWidth);
|
|
202
|
+
if (width === lastWidth && height === lastHeight) return;
|
|
203
|
+
lastWidth = width; lastHeight = height;
|
|
204
|
+
notify("ui/notifications/size-changed", { width: width, height: height });
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
remeasure = measure;
|
|
208
|
+
measure();
|
|
209
|
+
if (typeof ResizeObserver === "function") {
|
|
210
|
+
var observer = new ResizeObserver(measure);
|
|
211
|
+
observer.observe(document.documentElement);
|
|
212
|
+
observer.observe(document.body);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
186
215
|
send("ui/initialize", {
|
|
187
216
|
protocolVersion: "2026-01-26",
|
|
188
217
|
appInfo: { name: "Ziggs inbox card", version: "1" },
|
|
189
218
|
appCapabilities: {}
|
|
190
|
-
}).then(function () {
|
|
219
|
+
}).then(function () {
|
|
220
|
+
notify("ui/notifications/initialized", {});
|
|
221
|
+
reportSize();
|
|
222
|
+
}, function () {});
|
|
191
223
|
})();
|
|
192
224
|
</script>
|
|
193
225
|
</body>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { InboxClient, type Creds } from '@ziggs-ai/api-client';
|
|
3
|
+
export type InboxChangesOptions = NonNullable<Parameters<InboxClient['getChanges']>[0]>;
|
|
4
|
+
export type InboxChangesResult = Awaited<ReturnType<InboxClient['getChanges']>>;
|
|
5
|
+
/**
|
|
6
|
+
* The Claude Code channel: an open Claude Code session hears new Ziggs mail
|
|
7
|
+
* within seconds, without polling a tool.
|
|
8
|
+
*
|
|
9
|
+
* A local (stdio) server that declares `experimental['claude/channel']` may
|
|
10
|
+
* push `notifications/claude/channel`; Claude Code puts each one in front of
|
|
11
|
+
* the model as a `<channel>` tag. The server holds the inbox observation
|
|
12
|
+
* long-poll (`/inbox/changes`) and rings once per new delivery.
|
|
13
|
+
*
|
|
14
|
+
* It rings, it does not deliver. An event carries references only (kind and
|
|
15
|
+
* ids), never message text: what a counterparty wrote would otherwise land in
|
|
16
|
+
* the session as if the person had typed it. The model reads the mail through
|
|
17
|
+
* the inbox tools, so claiming, acknowledging and who handles the work stay
|
|
18
|
+
* exactly where they are. Observation itself never claims or acknowledges.
|
|
19
|
+
*
|
|
20
|
+
* Off unless `ZIGGS_MCP_CHANNEL` is set, and Claude Code also has to load the
|
|
21
|
+
* server as a channel (`--dangerously-load-development-channels server:<name>`
|
|
22
|
+
* while channels are a research preview). Without that, Claude Code drops the
|
|
23
|
+
* events and the long-poll costs no tokens.
|
|
24
|
+
*/
|
|
25
|
+
export declare const CHANNEL_CAPABILITY: {
|
|
26
|
+
'claude/channel': {};
|
|
27
|
+
};
|
|
28
|
+
export declare const CHANNEL_NOTIFICATION = "notifications/claude/channel";
|
|
29
|
+
type Delivery = InboxChangesResult['deliveries'][number];
|
|
30
|
+
type Observe = (options: InboxChangesOptions) => Promise<InboxChangesResult>;
|
|
31
|
+
export interface ChannelEvent {
|
|
32
|
+
content: string;
|
|
33
|
+
meta: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
/** The observation read for these credentials: the agent's, or the connection's. */
|
|
36
|
+
export declare function observeFor(creds: Creds): Observe;
|
|
37
|
+
/** One delivery as a channel event: what arrived and where, never what it says. */
|
|
38
|
+
export declare function channelEvent(delivery: Delivery, agentId?: string): ChannelEvent;
|
|
39
|
+
/** Mail that was already waiting when the session started, as one event. */
|
|
40
|
+
export declare function backlogEvent(count: number): ChannelEvent;
|
|
41
|
+
/**
|
|
42
|
+
* Watch the inbox and ring the session for each new delivery. Starts by
|
|
43
|
+
* folding whatever was already waiting into one event, so a session does not
|
|
44
|
+
* open to a wall of tags. Returns a stop function.
|
|
45
|
+
*/
|
|
46
|
+
export declare function startInboxChannel(server: McpServer, observe: Observe, options?: {
|
|
47
|
+
agentId?: string;
|
|
48
|
+
holdSeconds?: number;
|
|
49
|
+
log?: (line: string) => void;
|
|
50
|
+
}): () => void;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { AssistantConnectionClient, decodeOperatorKeyClaims, InboxClient, } from '@ziggs-ai/api-client';
|
|
2
|
+
/**
|
|
3
|
+
* The Claude Code channel: an open Claude Code session hears new Ziggs mail
|
|
4
|
+
* within seconds, without polling a tool.
|
|
5
|
+
*
|
|
6
|
+
* A local (stdio) server that declares `experimental['claude/channel']` may
|
|
7
|
+
* push `notifications/claude/channel`; Claude Code puts each one in front of
|
|
8
|
+
* the model as a `<channel>` tag. The server holds the inbox observation
|
|
9
|
+
* long-poll (`/inbox/changes`) and rings once per new delivery.
|
|
10
|
+
*
|
|
11
|
+
* It rings, it does not deliver. An event carries references only (kind and
|
|
12
|
+
* ids), never message text: what a counterparty wrote would otherwise land in
|
|
13
|
+
* the session as if the person had typed it. The model reads the mail through
|
|
14
|
+
* the inbox tools, so claiming, acknowledging and who handles the work stay
|
|
15
|
+
* exactly where they are. Observation itself never claims or acknowledges.
|
|
16
|
+
*
|
|
17
|
+
* Off unless `ZIGGS_MCP_CHANNEL` is set, and Claude Code also has to load the
|
|
18
|
+
* server as a channel (`--dangerously-load-development-channels server:<name>`
|
|
19
|
+
* while channels are a research preview). Without that, Claude Code drops the
|
|
20
|
+
* events and the long-poll costs no tokens.
|
|
21
|
+
*/
|
|
22
|
+
export const CHANNEL_CAPABILITY = { 'claude/channel': {} };
|
|
23
|
+
export const CHANNEL_NOTIFICATION = 'notifications/claude/channel';
|
|
24
|
+
/** The longest hold `/inbox/changes` grants. */
|
|
25
|
+
const HOLD_SECONDS = 110;
|
|
26
|
+
const RETRY_MS = [1_000, 5_000, 15_000, 60_000];
|
|
27
|
+
/** The observation read for these credentials: the agent's, or the connection's. */
|
|
28
|
+
export function observeFor(creds) {
|
|
29
|
+
if (decodeOperatorKeyClaims(creds.operatorKey)?.connectionClientId) {
|
|
30
|
+
const client = new AssistantConnectionClient(creds);
|
|
31
|
+
return (options) => client.getChanges(options);
|
|
32
|
+
}
|
|
33
|
+
const inbox = new InboxClient(creds.operatorKey, creds.agentId, undefined, creds.instanceId, {
|
|
34
|
+
clientSessionId: creds.clientSessionId,
|
|
35
|
+
workspaceIds: creds.workspaceIds,
|
|
36
|
+
app: creds.app,
|
|
37
|
+
sessionName: creds.sessionName,
|
|
38
|
+
});
|
|
39
|
+
return (options) => inbox.getChanges(options);
|
|
40
|
+
}
|
|
41
|
+
/** One delivery as a channel event: what arrived and where, never what it says. */
|
|
42
|
+
export function channelEvent(delivery, agentId) {
|
|
43
|
+
const forYou = !!agentId && delivery.assigneeId === agentId;
|
|
44
|
+
const meta = { kind: delivery.kind, resource_id: delivery.resourceId };
|
|
45
|
+
if (delivery.chatId)
|
|
46
|
+
meta.chat_id = delivery.chatId;
|
|
47
|
+
if (delivery.taskId)
|
|
48
|
+
meta.task_id = delivery.taskId;
|
|
49
|
+
if (delivery.agreementId)
|
|
50
|
+
meta.agreement_id = delivery.agreementId;
|
|
51
|
+
if (delivery.orgId)
|
|
52
|
+
meta.org_id = delivery.orgId;
|
|
53
|
+
if (forYou)
|
|
54
|
+
meta.for_you = 'true';
|
|
55
|
+
if (delivery.needsHuman)
|
|
56
|
+
meta.needs_human = 'true';
|
|
57
|
+
const who = forYou ? ' It is assigned to you.' : delivery.needsHuman ? ' It asks for your person.' : '';
|
|
58
|
+
return {
|
|
59
|
+
content: `New Ziggs mail (${delivery.kind.replace(/_/g, ' ')}).${who} Read it with ziggs_inbox and follow its rules.`,
|
|
60
|
+
meta,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Mail that was already waiting when the session started, as one event. */
|
|
64
|
+
export function backlogEvent(count) {
|
|
65
|
+
return {
|
|
66
|
+
content: `${count} ${count === 1 ? 'item was' : 'items were'} already waiting in Ziggs when this session started. Read them with ziggs_inbox when you are free.`,
|
|
67
|
+
meta: { kind: 'backlog', count: String(count) },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function apiStatus(error) {
|
|
71
|
+
const status = error?.status;
|
|
72
|
+
return typeof status === 'number' ? status : undefined;
|
|
73
|
+
}
|
|
74
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
75
|
+
if (signal.aborted)
|
|
76
|
+
return resolve();
|
|
77
|
+
const timer = setTimeout(resolve, ms);
|
|
78
|
+
signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
79
|
+
});
|
|
80
|
+
/**
|
|
81
|
+
* Watch the inbox and ring the session for each new delivery. Starts by
|
|
82
|
+
* folding whatever was already waiting into one event, so a session does not
|
|
83
|
+
* open to a wall of tags. Returns a stop function.
|
|
84
|
+
*/
|
|
85
|
+
export function startInboxChannel(server, observe, options = {}) {
|
|
86
|
+
const stop = new AbortController();
|
|
87
|
+
const signal = stop.signal;
|
|
88
|
+
const hold = options.holdSeconds ?? HOLD_SECONDS;
|
|
89
|
+
const log = options.log ?? ((line) => process.stderr.write(`[ziggs-mcp channel] ${line}\n`));
|
|
90
|
+
const ring = (event) => server.server.notification({ method: CHANNEL_NOTIFICATION, params: { ...event } });
|
|
91
|
+
void (async () => {
|
|
92
|
+
let cursor;
|
|
93
|
+
let failures = 0;
|
|
94
|
+
while (!signal.aborted) {
|
|
95
|
+
try {
|
|
96
|
+
if (cursor === undefined) {
|
|
97
|
+
// Catch up without a hold: what is retained now is the backlog.
|
|
98
|
+
let waiting = 0;
|
|
99
|
+
let page;
|
|
100
|
+
do {
|
|
101
|
+
page = await observe({ cursor, signal });
|
|
102
|
+
cursor = page.cursor;
|
|
103
|
+
waiting += page.deliveries.length;
|
|
104
|
+
} while (page.hasMore && !signal.aborted);
|
|
105
|
+
if (waiting > 0 && !signal.aborted)
|
|
106
|
+
await ring(backlogEvent(waiting));
|
|
107
|
+
}
|
|
108
|
+
const page = await observe({ cursor, waitSeconds: hold, signal });
|
|
109
|
+
cursor = page.cursor;
|
|
110
|
+
failures = 0;
|
|
111
|
+
for (const delivery of page.deliveries) {
|
|
112
|
+
if (signal.aborted)
|
|
113
|
+
break;
|
|
114
|
+
await ring(channelEvent(delivery, options.agentId));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (signal.aborted)
|
|
119
|
+
break;
|
|
120
|
+
// An expired cursor starts over from what is retained.
|
|
121
|
+
if (apiStatus(error) === 410) {
|
|
122
|
+
cursor = undefined;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const wait = RETRY_MS[Math.min(failures, RETRY_MS.length - 1)];
|
|
126
|
+
failures += 1;
|
|
127
|
+
log(`inbox watch failed (${error?.message ?? String(error)}); retrying in ${wait / 1000}s`);
|
|
128
|
+
await sleep(wait, signal);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
return () => stop.abort();
|
|
133
|
+
}
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The same text for every client. The two doors differ only where their tools
|
|
12
12
|
* do: the directory door's inbox read observes and a claim takes the lease,
|
|
13
|
-
* while the compact door's full read takes it
|
|
14
|
-
* reached through the catalog. Everything else an agent needs is said where it
|
|
13
|
+
* while the compact door's full read takes it. Everything else an agent needs is said where it
|
|
15
14
|
* applies — in the description of the tool it is about, as a fact about that
|
|
16
15
|
* tool, or in the refusal that teaches it at the moment it matters. The long
|
|
17
16
|
* protocol (`delegateProtocol.ts`) stays the source for SKILL.md and
|
|
@@ -27,5 +26,7 @@ export declare function sessionRules(door: ConnectDoor): string[];
|
|
|
27
26
|
export interface ConnectTextOptions {
|
|
28
27
|
/** The serving backend delivers `ziggs.inbox.updated` events. */
|
|
29
28
|
events?: boolean;
|
|
29
|
+
/** The local server rings a Claude Code session through a channel. */
|
|
30
|
+
channel?: boolean;
|
|
30
31
|
}
|
|
31
32
|
export declare function renderConnectText(door: ConnectDoor, options?: ConnectTextOptions): string;
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The same text for every client. The two doors differ only where their tools
|
|
12
12
|
* do: the directory door's inbox read observes and a claim takes the lease,
|
|
13
|
-
* while the compact door's full read takes it
|
|
14
|
-
* reached through the catalog. Everything else an agent needs is said where it
|
|
13
|
+
* while the compact door's full read takes it. Everything else an agent needs is said where it
|
|
15
14
|
* applies — in the description of the tool it is about, as a fact about that
|
|
16
15
|
* tool, or in the refusal that teaches it at the moment it matters. The long
|
|
17
16
|
* protocol (`delegateProtocol.ts`) stays the source for SKILL.md and
|
|
@@ -46,7 +45,6 @@ const RULES = [
|
|
|
46
45
|
export function sessionRules(door) {
|
|
47
46
|
return [DATA, INBOX[door], WAIT, ACCESS, REPLY, GROUNDING];
|
|
48
47
|
}
|
|
49
|
-
const CATALOG = 'Only the everyday tools are loaded. ziggs_tools lists the rest; call them through ziggs_tool_read or ziggs_tool_write.';
|
|
50
48
|
/**
|
|
51
49
|
* Directory door only: inbox events ride the connection key that door is
|
|
52
50
|
* minted for. Said only when the serving backend has events on, so a host is
|
|
@@ -55,13 +53,18 @@ const CATALOG = 'Only the everyday tools are loaded. ziggs_tools lists the rest;
|
|
|
55
53
|
* text was already sized: that append is what cut the text mid-word.
|
|
56
54
|
*/
|
|
57
55
|
const EVENTS = 'Inbox events: a host may subscribe to ziggs.inbox.updated per workspace; promise alerts only once it confirms. An event carries only orgId: read that inbox and follow its rules.';
|
|
56
|
+
/**
|
|
57
|
+
* Compact door only, when the local server rings a Claude Code session. The
|
|
58
|
+
* event says that mail arrived, never what it says.
|
|
59
|
+
*/
|
|
60
|
+
const CHANNEL = 'A <channel> event from Ziggs means new mail (ids only): read it with ziggs_inbox and follow its rules.';
|
|
58
61
|
export function renderConnectText(door, options = {}) {
|
|
59
62
|
return [
|
|
60
63
|
`- ${DATA}`,
|
|
61
64
|
`- ${SESSION}`,
|
|
62
65
|
`- ${INBOX[door]}`,
|
|
63
66
|
...RULES.map((r) => `- ${r}`),
|
|
64
|
-
...(door === 'compact' ? [`- ${CATALOG}`] : []),
|
|
65
67
|
...(door === 'directory' && options.events ? [`- ${EVENTS}`] : []),
|
|
68
|
+
...(door === 'compact' && options.channel ? [`- ${CHANNEL}`] : []),
|
|
66
69
|
].join('\n');
|
|
67
70
|
}
|
package/dist/server.js
CHANGED
|
@@ -8,9 +8,10 @@ import { AssistantConnectionClient, decodeOperatorKeyClaims, InboxClient } from
|
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { credsFromConfig } from './creds.js';
|
|
10
10
|
import { registerZiggsTools } from './tools.js';
|
|
11
|
-
import { applySurfacePolicy } from './surface.js';
|
|
11
|
+
import { applySurfacePolicy, listWithoutSchemaDialect } from './surface.js';
|
|
12
12
|
import { backendToolPick } from './toolPick.js';
|
|
13
13
|
import { renderConnectText } from './protocol/connectText.js';
|
|
14
|
+
import { CHANNEL_CAPABILITY, observeFor, startInboxChannel } from './inboxChannel.js';
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
const { version } = require('../package.json');
|
|
16
17
|
/** Shared MCP server factory — stdio (local) and remote HTTP (backend) reuse this. */
|
|
@@ -25,8 +26,11 @@ export function createZiggsMcpServer(creds, cfg) {
|
|
|
25
26
|
// stdio and remote HTTP alike, since both build the server here.
|
|
26
27
|
instructions: renderConnectText(cfg.directConnection === false ? 'directory' : 'compact', {
|
|
27
28
|
events: cfg.eventsEnabled === true,
|
|
29
|
+
channel: cfg.channel === true && cfg.directConnection !== false,
|
|
28
30
|
}),
|
|
29
31
|
});
|
|
32
|
+
if (cfg.channel === true && cfg.directConnection !== false)
|
|
33
|
+
server.server.registerCapabilities({ experimental: CHANNEL_CAPABILITY });
|
|
30
34
|
if (cfg.directConnection === false)
|
|
31
35
|
installDirectorySurface(server);
|
|
32
36
|
installConnectionNotices(server, creds, version);
|
|
@@ -37,6 +41,7 @@ export function createZiggsMcpServer(creds, cfg) {
|
|
|
37
41
|
// catalog remains only for self-configured operator-key clients.
|
|
38
42
|
if (cfg.directConnection !== false)
|
|
39
43
|
applySurfacePolicy(server, { pickTool: backendToolPick(creds) });
|
|
44
|
+
listWithoutSchemaDialect(server);
|
|
40
45
|
return server;
|
|
41
46
|
}
|
|
42
47
|
/** Drop this process's inbox host. Best-effort — shutdown must not fail on it. */
|
|
@@ -74,4 +79,6 @@ export async function startStdioServer() {
|
|
|
74
79
|
installStdioInboxHostRelease(creds);
|
|
75
80
|
const transport = new StdioServerTransport();
|
|
76
81
|
await server.connect(transport);
|
|
82
|
+
if (cfg.channel)
|
|
83
|
+
startInboxChannel(server, observeFor(creds), { agentId: creds.agentId });
|
|
77
84
|
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -41,27 +41,23 @@ export declare const NATIVE_TOOLS: readonly string[];
|
|
|
41
41
|
* when it refuses.
|
|
42
42
|
*/
|
|
43
43
|
export declare const CATALOG_TOOLS: readonly string[];
|
|
44
|
-
/** Per-tool how-to-call,
|
|
44
|
+
/** Per-tool how-to-call: every tool is listed, so by name, or through its lane's dispatcher. */
|
|
45
45
|
export declare function callLine(entry: CatalogEntry): string;
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* So every tool stays ENABLED, and the listing is narrowed instead. A name is
|
|
59
|
-
* always callable; the list is still the native set plus the catalog tools.
|
|
47
|
+
* Drop the `$schema` line the SDK writes into every tool's input and output
|
|
48
|
+
* schema. It names a JSON Schema dialect no MCP client reads, and on the
|
|
49
|
+
* directory door it was the same 52 characters 64 times in every
|
|
50
|
+
* `tools/list`. Wraps whatever handler is installed last, so call it after
|
|
51
|
+
* every other `tools/list` filter.
|
|
52
|
+
*/
|
|
53
|
+
export declare function listWithoutSchemaDialect(server: McpServer): void;
|
|
54
|
+
/**
|
|
55
|
+
* Register the catalog and one dispatcher per annotation lane, next to every
|
|
56
|
+
* tool listed by name.
|
|
60
57
|
*
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* the tools every caller loads. We filter its output and change nothing else.
|
|
58
|
+
* The catalog answers by search and by plain-sentence need, and the
|
|
59
|
+
* dispatchers reach any tool by name, so a client that mounts only part of
|
|
60
|
+
* the list (an allow-list) still reaches the rest.
|
|
65
61
|
*/
|
|
66
62
|
export declare function applySurfacePolicy(server: McpServer, opts?: {
|
|
67
63
|
/** Answers `ziggs_tools need=`. Absent: `need` falls back to the full list. */
|
package/dist/surface.js
CHANGED
|
@@ -63,7 +63,6 @@ export const CATALOG_TOOLS = [
|
|
|
63
63
|
TOOL_READ_NAME,
|
|
64
64
|
TOOL_WRITE_NAME,
|
|
65
65
|
];
|
|
66
|
-
const CATALOG_TOOL_NAMES = new Set(CATALOG_TOOLS);
|
|
67
66
|
/**
|
|
68
67
|
* A tool that takes a freeform target and payload has to say which API it
|
|
69
68
|
* reaches; a store reviewer cannot otherwise tell what it does.
|
|
@@ -77,21 +76,15 @@ const TOOL_WRITE_DESCRIPTION = `Call a Ziggs tool that creates or changes someth
|
|
|
77
76
|
'This reaches every writing tool ziggs_tools lists, native or not. Read-only tools are refused here and pointed at ziggs_tool_read. Most writes cannot be taken back (a sent message, a taken decision, an overwritten plan), so a call through here may ask the human first. ' +
|
|
78
77
|
'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. ' +
|
|
79
78
|
'The result is exactly what the tool itself returns.';
|
|
80
|
-
|
|
81
|
-
return NATIVE_TOOLS.includes(name);
|
|
82
|
-
}
|
|
83
|
-
/** Per-tool how-to-call, derived from NATIVE_TOOLS and the annotations. */
|
|
79
|
+
/** Per-tool how-to-call: every tool is listed, so by name, or through its lane's dispatcher. */
|
|
84
80
|
export function callLine(entry) {
|
|
85
|
-
|
|
86
|
-
if (isNative(entry.name)) {
|
|
87
|
-
return `call it by name: ${entry.name}; ${dispatcher} { tool: "${entry.name}", args } also works`;
|
|
88
|
-
}
|
|
89
|
-
return `call via ${dispatcher} { tool, args }`;
|
|
81
|
+
return `call it by name: ${entry.name}; ${laneOf(entry)} { tool: "${entry.name}", args } also works`;
|
|
90
82
|
}
|
|
91
83
|
function withCall(entry, base) {
|
|
92
84
|
return {
|
|
93
85
|
...base,
|
|
94
|
-
|
|
86
|
+
// Every tool is listed by name now, so every row is native.
|
|
87
|
+
native: true,
|
|
95
88
|
call: callLine(entry),
|
|
96
89
|
};
|
|
97
90
|
}
|
|
@@ -103,31 +96,32 @@ function laneOf(entry) {
|
|
|
103
96
|
return entry.annotations.readOnlyHint ? TOOL_READ_NAME : TOOL_WRITE_NAME;
|
|
104
97
|
}
|
|
105
98
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* degrades: if a future SDK stops keeping handlers there, the server refuses to
|
|
112
|
-
* start and says why. A silent fallback would ship the whole 58-tool surface to
|
|
113
|
-
* every caller — the regression this policy exists to prevent — and it would
|
|
114
|
-
* look like it worked.
|
|
99
|
+
* Drop the `$schema` line the SDK writes into every tool's input and output
|
|
100
|
+
* schema. It names a JSON Schema dialect no MCP client reads, and on the
|
|
101
|
+
* directory door it was the same 52 characters 64 times in every
|
|
102
|
+
* `tools/list`. Wraps whatever handler is installed last, so call it after
|
|
103
|
+
* every other `tools/list` filter.
|
|
115
104
|
*/
|
|
116
|
-
function
|
|
105
|
+
export function listWithoutSchemaDialect(server) {
|
|
117
106
|
const inner = server.server;
|
|
118
107
|
const built = inner._requestHandlers?.get('tools/list');
|
|
119
|
-
if (!built)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
108
|
+
if (!built)
|
|
109
|
+
return;
|
|
110
|
+
const strip = (schema) => {
|
|
111
|
+
if (!schema || typeof schema !== 'object')
|
|
112
|
+
return schema;
|
|
113
|
+
const { $schema: _dialect, ...rest } = schema;
|
|
114
|
+
return rest;
|
|
115
|
+
};
|
|
126
116
|
server.server.setRequestHandler(ListToolsRequestSchema, async (req, extra) => {
|
|
127
117
|
const all = await built(req, extra);
|
|
128
118
|
return {
|
|
129
119
|
...all,
|
|
130
|
-
tools: all.tools.
|
|
120
|
+
tools: all.tools.map((tool) => ({
|
|
121
|
+
...tool,
|
|
122
|
+
inputSchema: strip(tool.inputSchema),
|
|
123
|
+
...(tool.outputSchema ? { outputSchema: strip(tool.outputSchema) } : {}),
|
|
124
|
+
})),
|
|
131
125
|
};
|
|
132
126
|
});
|
|
133
127
|
}
|
|
@@ -180,28 +174,20 @@ function registerDispatcher(server, lane) {
|
|
|
180
174
|
});
|
|
181
175
|
}
|
|
182
176
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* Narrowing by `.disable()` would be the shorter way to do this, and it is the
|
|
187
|
-
* wrong one: disabling takes a tool out of `tools/list` AND out of
|
|
188
|
-
* `tools/call`. The SDK answers a direct call on a disabled tool with
|
|
189
|
-
* `Tool <name> disabled`, thrown from inside its own handler where we cannot
|
|
190
|
-
* reword it — so a caller holding a name (a saved config, a script, a model
|
|
191
|
-
* that learned the name in an earlier session) hits a wall that does not say
|
|
192
|
-
* the tool exists, where it went, or how to reach it.
|
|
193
|
-
*
|
|
194
|
-
* So every tool stays ENABLED, and the listing is narrowed instead. A name is
|
|
195
|
-
* always callable; the list is still the native set plus the catalog tools.
|
|
177
|
+
* Register the catalog and one dispatcher per annotation lane, next to every
|
|
178
|
+
* tool listed by name.
|
|
196
179
|
*
|
|
197
|
-
* The
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* the tools every caller loads. We filter its output and change nothing else.
|
|
180
|
+
* The catalog answers by search and by plain-sentence need, and the
|
|
181
|
+
* dispatchers reach any tool by name, so a client that mounts only part of
|
|
182
|
+
* the list (an allow-list) still reaches the rest.
|
|
201
183
|
*/
|
|
202
184
|
export function applySurfacePolicy(server, opts = {}) {
|
|
203
|
-
|
|
204
|
-
|
|
185
|
+
// Every tool is listed, as on the directory door. Listing everything costs
|
|
186
|
+
// the same or less per task, because a tool outside the everyday set no
|
|
187
|
+
// longer needs a catalog call first, and a host's permission prompt names
|
|
188
|
+
// the real tool.
|
|
189
|
+
// The catalog stays listed for clients that mount only an allow-list (the
|
|
190
|
+
// Copilot coding agent) and reach the rest through it.
|
|
205
191
|
registerStrictTool(server, TOOLS_CATALOG_NAME, {
|
|
206
192
|
describe: z
|
|
207
193
|
.array(z.string())
|
package/dist/tools.js
CHANGED
|
@@ -927,12 +927,12 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
927
927
|
.string()
|
|
928
928
|
.max(80)
|
|
929
929
|
.optional()
|
|
930
|
-
.describe('Short
|
|
930
|
+
.describe('Short list label, around 60 characters. Set it when this is one of many tasks; otherwise the list shows the description.'),
|
|
931
931
|
parentTaskId: z.string().optional().describe('Parent task id for sub-tasks'),
|
|
932
932
|
assigneeId: z
|
|
933
933
|
.string()
|
|
934
934
|
.optional()
|
|
935
|
-
.describe('Agent or user
|
|
935
|
+
.describe('Agent or user to assign it to; they must already hold a grant on the agreement.'),
|
|
936
936
|
inputArtifactIds: z
|
|
937
937
|
.array(z.string())
|
|
938
938
|
.optional()
|
|
@@ -940,7 +940,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
940
940
|
waitsOn: z
|
|
941
941
|
.array(z.string())
|
|
942
942
|
.optional()
|
|
943
|
-
.describe('Task ids
|
|
943
|
+
.describe('Task ids to wait for: created now, but its assignee is not woken until all of them end, in any state. States a join up front instead of polling. Name only tasks you can read.'),
|
|
944
944
|
closesBy: z
|
|
945
945
|
.enum(['result', 'children'])
|
|
946
946
|
.optional()
|
|
@@ -965,7 +965,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
965
965
|
.min(1)
|
|
966
966
|
.max(100)
|
|
967
967
|
.optional()
|
|
968
|
-
.describe('Children created with this task, all or none
|
|
968
|
+
.describe('Children created with this task, all or none; waitsOn names sibling names, and the answer maps each name to its task id. Children that wait on nothing get this task\'s inputArtifactIds. before_execution holds every child until the plan is acknowledged.'),
|
|
969
969
|
// POST /tasks and the SDK's task_create have always taken
|
|
970
970
|
// these three; only this surface hid them, so an agent on MCP had to
|
|
971
971
|
// create-then-replace_plan even when it already knew the steps.
|
|
@@ -1021,7 +1021,7 @@ export function registerZiggsTools(server, creds, cfg) {
|
|
|
1021
1021
|
.describe('Total reminders allowed per person asked, after the first ask. Then the holder stops and reports.'),
|
|
1022
1022
|
})
|
|
1023
1023
|
.optional()
|
|
1024
|
-
.describe('How often the holder may remind
|
|
1024
|
+
.describe('How often the holder may remind and how many times at most; both are required. Omit for a task nobody needs reminding about.'),
|
|
1025
1025
|
sources: sourcesField(),
|
|
1026
1026
|
}, write('Create a task'), async ({ agreementId, description, title, parentTaskId, assigneeId, inputArtifactIds, waitsOn, closesBy, children, plan, planReviewTiming, requireMidWorkPlanAck, dueAt, checkBack, sources, }) => {
|
|
1027
1027
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/ziggs-mcp",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.21",
|
|
4
4
|
"description": "MCP server for Claude Code, Cursor, and other MCP hosts \u2014 act as your Ziggs delegate agent",
|
|
5
5
|
"homepage": "https://ziggsai.com/docs",
|
|
6
6
|
"bugs": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54
|
-
"@ziggs-ai/api-client": "0.37.
|
|
54
|
+
"@ziggs-ai/api-client": "0.37.20",
|
|
55
55
|
"dotenv": "^16.6.1",
|
|
56
56
|
"zod": "^3.24.2",
|
|
57
57
|
"zod-to-json-schema": "^3.25.1"
|