@zeph-to/mcp-server 1.9.2 → 1.10.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
@@ -52,6 +52,7 @@ Add to `~/.claude/settings.json`:
52
52
  | `ZEPH_HOOK_ID` | No | Hook ID (optional — only needed for interactive tools like `zeph_ask`/`zeph_prompt`/`zeph_input`) |
53
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
+ | `ZEPH_DISABLE_SESSION_CACHE` | No | Set to `1`/`true` to skip writing the session-id handoff file under `~/.cache/zeph/`. Useful for read-only filesystems, ephemeral CI runners, or sandboxed envs that audit filesystem writes. The plugin's stop hook still works without it (transcript-path UUID extraction is the primary path; the cache is a fallback for older Claude Code versions). |
55
56
 
56
57
  \* 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
58
 
@@ -266,9 +267,11 @@ The API key needs the following scopes:
266
267
 
267
268
  Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
268
269
 
269
- ## E2E Encryption
270
+ ## Encryption
270
271
 
271
- Push notifications are encrypted end-to-end by default (AES-256-GCM + ECDH P-256). Keys are synced with the server on startup. When encryption is disabled in the Zeph app, the server sends plaintext. No configuration needed — encryption is automatic.
272
+ Push bodies are encrypted with AES-256-GCM. The wrapping key is derived via ECDH P-256 and synced across your own devices on first server startup so every device can read the same push. Toggle encryption in the Zeph app (Settings → Encryption); when disabled, the server sends plaintext. No configuration needed.
273
+
274
+ **Threat model honesty:** keys are persisted on the Zeph backend to enable cross-device sync, so this is *device-shared* encryption — not true end-to-end. It protects push contents from passive network observers and from a leaked database snapshot taken without the key store, but it does **not** protect against the Zeph backend itself (it has the keys it serves to your devices). A true E2E mode (per-device keypairs, server stores only public keys, no key escrow) is on the roadmap.
272
275
 
273
276
  ## License
274
277
 
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAwCD,eAAO,MAAM,UAAU,QAAO,eA0B7B,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAgGD,eAAO,MAAM,UAAU,QAAO,eAqB7B,CAAC"}
package/dist/config.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadConfig = void 0;
4
4
  const fs_1 = require("fs");
5
+ const os_1 = require("os");
5
6
  const crypto_1 = require("crypto");
6
7
  const child_process_1 = require("child_process");
7
8
  const path_1 = require("path");
@@ -12,7 +13,7 @@ const resolvedEnv = (key) => {
12
13
  };
