wake-bridge 0.9.0-preview.8

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.
Files changed (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +222 -0
  3. package/dist/src/adapters/botlingknows.d.ts +42 -0
  4. package/dist/src/adapters/botlingknows.js +141 -0
  5. package/dist/src/adapters/botlingknows.js.map +1 -0
  6. package/dist/src/adapters/group-chat.d.ts +43 -0
  7. package/dist/src/adapters/group-chat.js +120 -0
  8. package/dist/src/adapters/group-chat.js.map +1 -0
  9. package/dist/src/cli.d.ts +2 -0
  10. package/dist/src/cli.js +637 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/connector-catalog.d.ts +17 -0
  13. package/dist/src/connector-catalog.js +43 -0
  14. package/dist/src/connector-catalog.js.map +1 -0
  15. package/dist/src/connectors/botlingknows-mcp.d.ts +15 -0
  16. package/dist/src/connectors/botlingknows-mcp.js +130 -0
  17. package/dist/src/connectors/botlingknows-mcp.js.map +1 -0
  18. package/dist/src/connectors/group-chat-http.d.ts +17 -0
  19. package/dist/src/connectors/group-chat-http.js +226 -0
  20. package/dist/src/connectors/group-chat-http.js.map +1 -0
  21. package/dist/src/connectors/mcp-http-client.d.ts +11 -0
  22. package/dist/src/connectors/mcp-http-client.js +108 -0
  23. package/dist/src/connectors/mcp-http-client.js.map +1 -0
  24. package/dist/src/core.d.ts +170 -0
  25. package/dist/src/core.js +1720 -0
  26. package/dist/src/core.js.map +1 -0
  27. package/dist/src/daemon.d.ts +31 -0
  28. package/dist/src/daemon.js +605 -0
  29. package/dist/src/daemon.js.map +1 -0
  30. package/dist/src/db.d.ts +31 -0
  31. package/dist/src/db.js +513 -0
  32. package/dist/src/db.js.map +1 -0
  33. package/dist/src/event-sdk.d.ts +43 -0
  34. package/dist/src/event-sdk.js +152 -0
  35. package/dist/src/event-sdk.js.map +1 -0
  36. package/dist/src/host-adapter-registry.d.ts +21 -0
  37. package/dist/src/host-adapter-registry.js +59 -0
  38. package/dist/src/host-adapter-registry.js.map +1 -0
  39. package/dist/src/index.d.ts +5 -0
  40. package/dist/src/index.js +6 -0
  41. package/dist/src/index.js.map +1 -0
  42. package/dist/src/instance-config.d.ts +46 -0
  43. package/dist/src/instance-config.js +176 -0
  44. package/dist/src/instance-config.js.map +1 -0
  45. package/dist/src/mcp.d.ts +51 -0
  46. package/dist/src/mcp.js +728 -0
  47. package/dist/src/mcp.js.map +1 -0
  48. package/dist/src/operator-control.d.ts +82 -0
  49. package/dist/src/operator-control.js +220 -0
  50. package/dist/src/operator-control.js.map +1 -0
  51. package/dist/src/out-of-process-host.d.ts +37 -0
  52. package/dist/src/out-of-process-host.js +196 -0
  53. package/dist/src/out-of-process-host.js.map +1 -0
  54. package/dist/src/policy-control.d.ts +3 -0
  55. package/dist/src/policy-control.js +120 -0
  56. package/dist/src/policy-control.js.map +1 -0
  57. package/dist/src/release-lifecycle.d.ts +98 -0
  58. package/dist/src/release-lifecycle.js +378 -0
  59. package/dist/src/release-lifecycle.js.map +1 -0
  60. package/dist/src/sdk-error.d.ts +6 -0
  61. package/dist/src/sdk-error.js +12 -0
  62. package/dist/src/sdk-error.js.map +1 -0
  63. package/dist/src/source-adapter.d.ts +32 -0
  64. package/dist/src/source-adapter.js +164 -0
  65. package/dist/src/source-adapter.js.map +1 -0
  66. package/dist/src/source-connector.d.ts +50 -0
  67. package/dist/src/source-connector.js +358 -0
  68. package/dist/src/source-connector.js.map +1 -0
  69. package/dist/src/source-control-client.d.ts +38 -0
  70. package/dist/src/source-control-client.js +61 -0
  71. package/dist/src/source-control-client.js.map +1 -0
  72. package/dist/src/source-ingress.d.ts +16 -0
  73. package/dist/src/source-ingress.js +47 -0
  74. package/dist/src/source-ingress.js.map +1 -0
  75. package/dist/src/source-sdk.d.ts +6 -0
  76. package/dist/src/source-sdk.js +53 -0
  77. package/dist/src/source-sdk.js.map +1 -0
  78. package/dist/src/transport-sdk.d.ts +102 -0
  79. package/dist/src/transport-sdk.js +222 -0
  80. package/dist/src/transport-sdk.js.map +1 -0
  81. package/dist/src/transport.d.ts +23 -0
  82. package/dist/src/transport.js +71 -0
  83. package/dist/src/transport.js.map +1 -0
  84. package/dist/src/types.d.ts +522 -0
  85. package/dist/src/types.js +2 -0
  86. package/dist/src/types.js.map +1 -0
  87. package/dist/src/util.d.ts +26 -0
  88. package/dist/src/util.js +136 -0
  89. package/dist/src/util.js.map +1 -0
  90. package/dist/src/version.d.ts +1 -0
  91. package/dist/src/version.js +2 -0
  92. package/dist/src/version.js.map +1 -0
  93. package/docs/adapters/botlingknows.md +40 -0
  94. package/docs/adapters/gmail-planned.md +16 -0
  95. package/docs/adapters/group-chat.md +126 -0
  96. package/docs/operations/connector-catalog.md +40 -0
  97. package/docs/operations/operator-control.md +83 -0
  98. package/docs/operations/out-of-process-host-adapter.md +150 -0
  99. package/docs/operations/policies.md +106 -0
  100. package/docs/operations/public-sdk.md +87 -0
  101. package/docs/operations/release-lifecycle.md +154 -0
  102. package/docs/operations/source-connectors.md +163 -0
  103. package/docs/operations/source-ingress.md +108 -0
  104. package/docs/operations/templates/botlingknows-connector.env.example +5 -0
  105. package/docs/operations/templates/sources.json.example +15 -0
  106. package/docs/quickstart.md +143 -0
  107. package/docs/releases/0.9.0-preview.8.md +44 -0
  108. package/docs/verification.md +19 -0
  109. package/examples/policies/botlingknows-conservative.json +44 -0
  110. package/examples/reference-host-adapter/README.md +41 -0
  111. package/examples/reference-host-adapter/adapter.mjs +173 -0
  112. package/examples/reference-host-adapter/host-adapters.json.example +26 -0
  113. package/package.json +94 -0
  114. package/schemas/event-v1.schema.json +26 -0
  115. package/schemas/policy-v1.schema.json +66 -0
@@ -0,0 +1,728 @@
1
+ import { createInterface } from "node:readline";
2
+ import { BridgeError, WakeBridge } from "./core.js";
3
+ import { RELEASE_VERSION } from "./version.js";
4
+ /** MCP protocol version understood by this local stdio server. */
5
+ export const MCP_PROTOCOL_VERSION = "2025-06-18";
6
+ export const MCP_SERVER_NAME = "wake-bridge";
7
+ export const MCP_SERVER_VERSION = RELEASE_VERSION;
8
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set([MCP_PROTOCOL_VERSION, "2025-03-26", "2024-11-05"]);
9
+ const CLAIM_STATES = ["pending", "deferred", "eligible", "batched", "consumed", "dismissed", "expired"];
10
+ const BATCH_STATES = ["pending", "waiting_for_endpoint", "waiting_for_waiter", "dispatching", "retry_wait", "dispatched", "seen", "cancelled", "needs_attention", "dead_letter"];
11
+ const EVENT_STATES = ["received", "matched", "suppressed", "batched", "consumed", "expired"];
12
+ const ABSOLUTE_RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u;
13
+ function isPlainObject(value) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value))
15
+ return false;
16
+ const prototype = Object.getPrototypeOf(value);
17
+ return prototype === Object.prototype || prototype === null;
18
+ }
19
+ function compactClaim(claim) {
20
+ return {
21
+ id: claim.id,
22
+ claim_id: claim.id,
23
+ origin: claim.origin,
24
+ state: claim.state,
25
+ resource: claim.resource,
26
+ attention_channel: claim.attention_channel,
27
+ eligible_after: claim.eligible_after,
28
+ expires_at: claim.expires_at ?? null,
29
+ defer_while_presence: claim.defer_while_presence,
30
+ reason_code: claim.reason_code,
31
+ ...(claim.note == null ? {} : { note: claim.note }),
32
+ ...(claim.snooze_until == null ? {} : { snooze_until: claim.snooze_until }),
33
+ event_ids: claim.event_ids,
34
+ };
35
+ }
36
+ function compactBatch(batch) {
37
+ return {
38
+ id: batch.id,
39
+ batch_id: batch.id,
40
+ state: batch.state,
41
+ attention_channel: batch.attention_channel,
42
+ claim_ids: batch.claim_ids,
43
+ event_ids: batch.event_ids,
44
+ not_before: batch.not_before,
45
+ deadline: batch.deadline ?? null,
46
+ attempt: batch.attempt,
47
+ binding_generation: batch.binding_generation ?? null,
48
+ ...(batch.last_error == null ? {} : { last_error: batch.last_error }),
49
+ };
50
+ }
51
+ function compactEvent(event) {
52
+ return {
53
+ event_id: event.id,
54
+ source: event.source,
55
+ type: event.type,
56
+ state: event.state,
57
+ occurred_at: event.occurred_at,
58
+ received_at: event.received_at,
59
+ resource: event.resource,
60
+ attention_channel_hint: event.attention_channel_hint ?? null,
61
+ matched_policy_id: event.matched_policy_id ?? null,
62
+ matched_policy_version: event.matched_policy_version ?? null,
63
+ };
64
+ }
65
+ function asString(args, key, required = true, max = 4096) {
66
+ const value = args[key];
67
+ if (value === undefined && !required)
68
+ return undefined;
69
+ if (typeof value !== "string" || !value.trim() || value.length > max) {
70
+ throw new BridgeError(`${key} is required and bounded`, "invalid_arguments", 400);
71
+ }
72
+ return value;
73
+ }
74
+ function asOptionalString(args, key, max = 4096) {
75
+ return asString(args, key, false, max);
76
+ }
77
+ function asOptionalBoolean(args, key) {
78
+ const value = args[key];
79
+ if (value === undefined)
80
+ return undefined;
81
+ if (typeof value !== "boolean")
82
+ throw new BridgeError(`${key} must be boolean`, "invalid_arguments", 400);
83
+ return value;
84
+ }
85
+ function asInteger(args, key, minimum, maximum) {
86
+ const value = args[key];
87
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
88
+ throw new BridgeError(`${key} must be an integer between ${minimum} and ${maximum}`, "invalid_arguments", 400);
89
+ }
90
+ return Number(value);
91
+ }
92
+ function asAbsoluteTimestamp(args, key) {
93
+ const value = asString(args, key, true, 80);
94
+ if (!ABSOLUTE_RFC3339.test(value) || Number.isNaN(new Date(value).getTime())) {
95
+ throw new BridgeError(`${key} must be an absolute RFC3339 timestamp with Z or numeric offset`, "invalid_arguments", 400);
96
+ }
97
+ return value;
98
+ }
99
+ function normalizeResource(value) {
100
+ if (typeof value === "string") {
101
+ if (!value.trim() || value.length > 2048)
102
+ throw new BridgeError("resource URI is required and bounded", "invalid_arguments", 400);
103
+ return { uri: value };
104
+ }
105
+ if (!isPlainObject(value))
106
+ throw new BridgeError("resource must be a URI or object", "invalid_arguments", 400);
107
+ const unknown = Object.keys(value).filter((key) => !["uri", "cursor"].includes(key));
108
+ if (unknown.length)
109
+ throw new BridgeError("resource has unsupported fields", "invalid_arguments", 400);
110
+ const uri = asString(value, "uri", true, 2048);
111
+ const cursor = value.cursor;
112
+ if (cursor !== undefined && cursor !== null && typeof cursor !== "string" && !(typeof cursor === "number" && Number.isFinite(cursor))) {
113
+ throw new BridgeError("resource.cursor must be a string, number, or null", "invalid_arguments", 400);
114
+ }
115
+ return { uri, ...(cursor === undefined ? {} : { cursor }) };
116
+ }
117
+ function jsonSize(value) {
118
+ try {
119
+ return JSON.stringify(value).length;
120
+ }
121
+ catch {
122
+ return Number.POSITIVE_INFINITY;
123
+ }
124
+ }
125
+ function asJsonValue(value, key, max = 4096) {
126
+ if (value === undefined)
127
+ return undefined;
128
+ if (jsonSize(value) > max)
129
+ throw new BridgeError(`${key} is too large`, "oversized_arguments", 413);
130
+ try {
131
+ JSON.stringify(value);
132
+ }
133
+ catch {
134
+ throw new BridgeError(`${key} must be JSON`, "invalid_arguments", 400);
135
+ }
136
+ return value;
137
+ }
138
+ function assertAllowed(args, allowed) {
139
+ const unknown = Object.keys(args).filter((key) => !allowed.includes(key));
140
+ if (unknown.length)
141
+ throw new BridgeError("unsupported tool arguments", "invalid_arguments", 400);
142
+ }
143
+ function toolText(value, isError = false) {
144
+ return {
145
+ content: [{ type: "text", text: JSON.stringify(value) }],
146
+ structuredContent: value,
147
+ ...(isError ? { isError: true } : {}),
148
+ };
149
+ }
150
+ function rpcError(id, code, message) {
151
+ return { jsonrpc: "2.0", id, error: { code, message } };
152
+ }
153
+ function requestId(value) {
154
+ return typeof value === "string" || (typeof value === "number" && Number.isFinite(value)) || value === null
155
+ ? value
156
+ : null;
157
+ }
158
+ function toolDefinitions() {
159
+ const resourceSchema = {
160
+ anyOf: [
161
+ { type: "string", minLength: 1, maxLength: 2048 },
162
+ {
163
+ type: "object",
164
+ properties: {
165
+ uri: { type: "string", minLength: 1, maxLength: 2048 },
166
+ cursor: { anyOf: [{ type: "string" }, { type: "number" }, { type: "null" }] },
167
+ },
168
+ required: ["uri"],
169
+ additionalProperties: false,
170
+ },
171
+ ],
172
+ };
173
+ return [
174
+ {
175
+ name: "attention_schedule",
176
+ description: "Schedule a durable owner attention claim for a resource. eligible_after must be RFC3339 with Z or numeric offset. defer_while_presence defaults to true: while the owner is actively present on the bound channel, delivery waits instead of interrupting.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ resource: resourceSchema,
181
+ eligible_after: { type: "string", minLength: 1, pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" },
182
+ attention_channel: { type: "string", minLength: 1, maxLength: 200 },
183
+ reason_code: { type: "string", maxLength: 256 },
184
+ note: { type: ["string", "null"], maxLength: 500 },
185
+ idempotency_key: { type: "string", maxLength: 500 },
186
+ defer_while_presence: { type: "boolean", default: true },
187
+ },
188
+ required: ["resource", "eligible_after"],
189
+ additionalProperties: false,
190
+ },
191
+ },
192
+ {
193
+ name: "attention_ack",
194
+ description: "Acknowledge a dispatched wake batch. Pass wake_batch_id from the Wake Bridge wake envelope.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ wake_batch_id: { type: "string", minLength: 1, maxLength: 200 },
199
+ // batch_id remains a backwards-compatible alias for non-envelope
200
+ // callers; endpoint/generation are intentionally not agent inputs.
201
+ batch_id: { type: "string", minLength: 1, maxLength: 200 },
202
+ },
203
+ anyOf: [{ required: ["wake_batch_id"] }, { required: ["batch_id"] }],
204
+ additionalProperties: false,
205
+ },
206
+ },
207
+ {
208
+ name: "attention_consume",
209
+ description: "Mark one attention claim consumed after it was reviewed or handled.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ claim_id: { type: "string", minLength: 1, maxLength: 200 },
214
+ result: {},
215
+ },
216
+ required: ["claim_id"],
217
+ additionalProperties: false,
218
+ },
219
+ },
220
+ {
221
+ name: "attention_snooze",
222
+ description: "Defer one attention claim until a future ISO timestamp.",
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ claim_id: { type: "string", minLength: 1, maxLength: 200 },
227
+ until: { type: "string", minLength: 1, pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" },
228
+ },
229
+ required: ["claim_id", "until"],
230
+ additionalProperties: false,
231
+ },
232
+ },
233
+ {
234
+ name: "attention_dismiss",
235
+ description: "Dismiss one attention claim without changing its authoritative source. Stale claims are expected when work was handled elsewhere and are cheap to dismiss; repeated irrelevant or identity-mismatched claims can still indicate a source or policy fault.",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ claim_id: { type: "string", minLength: 1, maxLength: 200 },
240
+ reason: { type: "string", maxLength: 500 },
241
+ },
242
+ required: ["claim_id"],
243
+ additionalProperties: false,
244
+ },
245
+ },
246
+ {
247
+ name: "attention_status",
248
+ description: "Inspect compact claim and wake-batch counts, or one requested item.",
249
+ inputSchema: {
250
+ type: "object",
251
+ properties: {
252
+ claim_id: { type: "string", minLength: 1, maxLength: 200 },
253
+ batch_id: { type: "string", minLength: 1, maxLength: 200 },
254
+ channel: { type: "string", minLength: 1, maxLength: 200 },
255
+ },
256
+ additionalProperties: false,
257
+ },
258
+ },
259
+ {
260
+ name: "attention_list",
261
+ description: "List compact attention claims by state, channel, or source.",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ state: { type: "string", enum: CLAIM_STATES },
266
+ channel: { type: "string", minLength: 1, maxLength: 200 },
267
+ source: { type: "string", minLength: 1, maxLength: 128 },
268
+ limit: { type: "integer", minimum: 1, maximum: 1000 },
269
+ },
270
+ additionalProperties: false,
271
+ },
272
+ },
273
+ {
274
+ name: "attention_event_list",
275
+ description: "List compact durable source events and their current match/consume state. Provider payloads and credentials are not returned.",
276
+ inputSchema: {
277
+ type: "object",
278
+ properties: {
279
+ state: { type: "string", enum: EVENT_STATES },
280
+ source: { type: "string", minLength: 1, maxLength: 128 },
281
+ after: { type: "string", minLength: 1, pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" },
282
+ limit: { type: "integer", minimum: 1, maximum: 1000 },
283
+ },
284
+ additionalProperties: false,
285
+ },
286
+ },
287
+ {
288
+ name: "attention_binding_status",
289
+ description: "Inspect the current channel-to-host binding, lease liveness, and public host capabilities without route credentials.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: { channel: { type: "string", minLength: 1, maxLength: 200 } },
293
+ additionalProperties: false,
294
+ },
295
+ },
296
+ {
297
+ name: "attention_wake_health",
298
+ description: "Inspect delivery health. Pass batch_id to drill into its claims, events, attempts, and receipts; omit it for compact actionable delivery issues.",
299
+ inputSchema: {
300
+ type: "object",
301
+ properties: {
302
+ batch_id: { type: "string", minLength: 1, maxLength: 200 },
303
+ channel: { type: "string", minLength: 1, maxLength: 200 },
304
+ limit: { type: "integer", minimum: 1, maximum: 100 },
305
+ },
306
+ additionalProperties: false,
307
+ },
308
+ },
309
+ {
310
+ name: "attention_watch_status",
311
+ description: "Inspect persistent inactivity watches, current binding generation, epoch, sequence, activity baseline, and next due time. Omit attention_channel to list all watches.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: { attention_channel: { type: "string", minLength: 1, maxLength: 200 } },
315
+ additionalProperties: false,
316
+ },
317
+ },
318
+ {
319
+ name: "attention_watch_configure",
320
+ description: "Enable, update, or disable a persistent inactivity watch for one attention channel. mode=once triggers once per inactive epoch; mode=repeat continues at repeat_after_seconds while the inactivity condition remains true. Repeating the same configuration is idempotent. The bound host supplies activity evidence.",
321
+ inputSchema: {
322
+ type: "object",
323
+ properties: {
324
+ attention_channel: { type: "string", minLength: 1, maxLength: 200 },
325
+ enabled: { type: "boolean" },
326
+ idle_after_seconds: { type: "integer", minimum: 60, maximum: 604800 },
327
+ mode: { type: "string", enum: ["once", "repeat"], default: "once" },
328
+ repeat_after_seconds: { type: "integer", minimum: 60, maximum: 604800 },
329
+ },
330
+ required: ["attention_channel", "enabled", "idle_after_seconds"],
331
+ additionalProperties: false,
332
+ },
333
+ },
334
+ {
335
+ name: "attention_source_status",
336
+ description: "List installed source connector slots, durable identity checkpoints, and current enable/health state. Provider credentials are never returned.",
337
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
338
+ },
339
+ {
340
+ name: "attention_source_verify",
341
+ description: "Read one installed connector manifest and compare its non-secret subject/binding identity with the durable checkpoint. This does not enable or poll the source.",
342
+ inputSchema: { type: "object", properties: { source: { type: "string", minLength: 1, maxLength: 128 } }, required: ["source"], additionalProperties: false },
343
+ },
344
+ {
345
+ name: "attention_source_bootstrap",
346
+ description: "Create a first from-now checkpoint for a disabled source. Pass the exact subject_ref and binding_fingerprint returned by attention_source_verify; no historical events are created.",
347
+ inputSchema: {
348
+ type: "object",
349
+ properties: {
350
+ source: { type: "string", minLength: 1, maxLength: 128 },
351
+ expected_subject_ref: { type: "string", minLength: 1, maxLength: 200 },
352
+ expected_binding_fingerprint: { type: "string", pattern: "^sha256:[0-9a-f]{64}$" },
353
+ },
354
+ required: ["source", "expected_subject_ref", "expected_binding_fingerprint"], additionalProperties: false,
355
+ },
356
+ },
357
+ {
358
+ name: "attention_source_rebind",
359
+ description: "Replace a disabled source checkpoint after connector identity changes. The previous checkpoint is archived; pass its exact revision and the newly verified subject/binding.",
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ source: { type: "string", minLength: 1, maxLength: 128 },
364
+ expected_checkpoint_revision: { type: "integer", minimum: 1 },
365
+ expected_subject_ref: { type: "string", minLength: 1, maxLength: 200 },
366
+ expected_binding_fingerprint: { type: "string", pattern: "^sha256:[0-9a-f]{64}$" },
367
+ reason: { type: "string", minLength: 1, maxLength: 300 },
368
+ },
369
+ required: ["source", "expected_checkpoint_revision", "expected_subject_ref", "expected_binding_fingerprint", "reason"], additionalProperties: false,
370
+ },
371
+ },
372
+ {
373
+ name: "attention_source_enable",
374
+ description: "Enable polling for a source only when its live connector identity matches its durable checkpoint. Enable intent survives daemon restart.",
375
+ inputSchema: { type: "object", properties: { source: { type: "string", minLength: 1, maxLength: 128 } }, required: ["source"], additionalProperties: false },
376
+ },
377
+ {
378
+ name: "attention_source_disable",
379
+ description: "Stop future polling for a source without deleting events, claims, receipts, or checkpoint history. Disable intent survives daemon restart.",
380
+ inputSchema: { type: "object", properties: { source: { type: "string", minLength: 1, maxLength: 128 } }, required: ["source"], additionalProperties: false },
381
+ },
382
+ ];
383
+ }
384
+ /**
385
+ * In-process MCP implementation. It deliberately has no wait/warm tool: a
386
+ * stdio invocation must finish so normal turns can settle and cold transport
387
+ * remains the lifecycle baseline.
388
+ */
389
+ export class WakeBridgeMcpServer {
390
+ bridge;
391
+ sourceControl;
392
+ constructor(options) {
393
+ this.bridge = options instanceof WakeBridge ? options : options.bridge;
394
+ this.sourceControl = options instanceof WakeBridge ? undefined : options.source_control;
395
+ }
396
+ async handleRequest(request) {
397
+ if (!isPlainObject(request) || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
398
+ return rpcError(requestId(isPlainObject(request) ? request.id : null), -32600, "Invalid Request");
399
+ }
400
+ const idPresent = Object.prototype.hasOwnProperty.call(request, "id");
401
+ const id = requestId(request.id);
402
+ const method = request.method;
403
+ if (!idPresent) {
404
+ // MCP notifications, including initialized and cancellation notices,
405
+ // never receive a response. Unknown notifications are ignored.
406
+ return null;
407
+ }
408
+ if (method === "initialize") {
409
+ const initializeParams = isPlainObject(request.params) ? request.params : null;
410
+ const requestedProtocol = typeof initializeParams?.protocolVersion === "string"
411
+ ? initializeParams.protocolVersion
412
+ : MCP_PROTOCOL_VERSION;
413
+ return {
414
+ jsonrpc: "2.0",
415
+ id,
416
+ result: {
417
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.has(requestedProtocol) ? requestedProtocol : MCP_PROTOCOL_VERSION,
418
+ capabilities: { tools: { listChanged: false } },
419
+ serverInfo: { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
420
+ },
421
+ };
422
+ }
423
+ if (method === "ping")
424
+ return { jsonrpc: "2.0", id, result: {} };
425
+ if (method === "tools/list") {
426
+ return { jsonrpc: "2.0", id, result: { tools: toolDefinitions() } };
427
+ }
428
+ if (method !== "tools/call")
429
+ return rpcError(id, -32601, "Method not found");
430
+ const params = request.params;
431
+ if (!isPlainObject(params) || typeof params.name !== "string")
432
+ return rpcError(id, -32602, "Invalid tool call");
433
+ const args = params.arguments === undefined ? {} : params.arguments;
434
+ if (!isPlainObject(args))
435
+ return rpcError(id, -32602, "Tool arguments must be an object");
436
+ try {
437
+ const result = await this.callTool(params.name, args);
438
+ return { jsonrpc: "2.0", id, result: toolText(result) };
439
+ }
440
+ catch (error) {
441
+ const normalized = this.errorValue(error);
442
+ return { jsonrpc: "2.0", id, result: toolText(normalized, true) };
443
+ }
444
+ }
445
+ async callTool(name, args) {
446
+ switch (name) {
447
+ case "attention_schedule": {
448
+ assertAllowed(args, ["resource", "eligible_after", "attention_channel", "reason_code", "note", "idempotency_key", "defer_while_presence"]);
449
+ const scheduled = this.bridge.scheduleClaim({
450
+ resource: normalizeResource(args.resource),
451
+ eligible_after: asAbsoluteTimestamp(args, "eligible_after"),
452
+ attention_channel: asOptionalString(args, "attention_channel", 200),
453
+ reason_code: asOptionalString(args, "reason_code", 256),
454
+ note: args.note === null ? null : asOptionalString(args, "note", 500),
455
+ idempotency_key: asOptionalString(args, "idempotency_key", 500),
456
+ defer_while_presence: asOptionalBoolean(args, "defer_while_presence"),
457
+ });
458
+ return { claim: compactClaim(scheduled.claim), event_id: scheduled.event.id, duplicate: scheduled.duplicate };
459
+ }
460
+ case "attention_ack": {
461
+ assertAllowed(args, ["wake_batch_id", "batch_id"]);
462
+ const envelopeBatchId = asOptionalString(args, "wake_batch_id", 200);
463
+ const aliasBatchId = asOptionalString(args, "batch_id", 200);
464
+ if (!envelopeBatchId && !aliasBatchId)
465
+ throw new BridgeError("wake_batch_id is required", "invalid_arguments", 400);
466
+ if (envelopeBatchId && aliasBatchId && envelopeBatchId !== aliasBatchId) {
467
+ throw new BridgeError("wake_batch_id and batch_id disagree", "invalid_arguments", 400);
468
+ }
469
+ const batchId = envelopeBatchId || aliasBatchId;
470
+ const batch = this.bridge.getBatch(batchId);
471
+ if (!batch)
472
+ throw new BridgeError("batch not found", "batch_not_found", 404);
473
+ const accepted = this.bridge.listAttempts(batchId)
474
+ .filter((attempt) => String(attempt.state) === "accepted")
475
+ .sort((left, right) => String(right.started_at).localeCompare(String(left.started_at)))[0];
476
+ if (!accepted?.endpoint_id || accepted.binding_generation == null) {
477
+ throw new BridgeError("batch has no accepted transport attempt", "batch_not_dispatched", 409);
478
+ }
479
+ // Core re-checks the current binding and generation. Deriving these
480
+ // values from the durable attempt prevents an agent from inventing an
481
+ // endpoint while keeping the public tool usable from WakePayload alone.
482
+ const acknowledged = this.bridge.ackBatch(batchId, String(accepted.endpoint_id), Number(accepted.binding_generation));
483
+ return { batch: compactBatch(acknowledged) };
484
+ }
485
+ case "attention_consume": {
486
+ assertAllowed(args, ["claim_id", "result"]);
487
+ const claim = this.bridge.consumeClaim(asString(args, "claim_id", true, 200), asJsonValue(args.result, "result"));
488
+ return { claim: compactClaim(claim) };
489
+ }
490
+ case "attention_snooze": {
491
+ assertAllowed(args, ["claim_id", "until"]);
492
+ const claim = this.bridge.snoozeClaim(asString(args, "claim_id", true, 200), asAbsoluteTimestamp(args, "until"));
493
+ return { claim: compactClaim(claim) };
494
+ }
495
+ case "attention_dismiss": {
496
+ assertAllowed(args, ["claim_id", "reason"]);
497
+ const claim = this.bridge.dismissClaim(asString(args, "claim_id", true, 200), asOptionalString(args, "reason", 500) || "dismissed");
498
+ return { claim: compactClaim(claim) };
499
+ }
500
+ case "attention_status": {
501
+ assertAllowed(args, ["claim_id", "batch_id", "channel"]);
502
+ const claimId = asOptionalString(args, "claim_id", 200);
503
+ const batchId = asOptionalString(args, "batch_id", 200);
504
+ const channel = asOptionalString(args, "channel", 200);
505
+ if (claimId) {
506
+ const claim = this.bridge.getClaim(claimId);
507
+ if (!claim)
508
+ throw new BridgeError("claim not found", "claim_not_found", 404);
509
+ return { claim: compactClaim(claim) };
510
+ }
511
+ if (batchId) {
512
+ const batch = this.bridge.getBatch(batchId);
513
+ if (!batch)
514
+ throw new BridgeError("batch not found", "batch_not_found", 404);
515
+ return { batch: compactBatch(batch) };
516
+ }
517
+ const claims = this.bridge.listClaims(channel ? { channel } : {});
518
+ const batches = this.bridge.listBatches(channel ? { channel } : {});
519
+ const claimCounts = Object.fromEntries(CLAIM_STATES.map((state) => [state, claims.filter((claim) => claim.state === state).length]));
520
+ const batchCounts = Object.fromEntries(BATCH_STATES.map((state) => [state, batches.filter((batch) => batch.state === state).length]));
521
+ return { now: this.bridge.nowIso(), claims: claimCounts, batches: batchCounts };
522
+ }
523
+ case "attention_list": {
524
+ assertAllowed(args, ["state", "channel", "source", "limit"]);
525
+ const state = asOptionalString(args, "state", 32);
526
+ if (state && !CLAIM_STATES.includes(state))
527
+ throw new BridgeError("invalid claim state", "invalid_arguments", 400);
528
+ const source = asOptionalString(args, "source", 128);
529
+ const channel = asOptionalString(args, "channel", 200);
530
+ const rawLimit = args.limit;
531
+ if (rawLimit !== undefined && (!Number.isInteger(rawLimit) || Number(rawLimit) < 1 || Number(rawLimit) > 1000)) {
532
+ throw new BridgeError("limit must be an integer between 1 and 1000", "invalid_arguments", 400);
533
+ }
534
+ const claims = this.bridge.listClaims({ state, channel, source, limit: rawLimit });
535
+ return { claims: claims.map(compactClaim) };
536
+ }
537
+ case "attention_event_list": {
538
+ assertAllowed(args, ["state", "source", "after", "limit"]);
539
+ const state = asOptionalString(args, "state", 32);
540
+ if (state && !EVENT_STATES.includes(state))
541
+ throw new BridgeError("invalid event state", "invalid_arguments", 400);
542
+ const source = asOptionalString(args, "source", 128);
543
+ const after = args.after === undefined ? undefined : asAbsoluteTimestamp(args, "after");
544
+ const limit = args.limit === undefined ? undefined : asInteger(args, "limit", 1, 1000);
545
+ return { events: this.bridge.listEvents({ state, source, after, limit }).map(compactEvent) };
546
+ }
547
+ case "attention_binding_status": {
548
+ assertAllowed(args, ["channel"]);
549
+ const channel = asOptionalString(args, "channel", 200);
550
+ const now = this.bridge.nowIso();
551
+ const bindings = (channel ? [this.bridge.getBinding(channel)].filter(Boolean) : this.bridge.listBindings()).map((binding) => {
552
+ const endpoint = this.bridge.getEndpoint(binding.endpoint_id);
553
+ return {
554
+ attention_channel: binding.attention_channel,
555
+ endpoint_id: binding.endpoint_id,
556
+ generation: binding.generation,
557
+ bound_at: binding.bound_at,
558
+ live: Boolean(endpoint && endpoint.lease_expires_at > now),
559
+ endpoint: endpoint ? {
560
+ host_kind: endpoint.host_kind,
561
+ session_ref: endpoint.session_ref,
562
+ lease_expires_at: endpoint.lease_expires_at,
563
+ capabilities: endpoint.capabilities,
564
+ } : null,
565
+ };
566
+ });
567
+ if (channel)
568
+ return { now, binding: bindings[0] ?? null };
569
+ return { now, bindings };
570
+ }
571
+ case "attention_wake_health": {
572
+ assertAllowed(args, ["batch_id", "channel", "limit"]);
573
+ const batchId = asOptionalString(args, "batch_id", 200);
574
+ const channel = asOptionalString(args, "channel", 200);
575
+ const limit = args.limit === undefined ? 50 : asInteger(args, "limit", 1, 100);
576
+ const now = this.bridge.nowIso();
577
+ const deliveryState = (batch) => batch.state === "dispatched" ? "accepted_but_unseen" : batch.state;
578
+ const deliverySummary = (batch) => {
579
+ const receipts = this.bridge.listReceipts(batch.id);
580
+ const accepted = receipts.filter((receipt) => receipt.stage === "transport_accepted").at(-1);
581
+ const seen = receipts.some((receipt) => receipt.stage === "agent_seen");
582
+ return {
583
+ ...compactBatch(batch),
584
+ delivery_state: seen ? "seen" : deliveryState(batch),
585
+ unseen_since: !seen && accepted ? accepted.at : null,
586
+ health_evaluated_at: now,
587
+ };
588
+ };
589
+ if (batchId) {
590
+ const batch = this.bridge.getBatch(batchId);
591
+ if (!batch)
592
+ throw new BridgeError("batch not found", "batch_not_found", 404);
593
+ const claims = batch.claim_ids.map((claimId) => this.bridge.getClaim(claimId)).filter((claim) => Boolean(claim));
594
+ const events = batch.event_ids.map((eventId) => this.bridge.getEvent(eventId)).filter((event) => Boolean(event));
595
+ const attempts = this.bridge.listAttempts(batch.id).map((attempt) => ({
596
+ attempt_id: String(attempt.id),
597
+ attempt_no: Number(attempt.attempt_no),
598
+ state: String(attempt.state),
599
+ transport_kind: attempt.transport_kind == null ? null : String(attempt.transport_kind),
600
+ endpoint_id: attempt.endpoint_id == null ? null : String(attempt.endpoint_id),
601
+ binding_generation: attempt.binding_generation == null ? null : Number(attempt.binding_generation),
602
+ started_at: String(attempt.started_at),
603
+ finished_at: attempt.finished_at == null ? null : String(attempt.finished_at),
604
+ error_class: attempt.error_class == null ? null : String(attempt.error_class),
605
+ }));
606
+ const receipts = this.bridge.listReceipts(batch.id).map((receipt) => ({
607
+ receipt_id: receipt.id,
608
+ claim_id: receipt.claim_id,
609
+ stage: receipt.stage,
610
+ at: receipt.at,
611
+ endpoint_id: receipt.endpoint_id,
612
+ binding_generation: receipt.binding_generation,
613
+ transport_kind: receipt.transport_kind,
614
+ }));
615
+ return { delivery: deliverySummary(batch), claims: claims.map(compactClaim), events: events.map(compactEvent), attempts, receipts };
616
+ }
617
+ const actionable = new Set(["waiting_for_endpoint", "waiting_for_waiter", "retry_wait", "dispatched", "needs_attention", "dead_letter"]);
618
+ const batches = this.bridge.listBatches({ channel, limit: 1000 }).filter((batch) => actionable.has(batch.state)).slice(-limit);
619
+ return {
620
+ now,
621
+ deliveries: batches.map(deliverySummary),
622
+ activity_watches: channel ? [this.bridge.getActivityWatch(channel)].filter(Boolean) : this.bridge.listActivityWatches(),
623
+ };
624
+ }
625
+ case "attention_watch_status": {
626
+ assertAllowed(args, ["attention_channel"]);
627
+ const channel = asOptionalString(args, "attention_channel", 200);
628
+ if (channel)
629
+ return { activity_watch: this.bridge.getActivityWatch(channel) };
630
+ return { activity_watches: this.bridge.listActivityWatches() };
631
+ }
632
+ case "attention_watch_configure": {
633
+ assertAllowed(args, ["attention_channel", "enabled", "idle_after_seconds", "mode", "repeat_after_seconds"]);
634
+ if (typeof args.enabled !== "boolean")
635
+ throw new BridgeError("enabled must be boolean", "invalid_arguments", 400);
636
+ const watch = this.bridge.configureActivityWatch({
637
+ attention_channel: asString(args, "attention_channel", true, 200),
638
+ enabled: args.enabled,
639
+ idle_after_ms: asInteger(args, "idle_after_seconds", 60, 604800) * 1000,
640
+ mode: asOptionalString(args, "mode", 32),
641
+ repeat_after_ms: args.repeat_after_seconds === undefined
642
+ ? undefined
643
+ : asInteger(args, "repeat_after_seconds", 60, 604800) * 1000,
644
+ });
645
+ return { activity_watch: watch };
646
+ }
647
+ case "attention_source_status":
648
+ assertAllowed(args, []);
649
+ return await this.sources().status();
650
+ case "attention_source_verify":
651
+ assertAllowed(args, ["source"]);
652
+ return await this.sources().verify(asString(args, "source", true, 128));
653
+ case "attention_source_bootstrap":
654
+ assertAllowed(args, ["source", "expected_subject_ref", "expected_binding_fingerprint"]);
655
+ return await this.sources().bootstrap(asString(args, "source", true, 128), {
656
+ expected_subject_ref: asString(args, "expected_subject_ref", true, 200),
657
+ expected_binding_fingerprint: asString(args, "expected_binding_fingerprint", true, 80),
658
+ });
659
+ case "attention_source_rebind": {
660
+ assertAllowed(args, ["source", "expected_checkpoint_revision", "expected_subject_ref", "expected_binding_fingerprint", "reason"]);
661
+ const revision = Number(args.expected_checkpoint_revision);
662
+ if (!Number.isSafeInteger(revision) || revision < 1)
663
+ throw new BridgeError("expected_checkpoint_revision is invalid", "invalid_arguments", 400);
664
+ return await this.sources().rebind(asString(args, "source", true, 128), {
665
+ expected_checkpoint_revision: revision,
666
+ expected_subject_ref: asString(args, "expected_subject_ref", true, 200),
667
+ expected_binding_fingerprint: asString(args, "expected_binding_fingerprint", true, 80),
668
+ reason: asString(args, "reason", true, 300),
669
+ });
670
+ }
671
+ case "attention_source_enable":
672
+ assertAllowed(args, ["source"]);
673
+ return await this.sources().enable(asString(args, "source", true, 128));
674
+ case "attention_source_disable":
675
+ assertAllowed(args, ["source"]);
676
+ return await this.sources().disable(asString(args, "source", true, 128));
677
+ default:
678
+ throw new BridgeError("unknown tool", "unknown_tool", 404);
679
+ }
680
+ }
681
+ sources() {
682
+ if (!this.sourceControl)
683
+ throw new BridgeError("Wake Bridge source control daemon is not configured", "source_control_unavailable", 503);
684
+ return this.sourceControl;
685
+ }
686
+ errorValue(error) {
687
+ if (error instanceof BridgeError) {
688
+ return { error: error.code, message: error.message.slice(0, 300) };
689
+ }
690
+ // Do not send stack traces, SQL, credentials, or arbitrary provider text
691
+ // over the agent protocol. The detailed failure remains local to Core.
692
+ return { error: "internal_error", message: "Wake Bridge operation failed" };
693
+ }
694
+ }
695
+ export function createMcpServer(options) {
696
+ return new WakeBridgeMcpServer(options);
697
+ }
698
+ /** Run newline-delimited JSON-RPC over stdin/stdout, with no stdout logging. */
699
+ export async function runMcpStdio(server, input = process.stdin, output = process.stdout) {
700
+ const lines = createInterface({ input, crlfDelay: Infinity });
701
+ for await (const line of lines) {
702
+ if (!String(line).trim())
703
+ continue;
704
+ let request;
705
+ try {
706
+ request = JSON.parse(String(line));
707
+ }
708
+ catch {
709
+ output.write(`${JSON.stringify(rpcError(null, -32700, "Parse error"))}\n`);
710
+ continue;
711
+ }
712
+ const response = await server.handleRequest(request);
713
+ if (response)
714
+ output.write(`${JSON.stringify(response)}\n`);
715
+ }
716
+ }
717
+ /** Construct a bridge and serve it until stdin closes. Used by the CLI. */
718
+ export async function runMcp(config) {
719
+ const bridge = new WakeBridge(config);
720
+ try {
721
+ await runMcpStdio(new WakeBridgeMcpServer(bridge));
722
+ }
723
+ finally {
724
+ bridge.close();
725
+ }
726
+ }
727
+ export default WakeBridgeMcpServer;
728
+ //# sourceMappingURL=mcp.js.map