@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 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
@@ -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>;
@@ -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":"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;
@@ -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;AAgGF,eAAO,MAAM,UAAU,QAAO,eAsB7B,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
- /** Detect Claude Code session ID from ~/.claude/projects/{projectHash}/{sessionId}/ */
47
- const detectClaudeSessionId = () => {
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 name of (0, fs_1.readdirSync)(sessionsDir)) {
54
- if (!/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(name))
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 fullPath = (0, path_1.join)(sessionsDir, name);
57
- const stat = (0, fs_1.statSync)(fullPath);
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 sessionId = resolvedEnv('ZEPH_SESSION_ID') ?? detectClaudeSessionId() ?? `sess_${(0, crypto_1.randomBytes)(12).toString('base64url')}`;
129
- const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
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(detectProjectDir()),
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: any) => Promise<void>;
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
@@ -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,CA6ClC,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
- const pollForResponse = async (client, hookId, eventId, timeoutSeconds, ctx) => {
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
- while (Date.now() - startTime < timeoutMs) {
13
- const event = await client.getHookEvent(hookId, eventId);
14
- if (event.data.status === 'responded')
15
- return event;
16
- if (event.data.status === 'cancelled')
17
- throw new Error('User cancelled the request');
18
- if (event.data.status === 'timed_out')
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
- }
25
- // Throttled progress notification (every 5s)
26
- if (progressToken !== undefined) {
27
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
28
- if (elapsed - lastProgressAt >= PROGRESS_INTERVAL_SECONDS) {
29
- lastProgressAt = elapsed;
30
- await ctx.sendNotification({
31
- method: 'notifications/progress',
32
- params: {
33
- progressToken,
34
- progress: elapsed,
35
- total: timeoutSeconds,
36
- message: `Waiting for user response... (${elapsed}s / ${timeoutSeconds}s)`,
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
- // Adaptive interval: poll every 1s while the user is likely still at
42
- // their device (first ~60 attempts ≈ 1 min), then back off to 3s for
43
- // long waits. The tight 1s window keeps post-tap detection snappy.
44
- const interval = attempt < 60 ? 1000 : 3000;
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;
@@ -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
- export declare const registerAskTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
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
@@ -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,EAAmB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAwBrE,eAAO,MAAM,eAAe,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAgIhG,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 });
@@ -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
- export declare const registerInputTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
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;AAGrE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAwDlG,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"}
@@ -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 });
@@ -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
- export declare const registerPromptTool: (server: McpServer, client: ZephApiClient, config: McpServerConfig) => void;
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;AAGrE,eAAO,MAAM,kBAAkB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,EAAE,QAAQ,eAAe,SAuEnG,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"}
@@ -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;
@@ -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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/mcp-server",
3
- "version": "1.11.3",
3
+ "version": "1.13.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",