@hwip/mcp-server 0.0.0-stage → 0.1.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.
Files changed (78) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +10 -0
  3. package/README.md +413 -2
  4. package/dist/agent-target.d.ts +45 -0
  5. package/dist/agent-target.js +81 -0
  6. package/dist/api-client.d.ts +162 -0
  7. package/dist/api-client.js +517 -0
  8. package/dist/ask-context-attach.d.ts +67 -0
  9. package/dist/ask-context-attach.js +147 -0
  10. package/dist/ask-seal.d.ts +118 -0
  11. package/dist/ask-seal.js +194 -0
  12. package/dist/command-seal.d.ts +49 -0
  13. package/dist/command-seal.js +90 -0
  14. package/dist/command-seq.d.ts +30 -0
  15. package/dist/command-seq.js +147 -0
  16. package/dist/config.d.ts +93 -0
  17. package/dist/config.js +561 -0
  18. package/dist/crypto.d.ts +234 -0
  19. package/dist/crypto.js +606 -0
  20. package/dist/error-format.d.ts +7 -0
  21. package/dist/error-format.js +77 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +30 -0
  24. package/dist/key-changes.d.ts +50 -0
  25. package/dist/key-changes.js +163 -0
  26. package/dist/mime.d.ts +16 -0
  27. package/dist/mime.js +58 -0
  28. package/dist/peer-key.d.ts +36 -0
  29. package/dist/peer-key.js +35 -0
  30. package/dist/poll.d.ts +24 -0
  31. package/dist/poll.js +156 -0
  32. package/dist/push-audience.d.ts +35 -0
  33. package/dist/push-audience.js +64 -0
  34. package/dist/redirect.d.ts +8 -0
  35. package/dist/redirect.js +17 -0
  36. package/dist/remote-state.d.ts +38 -0
  37. package/dist/remote-state.js +81 -0
  38. package/dist/resources/channels.d.ts +4 -0
  39. package/dist/resources/channels.js +24 -0
  40. package/dist/resources/devices.d.ts +4 -0
  41. package/dist/resources/devices.js +24 -0
  42. package/dist/sanitize.d.ts +41 -0
  43. package/dist/sanitize.js +106 -0
  44. package/dist/self-register.d.ts +34 -0
  45. package/dist/self-register.js +51 -0
  46. package/dist/server.d.ts +4 -0
  47. package/dist/server.js +63 -0
  48. package/dist/tools/agent-local.d.ts +17 -0
  49. package/dist/tools/agent-local.js +139 -0
  50. package/dist/tools/agent-send.d.ts +6 -0
  51. package/dist/tools/agent-send.js +89 -0
  52. package/dist/tools/ask.d.ts +9 -0
  53. package/dist/tools/ask.js +275 -0
  54. package/dist/tools/broadcast.d.ts +5 -0
  55. package/dist/tools/broadcast.js +42 -0
  56. package/dist/tools/clipboard.d.ts +5 -0
  57. package/dist/tools/clipboard.js +47 -0
  58. package/dist/tools/dismiss.d.ts +5 -0
  59. package/dist/tools/dismiss.js +49 -0
  60. package/dist/tools/file.d.ts +5 -0
  61. package/dist/tools/file.js +169 -0
  62. package/dist/tools/list.d.ts +4 -0
  63. package/dist/tools/list.js +44 -0
  64. package/dist/tools/lookup.d.ts +10 -0
  65. package/dist/tools/lookup.js +45 -0
  66. package/dist/tools/notify.d.ts +5 -0
  67. package/dist/tools/notify.js +118 -0
  68. package/dist/tools/rename.d.ts +5 -0
  69. package/dist/tools/rename.js +45 -0
  70. package/dist/trust-pins.d.ts +70 -0
  71. package/dist/trust-pins.js +269 -0
  72. package/dist/types.d.ts +157 -0
  73. package/dist/types.js +2 -0
  74. package/dist/url-check.d.ts +11 -0
  75. package/dist/url-check.js +57 -0
  76. package/dist/ws-wait.d.ts +72 -0
  77. package/dist/ws-wait.js +200 -0
  78. package/package.json +70 -4
