@zeph-to/mcp-server 1.11.1 → 1.11.3
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 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +7 -2
- package/dist/config.js +1 -1
- package/dist/crypto.d.ts +20 -5
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +79 -80
- package/dist/poll.d.ts.map +1 -1
- package/dist/poll.js +5 -0
- package/dist/sanitize.d.ts.map +1 -1
- package/dist/sanitize.js +9 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @zeph-to/mcp-server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@zeph-to/mcp-server)
|
|
4
|
+
[](https://www.npmjs.com/package/@zeph-to/mcp-server)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
3
8
|
Zeph MCP server for AI agents. Send notifications, copy to clipboard, request confirmations, and collect text input from users across their devices — all via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
4
9
|
|
|
5
10
|
## Setup
|
|
@@ -7,7 +12,7 @@ Zeph MCP server for AI agents. Send notifications, copy to clipboard, request co
|
|
|
7
12
|
The easiest way to set up for all agents at once:
|
|
8
13
|
|
|
9
14
|
```bash
|
|
10
|
-
npx @zeph-to/
|
|
15
|
+
npx @zeph-to/cli install
|
|
11
16
|
```
|
|
12
17
|
|
|
13
18
|
This saves credentials to `~/.zeph/config.json` and configures your agents automatically. The MCP server reads from this file — no env vars needed.
|
|
@@ -54,7 +59,7 @@ Add to `~/.claude/settings.json`:
|
|
|
54
59
|
| `ZEPH_BASE_URL` | No | API base URL (default: `https://api.zeph.to/v1`) |
|
|
55
60
|
| `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). |
|
|
56
61
|
|
|
57
|
-
\* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `npx @zeph-to/
|
|
62
|
+
\* If env vars are not set, the server reads from `~/.zeph/config.json` (created by `npx @zeph-to/cli install`). Unresolved `${...}` interpolations are also treated as unset.
|
|
58
63
|
|
|
59
64
|
## Tools
|
|
60
65
|
|
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":"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
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
package/dist/api-client.js
CHANGED
|
@@ -91,12 +91,17 @@ class ZephApiClient {
|
|
|
91
91
|
throw err;
|
|
92
92
|
}
|
|
93
93
|
if (!response.ok) {
|
|
94
|
+
// Read the body as text once, then try to parse it. A non-JSON
|
|
95
|
+
// error (HTML proxy page, gateway error) would otherwise be swallowed
|
|
96
|
+
// silently — surface a truncated snippet so failures are debuggable.
|
|
97
|
+
const raw = await response.text().catch(() => '');
|
|
94
98
|
let errorBody = null;
|
|
95
99
|
try {
|
|
96
|
-
errorBody =
|
|
100
|
+
errorBody = raw ? JSON.parse(raw) : null;
|
|
97
101
|
}
|
|
98
102
|
catch {
|
|
99
|
-
|
|
103
|
+
if (raw)
|
|
104
|
+
console.error(`[api] ${response.status} non-JSON body: ${raw.slice(0, 200)}`);
|
|
100
105
|
}
|
|
101
106
|
const message = errorBody?.error?.message ?? `Request failed with status ${response.status}`;
|
|
102
107
|
const code = errorBody?.error?.code ?? 'UNKNOWN_ERROR';
|
package/dist/config.js
CHANGED
|
@@ -123,7 +123,7 @@ const loadConfig = () => {
|
|
|
123
123
|
const fileConfig = loadFileConfig();
|
|
124
124
|
const apiKey = resolvedEnv('ZEPH_API_KEY') ?? fileConfig.apiKey;
|
|
125
125
|
if (!apiKey) {
|
|
126
|
-
throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/
|
|
126
|
+
throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/cli install" or set ZEPH_API_KEY env var.');
|
|
127
127
|
}
|
|
128
128
|
const sessionId = resolvedEnv('ZEPH_SESSION_ID') ?? detectClaudeSessionId() ?? `sess_${(0, crypto_1.randomBytes)(12).toString('base64url')}`;
|
|
129
129
|
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
package/dist/crypto.d.ts
CHANGED
|
@@ -27,13 +27,28 @@
|
|
|
27
27
|
* sensitive-but-not-secret.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
|
-
* Initialize crypto
|
|
31
|
-
*
|
|
30
|
+
* Initialize crypto.
|
|
31
|
+
*
|
|
32
|
+
* The MCP server is a CONSUMER of encryption keys, not a generator. Keys
|
|
33
|
+
* are created in the Zeph app where the user explicitly opts in (Settings
|
|
34
|
+
* → Encryption). This function only imports keys that the server already
|
|
35
|
+
* has, and only when the server confirms encryption is enabled.
|
|
36
|
+
*
|
|
37
|
+
* Any other state — server says disabled, server has no keys, server is
|
|
38
|
+
* unreachable — leaves encryption OFF (cache empty, no fallback). A
|
|
39
|
+
* previous version generated and uploaded a fresh keypair on the "no keys
|
|
40
|
+
* anywhere" path; combined with a transient fetch failure, that silently
|
|
41
|
+
* turned encryption on without user consent and locked the account into
|
|
42
|
+
* an "encryption enabled" state on the server.
|
|
43
|
+
*
|
|
44
|
+
* Opt-out: `ZEPH_DISABLE_ENCRYPTION=1` forces crypto off regardless of
|
|
45
|
+
* server state — useful while cleaning up legacy state or for users who
|
|
46
|
+
* never want encryption.
|
|
47
|
+
*
|
|
32
48
|
* Safe to call concurrently — deduplicates to single init.
|
|
33
|
-
* Returns the exported public key
|
|
49
|
+
* Returns the exported public key when encryption is active, '' otherwise.
|
|
34
50
|
*
|
|
35
|
-
* NOTE: when `apiKey` is provided, `baseUrl` is required
|
|
36
|
-
* caller in a dev environment would silently upload keys to prod.
|
|
51
|
+
* NOTE: when `apiKey` is provided, `baseUrl` is required.
|
|
37
52
|
*/
|
|
38
53
|
export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
|
|
39
54
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAoJH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CAsE5E,CAAC;AA8BF,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
|
@@ -34,32 +34,17 @@ const fs_1 = require("fs");
|
|
|
34
34
|
const os_1 = require("os");
|
|
35
35
|
const path_1 = require("path");
|
|
36
36
|
// ─── Base64 helpers ───
|
|
37
|
-
const toBase64 = (buffer) =>
|
|
38
|
-
const bytes = new Uint8Array(buffer);
|
|
39
|
-
let binary = '';
|
|
40
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
41
|
-
binary += String.fromCharCode(bytes[i]);
|
|
42
|
-
}
|
|
43
|
-
return btoa(binary);
|
|
44
|
-
};
|
|
37
|
+
const toBase64 = (buffer) => Buffer.from(buffer).toString('base64');
|
|
45
38
|
const fromBase64 = (base64) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
return bytes.buffer;
|
|
39
|
+
const buf = Buffer.from(base64, 'base64');
|
|
40
|
+
// Slice to the exact byte range — Buffer may share a larger pooled
|
|
41
|
+
// ArrayBuffer, so `.buffer` alone could expose unrelated memory.
|
|
42
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
52
43
|
};
|
|
53
44
|
// ─── ECDH key management ───
|
|
54
45
|
const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const [publicRaw, privateRaw] = await Promise.all([
|
|
58
|
-
crypto.subtle.exportKey('spki', keyPair.publicKey),
|
|
59
|
-
crypto.subtle.exportKey('pkcs8', keyPair.privateKey),
|
|
60
|
-
]);
|
|
61
|
-
return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
|
|
62
|
-
};
|
|
46
|
+
// generateKeyPair / exportKeyPair were removed in fix/no-auto-encryption.
|
|
47
|
+
// This module imports keys only; it never creates or exports them.
|
|
63
48
|
const importPublicKey = async (base64) => crypto.subtle.importKey('spki', fromBase64(base64), ECDH_PARAMS, true, []);
|
|
64
49
|
const importPrivateKey = async (base64) => crypto.subtle.importKey('pkcs8', fromBase64(base64), ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
|
|
65
50
|
const importKeyPair = async (exported) => {
|
|
@@ -117,80 +102,102 @@ const storeKeys = (exported) => {
|
|
|
117
102
|
(0, fs_1.mkdirSync)(KEYS_DIR, { recursive: true, mode: 0o700 });
|
|
118
103
|
(0, fs_1.writeFileSync)(KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
|
|
119
104
|
};
|
|
105
|
+
const deleteStoredKeys = () => {
|
|
106
|
+
try {
|
|
107
|
+
(0, fs_1.unlinkSync)(KEYS_PATH);
|
|
108
|
+
}
|
|
109
|
+
catch { /* not present — fine */ }
|
|
110
|
+
};
|
|
111
|
+
const envIsTrue = (key) => {
|
|
112
|
+
const v = process.env[key];
|
|
113
|
+
return !!v && /^(1|true|yes|on)$/i.test(v.trim());
|
|
114
|
+
};
|
|
120
115
|
// ─── Cached state ───
|
|
121
116
|
let cachedKeyPair = null;
|
|
122
117
|
let cachedExportedPublicKey = null;
|
|
123
118
|
let cachedOwnPublicKey = null;
|
|
124
119
|
let initPromise = null;
|
|
125
120
|
/**
|
|
126
|
-
* Initialize crypto
|
|
127
|
-
*
|
|
121
|
+
* Initialize crypto.
|
|
122
|
+
*
|
|
123
|
+
* The MCP server is a CONSUMER of encryption keys, not a generator. Keys
|
|
124
|
+
* are created in the Zeph app where the user explicitly opts in (Settings
|
|
125
|
+
* → Encryption). This function only imports keys that the server already
|
|
126
|
+
* has, and only when the server confirms encryption is enabled.
|
|
127
|
+
*
|
|
128
|
+
* Any other state — server says disabled, server has no keys, server is
|
|
129
|
+
* unreachable — leaves encryption OFF (cache empty, no fallback). A
|
|
130
|
+
* previous version generated and uploaded a fresh keypair on the "no keys
|
|
131
|
+
* anywhere" path; combined with a transient fetch failure, that silently
|
|
132
|
+
* turned encryption on without user consent and locked the account into
|
|
133
|
+
* an "encryption enabled" state on the server.
|
|
134
|
+
*
|
|
135
|
+
* Opt-out: `ZEPH_DISABLE_ENCRYPTION=1` forces crypto off regardless of
|
|
136
|
+
* server state — useful while cleaning up legacy state or for users who
|
|
137
|
+
* never want encryption.
|
|
138
|
+
*
|
|
128
139
|
* Safe to call concurrently — deduplicates to single init.
|
|
129
|
-
* Returns the exported public key
|
|
140
|
+
* Returns the exported public key when encryption is active, '' otherwise.
|
|
130
141
|
*
|
|
131
|
-
* NOTE: when `apiKey` is provided, `baseUrl` is required
|
|
132
|
-
* caller in a dev environment would silently upload keys to prod.
|
|
142
|
+
* NOTE: when `apiKey` is provided, `baseUrl` is required.
|
|
133
143
|
*/
|
|
134
144
|
const initCrypto = (apiKey, baseUrl) => {
|
|
145
|
+
// Hard opt-out — skip everything, leave cache empty.
|
|
146
|
+
if (envIsTrue('ZEPH_DISABLE_ENCRYPTION')) {
|
|
147
|
+
cachedKeyPair = null;
|
|
148
|
+
cachedExportedPublicKey = null;
|
|
149
|
+
cachedOwnPublicKey = null;
|
|
150
|
+
return Promise.resolve('');
|
|
151
|
+
}
|
|
135
152
|
if (apiKey && !baseUrl) {
|
|
136
153
|
return Promise.reject(new Error('initCrypto: baseUrl is required when apiKey is provided. ' +
|
|
137
|
-
'Pass the resolved config.baseUrl to avoid
|
|
154
|
+
'Pass the resolved config.baseUrl to avoid talking to the wrong environment.'));
|
|
138
155
|
}
|
|
139
156
|
if (initPromise)
|
|
140
157
|
return initPromise;
|
|
141
|
-
// The check above guarantees baseUrl is defined when apiKey is — narrow once.
|
|
142
158
|
const baseUrlRequired = apiKey ? baseUrl : baseUrl;
|
|
143
159
|
initPromise = (async () => {
|
|
144
|
-
const stored = loadStoredKeys();
|
|
145
|
-
// Try server sync if API key available
|
|
146
160
|
if (apiKey) {
|
|
147
161
|
const serverResult = await fetchServerKeys(apiKey, baseUrlRequired);
|
|
148
|
-
//
|
|
149
|
-
|
|
162
|
+
// The only path that turns encryption ON: server confirms enabled AND
|
|
163
|
+
// hands us a real keypair. Everything else leaves the cache empty.
|
|
164
|
+
const haveServerKeys = !!serverResult && serverResult.encryptionEnabled && !!serverResult.keys;
|
|
165
|
+
if (!haveServerKeys) {
|
|
150
166
|
cachedKeyPair = null;
|
|
151
167
|
cachedExportedPublicKey = null;
|
|
152
168
|
cachedOwnPublicKey = null;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
157
|
-
|
|
169
|
+
// If the server is reachable and explicitly says encryption is off,
|
|
170
|
+
// drop any stale local cache so a future regression can't resurrect
|
|
171
|
+
// a keypair that the user already disabled.
|
|
172
|
+
if (serverResult && !serverResult.encryptionEnabled) {
|
|
173
|
+
deleteStoredKeys();
|
|
158
174
|
}
|
|
159
|
-
|
|
160
|
-
cachedExportedPublicKey = serverResult.keys.publicKey;
|
|
161
|
-
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
162
|
-
return serverResult.keys.publicKey;
|
|
175
|
+
return '';
|
|
163
176
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
169
|
-
return stored.publicKey;
|
|
177
|
+
const keys = serverResult.keys;
|
|
178
|
+
const stored = loadStoredKeys();
|
|
179
|
+
if (!stored || stored.publicKey !== keys.publicKey) {
|
|
180
|
+
storeKeys(keys);
|
|
170
181
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
storeKeys(exported);
|
|
174
|
-
await uploadServerKeys(exported, apiKey, baseUrlRequired);
|
|
175
|
-
cachedKeyPair = keyPair;
|
|
176
|
-
cachedExportedPublicKey = exported.publicKey;
|
|
177
|
-
cachedOwnPublicKey = keyPair.publicKey;
|
|
178
|
-
return exported.publicKey;
|
|
179
|
-
}
|
|
180
|
-
// No API key — local-only mode
|
|
181
|
-
if (stored) {
|
|
182
|
-
cachedKeyPair = await importKeyPair(stored);
|
|
183
|
-
cachedExportedPublicKey = stored.publicKey;
|
|
182
|
+
cachedKeyPair = await importKeyPair(keys);
|
|
183
|
+
cachedExportedPublicKey = keys.publicKey;
|
|
184
184
|
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
185
|
-
return
|
|
185
|
+
return keys.publicKey;
|
|
186
186
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
// Local-only mode (no apiKey): load stored keys if they exist; do NOT
|
|
188
|
+
// generate. Used by tests and offline / pre-provisioned setups where
|
|
189
|
+
// a keypair has been dropped into ~/.config/zeph/keys.json out-of-band.
|
|
190
|
+
const stored = loadStoredKeys();
|
|
191
|
+
if (!stored) {
|
|
192
|
+
cachedKeyPair = null;
|
|
193
|
+
cachedExportedPublicKey = null;
|
|
194
|
+
cachedOwnPublicKey = null;
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
cachedKeyPair = await importKeyPair(stored);
|
|
198
|
+
cachedExportedPublicKey = stored.publicKey;
|
|
199
|
+
cachedOwnPublicKey = cachedKeyPair.publicKey;
|
|
200
|
+
return stored.publicKey;
|
|
194
201
|
})().catch((err) => {
|
|
195
202
|
initPromise = null;
|
|
196
203
|
throw err;
|
|
@@ -216,17 +223,9 @@ const fetchServerKeys = async (apiKey, baseUrl) => {
|
|
|
216
223
|
return null;
|
|
217
224
|
}
|
|
218
225
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
await fetch(url, {
|
|
223
|
-
method: 'PUT',
|
|
224
|
-
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
225
|
-
body: JSON.stringify(keys),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
catch { /* non-critical */ }
|
|
229
|
-
};
|
|
226
|
+
// uploadServerKeys was removed in fix/no-auto-encryption — the MCP server
|
|
227
|
+
// must never write to /users/me/keys. Keys are created by the Zeph app
|
|
228
|
+
// where the user explicitly opts in.
|
|
230
229
|
const getKeyPair = () => cachedKeyPair;
|
|
231
230
|
exports.getKeyPair = getKeyPair;
|
|
232
231
|
const getPublicKey = () => cachedExportedPublicKey;
|
package/dist/poll.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAE5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAOD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,KACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAE5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAOD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,KACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,CA6ClC,CAAC"}
|
package/dist/poll.js
CHANGED
|
@@ -17,6 +17,11 @@ const pollForResponse = async (client, hookId, eventId, timeoutSeconds, ctx) =>
|
|
|
17
17
|
throw new Error('User cancelled the request');
|
|
18
18
|
if (event.data.status === 'timed_out')
|
|
19
19
|
return null;
|
|
20
|
+
// Only 'pending' should keep us polling. Anything else is server
|
|
21
|
+
// contract drift — fail fast instead of spinning until the timeout.
|
|
22
|
+
if (event.data.status !== 'pending') {
|
|
23
|
+
throw new Error(`Unexpected hook event status: ${String(event.data.status)}`);
|
|
24
|
+
}
|
|
20
25
|
// Throttled progress notification (every 5s)
|
|
21
26
|
if (progressToken !== undefined) {
|
|
22
27
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
package/dist/sanitize.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5C;AAeD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAIhE,CAAC;
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;CAC5C;AAeD;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,SAIhE,CAAC;AAkBF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,eAAe,EAAE,GAAG,SAgB7E,CAAC"}
|
package/dist/sanitize.js
CHANGED
|
@@ -42,9 +42,17 @@ const sanitizeText = (text) => {
|
|
|
42
42
|
return cut === -1 ? text : text.slice(0, cut).trimEnd();
|
|
43
43
|
};
|
|
44
44
|
exports.sanitizeText = sanitizeText;
|
|
45
|
+
// Bounds mirror the zeph_ask contract (1–4 actions). Length caps on the
|
|
46
|
+
// recovered strings keep a malformed/oversized leak from producing absurd
|
|
47
|
+
// buttons; the real server enforces its own limits too.
|
|
45
48
|
const isActionArray = (value) => Array.isArray(value) &&
|
|
46
49
|
value.length > 0 &&
|
|
47
|
-
value.
|
|
50
|
+
value.length <= 4 &&
|
|
51
|
+
value.every((a) => a &&
|
|
52
|
+
typeof a.id === 'string' &&
|
|
53
|
+
a.id.length <= 50 &&
|
|
54
|
+
typeof a.label === 'string' &&
|
|
55
|
+
a.label.length <= 100);
|
|
48
56
|
/**
|
|
49
57
|
* Recover an `actions` array that leaked into the body of a malformed
|
|
50
58
|
* zeph_ask call. Returns undefined when nothing parseable is found.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeph-to/mcp-server",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.3",
|
|
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",
|
|
@@ -62,5 +62,9 @@
|
|
|
62
62
|
"cursor",
|
|
63
63
|
"gemini"
|
|
64
64
|
],
|
|
65
|
-
"license": "Apache-2.0"
|
|
65
|
+
"license": "Apache-2.0",
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public",
|
|
68
|
+
"registry": "https://registry.npmjs.org/"
|
|
69
|
+
}
|
|
66
70
|
}
|