@zeph-to/mcp-server 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,11 +49,11 @@ Add to `~/.claude/settings.json`:
49
49
  | Variable | Required | Description |
50
50
  |----------|----------|-------------|
51
51
  | `ZEPH_API_KEY` | Yes* | API key from Settings > API Keys |
52
- | `ZEPH_HOOK_ID` | No | Hook ID for interactive tools (`zeph_prompt`, `zeph_input`) |
53
- | `ZEPH_DEVICE_ID` | No | Target device ID. Omit to send to all devices |
52
+ | `ZEPH_HOOK_ID` | No | Hook ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`) |
53
+ | `ZEPH_DEVICE_ID` | No | Target device ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`). Omit to send to all devices |
54
54
  | `ZEPH_BASE_URL` | No | API base URL (default: `https://api.zeph.to/v1`) |
55
55
 
56
- \* All env vars fall back to `~/.zeph/config.json` if not set or if the value is an unresolved `${...}` interpolation.
56
+ \* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `npx @zeph-to/hook-sdk install`). Unresolved `${...}` interpolations are also treated as unset.
57
57
 
58
58
  ## Tools
59
59
 
@@ -145,6 +145,25 @@ fallback: "no" (auto-select on timeout, optional)
145
145
 
146
146
  Returns: `{ actionId: "yes", timedOut: false }`
147
147
 
148
+ ### zeph_ask
149
+
150
+ Ask the user a question with optional quick-reply buttons and a text input field. Combines prompt (buttons) and input (text) in a single notification. Blocks until response or timeout.
151
+
152
+ Requires `ZEPH_HOOK_ID`.
153
+
154
+ ```
155
+ title: "What should we do?"
156
+ body: "3 tests failed in auth module" (optional)
157
+ actions: [{ id: "fix", label: "Fix now", style: "primary" },
158
+ { id: "skip", label: "Skip", style: "secondary" }] (optional, 1-4)
159
+ placeholder: "Or type a custom response..." (optional)
160
+ inputType: "text" | "multiline" (default: text)
161
+ timeout: 120 (seconds, default: 120, max: 600)
162
+ fallback: "skip" (auto-select on timeout, optional)
163
+ ```
164
+
165
+ Returns: `{ actionId: "fix", timedOut: false }` or `{ value: "custom text", timedOut: false }`
166
+
148
167
  ### zeph_input
149
168
 
150
169
  Request free-form text input from the user. Blocks until response or timeout.
package/dist/crypto.d.ts CHANGED
@@ -12,20 +12,6 @@
12
12
  export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
13
13
  export declare const getKeyPair: () => CryptoKeyPair | null;
14
14
  export declare const getPublicKey: () => string | null;
15
- /**
16
- * Encrypt push body for a recipient.
17
- * Returns fields ready to merge into the sendPush payload.
18
- */
19
- export declare const encryptPushBody: (input: {
20
- title?: string;
21
- body?: string;
22
- url?: string;
23
- }, recipientPublicKeyRaw: string) => Promise<{
24
- body: string;
25
- encryptedKey: string;
26
- senderPublicKey: string;
27
- isEncrypted: true;
28
- }>;
29
15
  /**
30
16
  * Encrypt push body for self (all own devices).
31
17
  */