@@ -0,0 +1,200 @@
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 < 22), 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. A socket that drops while a wait is
21
+ * pending is opened again after 1 s, doubling to 30 s.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.HookResponseWaiter = exports.wsFastPathNotice = void 0;
25
+ const WS_OPEN = 1;
26
+ const PING_INTERVAL_MS = 25_000;
27
+ const IDLE_CLOSE_MS = 30_000;
28
+ const RECONNECT_FIRST_MS = 1_000;
29
+ const RECONNECT_MAX_MS = 30_000;
30
+ const defaultFactory = () => {
31
+ const WS = globalThis.WebSocket;
32
+ if (!WS)
33
+ return undefined;
34
+ return (url) => new WS(url);
35
+ };
36
+ /**
37
+ * A start-up line when hwip_ask cannot be woken over the WebSocket, or null. Without it the
38
+ * fast path is off in silence and every answer waits for the next poll (`poll.ts`).
39
+ */
40
+ const wsFastPathNotice = (wsUrl, hasWebSocket = !!defaultFactory()) => {
41
+ if (!wsUrl) {
42
+ return '[WS] No WebSocket URL for this API — hwip_ask waits by polling. Set "wsUrl" in ~/.hwip/config.json (`hwip login` writes it).';
43
+ }
44
+ if (!hasWebSocket) {
45
+ return `[WS] Node ${process.versions.node} has no WebSocket — hwip_ask waits by polling. Node 22 or later answers at once.`;
46
+ }
47
+ return null;
48
+ };
49
+ exports.wsFastPathNotice = wsFastPathNotice;
50
+ class HookResponseWaiter {
51
+ wsUrl;
52
+ apiKey;
53
+ factory;
54
+ sock = null;
55
+ connected = false;
56
+ pending = new Map();
57
+ pingTimer = null;
58
+ idleTimer = null;
59
+ reconnectTimer = null;
60
+ /** Reconnects since the socket was last open: the next one waits 2^n s, up to 30 s. */
61
+ reconnects = 0;
62
+ constructor(opts) {
63
+ this.wsUrl = opts.wsUrl;
64
+ this.apiKey = opts.apiKey;
65
+ this.factory = opts.factory ?? defaultFactory();
66
+ }
67
+ /** False when the fast path can't exist — caller skips subscribe(). */
68
+ canWait() {
69
+ return !!this.wsUrl && !!this.factory;
70
+ }
71
+ subscribe(eventId) {
72
+ let resolveSettled = () => undefined;
73
+ const settled = new Promise((resolve) => { resolveSettled = resolve; });
74
+ this.pending.set(eventId, resolveSettled);
75
+ if (this.idleTimer) {
76
+ clearTimeout(this.idleTimer);
77
+ this.idleTimer = null;
78
+ }
79
+ this.ensureSocket();
80
+ return {
81
+ settled,
82
+ isConnected: () => this.connected,
83
+ dispose: () => {
84
+ this.pending.delete(eventId);
85
+ if (this.pending.size === 0)
86
+ this.scheduleIdleClose();
87
+ },
88
+ };
89
+ }
90
+ ensureSocket() {
91
+ if (this.sock || !this.wsUrl || !this.factory)
92
+ return;
93
+ if (this.reconnectTimer) {
94
+ clearTimeout(this.reconnectTimer);
95
+ this.reconnectTimer = null;
96
+ }
97
+ try {
98
+ const sep = this.wsUrl.includes('?') ? '&' : '?';
99
+ // KNOWN TRADEOFF: apiKey rides the query string — the WHATWG
100
+ // WebSocket (Node 22+ global) can't set headers, and the API GW
101
+ // $connect route authenticates on the query param. It can land in
102
+ // gateway access logs; first-message auth needs a server change.
103
+ const sock = this.factory(`${this.wsUrl}${sep}apiKey=${encodeURIComponent(this.apiKey)}`);
104
+ this.sock = sock;
105
+ sock.addEventListener('open', () => {
106
+ if (this.sock !== sock)
107
+ return; // stale socket — already replaced
108
+ this.connected = true;
109
+ this.reconnects = 0;
110
+ this.pingTimer = setInterval(() => {
111
+ try {
112
+ if (sock.readyState === WS_OPEN)
113
+ sock.send(JSON.stringify({ type: 'ping' }));
114
+ }
115
+ catch { /* socket died mid-send — close handler cleans up */ }
116
+ }, PING_INTERVAL_MS);
117
+ // MCP servers are short-lived child processes; an unref'd
118
+ // timer must not keep one alive after stdio closes.
119
+ this.pingTimer.unref?.();
120
+ });
121
+ sock.addEventListener('message', (event) => this.handleMessage(event.data));
122
+ // Guard against stale events: 'error' and 'close' both fire on a
123
+ // dying socket, and a new subscribe() may have already created a
124
+ // replacement between them — a late event from the old socket
125
+ // must not tear down the new one.
126
+ const drop = () => {
127
+ if (this.sock !== sock)
128
+ return;
129
+ this.teardownSocket();
130
+ this.scheduleReconnect();
131
+ };
132
+ sock.addEventListener('close', drop);
133
+ sock.addEventListener('error', drop);
134
+ }
135
+ catch {
136
+ this.teardownSocket();
137
+ }
138
+ }
139
+ handleMessage(data) {
140
+ if (typeof data !== 'string')
141
+ return;
142
+ try {
143
+ const msg = JSON.parse(data);
144
+ if (msg.type !== 'hook.responded' && msg.type !== 'hook.cancelled')
145
+ return;
146
+ const eventId = msg.data?.eventId;
147
+ if (typeof eventId !== 'string')
148
+ return;
149
+ // Resolve-and-keep: the poll loop confirms over REST and then
150
+ // disposes; resolving a missing id is a no-op by design.
151
+ this.pending.get(eventId)?.();
152
+ }
153
+ catch {
154
+ /* non-JSON frame — ignore */
155
+ }
156
+ }
157
+ /** A wait still pending gets its fast path back; the poll loop covers the gap. */
158
+ scheduleReconnect() {
159
+ if (this.pending.size === 0 || this.reconnectTimer)
160
+ return;
161
+ const delay = Math.min(RECONNECT_FIRST_MS * 2 ** this.reconnects, RECONNECT_MAX_MS);
162
+ this.reconnects += 1;
163
+ this.reconnectTimer = setTimeout(() => {
164
+ this.reconnectTimer = null;
165
+ if (this.pending.size > 0)
166
+ this.ensureSocket();
167
+ }, delay);
168
+ this.reconnectTimer.unref?.();
169
+ }
170
+ scheduleIdleClose() {
171
+ if (this.idleTimer)
172
+ clearTimeout(this.idleTimer);
173
+ this.idleTimer = setTimeout(() => this.teardownSocket(), IDLE_CLOSE_MS);
174
+ this.idleTimer.unref?.();
175
+ }
176
+ teardownSocket() {
177
+ this.connected = false;
178
+ if (this.pingTimer) {
179
+ clearInterval(this.pingTimer);
180
+ this.pingTimer = null;
181
+ }
182
+ if (this.idleTimer) {
183
+ clearTimeout(this.idleTimer);
184
+ this.idleTimer = null;
185
+ }
186
+ if (this.reconnectTimer) {
187
+ clearTimeout(this.reconnectTimer);
188
+ this.reconnectTimer = null;
189
+ }
190
+ const sock = this.sock;
191
+ this.sock = null;
192
+ try {
193
+ sock?.close();
194
+ }
195
+ catch { /* already closed */ }
196
+ // Pending waits stay pending — they simply lose the fast path and
197
+ // their poll loops (which check isConnected) tighten back up.
198
+ }
199
+ }
200
+ exports.HookResponseWaiter = HookResponseWaiter;
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "@hwip/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Hwip MCP server — AI agent notifications, prompts, and input via MCP protocol",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "hwip-mcp": "./dist/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.17.0"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "!dist/**/*.map",
16
+ "!dist/**/*.tsbuildinfo",
17
+ "NOTICE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.31.0",
27
+ "zod": "^3.25.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "typescript": "^5.8.0",
32
+ "vitest": "^4.1.11"
33
+ },
34
+ "release": {
35
+ "branches": [
36
+ "main"
37
+ ],
38
+ "plugins": [
39
+ "@semantic-release/commit-analyzer",
40
+ "@semantic-release/release-notes-generator",
41
+ "@semantic-release/npm",
42
+ "@semantic-release/github"
43
+ ]
44
+ },
45
+ "author": "Hwip <dev@hwip.app>",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/hwipit/mcp-server"
49
+ },
50
+ "homepage": "https://github.com/hwipit/mcp-server",
51
+ "bugs": {
52
+ "url": "https://github.com/hwipit/mcp-server/issues"
53
+ },
54
+ "keywords": [
55
+ "hwip",
56
+ "mcp",
57
+ "ai",
58
+ "notification",
59
+ "prompt",
60
+ "model-context-protocol",
61
+ "claude",
62
+ "ai-agent",
63
+ "devtools",
64
+ "cursor",
65
+ "gemini"
66
+ ],
67
+ "license": "Apache-2.0",
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "registry": "https://registry.npmjs.org/"
71
+ }
72
+ }