@zeph-to/mcp-server 1.4.1 → 1.6.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 +7 -3
- package/dist/api-client.d.ts +8 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +19 -9
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +40 -2
- package/package.json +1 -1
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
|
|
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
|
-
\*
|
|
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
|
|
|
@@ -266,6 +266,10 @@ The API key needs the following scopes:
|
|
|
266
266
|
|
|
267
267
|
Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
|
|
268
268
|
|
|
269
|
+
## E2E Encryption
|
|
270
|
+
|
|
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
|
+
|
|
269
273
|
## License
|
|
270
274
|
|
|
271
275
|
Apache-2.0
|
package/dist/api-client.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export declare class ZephApiClient {
|
|
|
40
40
|
fallback?: string;
|
|
41
41
|
metadata?: Record<string, unknown>;
|
|
42
42
|
hookType?: 'one-way' | 'interactive' | 'input' | 'combo';
|
|
43
|
+
files?: {
|
|
44
|
+
fileKey: string;
|
|
45
|
+
fileName: string;
|
|
46
|
+
fileSize: number;
|
|
47
|
+
fileType: string;
|
|
48
|
+
iv?: string;
|
|
49
|
+
encryptedKey?: string;
|
|
50
|
+
}[];
|
|
43
51
|
}): Promise<HookTriggerResponse>;
|
|
44
52
|
getHookEvent(hookId: string, eventId: string): Promise<HookEventResponse>;
|
|
45
53
|
listDevices(): Promise<DevicesResponse>;
|
package/dist/api-client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;CAKjC;AAKD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAK7B,QAAQ,CAAC,MAAM,EAAE;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,YAAY,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QACxH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GAAG,OAAO,CAAC,YAAY,CAAC;IAInB,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,QAAQ,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;CAKjC;AAKD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAK7B,QAAQ,CAAC,MAAM,EAAE;QACrB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,YAAY,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QACxH,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,GAAG,OAAO,CAAC,YAAY,CAAC;IAInB,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,QAAQ,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,OAAO,CAAC;QACzD,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,EAAE,CAAC,EAAE,MAAM,CAAC;YAAC,YAAY,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KACzH,GACA,OAAO,CAAC,mBAAmB,CAAC;IAIzB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIzE,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC;IAIvC,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQjF,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAIrD,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC;IAI5C,YAAY,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAIzC,aAAa,CAAC,MAAM,EAAE;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAI5B,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAc7E,OAAO;CAsCtB"}
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
120
|
-
cachedExportedPublicKey =
|
|
126
|
+
cachedKeyPair = await importKeyPair(serverResult.keys);
|
|
127
|
+
cachedExportedPublicKey = serverResult.keys.publicKey;
|
|
121
128
|
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
122
|
-
return
|
|
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
|
-
|
|
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;
|
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;AAYpD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAkHhG,CAAC"}
|
package/dist/tools/ask.js
CHANGED
|
@@ -4,6 +4,14 @@ exports.registerAskTool = void 0;
|
|
|
4
4
|
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
|
+
const crypto_js_1 = require("../crypto.js");
|
|
8
|
+
const BODY_FILE_THRESHOLD = 512;
|
|
9
|
+
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
|
+
};
|
|
7
15
|
const registerAskTool = (server, client, config) => {
|
|
8
16
|
server.registerTool('zeph_ask', {
|
|
9
17
|
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.',
|
|
@@ -46,14 +54,44 @@ const registerAskTool = (server, client, config) => {
|
|
|
46
54
|
if (!config.hookId)
|
|
47
55
|
return (0, error_format_js_1.hookNotConfiguredError)();
|
|
48
56
|
try {
|
|
57
|
+
const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
|
|
58
|
+
const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
|
|
59
|
+
let triggerBody = body;
|
|
60
|
+
let files;
|
|
61
|
+
if (isLongBody && body) {
|
|
62
|
+
const fileName = 'response.md';
|
|
63
|
+
const fileType = inferMimeType(fileName);
|
|
64
|
+
const canEncrypt = !!(0, crypto_js_1.getKeyPair)() && !!(0, crypto_js_1.getPublicKey)();
|
|
65
|
+
let uploadContent = body;
|
|
66
|
+
let uploadContentType = fileType;
|
|
67
|
+
let fileIv;
|
|
68
|
+
let fileEncryptedKey;
|
|
69
|
+
if (canEncrypt) {
|
|
70
|
+
try {
|
|
71
|
+
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
|
|
72
|
+
uploadContent = encrypted.ciphertext;
|
|
73
|
+
uploadContentType = 'application/octet-stream';
|
|
74
|
+
fileIv = encrypted.iv;
|
|
75
|
+
fileEncryptedKey = encrypted.encryptedKey;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
console.error('[Crypto] File encryption failed, sending plaintext:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const upload = await client.requestUpload({ fileName, fileType: uploadContentType, fileSize: typeof uploadContent === 'string' ? bodyBytes : uploadContent.length });
|
|
82
|
+
await client.uploadToS3(upload.data.uploadUrl, uploadContent, uploadContentType);
|
|
83
|
+
triggerBody = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
|
|
84
|
+
files = [{ fileKey: upload.data.fileKey, fileName, fileSize: bodyBytes, fileType, iv: fileIv, encryptedKey: fileEncryptedKey }];
|
|
85
|
+
}
|
|
49
86
|
const trigger = await client.triggerHook(config.hookId, {
|
|
50
87
|
title,
|
|
51
|
-
body,
|
|
88
|
+
body: triggerBody,
|
|
52
89
|
actions,
|
|
53
90
|
timeout,
|
|
54
91
|
fallback,
|
|
55
92
|
hookType: 'combo',
|
|
56
|
-
metadata: { placeholder, inputType },
|
|
93
|
+
metadata: { placeholder, inputType, files },
|
|
94
|
+
files,
|
|
57
95
|
});
|
|
58
96
|
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx);
|
|
59
97
|
if (!event) {
|