@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 +5 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +70 -15
- package/dist/crypto.d.ts +29 -3
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +42 -9
- package/dist/mime.d.ts +9 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +30 -0
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +2 -6
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +3 -21
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +2 -6
- package/package.json +2 -2
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
|
-
##
|
|
270
|
+
## Encryption
|
|
270
271
|
|
|
271
|
-
Push
|
|
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
|
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
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)(
|
|
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)(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
*
|
|
3
|
-
* Mirrors @zeph/crypto API but bundled inline (no external
|
|
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;
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
4
|
-
* Mirrors @zeph/crypto API but bundled inline (no external
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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 = `${
|
|
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 = `${
|
|
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;
|
package/dist/tools/ask.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
package/dist/tools/file.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/tools/file.js
CHANGED
|
@@ -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
|
|
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;
|
|
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"}
|
package/dist/tools/notify.js
CHANGED
|
@@ -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.
|
|
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.
|
|
11
|
+
"node": ">=18.17.0"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|