@zeph-to/mcp-server 1.15.2 → 1.16.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 +11 -5
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +7 -1
- package/dist/index.js +1 -1
- package/dist/mime.d.ts.map +1 -1
- package/dist/mime.js +20 -0
- package/dist/tools/file.d.ts.map +1 -1
- package/dist/tools/file.js +62 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,15 +138,21 @@ priority: "normal"
|
|
|
138
138
|
|
|
139
139
|
### zeph_file
|
|
140
140
|
|
|
141
|
-
Send a
|
|
141
|
+
Send a file to the user's device. Either `filePath` (a file already on disk) or
|
|
142
|
+
`content` (text you generated) is required.
|
|
142
143
|
|
|
143
144
|
```
|
|
144
|
-
|
|
145
|
-
content: "{\"status\": \"ok\"}"
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
filePath: "/abs/path/screenshot.png" (images, PDFs, logs — anything on disk)
|
|
146
|
+
content: "{\"status\": \"ok\"}" (text only; requires fileName)
|
|
147
|
+
fileName: "report.json" (required with content; defaults to basename of filePath)
|
|
148
|
+
title: "Build Report" (optional, defaults to fileName)
|
|
149
|
+
targetDeviceId: "dev_..." (optional)
|
|
148
150
|
```
|
|
149
151
|
|
|
152
|
+
Images are delivered with their real mime type and render inline on the device.
|
|
153
|
+
Never base64 a binary file into `content` — pass `filePath` and the server reads
|
|
154
|
+
the bytes off disk.
|
|
155
|
+
|
|
150
156
|
Returns: `{ pushId: "...", fileKey: "...", fileSize: 42 }`
|
|
151
157
|
|
|
152
158
|
### zeph_session_rename
|
package/dist/crypto.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare const encryptPushBodyForSelf: (input: {
|
|
|
80
80
|
/**
|
|
81
81
|
* Encrypt file content for self (all own devices).
|
|
82
82
|
*/
|
|
83
|
-
export declare const encryptFileForSelf: (content: string) => Promise<{
|
|
83
|
+
export declare const encryptFileForSelf: (content: string | Buffer) => Promise<{
|
|
84
84
|
ciphertext: Buffer;
|
|
85
85
|
iv: string;
|
|
86
86
|
encryptedKey: string;
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAiKH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AAmCF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,QAAO,IAIhC,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;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,GAAG,MAAM,KACvB,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
|
@@ -78,7 +78,13 @@ const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey) => {
|
|
|
78
78
|
};
|
|
79
79
|
// ─── File encryption ───
|
|
80
80
|
const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey) => {
|
|
81
|
-
|
|
81
|
+
// Binary attachments arrive as a Buffer and must be encrypted byte for byte —
|
|
82
|
+
// running them through TextEncoder would UTF-8 mangle every non-ASCII byte.
|
|
83
|
+
// Both branches are views; subtle.encrypt honours byteOffset/byteLength, so a
|
|
84
|
+
// Buffer carved out of Node's pool encrypts only its own bytes.
|
|
85
|
+
const buffer = typeof content === 'string'
|
|
86
|
+
? new TextEncoder().encode(content)
|
|
87
|
+
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
82
88
|
const fileKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
83
89
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
84
90
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, fileKey, buffer);
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ const createServer = (config) => {
|
|
|
49
49
|
'- zeph_dismiss: Mark a push as read',
|
|
50
50
|
'- zeph_dismiss_all: Clear all notifications',
|
|
51
51
|
'- zeph_broadcast: Send to all subscribers of a channel',
|
|
52
|
-
'- zeph_file: Send a
|
|
52
|
+
'- zeph_file: Send a file — pass filePath for anything on disk (images, PDFs, logs), or content for generated text',
|
|
53
53
|
'- zeph_prompt: Ask user to choose from options (requires ZEPH_HOOK_ID)',
|
|
54
54
|
'- zeph_input: Request text input from user (requires ZEPH_HOOK_ID)',
|
|
55
55
|
'- zeph_ask: Ask user with buttons + text input combined (requires ZEPH_HOOK_ID). Prefer this over zeph_prompt/zeph_input when you need both options and free-text.',
|
package/dist/mime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsCH,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MAGhD,CAAC"}
|
package/dist/mime.js
CHANGED
|
@@ -22,6 +22,26 @@ const EXT_TO_MIME = {
|
|
|
22
22
|
js: 'text/javascript',
|
|
23
23
|
py: 'text/x-python',
|
|
24
24
|
sh: 'text/x-shellscript',
|
|
25
|
+
// Keep this image set in sync with IMAGE_EXTENSIONS in the Zeph client
|
|
26
|
+
// (`libs/shared/src/utils/file.ts`) — an extension the client calls an image
|
|
27
|
+
// but this map doesn't will be labelled text/plain and decoded as UTF-8 on
|
|
28
|
+
// open, which corrupts it. Separate repos, so nothing enforces the match.
|
|
29
|
+
png: 'image/png',
|
|
30
|
+
jpg: 'image/jpeg',
|
|
31
|
+
jpeg: 'image/jpeg',
|
|
32
|
+
gif: 'image/gif',
|
|
33
|
+
webp: 'image/webp',
|
|
34
|
+
heic: 'image/heic',
|
|
35
|
+
heif: 'image/heif',
|
|
36
|
+
bmp: 'image/bmp',
|
|
37
|
+
avif: 'image/avif',
|
|
38
|
+
ico: 'image/x-icon',
|
|
39
|
+
tiff: 'image/tiff',
|
|
40
|
+
tif: 'image/tiff',
|
|
41
|
+
// No svg entry on purpose: an SVG is a script-bearing document, and the
|
|
42
|
+
// clients open decrypted attachments from a same-origin blob URL. It falls
|
|
43
|
+
// through to text/plain, which the text viewer handles fine.
|
|
44
|
+
pdf: 'application/pdf',
|
|
25
45
|
};
|
|
26
46
|
const inferMimeType = (fileName) => {
|
|
27
47
|
const ext = fileName.split('.').pop()?.toLowerCase();
|
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":"
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/tools/file.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AA4BrE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAoHjG,CAAC"}
|
package/dist/tools/file.js
CHANGED
|
@@ -2,40 +2,95 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerFileTool = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
5
7
|
const config_js_1 = require("../config.js");
|
|
6
8
|
const error_format_js_1 = require("../error-format.js");
|
|
7
9
|
const crypto_js_1 = require("../crypto.js");
|
|
8
10
|
const e2e_fallback_js_1 = require("../e2e-fallback.js");
|
|
9
11
|
const mime_js_1 = require("../mime.js");
|
|
12
|
+
/**
|
|
13
|
+
* Collapse the two accepted input shapes into one payload.
|
|
14
|
+
*
|
|
15
|
+
* A `filePath` stays a Buffer end to end — images and PDFs only survive the
|
|
16
|
+
* trip as raw bytes, and reading from disk keeps a several-hundred-KB
|
|
17
|
+
* attachment out of the tool call itself (base64 in the arguments would
|
|
18
|
+
* inflate it by a third and burn the agent's context).
|
|
19
|
+
*/
|
|
20
|
+
const resolvePayload = async (args) => {
|
|
21
|
+
if (args.filePath) {
|
|
22
|
+
const body = await (0, promises_1.readFile)(args.filePath);
|
|
23
|
+
return { fileName: args.fileName ?? (0, node_path_1.basename)(args.filePath), body, size: body.byteLength };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
fileName: args.fileName,
|
|
27
|
+
body: args.content,
|
|
28
|
+
size: new TextEncoder().encode(args.content).byteLength,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
10
31
|
const registerFileTool = (server, client, config) => {
|
|
11
32
|
server.registerTool('zeph_file', {
|
|
12
|
-
description: 'Send a
|
|
33
|
+
description: 'Send a file to the user\'s device. Pass `filePath` to send a file that already exists on disk — images (png/jpg/gif/webp/heic), PDFs, logs, anything. Pass `content` instead to send text you generated. Images render inline on the device.',
|
|
13
34
|
annotations: {
|
|
14
35
|
readOnlyHint: false,
|
|
15
36
|
destructiveHint: false,
|
|
16
37
|
openWorldHint: true,
|
|
17
38
|
},
|
|
18
39
|
inputSchema: {
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
filePath: zod_1.z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Absolute path to a local file to send. Required for images, PDFs, and any other binary — never base64 a file into `content`.'),
|
|
44
|
+
content: zod_1.z.string().optional().describe('Text content of the file. Use only for text you generated; requires `fileName`.'),
|
|
45
|
+
fileName: zod_1.z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('File name with extension (e.g., "report.txt"). Required with `content`; defaults to the basename of `filePath`.'),
|
|
21
49
|
title: zod_1.z.string().optional().describe('Notification title (defaults to fileName)'),
|
|
22
50
|
targetDeviceId: zod_1.z.string().optional().describe('Target device ID. Omit to use configured default or send to all devices.'),
|
|
23
51
|
},
|
|
24
|
-
}, async ({ fileName, content, title, targetDeviceId }) => {
|
|
52
|
+
}, async ({ filePath, fileName: fileNameArg, content, title, targetDeviceId }) => {
|
|
53
|
+
if (!filePath && content === undefined) {
|
|
54
|
+
return (0, error_format_js_1.errorResult)({
|
|
55
|
+
error: 'INVALID_INPUT',
|
|
56
|
+
message: 'Either filePath or content is required',
|
|
57
|
+
suggestion: 'Pass filePath for a file on disk (images, PDFs), or content + fileName for generated text',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!filePath && !fileNameArg) {
|
|
61
|
+
return (0, error_format_js_1.errorResult)({
|
|
62
|
+
error: 'INVALID_INPUT',
|
|
63
|
+
message: 'fileName is required when sending content',
|
|
64
|
+
suggestion: 'Add fileName with an extension, e.g. "report.md"',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
let payload;
|
|
68
|
+
try {
|
|
69
|
+
// Resolved once, outside `send`, so the plaintext retry below doesn't
|
|
70
|
+
// read the file from disk a second time.
|
|
71
|
+
payload = await resolvePayload({ filePath, content, fileName: fileNameArg });
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return (0, error_format_js_1.errorResult)({
|
|
75
|
+
error: 'FILE_READ_FAILED',
|
|
76
|
+
message: err instanceof Error ? err.message : String(err),
|
|
77
|
+
suggestion: 'Check that filePath is an absolute path to a readable file',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const { fileName, body, size: originalSize } = payload;
|
|
25
81
|
// Runs a second time as plaintext if the server refuses E2E (Pro-only,
|
|
26
82
|
// ADR-0008). The retry re-uploads the file unencrypted, so the whole
|
|
27
83
|
// upload-then-send sequence has to sit inside this closure.
|
|
28
84
|
const send = async (canEncrypt) => {
|
|
29
85
|
let fileType = (0, mime_js_1.inferMimeType)(fileName);
|
|
30
|
-
const originalSize = new TextEncoder().encode(content).byteLength;
|
|
31
86
|
// Step 1: Optionally encrypt file content
|
|
32
|
-
let uploadContent =
|
|
87
|
+
let uploadContent = body;
|
|
33
88
|
let uploadSize = originalSize;
|
|
34
89
|
let fileIv;
|
|
35
90
|
let fileEncryptedKey;
|
|
36
91
|
if (canEncrypt) {
|
|
37
92
|
try {
|
|
38
|
-
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(
|
|
93
|
+
const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
|
|
39
94
|
uploadContent = encrypted.ciphertext;
|
|
40
95
|
uploadSize = encrypted.ciphertext.length;
|
|
41
96
|
fileType = 'application/octet-stream';
|
package/package.json
CHANGED