@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.
- package/LICENSE +190 -0
- package/NOTICE +10 -0
- package/README.md +413 -2
- package/dist/agent-target.d.ts +45 -0
- package/dist/agent-target.js +81 -0
- package/dist/api-client.d.ts +162 -0
- package/dist/api-client.js +517 -0
- package/dist/ask-context-attach.d.ts +67 -0
- package/dist/ask-context-attach.js +147 -0
- package/dist/ask-seal.d.ts +118 -0
- package/dist/ask-seal.js +194 -0
- package/dist/command-seal.d.ts +49 -0
- package/dist/command-seal.js +90 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +147 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.js +561 -0
- package/dist/crypto.d.ts +234 -0
- package/dist/crypto.js +606 -0
- package/dist/error-format.d.ts +7 -0
- package/dist/error-format.js +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +30 -0
- package/dist/key-changes.d.ts +50 -0
- package/dist/key-changes.js +163 -0
- package/dist/mime.d.ts +16 -0
- package/dist/mime.js +58 -0
- package/dist/peer-key.d.ts +36 -0
- package/dist/peer-key.js +35 -0
- package/dist/poll.d.ts +24 -0
- package/dist/poll.js +156 -0
- package/dist/push-audience.d.ts +35 -0
- package/dist/push-audience.js +64 -0
- package/dist/redirect.d.ts +8 -0
- package/dist/redirect.js +17 -0
- package/dist/remote-state.d.ts +38 -0
- package/dist/remote-state.js +81 -0
- package/dist/resources/channels.d.ts +4 -0
- package/dist/resources/channels.js +24 -0
- package/dist/resources/devices.d.ts +4 -0
- package/dist/resources/devices.js +24 -0
- package/dist/sanitize.d.ts +41 -0
- package/dist/sanitize.js +106 -0
- package/dist/self-register.d.ts +34 -0
- package/dist/self-register.js +51 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +63 -0
- package/dist/tools/agent-local.d.ts +17 -0
- package/dist/tools/agent-local.js +139 -0
- package/dist/tools/agent-send.d.ts +6 -0
- package/dist/tools/agent-send.js +89 -0
- package/dist/tools/ask.d.ts +9 -0
- package/dist/tools/ask.js +275 -0
- package/dist/tools/broadcast.d.ts +5 -0
- package/dist/tools/broadcast.js +42 -0
- package/dist/tools/clipboard.d.ts +5 -0
- package/dist/tools/clipboard.js +47 -0
- package/dist/tools/dismiss.d.ts +5 -0
- package/dist/tools/dismiss.js +49 -0
- package/dist/tools/file.d.ts +5 -0
- package/dist/tools/file.js +169 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.js +44 -0
- package/dist/tools/lookup.d.ts +10 -0
- package/dist/tools/lookup.js +45 -0
- package/dist/tools/notify.d.ts +5 -0
- package/dist/tools/notify.js +118 -0
- package/dist/tools/rename.d.ts +5 -0
- package/dist/tools/rename.js +45 -0
- package/dist/trust-pins.d.ts +70 -0
- package/dist/trust-pins.js +269 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +2 -0
- package/dist/url-check.d.ts +11 -0
- package/dist/url-check.js +57 -0
- package/dist/ws-wait.d.ts +72 -0
- package/dist/ws-wait.js +200 -0
- package/package.json +70 -4
package/dist/ws-wait.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|