@zeph-to/mcp-server 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAE5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAOD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,KACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAsClC,CAAC"}
1
+ {"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAE5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAOD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,KACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAwClC,CAAC"}
package/dist/poll.js CHANGED
@@ -33,8 +33,10 @@ const pollForResponse = async (client, hookId, eventId, timeoutSeconds, ctx) =>
33
33
  });
34
34
  }
35
35
  }
36
- // Adaptive interval: 2s for first 5 attempts, then 3s
37
- const interval = attempt < 5 ? 2000 : 3000;
36
+ // Adaptive interval: poll every 1s while the user is likely still at
37
+ // their device (first ~60 attempts 1 min), then back off to 3s for
38
+ // long waits. The tight 1s window keeps post-tap detection snappy.
39
+ const interval = attempt < 60 ? 1000 : 3000;
38
40
  await sleep(interval);
39
41
  attempt++;
40
42
  }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Defensive cleanup for tool arguments.
3
+ *
4
+ * An agent occasionally emits a malformed tool call where a parameter's
5
+ * closing tag is wrong, so the serialized markup of the *following*
6
+ * parameters bleeds into an earlier string argument.
7
+ *
8
+ * Observed in the wild — a zeph_ask `body` arrived as:
9
+ *
10
+ * "...Commit now or test first?</body>
11
+ * <parameter name=\"actions\">[{\"id\":\"commit\",...}]"
12
+ *
13
+ * The actions JSON leaked into `body`, and `actions` itself arrived
14
+ * undefined — so the push showed the raw markup and no buttons.
15
+ *
16
+ * sanitizeText strips the leaked markup; recoverActions pulls the
17
+ * swallowed actions array back out so the call still works.
18
+ */
19
+ export interface RecoveredAction {
20
+ id: string;
21
+ label: string;
22
+ style?: 'primary' | 'secondary' | 'danger';
23
+ }
24
+ /**
25
+ * Strip leaked tool-call markup from a free-text argument. Returns the
26
+ * text unchanged when no leak is detected.
27
+ */
28
+ export declare const sanitizeText: (text: string | undefined) => string | undefined;
29
+ /**
30
+ * Recover an `actions` array that leaked into the body of a malformed
31
+ * zeph_ask call. Returns undefined when nothing parseable is found.
32
+ */
33
+ export declare const recoverActions: (text: string | undefined) => RecoveredAction[] | undefined;
34
+ //# sourceMappingURL=sanitize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5C;AAeD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAIhE,CAAC;AASF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,eAAe,EAAE,GAAG,SAgB7E,CAAC"}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * Defensive cleanup for tool arguments.
4
+ *
5
+ * An agent occasionally emits a malformed tool call where a parameter's
6
+ * closing tag is wrong, so the serialized markup of the *following*
7
+ * parameters bleeds into an earlier string argument.
8
+ *
9
+ * Observed in the wild — a zeph_ask `body` arrived as:
10
+ *
11
+ * "...Commit now or test first?</body>
12
+ * <parameter name=\"actions\">[{\"id\":\"commit\",...}]"
13
+ *
14
+ * The actions JSON leaked into `body`, and `actions` itself arrived
15
+ * undefined — so the push showed the raw markup and no buttons.
16
+ *
17
+ * sanitizeText strips the leaked markup; recoverActions pulls the
18
+ * swallowed actions array back out so the call still works.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.recoverActions = exports.sanitizeText = void 0;
22
+ // Markers that should never appear in a real notification body — their
23
+ // presence means tool-call markup has leaked in. Cut at the earliest one.
24
+ const LEAK_MARKERS = ['</body>', '</parameter>', '<parameter name=', '<parameter '];
25
+ const findLeakStart = (text) => {
26
+ let earliest = -1;
27
+ for (const marker of LEAK_MARKERS) {
28
+ const idx = text.indexOf(marker);
29
+ if (idx !== -1 && (earliest === -1 || idx < earliest))
30
+ earliest = idx;
31
+ }
32
+ return earliest;
33
+ };
34
+ /**
35
+ * Strip leaked tool-call markup from a free-text argument. Returns the
36
+ * text unchanged when no leak is detected.
37
+ */
38
+ const sanitizeText = (text) => {
39
+ if (!text)
40
+ return text;
41
+ const cut = findLeakStart(text);
42
+ return cut === -1 ? text : text.slice(0, cut).trimEnd();
43
+ };
44
+ exports.sanitizeText = sanitizeText;
45
+ const isActionArray = (value) => Array.isArray(value) &&
46
+ value.length > 0 &&
47
+ value.every((a) => a && typeof a.id === 'string' && typeof a.label === 'string');
48
+ /**
49
+ * Recover an `actions` array that leaked into the body of a malformed
50
+ * zeph_ask call. Returns undefined when nothing parseable is found.
51
+ */
52
+ const recoverActions = (text) => {
53
+ if (!text)
54
+ return undefined;
55
+ const marker = text.search(/<(?:antml:)?parameter name="actions">/);
56
+ if (marker === -1)
57
+ return undefined;
58
+ const after = text.slice(marker);
59
+ const start = after.indexOf('[');
60
+ const end = after.lastIndexOf(']');
61
+ if (start === -1 || end <= start)
62
+ return undefined;
63
+ try {
64
+ const parsed = JSON.parse(after.slice(start, end + 1));
65
+ return isActionArray(parsed) ? parsed : undefined;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ };
71
+ exports.recoverActions = recoverActions;
@@ -1 +1 @@
1
- {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/tools/ask.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAuBrE,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwHhG,CAAC"}
1
+ {"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/tools/ask.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAwBrE,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAgIhG,CAAC"}
package/dist/tools/ask.js CHANGED
@@ -7,6 +7,7 @@ const poll_js_1 = require("../poll.js");
7
7
  const config_js_1 = require("../config.js");
8
8
  const crypto_js_1 = require("../crypto.js");
9
9
  const mime_js_1 = require("../mime.js");
10
+ const sanitize_js_1 = require("../sanitize.js");
10
11
  // The device feed shows a short preview of the body. Anything longer than
11
12
  // this gets truncated there, so we attach the full text as a file — the
12
13
  // user can always open the complete content instead of squinting at a
@@ -63,16 +64,22 @@ const registerAskTool = (server, client, config) => {
63
64
  return (0, error_format_js_1.hookNotConfiguredError)();
64
65
  try {
65
66
  const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
67
+ // Defend against malformed tool calls where the actions array leaked
68
+ // into the body (a mis-closed `body` parameter). Recover the actions
69
+ // from the raw body first, then strip the leaked markup. Without this
70
+ // the push arrives with no buttons and raw markup in the text.
71
+ const effectiveActions = actions && actions.length > 0 ? actions : (0, sanitize_js_1.recoverActions)(body);
72
+ const cleanBody = (0, sanitize_js_1.sanitizeText)(body);
66
73
  // Attach a file whenever the body would be clipped in the feed preview.
67
- const exceedsPreview = !!body && body.length > PREVIEW_LENGTH;
68
- let triggerBody = body;
74
+ const exceedsPreview = !!cleanBody && cleanBody.length > PREVIEW_LENGTH;
75
+ let triggerBody = cleanBody;
69
76
  let files;
70
- if (exceedsPreview && body) {
77
+ if (exceedsPreview && cleanBody) {
71
78
  const fileName = 'response.md';
72
79
  const fileType = (0, mime_js_1.inferMimeType)(fileName);
73
80
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
74
81
  // Self-contained Markdown so the file alone tells the whole story.
75
- const fileMarkdown = buildAskMarkdown(title, body, actions);
82
+ const fileMarkdown = buildAskMarkdown(title, cleanBody, effectiveActions);
76
83
  const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
77
84
  let uploadContent = fileMarkdown;
78
85
  let uploadContentType = fileType;
@@ -92,13 +99,13 @@ const registerAskTool = (server, client, config) => {
92
99
  }
93
100
  const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
94
101
  await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
95
- triggerBody = body.slice(0, PREVIEW_LENGTH) + '...';
102
+ triggerBody = cleanBody.slice(0, PREVIEW_LENGTH) + '...';
96
103
  files = [{ fileKey: upload.data.fileKey, fileName, fileSize: fileBytes, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }];
97
104
  }
98
105
  const trigger = await client.triggerHook(config.hookId, {
99
106
  title: pushTitle,
100
107
  body: triggerBody,
101
- actions,
108
+ actions: effectiveActions,
102
109
  timeout,
103
110
  fallback,
104
111
  hookType: 'combo',
@@ -1 +1 @@
1
- {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwDlG,CAAC"}
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGrE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwDlG,CAAC"}
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const poll_js_1 = require("../poll.js");
7
7
  const config_js_1 = require("../config.js");
8
+ const sanitize_js_1 = require("../sanitize.js");
8
9
  const registerInputTool = (server, client, config) => {
9
10
  server.registerTool('zeph_input', {
10
11
  description: 'Request text input from the user via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
@@ -34,7 +35,7 @@ const registerInputTool = (server, client, config) => {
34
35
  try {
35
36
  const trigger = await client.triggerHook(config.hookId, {
36
37
  title: (0, config_js_1.formatPushTitle)(config.projectName, title),
37
- body,
38
+ body: (0, sanitize_js_1.sanitizeText)(body),
38
39
  timeout,
39
40
  hookType: 'input',
40
41
  metadata: { placeholder, inputType },
@@ -1 +1 @@
1
- {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAQrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAiHnG,CAAC"}
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/tools/notify.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AASrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAmHnG,CAAC"}
@@ -6,6 +6,7 @@ const error_format_js_1 = require("../error-format.js");
6
6
  const config_js_1 = require("../config.js");
7
7
  const crypto_js_1 = require("../crypto.js");
8
8
  const mime_js_1 = require("../mime.js");
9
+ const sanitize_js_1 = require("../sanitize.js");
9
10
  // The device feed shows a short preview of the body. Anything longer gets
10
11
  // truncated there, so we attach the full text as a file for full viewing.
11
12
  const PREVIEW_LENGTH = 200;
@@ -31,14 +32,16 @@ const registerNotifyTool = (server, client, config) => {
31
32
  try {
32
33
  const deviceId = targetDeviceId ?? config.deviceId;
33
34
  const pushTitle = (0, config_js_1.formatPushTitle)(config.projectName, title);
35
+ // Strip any tool-call markup that leaked into the body argument.
36
+ const cleanBody = (0, sanitize_js_1.sanitizeText)(body);
34
37
  // Attach a file whenever the body would be clipped in the feed preview.
35
- const isLongBody = !!body && body.length > PREVIEW_LENGTH;
38
+ const isLongBody = !!cleanBody && cleanBody.length > PREVIEW_LENGTH;
36
39
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
37
- if (isLongBody && body) {
40
+ if (isLongBody && cleanBody) {
38
41
  const fileName = 'response.md';
39
42
  const fileType = (0, mime_js_1.inferMimeType)(fileName);
40
43
  // Self-contained Markdown so the file alone carries the full text.
41
- const fileMarkdown = `# ${title}\n\n${body}`;
44
+ const fileMarkdown = `# ${title}\n\n${cleanBody}`;
42
45
  const fileBytes = new TextEncoder().encode(fileMarkdown).byteLength;
43
46
  // Encrypt file content if keys available
44
47
  let uploadContent = fileMarkdown;
@@ -59,7 +62,7 @@ const registerNotifyTool = (server, client, config) => {
59
62
  }
60
63
  const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? fileBytes : uploadContent.length });
61
64
  await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
62
- const preview = body.slice(0, PREVIEW_LENGTH) + '...';
65
+ const preview = cleanBody.slice(0, PREVIEW_LENGTH) + '...';
63
66
  // Encrypt push body (title/preview/url) if keys available
64
67
  let pushPayload = {
65
68
  title: pushTitle,
@@ -86,7 +89,7 @@ const registerNotifyTool = (server, client, config) => {
86
89
  // Short body — encrypt push only
87
90
  let pushPayload = {
88
91
  title: pushTitle,
89
- body,
92
+ body: cleanBody,
90
93
  url,
91
94
  type: 'hook',
92
95
  priority,
@@ -95,7 +98,7 @@ const registerNotifyTool = (server, client, config) => {
95
98
  };
96
99
  if (canEncrypt) {
97
100
  try {
98
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body, url });
101
+ const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: pushTitle, body: cleanBody, url });
99
102
  pushPayload = { ...pushPayload, title: undefined, body: enc.body, isEncrypted: enc.isEncrypted, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
100
103
  }
101
104
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAuEnG,CAAC"}
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAGrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAuEnG,CAAC"}
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const poll_js_1 = require("../poll.js");
7
7
  const config_js_1 = require("../config.js");
8
+ const sanitize_js_1 = require("../sanitize.js");
8
9
  const registerPromptTool = (server, client, config) => {
9
10
  server.registerTool('zeph_prompt', {
10
11
  description: 'Ask the user to choose from predefined options via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
@@ -43,7 +44,7 @@ const registerPromptTool = (server, client, config) => {
43
44
  try {
44
45
  const trigger = await client.triggerHook(config.hookId, {
45
46
  title: (0, config_js_1.formatPushTitle)(config.projectName, title),
46
- body,
47
+ body: (0, sanitize_js_1.sanitizeText)(body),
47
48
  actions,
48
49
  timeout,
49
50
  fallback,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "Zeph MCP server — AI agent notifications, prompts, and input via MCP protocol",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",