@@ -39,15 +25,6 @@ export declare const encryptPushBodyForSelf: (input: {
39
25
  senderPublicKey: string;
40
26
  isEncrypted: true;
41
27
  }>;
42
- /**
43
- * Encrypt file content for a recipient.
44
- * Returns encrypted buffer + key material for file attachment metadata.
45
- */
46
- export declare const encryptFileForRecipient: (content: string, recipientPublicKeyRaw: string) => Promise<{
47
- ciphertext: Buffer;
48
- iv: string;
49
- encryptedKey: string;
50
- }>;
51
28
  /**
52
29
  * Encrypt file content for self (all own devices).
53
30
  */
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAwD5E,CAAC;AA4BF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAiE5E,CAAC;AAqCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
package/dist/crypto.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
8
+ exports.encryptFileForSelf = exports.encryptPushBodyForSelf = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
9
9
  /// <reference lib="dom" />
10
10
  const fs_1 = require("fs");
11
11
  const path_1 = require("path");
@@ -111,15 +111,22 @@ const initCrypto = (apiKey, baseUrl) => {
111
111
  const stored = loadStoredKeys();
112
112
  // Try server sync if API key available
113
113
  if (apiKey) {
114
- const serverKeys = await fetchServerKeys(apiKey, baseUrl);
115
- if (serverKeys) {
116
- if (!stored || stored.publicKey !== serverKeys.publicKey) {
117
- storeKeys(serverKeys);
114
+ const serverResult = await fetchServerKeys(apiKey, baseUrl);
115
+ // Server says encryption disabled — skip crypto init
116
+ if (serverResult && !serverResult.encryptionEnabled) {
117
+ cachedKeyPair = null;
118
+ cachedExportedPublicKey = null;
119
+ cachedOwnPublicKey = null;
120
+ return '';
121
+ }
122
+ if (serverResult?.keys) {
123
+ if (!stored || stored.publicKey !== serverResult.keys.publicKey) {
124
+ storeKeys(serverResult.keys);
118
125
  }
119
- cachedKeyPair = await importKeyPair(serverKeys);
120
- cachedExportedPublicKey = serverKeys.publicKey;
126
+ cachedKeyPair = await importKeyPair(serverResult.keys);
127
+ cachedExportedPublicKey = serverResult.keys.publicKey;
121
128
  cachedOwnPublicKey = cachedKeyPair.publicKey;
122
- return serverKeys.publicKey;
129
+ return serverResult.keys.publicKey;
123
130
  }
124
131
  if (stored) {
125
132
  await uploadServerKeys(stored, apiKey, baseUrl);
@@ -158,7 +165,6 @@ const initCrypto = (apiKey, baseUrl) => {
158
165
  return initPromise;
159
166
  };
160
167
  exports.initCrypto = initCrypto;
161
- // ─── Server key sync helpers ───
162
168
  const fetchServerKeys = async (apiKey, baseUrl) => {
163
169
  try {
164
170
  const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
@@ -167,7 +173,11 @@ const fetchServerKeys = async (apiKey, baseUrl) => {
167
173
  return null;
168
174
  const json = await res.json();
169
175
  const keys = json.data?.encryptionKeys;
170
- return keys?.publicKey && keys?.privateKey ? keys : null;
176
+ const encryptionEnabled = json.data?.encryptionEnabled ?? (keys ? true : false);
177
+ return {
178
+ keys: keys?.publicKey && keys?.privateKey ? keys : null,
179
+ encryptionEnabled,
180
+ };
171
181
  }
172
182
  catch {
173
183
  return null;
@@ -188,23 +198,6 @@ const getKeyPair = () => cachedKeyPair;
188
198
  exports.getKeyPair = getKeyPair;
189
199
  const getPublicKey = () => cachedExportedPublicKey;
190
200
  exports.getPublicKey = getPublicKey;
191
- /**
192
- * Encrypt push body for a recipient.
193
- * Returns fields ready to merge into the sendPush payload.
194
- */
195
- const encryptPushBody = async (input, recipientPublicKeyRaw) => {
196
- if (!cachedKeyPair || !cachedExportedPublicKey)
197
- throw new Error('Crypto not initialized');
198
- const recipientKey = await importPublicKey(recipientPublicKeyRaw);
199
- const payload = await encrypt(JSON.stringify({ title: input.title, body: input.body, url: input.url }), cachedKeyPair.privateKey, recipientKey);
200
- return {
201
- body: JSON.stringify({ ciphertext: payload.ciphertext, iv: payload.iv }),
202
- encryptedKey: JSON.stringify({ encryptedKey: payload.encryptedKey, keyIv: payload.keyIv }),
203
- senderPublicKey: cachedExportedPublicKey,
204
- isEncrypted: true,
205
- };
206
- };
207
- exports.encryptPushBody = encryptPushBody;
208
201
  /**
209
202
  * Encrypt push body for self (all own devices).
210
203
  */
@@ -220,22 +213,6 @@ const encryptPushBodyForSelf = async (input) => {
220
213
  };
221
214
  };
222
215
  exports.encryptPushBodyForSelf = encryptPushBodyForSelf;
223
- /**
224
- * Encrypt file content for a recipient.
225
- * Returns encrypted buffer + key material for file attachment metadata.
226
- */
227
- const encryptFileForRecipient = async (content, recipientPublicKeyRaw) => {
228
- if (!cachedKeyPair)
229
- throw new Error('Crypto not initialized');
230
- const recipientKey = await importPublicKey(recipientPublicKeyRaw);
231
- const result = await encryptFileContent(content, cachedKeyPair.privateKey, recipientKey);
232
- return {
233
- ciphertext: result.ciphertext,
234
- iv: result.iv,
235
- encryptedKey: JSON.stringify({ encryptedKey: result.encryptedKey, keyIv: result.keyIv }),
236
- };
237
- };
238
- exports.encryptFileForRecipient = encryptFileForRecipient;
239
216
  /**
240
217
  * Encrypt file content for self (all own devices).
241
218
  */
@@ -31,7 +31,7 @@ const registerDismissAllTool = (server, client) => {
31
31
  description: 'Dismiss all push notifications at once. Clears the entire notification feed.',
32
32
  annotations: {
33
33
  readOnlyHint: false,
34
- destructiveHint: false,
34
+ destructiveHint: true,
35
35
  idempotentHint: true,
36
36
  openWorldHint: true,
37
37
  },
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/tools/list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,SAwCxE,CAAC"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/tools/list.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,SAyCxE,CAAC"}
@@ -9,6 +9,7 @@ const registerListTool = (server, client) => {
9
9
  annotations: {
10
10
  readOnlyHint: true,
11
11
  destructiveHint: false,
12
+ idempotentHint: true,
12
13
  openWorldHint: true,
13
14
  },
14
15
  inputSchema: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
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",