@zeph-to/mcp-server 1.11.3 → 1.13.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 +4 -0
- package/dist/api-client.d.ts +3 -17
- package/dist/api-client.d.ts.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -12
- package/dist/index.js +8 -4
- package/dist/poll.d.ts +4 -2
- package/dist/poll.d.ts.map +1 -1
- package/dist/poll.js +64 -35
- package/dist/tools/ask.d.ts +2 -1
- package/dist/tools/ask.d.ts.map +1 -1
- package/dist/tools/ask.js +2 -2
- package/dist/tools/input.d.ts +2 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +2 -2
- package/dist/tools/prompt.d.ts +2 -1
- package/dist/tools/prompt.d.ts.map +1 -1
- package/dist/tools/prompt.js +2 -2
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ws-wait.d.ts +61 -0
- package/dist/ws-wait.d.ts.map +1 -0
- package/dist/ws-wait.js +143 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -186,6 +186,10 @@ timeout: 120 (seconds, default: 120, max: 600)
|
|
|
186
186
|
|
|
187
187
|
Returns: `{ value: "feat: add clipboard sync", timedOut: false }`
|
|
188
188
|
|
|
189
|
+
### Client timeouts
|
|
190
|
+
|
|
191
|
+
`zeph_ask`, `zeph_prompt`, and `zeph_input` block until the user responds, up to their `timeout` (max 600s). That whole time the MCP request stays open. To keep the client from giving up early, the server emits a `notifications/progress` every 5s while waiting. Clients must either set a per-request timeout above the tool's `timeout`, or reset their timeout on progress notifications. Claude Code does the latter by default.
|
|
192
|
+
|
|
189
193
|
## Resources
|
|
190
194
|
|
|
191
195
|
### zeph://devices
|
package/dist/api-client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { McpServerConfig } from './config.js';
|
|
2
|
-
import type { PushResponse, HookTriggerResponse, HookEventResponse, DevicesResponse, PushListResponse, DismissResponse, ChannelsResponse, UploadRequestResponse } from './types.js';
|
|
2
|
+
import type { PushResponse, HookTriggerResponse, HookEventResponse, DevicesResponse, PushListResponse, DismissResponse, ChannelsResponse, UploadRequestResponse, AttachedFile } from './types.js';
|
|
3
3
|
export declare class ApiError extends Error {
|
|
4
4
|
readonly code: string;
|
|
5
5
|
readonly status: number;
|
|
@@ -18,14 +18,7 @@ export declare class ZephApiClient {
|
|
|
18
18
|
targetDeviceId?: string;
|
|
19
19
|
channelId?: string;
|
|
20
20
|
sessionId?: string;
|
|
21
|
-
files?:
|
|
22
|
-
fileKey: string;
|
|
23
|
-
fileName: string;
|
|
24
|
-
fileSize: number;
|
|
25
|
-
fileType: string;
|
|
26
|
-
iv?: string;
|
|
27
|
-
encryptedKey?: string;
|
|
28
|
-
}[];
|
|
21
|
+
files?: AttachedFile[];
|
|
29
22
|
isEncrypted?: boolean;
|
|
30
23
|
encryptedKey?: string;
|
|
31
24
|
senderPublicKey?: string;
|
|
@@ -41,14 +34,7 @@ export declare class ZephApiClient {
|
|
|
41
34
|
fallback?: string;
|
|
42
35
|
metadata?: Record<string, unknown>;
|
|
43
36
|
hookType?: 'one-way' | 'interactive' | 'input' | 'combo';
|
|
44
|
-
files?:
|
|
45
|
-
fileKey: string;
|
|
46
|
-
fileName: string;
|
|
47
|
-
fileSize: number;
|
|
48
|
-
fileType: string;
|
|
49
|
-
iv?: string;
|
|
50
|
-
encryptedKey?: string;
|
|
51
|
-
}[];
|
|
37
|
+
files?: AttachedFile[];
|
|
52
38
|
sessionId?: string;
|
|
53
39
|
}): Promise<HookTriggerResponse>;
|
|
54
40
|
getHookEvent(hookId: string, eventId: string): Promise<HookEventResponse>;
|
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,
|
|
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,EACrB,YAAY,EACb,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,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,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,YAAY,EAAE,CAAC;QACvB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,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;CA0CtB"}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export interface McpServerConfig {
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseUrl: string;
|
|
4
|
+
/** WebSocket endpoint for the hook-response fast path (§S3). Optional —
|
|
5
|
+
* without it (or without a WebSocket global) waits are poll-only. */
|
|
6
|
+
wsUrl?: string;
|
|
4
7
|
hookId?: string;
|
|
5
8
|
deviceId?: string;
|
|
6
9
|
sessionId?: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACvB;AAiBD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,KAAG,MAGpE,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;0EACsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACvB;AAiBD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,EAAE,OAAO,MAAM,KAAG,MAGpE,CAAC;AAuGF,eAAO,MAAM,UAAU,QAAO,eAuB7B,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -43,22 +43,27 @@ const loadFileConfig = () => {
|
|
|
43
43
|
return {};
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const SESSION_UUID_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
47
|
+
/**
|
|
48
|
+
* Detect the Claude Code session ID for `projectDir`. Claude Code writes
|
|
49
|
+
* one transcript per session at `~/.claude/projects/<projectHash>/<uuid>.jsonl`
|
|
50
|
+
* where the hash is the project path with `/` replaced by `-`. We pick the
|
|
51
|
+
* most recently modified transcript — that's the live session.
|
|
52
|
+
*/
|
|
53
|
+
const detectClaudeSessionId = (projectDir) => {
|
|
48
54
|
try {
|
|
49
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
50
55
|
const projectHash = projectDir.replace(/\//g, '-');
|
|
51
56
|
const sessionsDir = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'projects', projectHash);
|
|
52
57
|
let latest;
|
|
53
|
-
for (const
|
|
54
|
-
|
|
58
|
+
for (const entry of (0, fs_1.readdirSync)(sessionsDir)) {
|
|
59
|
+
const match = SESSION_UUID_RE.exec(entry);
|
|
60
|
+
if (!match)
|
|
55
61
|
continue;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
if (!stat.isDirectory())
|
|
62
|
+
const stat = (0, fs_1.statSync)((0, path_1.join)(sessionsDir, entry));
|
|
63
|
+
if (!stat.isFile())
|
|
59
64
|
continue;
|
|
60
65
|
if (!latest || stat.mtimeMs > latest.mtime) {
|
|
61
|
-
latest = { name, mtime: stat.mtimeMs };
|
|
66
|
+
latest = { name: match[1], mtime: stat.mtimeMs };
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
return latest?.name;
|
|
@@ -125,16 +130,17 @@ const loadConfig = () => {
|
|
|
125
130
|
if (!apiKey) {
|
|
126
131
|
throw new Error('ZEPH_API_KEY not found. Run "npx @zeph-to/cli install" or set ZEPH_API_KEY env var.');
|
|
127
132
|
}
|
|
128
|
-
const
|
|
129
|
-
const
|
|
133
|
+
const projectDir = detectProjectDir();
|
|
134
|
+
const sessionId = resolvedEnv('ZEPH_SESSION_ID') ?? detectClaudeSessionId(projectDir) ?? `sess_${(0, crypto_1.randomBytes)(12).toString('base64url')}`;
|
|
130
135
|
writeSessionCache(sessionId, projectDir);
|
|
131
136
|
return {
|
|
132
137
|
apiKey,
|
|
133
138
|
baseUrl: (resolvedEnv('ZEPH_BASE_URL') ?? fileConfig.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ''),
|
|
139
|
+
wsUrl: resolvedEnv('ZEPH_WS_URL') ?? fileConfig.wsUrl,
|
|
134
140
|
hookId: resolvedEnv('ZEPH_HOOK_ID') ?? fileConfig.hookId,
|
|
135
141
|
deviceId: resolvedEnv('ZEPH_DEVICE_ID') ?? fileConfig.deviceId,
|
|
136
142
|
sessionId,
|
|
137
|
-
projectName: projectNameFromDir(
|
|
143
|
+
projectName: projectNameFromDir(projectDir),
|
|
138
144
|
};
|
|
139
145
|
};
|
|
140
146
|
exports.loadConfig = loadConfig;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const path_1 = require("path");
|
|
|
6
6
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
7
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
8
|
const config_js_1 = require("./config.js");
|
|
9
|
+
const ws_wait_js_1 = require("./ws-wait.js");
|
|
9
10
|
const api_client_js_1 = require("./api-client.js");
|
|
10
11
|
const crypto_js_1 = require("./crypto.js");
|
|
11
12
|
const notify_js_1 = require("./tools/notify.js");
|
|
@@ -22,7 +23,7 @@ const channels_js_1 = require("./resources/channels.js");
|
|
|
22
23
|
const getVersion = () => {
|
|
23
24
|
try {
|
|
24
25
|
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf-8'));
|
|
25
|
-
return pkg.version;
|
|
26
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
26
27
|
}
|
|
27
28
|
catch {
|
|
28
29
|
return '0.0.0';
|
|
@@ -30,6 +31,9 @@ const getVersion = () => {
|
|
|
30
31
|
};
|
|
31
32
|
const createServer = (config) => {
|
|
32
33
|
const client = new api_client_js_1.ZephApiClient(config);
|
|
34
|
+
// Shared WS fast path for zeph_prompt/input/ask waits — degrades to
|
|
35
|
+
// pure polling when wsUrl or the WebSocket global is missing (§S3).
|
|
36
|
+
const waiter = new ws_wait_js_1.HookResponseWaiter({ wsUrl: config.wsUrl, apiKey: config.apiKey });
|
|
33
37
|
const server = new mcp_js_1.McpServer({
|
|
34
38
|
name: 'zeph',
|
|
35
39
|
version: getVersion(),
|
|
@@ -61,9 +65,9 @@ const createServer = (config) => {
|
|
|
61
65
|
(0, dismiss_js_1.registerDismissAllTool)(server, client);
|
|
62
66
|
(0, broadcast_js_1.registerBroadcastTool)(server, client, config);
|
|
63
67
|
(0, file_js_1.registerFileTool)(server, client, config);
|
|
64
|
-
(0, prompt_js_1.registerPromptTool)(server, client, config);
|
|
65
|
-
(0, input_js_1.registerInputTool)(server, client, config);
|
|
66
|
-
(0, ask_js_1.registerAskTool)(server, client, config);
|
|
68
|
+
(0, prompt_js_1.registerPromptTool)(server, client, config, waiter);
|
|
69
|
+
(0, input_js_1.registerInputTool)(server, client, config, waiter);
|
|
70
|
+
(0, ask_js_1.registerAskTool)(server, client, config, waiter);
|
|
67
71
|
(0, devices_js_1.registerDevicesResource)(server, client);
|
|
68
72
|
(0, channels_js_1.registerChannelsResource)(server, client);
|
|
69
73
|
return server;
|
package/dist/poll.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { ServerNotification } from '@modelcontextprotocol/sdk/types.js';
|
|
1
2
|
import type { ZephApiClient } from './api-client.js';
|
|
2
3
|
import type { HookEventResponse } from './types.js';
|
|
4
|
+
import type { HookResponseWaiter } from './ws-wait.js';
|
|
3
5
|
export interface PollContext {
|
|
4
6
|
_meta?: {
|
|
5
7
|
progressToken?: string | number;
|
|
6
8
|
};
|
|
7
|
-
sendNotification: (notification:
|
|
9
|
+
sendNotification: (notification: ServerNotification) => Promise<void>;
|
|
8
10
|
}
|
|
9
|
-
export declare const pollForResponse: (client: ZephApiClient, hookId: string, eventId: string, timeoutSeconds: number, ctx: PollContext) => Promise<HookEventResponse | null>;
|
|
11
|
+
export declare const pollForResponse: (client: ZephApiClient, hookId: string, eventId: string, timeoutSeconds: number, ctx: PollContext, waiter?: HookResponseWaiter) => Promise<HookEventResponse | null>;
|
|
10
12
|
//# sourceMappingURL=poll.d.ts.map
|
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;
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,cAAc,CAAC;AAEzE,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAeD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,EAChB,SAAS,kBAAkB,KAC1B,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAqElC,CAAC"}
|
package/dist/poll.js
CHANGED
|
@@ -3,48 +3,77 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.pollForResponse = void 0;
|
|
4
4
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
5
|
const PROGRESS_INTERVAL_SECONDS = 5;
|
|
6
|
-
|
|
6
|
+
// While the WS fast path is live, polling is only the consistency
|
|
7
|
+
// backstop — 10s is plenty. The WS wake races the sleep, so a live
|
|
8
|
+
// answer still lands in sub-second time (SPEC-AGENT-AWARENESS §S3).
|
|
9
|
+
const WS_BACKSTOP_INTERVAL_MS = 10_000;
|
|
10
|
+
// After a WS wake, REST may briefly still read 'pending' (DDB eventual
|
|
11
|
+
// consistency) — retry tightly but boundedly.
|
|
12
|
+
const POST_WAKE_RETRY_MS = 500;
|
|
13
|
+
const pollForResponse = async (client, hookId, eventId, timeoutSeconds, ctx, waiter) => {
|
|
7
14
|
const progressToken = ctx._meta?.progressToken;
|
|
8
15
|
const startTime = Date.now();
|
|
9
16
|
const timeoutMs = timeoutSeconds * 1000;
|
|
10
17
|
let attempt = 0;
|
|
11
18
|
let lastProgressAt = 0;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
let sub;
|
|
20
|
+
let wsSettled = false;
|
|
21
|
+
if (waiter?.canWait()) {
|
|
22
|
+
sub = waiter.subscribe(eventId);
|
|
23
|
+
void sub.settled.then(() => { wsSettled = true; });
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
27
|
+
const event = await client.getHookEvent(hookId, eventId);
|
|
28
|
+
if (event.data.status === 'responded')
|
|
29
|
+
return event;
|
|
30
|
+
if (event.data.status === 'cancelled')
|
|
31
|
+
throw new Error('User cancelled the request');
|
|
32
|
+
if (event.data.status === 'timed_out')
|
|
33
|
+
return null;
|
|
34
|
+
// Only 'pending' should keep us polling. Anything else is server
|
|
35
|
+
// contract drift — fail fast instead of spinning until the timeout.
|
|
36
|
+
if (event.data.status !== 'pending') {
|
|
37
|
+
throw new Error(`Unexpected hook event status: ${String(event.data.status)}`);
|
|
38
|
+
}
|
|
39
|
+
// Throttled progress notification (every 5s)
|
|
40
|
+
if (progressToken !== undefined) {
|
|
41
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
42
|
+
if (elapsed - lastProgressAt >= PROGRESS_INTERVAL_SECONDS) {
|
|
43
|
+
lastProgressAt = elapsed;
|
|
44
|
+
await ctx.sendNotification({
|
|
45
|
+
method: 'notifications/progress',
|
|
46
|
+
params: {
|
|
47
|
+
progressToken,
|
|
48
|
+
progress: elapsed,
|
|
49
|
+
total: timeoutSeconds,
|
|
50
|
+
message: `Waiting for user response... (${elapsed}s / ${timeoutSeconds}s)`,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
39
54
|
}
|
|
55
|
+
if (wsSettled) {
|
|
56
|
+
// Server said settled but this read still saw 'pending' —
|
|
57
|
+
// eventual consistency. Tight bounded retry until reads agree.
|
|
58
|
+
await sleep(POST_WAKE_RETRY_MS);
|
|
59
|
+
attempt++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
// Adaptive interval: poll every 1s while the user is likely still at
|
|
63
|
+
// their device (first ~60 attempts ≈ 1 min), then back off to 3s for
|
|
64
|
+
// long waits. The tight 1s window keeps post-tap detection snappy.
|
|
65
|
+
// With a live WS subscription the cadence relaxes to a backstop and
|
|
66
|
+
// the wake race handles snappiness instead.
|
|
67
|
+
const interval = sub?.isConnected()
|
|
68
|
+
? WS_BACKSTOP_INTERVAL_MS
|
|
69
|
+
: (attempt < 60 ? 1000 : 3000);
|
|
70
|
+
await Promise.race([sleep(interval), sub?.settled ?? new Promise(() => undefined)]);
|
|
71
|
+
attempt++;
|
|
40
72
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await sleep(interval);
|
|
46
|
-
attempt++;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
sub?.dispose();
|
|
47
77
|
}
|
|
48
|
-
return null;
|
|
49
78
|
};
|
|
50
79
|
exports.pollForResponse = pollForResponse;
|
package/dist/tools/ask.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ZephApiClient } from '../api-client.js';
|
|
3
3
|
import { type McpServerConfig } from '../config.js';
|
|
4
|
-
|
|
4
|
+
import type { HookResponseWaiter } from '../ws-wait.js';
|
|
5
|
+
export declare const registerAskTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig, waiter?: HookResponseWaiter) => void;
|
|
5
6
|
//# sourceMappingURL=ask.d.ts.map
|
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;
|
|
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;AAItD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAwBxD,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAiI7H,CAAC"}
|
package/dist/tools/ask.js
CHANGED
|
@@ -21,7 +21,7 @@ const buildAskMarkdown = (title, body, actions) => {
|
|
|
21
21
|
}
|
|
22
22
|
return parts.join('\n');
|
|
23
23
|
};
|
|
24
|
-
const registerAskTool = (server, client, config) => {
|
|
24
|
+
const registerAskTool = (server, client, config, waiter) => {
|
|
25
25
|
server.registerTool('zeph_ask', {
|
|
26
26
|
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.',
|
|
27
27
|
annotations: {
|
|
@@ -113,7 +113,7 @@ const registerAskTool = (server, client, config) => {
|
|
|
113
113
|
files,
|
|
114
114
|
sessionId: config.sessionId,
|
|
115
115
|
});
|
|
116
|
-
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx);
|
|
116
|
+
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
117
117
|
if (!event) {
|
|
118
118
|
if (fallback)
|
|
119
119
|
return (0, error_format_js_1.textResult)({ actionId: fallback, timedOut: true });
|
package/dist/tools/input.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ZephApiClient } from '../api-client.js';
|
|
3
3
|
import { type McpServerConfig } from '../config.js';
|
|
4
|
-
|
|
4
|
+
import type { HookResponseWaiter } from '../ws-wait.js';
|
|
5
|
+
export declare const registerInputTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig, waiter?: HookResponseWaiter) => void;
|
|
5
6
|
//# sourceMappingURL=input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../src/tools/input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAyD/H,CAAC"}
|
package/dist/tools/input.js
CHANGED
|
@@ -6,7 +6,7 @@ const error_format_js_1 = require("../error-format.js");
|
|
|
6
6
|
const poll_js_1 = require("../poll.js");
|
|
7
7
|
const config_js_1 = require("../config.js");
|
|
8
8
|
const sanitize_js_1 = require("../sanitize.js");
|
|
9
|
-
const registerInputTool = (server, client, config) => {
|
|
9
|
+
const registerInputTool = (server, client, config, waiter) => {
|
|
10
10
|
server.registerTool('zeph_input', {
|
|
11
11
|
description: 'Request text input from the user via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
|
|
12
12
|
annotations: {
|
|
@@ -41,7 +41,7 @@ const registerInputTool = (server, client, config) => {
|
|
|
41
41
|
metadata: { placeholder, inputType },
|
|
42
42
|
sessionId: config.sessionId,
|
|
43
43
|
});
|
|
44
|
-
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx);
|
|
44
|
+
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
45
45
|
if (!event)
|
|
46
46
|
return (0, error_format_js_1.timeoutError)(timeout, 'Try again with a longer timeout');
|
|
47
47
|
return (0, error_format_js_1.textResult)({ value: event.data.response?.value ?? '', timedOut: false });
|
package/dist/tools/prompt.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { ZephApiClient } from '../api-client.js';
|
|
3
3
|
import { type McpServerConfig } from '../config.js';
|
|
4
|
-
|
|
4
|
+
import type { HookResponseWaiter } from '../ws-wait.js';
|
|
5
|
+
export declare const registerPromptTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig, waiter?: HookResponseWaiter) => void;
|
|
5
6
|
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/tools/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGtD,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,EAAE,SAAS,kBAAkB,SAwEhI,CAAC"}
|
package/dist/tools/prompt.js
CHANGED
|
@@ -6,7 +6,7 @@ const error_format_js_1 = require("../error-format.js");
|
|
|
6
6
|
const poll_js_1 = require("../poll.js");
|
|
7
7
|
const config_js_1 = require("../config.js");
|
|
8
8
|
const sanitize_js_1 = require("../sanitize.js");
|
|
9
|
-
const registerPromptTool = (server, client, config) => {
|
|
9
|
+
const registerPromptTool = (server, client, config, waiter) => {
|
|
10
10
|
server.registerTool('zeph_prompt', {
|
|
11
11
|
description: 'Ask the user to choose from predefined options via push notification. The tool blocks until the user responds or the timeout is reached. Requires ZEPH_HOOK_ID environment variable.',
|
|
12
12
|
annotations: {
|
|
@@ -51,7 +51,7 @@ const registerPromptTool = (server, client, config) => {
|
|
|
51
51
|
hookType: 'interactive',
|
|
52
52
|
sessionId: config.sessionId,
|
|
53
53
|
});
|
|
54
|
-
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx);
|
|
54
|
+
const event = await (0, poll_js_1.pollForResponse)(client, config.hookId, trigger.data.eventId, timeout, ctx, waiter);
|
|
55
55
|
if (!event) {
|
|
56
56
|
if (fallback)
|
|
57
57
|
return (0, error_format_js_1.textResult)({ actionId: fallback, timedOut: true });
|
package/dist/types.d.ts
CHANGED
|
@@ -86,6 +86,14 @@ export interface UploadRequestResponse {
|
|
|
86
86
|
uploadUrl: string;
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
export interface AttachedFile {
|
|
90
|
+
fileKey: string;
|
|
91
|
+
fileName: string;
|
|
92
|
+
fileSize: number;
|
|
93
|
+
fileType: string;
|
|
94
|
+
iv?: string;
|
|
95
|
+
encryptedKey?: string;
|
|
96
|
+
}
|
|
89
97
|
export interface ToolError {
|
|
90
98
|
error: string;
|
|
91
99
|
message: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket fast path for hook-response waits (SPEC-AGENT-AWARENESS §S3).
|
|
3
|
+
*
|
|
4
|
+
* The server broadcasts `hook.responded` / `hook.cancelled` the moment
|
|
5
|
+
* the user answers. This module subscribes to that feed so a wait can
|
|
6
|
+
* wake instantly instead of sitting out its poll interval.
|
|
7
|
+
*
|
|
8
|
+
* Trust model: the WS message is a WAKE-UP SIGNAL ONLY. The poll loop
|
|
9
|
+
* remains the single source of truth — on wake it immediately re-reads
|
|
10
|
+
* the event over REST and acts on that. A spoofed/garbled/duplicated
|
|
11
|
+
* frame can therefore cost at most one extra GET, never a wrong answer.
|
|
12
|
+
* The same property makes degradation trivial: no WebSocket global
|
|
13
|
+
* (Node < 21), no wsUrl configured, or a dropped socket all just mean
|
|
14
|
+
* "nobody wakes us early" and the existing poll cadence carries on.
|
|
15
|
+
*
|
|
16
|
+
* Connection lifecycle: lazy connect on first subscribe, shared across
|
|
17
|
+
* concurrent waits, app-level ping every 25 s (API GW $default route
|
|
18
|
+
* answers 'pong'; native WebSocket has no protocol-ping API), closed
|
|
19
|
+
* 30 s after the last pending wait ends.
|
|
20
|
+
*/
|
|
21
|
+
export interface WsLike {
|
|
22
|
+
readyState: number;
|
|
23
|
+
send(data: string): void;
|
|
24
|
+
close(): void;
|
|
25
|
+
addEventListener(type: string, listener: (event: {
|
|
26
|
+
data?: unknown;
|
|
27
|
+
}) => void): void;
|
|
28
|
+
}
|
|
29
|
+
export type WsFactory = (url: string) => WsLike;
|
|
30
|
+
export interface WaitSubscription {
|
|
31
|
+
/** Resolves when the server says this event settled (answer or cancel). */
|
|
32
|
+
settled: Promise<void>;
|
|
33
|
+
/** Live socket right now? Drives the caller's poll cadence. */
|
|
34
|
+
isConnected(): boolean;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
export interface HookResponseWaiterOptions {
|
|
38
|
+
wsUrl?: string;
|
|
39
|
+
apiKey: string;
|
|
40
|
+
/** Test seam. Defaults to globalThis.WebSocket when present. */
|
|
41
|
+
factory?: WsFactory;
|
|
42
|
+
}
|
|
43
|
+
export declare class HookResponseWaiter {
|
|
44
|
+
private readonly wsUrl?;
|
|
45
|
+
private readonly apiKey;
|
|
46
|
+
private readonly factory?;
|
|
47
|
+
private sock;
|
|
48
|
+
private connected;
|
|
49
|
+
private readonly pending;
|
|
50
|
+
private pingTimer;
|
|
51
|
+
private idleTimer;
|
|
52
|
+
constructor(opts: HookResponseWaiterOptions);
|
|
53
|
+
/** False when the fast path can't exist — caller skips subscribe(). */
|
|
54
|
+
canWait(): boolean;
|
|
55
|
+
subscribe(eventId: string): WaitSubscription;
|
|
56
|
+
private ensureSocket;
|
|
57
|
+
private handleMessage;
|
|
58
|
+
private scheduleIdleClose;
|
|
59
|
+
private teardownSocket;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=ws-wait.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-wait.d.ts","sourceRoot":"","sources":["../src/ws-wait.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,MAAM;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;KAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CACxF;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;AAMhD,MAAM,WAAW,gBAAgB;IAC7B,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,+DAA+D;IAC/D,WAAW,IAAI,OAAO,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB;AAQD,qBAAa,kBAAkB;IAC3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAY;IACrC,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,SAAS,CAA8C;gBAEnD,IAAI,EAAE,yBAAyB;IAM3C,uEAAuE;IACvE,OAAO,IAAI,OAAO;IAIlB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;IAgB5C,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,cAAc;CAUzB"}
|
package/dist/ws-wait.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebSocket fast path for hook-response waits (SPEC-AGENT-AWARENESS §S3).
|
|
4
|
+
*
|
|
5
|
+
* The server broadcasts `hook.responded` / `hook.cancelled` the moment
|
|
6
|
+
* the user answers. This module subscribes to that feed so a wait can
|
|
7
|
+
* wake instantly instead of sitting out its poll interval.
|
|
8
|
+
*
|
|
9
|
+
* Trust model: the WS message is a WAKE-UP SIGNAL ONLY. The poll loop
|
|
10
|
+
* remains the single source of truth — on wake it immediately re-reads
|
|
11
|
+
* the event over REST and acts on that. A spoofed/garbled/duplicated
|
|
12
|
+
* frame can therefore cost at most one extra GET, never a wrong answer.
|
|
13
|
+
* The same property makes degradation trivial: no WebSocket global
|
|
14
|
+
* (Node < 21), no wsUrl configured, or a dropped socket all just mean
|
|
15
|
+
* "nobody wakes us early" and the existing poll cadence carries on.
|
|
16
|
+
*
|
|
17
|
+
* Connection lifecycle: lazy connect on first subscribe, shared across
|
|
18
|
+
* concurrent waits, app-level ping every 25 s (API GW $default route
|
|
19
|
+
* answers 'pong'; native WebSocket has no protocol-ping API), closed
|
|
20
|
+
* 30 s after the last pending wait ends.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.HookResponseWaiter = void 0;
|
|
24
|
+
const WS_OPEN = 1;
|
|
25
|
+
const PING_INTERVAL_MS = 25_000;
|
|
26
|
+
const IDLE_CLOSE_MS = 30_000;
|
|
27
|
+
const defaultFactory = () => {
|
|
28
|
+
const WS = globalThis.WebSocket;
|
|
29
|
+
if (!WS)
|
|
30
|
+
return undefined;
|
|
31
|
+
return (url) => new WS(url);
|
|
32
|
+
};
|
|
33
|
+
class HookResponseWaiter {
|
|
34
|
+
wsUrl;
|
|
35
|
+
apiKey;
|
|
36
|
+
factory;
|
|
37
|
+
sock = null;
|
|
38
|
+
connected = false;
|
|
39
|
+
pending = new Map();
|
|
40
|
+
pingTimer = null;
|
|
41
|
+
idleTimer = null;
|
|
42
|
+
constructor(opts) {
|
|
43
|
+
this.wsUrl = opts.wsUrl;
|
|
44
|
+
this.apiKey = opts.apiKey;
|
|
45
|
+
this.factory = opts.factory ?? defaultFactory();
|
|
46
|
+
}
|
|
47
|
+
/** False when the fast path can't exist — caller skips subscribe(). */
|
|
48
|
+
canWait() {
|
|
49
|
+
return !!this.wsUrl && !!this.factory;
|
|
50
|
+
}
|
|
51
|
+
subscribe(eventId) {
|
|
52
|
+
let resolveSettled = () => undefined;
|
|
53
|
+
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
54
|
+
this.pending.set(eventId, resolveSettled);
|
|
55
|
+
if (this.idleTimer) {
|
|
56
|
+
clearTimeout(this.idleTimer);
|
|
57
|
+
this.idleTimer = null;
|
|
58
|
+
}
|
|
59
|
+
this.ensureSocket();
|
|
60
|
+
return {
|
|
61
|
+
settled,
|
|
62
|
+
isConnected: () => this.connected,
|
|
63
|
+
dispose: () => {
|
|
64
|
+
this.pending.delete(eventId);
|
|
65
|
+
if (this.pending.size === 0)
|
|
66
|
+
this.scheduleIdleClose();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
ensureSocket() {
|
|
71
|
+
if (this.sock || !this.wsUrl || !this.factory)
|
|
72
|
+
return;
|
|
73
|
+
try {
|
|
74
|
+
const sep = this.wsUrl.includes('?') ? '&' : '?';
|
|
75
|
+
const sock = this.factory(`${this.wsUrl}${sep}apiKey=${encodeURIComponent(this.apiKey)}`);
|
|
76
|
+
this.sock = sock;
|
|
77
|
+
sock.addEventListener('open', () => {
|
|
78
|
+
this.connected = true;
|
|
79
|
+
this.pingTimer = setInterval(() => {
|
|
80
|
+
try {
|
|
81
|
+
if (sock.readyState === WS_OPEN)
|
|
82
|
+
sock.send(JSON.stringify({ type: 'ping' }));
|
|
83
|
+
}
|
|
84
|
+
catch { /* socket died mid-send — close handler cleans up */ }
|
|
85
|
+
}, PING_INTERVAL_MS);
|
|
86
|
+
// MCP servers are short-lived child processes; an unref'd
|
|
87
|
+
// timer must not keep one alive after stdio closes.
|
|
88
|
+
this.pingTimer.unref?.();
|
|
89
|
+
});
|
|
90
|
+
sock.addEventListener('message', (event) => this.handleMessage(event.data));
|
|
91
|
+
const drop = () => this.teardownSocket();
|
|
92
|
+
sock.addEventListener('close', drop);
|
|
93
|
+
sock.addEventListener('error', drop);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
this.teardownSocket();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
handleMessage(data) {
|
|
100
|
+
if (typeof data !== 'string')
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const msg = JSON.parse(data);
|
|
104
|
+
if (msg.type !== 'hook.responded' && msg.type !== 'hook.cancelled')
|
|
105
|
+
return;
|
|
106
|
+
const eventId = msg.data?.eventId;
|
|
107
|
+
if (typeof eventId !== 'string')
|
|
108
|
+
return;
|
|
109
|
+
// Resolve-and-keep: the poll loop confirms over REST and then
|
|
110
|
+
// disposes; resolving a missing id is a no-op by design.
|
|
111
|
+
this.pending.get(eventId)?.();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* non-JSON frame — ignore */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
scheduleIdleClose() {
|
|
118
|
+
if (this.idleTimer)
|
|
119
|
+
clearTimeout(this.idleTimer);
|
|
120
|
+
this.idleTimer = setTimeout(() => this.teardownSocket(), IDLE_CLOSE_MS);
|
|
121
|
+
this.idleTimer.unref?.();
|
|
122
|
+
}
|
|
123
|
+
teardownSocket() {
|
|
124
|
+
this.connected = false;
|
|
125
|
+
if (this.pingTimer) {
|
|
126
|
+
clearInterval(this.pingTimer);
|
|
127
|
+
this.pingTimer = null;
|
|
128
|
+
}
|
|
129
|
+
if (this.idleTimer) {
|
|
130
|
+
clearTimeout(this.idleTimer);
|
|
131
|
+
this.idleTimer = null;
|
|
132
|
+
}
|
|
133
|
+
const sock = this.sock;
|
|
134
|
+
this.sock = null;
|
|
135
|
+
try {
|
|
136
|
+
sock?.close();
|
|
137
|
+
}
|
|
138
|
+
catch { /* already closed */ }
|
|
139
|
+
// Pending waits stay pending — they simply lose the fast path and
|
|
140
|
+
// their poll loops (which check isConnected) tighten back up.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
exports.HookResponseWaiter = HookResponseWaiter;
|
package/package.json
CHANGED