13
14
  const loadFileConfig = () => {
14
15
  try {
15
- const configPath = (0, path_1.join)(process.env.HOME ?? '~', '.zeph', 'config.json');
16
+ const configPath = (0, path_1.join)((0, os_1.homedir)(), '.zeph', 'config.json');
16
17
  return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf-8'));
17
18
  }
18
19
  catch {
@@ -24,18 +25,77 @@ const detectClaudeSessionId = () => {
24
25
  try {
25
26
  const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
26
27
  const projectHash = projectDir.replace(/\//g, '-');
27
- const sessionsDir = (0, path_1.join)(process.env.HOME ?? '~', '.claude', 'projects', projectHash);
28
- const entries = (0, fs_1.readdirSync)(sessionsDir)
29
- .filter((name) => /^[0-9a-f]{8}-/.test(name))
30
- .filter((name) => (0, fs_1.statSync)((0, path_1.join)(sessionsDir, name)).isDirectory())
31
- .map((name) => ({ name, mtime: (0, fs_1.statSync)((0, path_1.join)(sessionsDir, name)).mtimeMs }))
32
- .sort((a, b) => b.mtime - a.mtime);
33
- return entries[0]?.name;
28
+ const sessionsDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects', projectHash);
29
+ let latest;
30
+ for (const name of (0, fs_1.readdirSync)(sessionsDir)) {
31
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(name))
32
+ continue;
33
+ const fullPath = (0, path_1.join)(sessionsDir, name);
34
+ const stat = (0, fs_1.statSync)(fullPath);
35
+ if (!stat.isDirectory())
36
+ continue;
37
+ if (!latest || stat.mtimeMs > latest.mtime) {
38
+ latest = { name, mtime: stat.mtimeMs };
39
+ }
40
+ }
41
+ return latest?.name;
34
42
  }
35
43
  catch {
36
44
  return undefined;
37
45
  }
38
46
  };
47
+ /**
48
+ * Truthy-env helper — accepts "1", "true", "yes", "on" (case-insensitive).
49
+ */
50
+ const envIsTrue = (key) => {
51
+ const v = process.env[key];
52
+ if (!v)
53
+ return false;
54
+ return /^(1|true|yes|on)$/i.test(v.trim());
55
+ };
56
+ /**
57
+ * Write the resolved session id to a per-user cache file so shell hooks
58
+ * (e.g. plugin/hooks/zeph-stop.sh) can pick it up when their own transcript
59
+ * scrape misses. The previous location was /tmp/zeph-session-<hash>, which
60
+ * on multi-user machines exposed a symlink race — predictable filename,
61
+ * world-writable directory. Living under ~/.cache (or $XDG_CACHE_HOME)
62
+ * removes that — only the owning user can write there. The file is also
63
+ * opened with O_NOFOLLOW so a pre-existing symlink can never redirect us
64
+ * to /etc/passwd or similar.
65
+ *
66
+ * Users can opt out entirely by setting ZEPH_DISABLE_SESSION_CACHE=1.
67
+ * Useful for:
68
+ * - Read-only filesystems (some container runtimes)
69
+ * - CI runners where ~/.cache isn't persisted anyway, so the write is
70
+ * pure overhead
71
+ * - Sandboxed environments where extra filesystem writes trigger audit
72
+ * noise
73
+ * The shell hook still works without the cache — it primarily extracts
74
+ * the session id from the transcript_path UUID; the cache is just a
75
+ * fallback for older Claude Code versions.
76
+ */
77
+ const writeSessionCache = (sessionId, projectDir) => {
78
+ if (envIsTrue('ZEPH_DISABLE_SESSION_CACHE'))
79
+ return;
80
+ try {
81
+ const hash = (0, child_process_1.execFileSync)('cksum', { input: projectDir, encoding: 'utf-8' }).split(' ')[0];
82
+ const cacheDir = (0, path_1.join)(process.env.XDG_CACHE_HOME ?? (0, path_1.join)((0, os_1.homedir)(), '.cache'), 'zeph');
83
+ (0, fs_1.mkdirSync)(cacheDir, { recursive: true, mode: 0o700 });
84
+ const cachePath = (0, path_1.join)(cacheDir, `session-${hash}`);
85
+ const flags = fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW;
86
+ const fd = (0, fs_1.openSync)(cachePath, flags, 0o600);
87
+ try {
88
+ (0, fs_1.writeSync)(fd, sessionId);
89
+ }
90
+ finally {
91
+ (0, fs_1.closeSync)(fd);
92
+ }
93
+ }
94
+ catch {
95
+ /* best-effort — hook stop script also extracts session id from
96
+ * the transcript path, so failing here is non-fatal. */
97
+ }
98
+ };
39
99
  const loadConfig = () => {
40
100
  const fileConfig = loadFileConfig();
41
101
  const apiKey = resolvedEnv('ZEPH_API_KEY') ?? fileConfig.apiKey;
@@ -43,13 +103,8 @@ const loadConfig = () => {
43
103
  throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/hook-sdk install" or set ZEPH_API_KEY env var.');
44
104
  }
45
105
  const sessionId = resolvedEnv('ZEPH_SESSION_ID') ?? detectClaudeSessionId() ?? `sess_${(0, crypto_1.randomBytes)(12).toString('base64url')}`;
46
- // Write sessionId to tmp file so shell hooks (zeph-stop.sh) can read it
47
- try {
48
- const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
49
- const hash = (0, child_process_1.execFileSync)('cksum', { input: projectDir, encoding: 'utf-8' }).split(' ')[0];
50
- (0, fs_1.writeFileSync)(`/tmp/zeph-session-${hash}`, sessionId);
51
- }
52
- catch { /* best-effort */ }
106
+ const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
107
+ writeSessionCache(sessionId, projectDir);
53
108
  return {
54
109
  apiKey,
55
110
  baseUrl: (resolvedEnv('ZEPH_BASE_URL') ?? fileConfig.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ''),
package/dist/crypto.d.ts CHANGED
@@ -1,13 +1,39 @@
1
1
  /**
2
- * E2E encryption for MCP server — self-contained ECDH P-256 + AES-256-GCM
3
- * Mirrors @zeph/crypto API but bundled inline (no external dependency).
4
- * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
2
+ * Device-shared encryption for MCP server — self-contained ECDH P-256 +
3
+ * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
4
+ * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
5
+ *
6
+ * Threat model honesty (do not call this "E2E" without a footnote):
7
+ *
8
+ * The Zeph backend persists the per-user private key in plaintext so it
9
+ * can be synced down to a fresh device (fetchServerKeys / uploadServerKeys
10
+ * below). That means the backend can decrypt any push body — this is NOT
11
+ * end-to-end in the standard sense. What it gives you is:
12
+ * • Protection against passive network observers
13
+ * • Protection against a leaked DB snapshot taken without the key store
14
+ * • Cross-device readability (all your devices share one keypair)
15
+ * What it does NOT give you:
16
+ * • Protection against the Zeph backend itself
17
+ * • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
18
+ * ECDH(self, self), which collapses to a static derived key. A single
19
+ * device compromise (since all your devices share the same keypair)
20
+ * lets the attacker decrypt every past push for which they have the
21
+ * ciphertext. The per-message AES key is random, but its wrap key is
22
+ * static, so wrapped keys are decryptable forever.
23
+ *
24
+ * True E2E would require a per-device keypair (server stores only public
25
+ * keys; senders wrap the message key once per recipient device public
26
+ * key). That refactor is on the roadmap; until then, treat push bodies as
27
+ * sensitive-but-not-secret.
5
28
  */
6
29
  /**
7
30
  * Initialize crypto: sync keys with server, then fallback to local/generate.
8
31
  * Server is source of truth for per-user key pair.
9
32
  * Safe to call concurrently — deduplicates to single init.
10
33
  * Returns the exported public key (Base64 SPKI).
34
+ *
35
+ * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
36
+ * caller in a dev environment would silently upload keys to prod.
11
37
  */
12
38
  export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
13
39
  export declare const getKeyPair: () => CryptoKeyPair | null;
@@ -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,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"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA2JH;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAyE5E,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
@@ -1,13 +1,37 @@
1
1
  "use strict";
2
2
  /**
3
- * E2E encryption for MCP server — self-contained ECDH P-256 + AES-256-GCM
4
- * Mirrors @zeph/crypto API but bundled inline (no external dependency).
5
- * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
3
+ * Device-shared encryption for MCP server — self-contained ECDH P-256 +
4
+ * AES-256-GCM. Mirrors @zeph/crypto API but bundled inline (no external
5
+ * dependency). Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
6
+ *
7
+ * Threat model honesty (do not call this "E2E" without a footnote):
8
+ *
9
+ * The Zeph backend persists the per-user private key in plaintext so it
10
+ * can be synced down to a fresh device (fetchServerKeys / uploadServerKeys
11
+ * below). That means the backend can decrypt any push body — this is NOT
12
+ * end-to-end in the standard sense. What it gives you is:
13
+ * • Protection against passive network observers
14
+ * • Protection against a leaked DB snapshot taken without the key store
15
+ * • Cross-device readability (all your devices share one keypair)
16
+ * What it does NOT give you:
17
+ * • Protection against the Zeph backend itself
18
+ * • Forward secrecy — encryptPushBodyForSelf / encryptFileForSelf do
19
+ * ECDH(self, self), which collapses to a static derived key. A single
20
+ * device compromise (since all your devices share the same keypair)
21
+ * lets the attacker decrypt every past push for which they have the
22
+ * ciphertext. The per-message AES key is random, but its wrap key is
23
+ * static, so wrapped keys are decryptable forever.
24
+ *
25
+ * True E2E would require a per-device keypair (server stores only public
26
+ * keys; senders wrap the message key once per recipient device public
27
+ * key). That refactor is on the roadmap; until then, treat push bodies as
28
+ * sensitive-but-not-secret.
6
29
  */
7
30
  Object.defineProperty(exports, "__esModule", { value: true });
8
31
  exports.encryptFileForSelf = exports.encryptPushBodyForSelf = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
9
32
  /// <reference lib="dom" />
10
33
  const fs_1 = require("fs");
34
+ const os_1 = require("os");
11
35
  const path_1 = require("path");
12
36
  // ─── Base64 helpers ───
13
37
  const toBase64 = (buffer) => {
@@ -79,7 +103,7 @@ const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey)
79
103
  };
80
104
  };
81
105
  // ─── Key persistence (~/.config/zeph/keys.json) ───
82
- const KEYS_DIR = (0, path_1.join)(process.env.HOME ?? '~', '.config', 'zeph');
106
+ const KEYS_DIR = (0, path_1.join)(process.env.XDG_CONFIG_HOME ?? (0, path_1.join)((0, os_1.homedir)(), '.config'), 'zeph');
83
107
  const KEYS_PATH = (0, path_1.join)(KEYS_DIR, 'keys.json');
84
108
  const loadStoredKeys = () => {
85
109
  try {
@@ -103,15 +127,24 @@ let initPromise = null;
103
127
  * Server is source of truth for per-user key pair.
104
128
  * Safe to call concurrently — deduplicates to single init.
105
129
  * Returns the exported public key (Base64 SPKI).
130
+ *
131
+ * NOTE: when `apiKey` is provided, `baseUrl` is required — otherwise a
132
+ * caller in a dev environment would silently upload keys to prod.
106
133
  */
107
134
  const initCrypto = (apiKey, baseUrl) => {
135
+ if (apiKey && !baseUrl) {
136
+ return Promise.reject(new Error('initCrypto: baseUrl is required when apiKey is provided. ' +
137
+ 'Pass the resolved config.baseUrl to avoid silently syncing dev keys to prod.'));
138
+ }
108
139
  if (initPromise)
109
140
  return initPromise;
141
+ // The check above guarantees baseUrl is defined when apiKey is — narrow once.
142
+ const baseUrlRequired = apiKey ? baseUrl : baseUrl;
110
143
  initPromise = (async () => {
111
144
  const stored = loadStoredKeys();
112
145
  // Try server sync if API key available
113
146
  if (apiKey) {
114
- const serverResult = await fetchServerKeys(apiKey, baseUrl);
147
+ const serverResult = await fetchServerKeys(apiKey, baseUrlRequired);
115
148
  // Server says encryption disabled — skip crypto init
116
149
  if (serverResult && !serverResult.encryptionEnabled) {
117
150
  cachedKeyPair = null;
@@ -129,7 +162,7 @@ const initCrypto = (apiKey, baseUrl) => {
129
162
  return serverResult.keys.publicKey;
130
163
  }
131
164
  if (stored) {
132
- await uploadServerKeys(stored, apiKey, baseUrl);
165
+ await uploadServerKeys(stored, apiKey, baseUrlRequired);
133
166
  cachedKeyPair = await importKeyPair(stored);
134
167
  cachedExportedPublicKey = stored.publicKey;
135
168
  cachedOwnPublicKey = cachedKeyPair.publicKey;
@@ -138,7 +171,7 @@ const initCrypto = (apiKey, baseUrl) => {
138
171
  const keyPair = await generateKeyPair();
139
172
  const exported = await exportKeyPair(keyPair);
140
173
  storeKeys(exported);
141
- await uploadServerKeys(exported, apiKey, baseUrl);
174
+ await uploadServerKeys(exported, apiKey, baseUrlRequired);
142
175
  cachedKeyPair = keyPair;
143
176
  cachedExportedPublicKey = exported.publicKey;
144
177
  cachedOwnPublicKey = keyPair.publicKey;
@@ -167,7 +200,7 @@ const initCrypto = (apiKey, baseUrl) => {
167
200
  exports.initCrypto = initCrypto;
168
201
  const fetchServerKeys = async (apiKey, baseUrl) => {
169
202
  try {
170
- const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
203
+ const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
171
204
  const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
172
205
  if (!res.ok)
173
206
  return null;
@@ -185,7 +218,7 @@ const fetchServerKeys = async (apiKey, baseUrl) => {
185
218
  };
186
219
  const uploadServerKeys = async (keys, apiKey, baseUrl) => {
187
220
  try {
188
- const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
221
+ const url = `${baseUrl.replace(/\/$/, '')}/users/me/keys`;
189
222
  await fetch(url, {
190
223
  method: 'PUT',
191
224
  headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
package/dist/mime.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared MIME-type inference for file/notify/ask payloads.
3
+ *
4
+ * Kept in one place so different tools don't produce inconsistent types
5
+ * for the same extension (e.g. `.csv` ending up as text/plain in one
6
+ * code path and text/csv in another).
7
+ */
8
+ export declare const inferMimeType: (fileName: string) => string;
9
+ //# sourceMappingURL=mime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkBH,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MAGhD,CAAC"}
package/dist/mime.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * Shared MIME-type inference for file/notify/ask payloads.
4
+ *
5
+ * Kept in one place so different tools don't produce inconsistent types
6
+ * for the same extension (e.g. `.csv` ending up as text/plain in one
7
+ * code path and text/csv in another).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.inferMimeType = void 0;
11
+ const EXT_TO_MIME = {
12
+ txt: 'text/plain',
13
+ log: 'text/plain',
14
+ md: 'text/markdown',
15
+ json: 'application/json',
16
+ csv: 'text/csv',
17
+ html: 'text/html',
18
+ xml: 'text/xml',
19
+ yaml: 'text/yaml',
20
+ yml: 'text/yaml',
21
+ ts: 'text/typescript',
22
+ js: 'text/javascript',
23
+ py: 'text/x-python',
24
+ sh: 'text/x-shellscript',
25
+ };
26
+ const inferMimeType = (fileName) => {
27
+ const ext = fileName.split('.').pop()?.toLowerCase();
28
+ return EXT_TO_MIME[ext ?? ''] ?? 'text/plain';
29
+ };
30
+ exports.inferMimeType = inferMimeType;
@@ -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,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAmHhG,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,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAOpD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAmHhG,CAAC"}
package/dist/tools/ask.js CHANGED
@@ -5,13 +5,9 @@ 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 crypto_js_1 = require("../crypto.js");
8
+ const mime_js_1 = require("../mime.js");
8
9
  const BODY_FILE_THRESHOLD = 512;
9
10
  const PREVIEW_LENGTH = 200;
10
- const inferMimeType = (fileName) => {
11
- const ext = fileName.split('.').pop()?.toLowerCase();
12
- const map = { md: 'text/markdown', txt: 'text/plain', json: 'application/json' };
13
- return map[ext ?? ''] ?? 'text/plain';
14
- };
15
11
  const registerAskTool = (server, client, config) => {
16
12
  server.registerTool('zeph_ask', {
17
13
  description: '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. The user can either tap a button or type a response. Blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
@@ -60,7 +56,7 @@ const registerAskTool = (server, client, config) => {
60
56
  let files;
61
57
  if (isLongBody && body) {
62
58
  const fileName = 'response.md';
63
- const fileType = inferMimeType(fileName);
59
+ const fileType = (0, mime_js_1.inferMimeType)(fileName);
64
60
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
65
61
  let uploadContent = body;
66
62
  let uploadContentType = fileType;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAwBpD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2EjG,CAAC"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAKpD,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA2EjG,CAAC"}
@@ -4,25 +4,7 @@ exports.registerFileTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const crypto_js_1 = require("../crypto.js");
7
- const inferMimeType = (fileName) => {
8
- const ext = fileName.split('.').pop()?.toLowerCase();
9
- const map = {
10
- txt: 'text/plain',
11
- json: 'application/json',
12
- csv: 'text/csv',
13
- md: 'text/markdown',
14
- html: 'text/html',
15
- xml: 'text/xml',
16
- yaml: 'text/yaml',
17
- yml: 'text/yaml',
18
- log: 'text/plain',
19
- ts: 'text/typescript',
20
- js: 'text/javascript',
21
- py: 'text/x-python',
22
- sh: 'text/x-shellscript',
23
- };
24
- return map[ext ?? ''] ?? 'text/plain';
25
- };
7
+ const mime_js_1 = require("../mime.js");
26
8
  const registerFileTool = (server, client, config) => {
27
9
  server.registerTool('zeph_file', {
28
10
  description: 'Send a text file to the user\'s device. The content is uploaded and delivered as a file push. Use for logs, reports, code snippets, or any text content.',
@@ -40,7 +22,7 @@ const registerFileTool = (server, client, config) => {
40
22
  }, async ({ fileName, content, title, targetDeviceId }) => {
41
23
  try {
42
24
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
43
- let fileType = inferMimeType(fileName);
25
+ let fileType = (0, mime_js_1.inferMimeType)(fileName);
44
26
  const originalSize = new TextEncoder().encode(content).byteLength;
45
27
  // Step 1: Optionally encrypt file content
46
28
  let uploadContent = content;
@@ -69,7 +51,7 @@ const registerFileTool = (server, client, config) => {
69
51
  let pushPayload = {
70
52
  title: pushTitle,
71
53
  type: 'file',
72
- files: [{ fileKey: upload.data.fileKey, fileName, fileSize: originalSize, fileType: inferMimeType(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
54
+ files: [{ fileKey: upload.data.fileKey, fileName, fileSize: originalSize, fileType: (0, mime_js_1.inferMimeType)(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
73
55
  targetDeviceId: targetDeviceId ?? config.deviceId,
74
56
  sessionId: config.sessionId,
75
57
  };
@@ -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,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYpD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA6GnG,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,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAOpD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SA6GnG,CAAC"}
@@ -4,13 +4,9 @@ exports.registerNotifyTool = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const error_format_js_1 = require("../error-format.js");
6
6
  const crypto_js_1 = require("../crypto.js");
7
+ const mime_js_1 = require("../mime.js");
7
8
  const BODY_FILE_THRESHOLD = 512;
8
9
  const PREVIEW_LENGTH = 200;
9
- const inferMimeType = (fileName) => {
10
- const ext = fileName.split('.').pop()?.toLowerCase();
11
- const map = { md: 'text/markdown', txt: 'text/plain', json: 'application/json' };
12
- return map[ext ?? ''] ?? 'text/plain';
13
- };
14
10
  const registerNotifyTool = (server, client, config) => {
15
11
  server.registerTool('zeph_notify', {
16
12
  description: 'Send a one-way push notification to the user\'s devices. Use this to inform the user about task completion, errors, or status updates. Long bodies (>512B) are automatically uploaded as a file for full viewing.',
@@ -37,7 +33,7 @@ const registerNotifyTool = (server, client, config) => {
37
33
  const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
38
34
  if (isLongBody && body) {
39
35
  const fileName = 'response.md';
40
- const fileType = inferMimeType(fileName);
36
+ const fileType = (0, mime_js_1.inferMimeType)(fileName);
41
37
  const fileSize = bodyBytes;
42
38
  // Encrypt file content if keys available
43
39
  let uploadContent = body;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.9.2",
3
+ "version": "1.10.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",
@@ -8,7 +8,7 @@
8
8
  "zeph-mcp": "./dist/index.js"
9
9
  },
10
10
  "engines": {
11
- "node": ">=18.0.0"
11
+ "node": ">=18.17.0"
12
12
  },
13
13
  "files": [
14
14
  "dist",