@ericmhalvorsen/aperture 0.2.1
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 +148 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +64 -0
- package/dist/client/handlers.d.ts +2 -0
- package/dist/client/handlers.js +171 -0
- package/dist/client/patches.d.ts +18 -0
- package/dist/client/patches.js +56 -0
- package/dist/client/storage.d.ts +5 -0
- package/dist/client/storage.js +31 -0
- package/dist/client/ui.d.ts +26 -0
- package/dist/client/ui.js +600 -0
- package/dist/client.d.ts +60 -0
- package/dist/client.js +435 -0
- package/dist/frameworks/next.d.ts +3 -0
- package/dist/frameworks/next.js +12 -0
- package/dist/frameworks/shared.d.ts +1 -0
- package/dist/frameworks/shared.js +61 -0
- package/dist/frameworks/vite.d.ts +10 -0
- package/dist/frameworks/vite.js +22 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/mcp-server.d.ts +39 -0
- package/dist/mcp-server.js +237 -0
- package/dist/react.d.ts +8 -0
- package/dist/react.js +20 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +2 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +315 -0
- package/dist/tools.d.ts +165 -0
- package/dist/tools.js +161 -0
- package/dist/transports.d.ts +29 -0
- package/dist/transports.js +86 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.js +1 -0
- package/dist-browser/client.js +457 -0
- package/package.json +123 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { TOOL_HANDLERS } from "./client/handlers.js";
|
|
2
|
+
import { patchConsole, patchFetch } from "./client/patches.js";
|
|
3
|
+
import { storage } from "./client/storage.js";
|
|
4
|
+
import { injectStyles, requestDisplayMedia, showApprovalDialog, showStatusDialog, } from "./client/ui.js";
|
|
5
|
+
export class ApertureClient {
|
|
6
|
+
ws = null;
|
|
7
|
+
config;
|
|
8
|
+
approved = false;
|
|
9
|
+
denied = false;
|
|
10
|
+
capabilities = [];
|
|
11
|
+
screenCaptureStream = null;
|
|
12
|
+
badgeElement = null;
|
|
13
|
+
focusListenersAdded = false;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
window.__apertureInstance__ = this;
|
|
17
|
+
injectStyles();
|
|
18
|
+
patchConsole();
|
|
19
|
+
patchFetch();
|
|
20
|
+
this.loadCachedApproval();
|
|
21
|
+
this.registerKeyboardShortcut();
|
|
22
|
+
if (typeof window !== "undefined") {
|
|
23
|
+
window.addEventListener("beforeunload", () => {
|
|
24
|
+
this.stopScreenCapture();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
registerKeyboardShortcut() {
|
|
29
|
+
if (typeof document === "undefined")
|
|
30
|
+
return;
|
|
31
|
+
document.addEventListener("keydown", (e) => {
|
|
32
|
+
const isMac = navigator.platform.toLowerCase().includes("mac");
|
|
33
|
+
const mod = isMac ? e.metaKey : e.ctrlKey;
|
|
34
|
+
if (mod && e.shiftKey && (e.key === "a" || e.key === "A")) {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
this.openStatusDialog();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
isBadgeHidden() {
|
|
41
|
+
const hiddenUntil = storage.get("aperture_badge_hidden_until");
|
|
42
|
+
if (hiddenUntil) {
|
|
43
|
+
return Date.now() < Number(hiddenUntil);
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
hideBadgeFor24h() {
|
|
48
|
+
const until = String(Date.now() + 24 * 60 * 60 * 1000);
|
|
49
|
+
storage.set("aperture_badge_hidden_until", until);
|
|
50
|
+
if (this.badgeElement) {
|
|
51
|
+
this.badgeElement.remove();
|
|
52
|
+
this.badgeElement = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
showBadge() {
|
|
56
|
+
storage.remove("aperture_badge_hidden_until");
|
|
57
|
+
if (this.ws) {
|
|
58
|
+
const status = this.ws.readyState === WebSocket.OPEN ? "connected" : "connecting";
|
|
59
|
+
this.updateBadge(status);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
loadCachedApproval() {
|
|
63
|
+
const approved = storage.get("aperture_approved");
|
|
64
|
+
const timestamp = storage.get("aperture_approved_at");
|
|
65
|
+
const storedTtl = storage.get("aperture_ttl_ms");
|
|
66
|
+
const defaultTtlMs = 60 * 60 * 1000;
|
|
67
|
+
const ttlMs = storedTtl ? Number(storedTtl) : defaultTtlMs;
|
|
68
|
+
const isStale = timestamp ? Date.now() - Number(timestamp) > ttlMs : true;
|
|
69
|
+
if (approved === "true" && !isStale) {
|
|
70
|
+
this.approved = true;
|
|
71
|
+
this.denied = false;
|
|
72
|
+
this.capabilities = JSON.parse(storage.get("aperture_capabilities") || "[]");
|
|
73
|
+
}
|
|
74
|
+
else if (approved === "false") {
|
|
75
|
+
this.denied = true;
|
|
76
|
+
this.approved = false;
|
|
77
|
+
}
|
|
78
|
+
if (isStale) {
|
|
79
|
+
storage.remove("aperture_approved");
|
|
80
|
+
storage.remove("aperture_approved_at");
|
|
81
|
+
storage.remove("aperture_capabilities");
|
|
82
|
+
storage.remove("aperture_ttl_ms");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
saveApproval(approved, capabilities, ttlMs) {
|
|
86
|
+
if (approved) {
|
|
87
|
+
storage.set("aperture_approved", "true");
|
|
88
|
+
storage.set("aperture_approved_at", String(Date.now()));
|
|
89
|
+
storage.set("aperture_capabilities", JSON.stringify(capabilities));
|
|
90
|
+
if (ttlMs) {
|
|
91
|
+
storage.set("aperture_ttl_ms", String(ttlMs));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
storage.set("aperture_approved", "false");
|
|
96
|
+
storage.remove("aperture_approved_at");
|
|
97
|
+
storage.remove("aperture_capabilities");
|
|
98
|
+
storage.remove("aperture_ttl_ms");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
revokeApproval() {
|
|
102
|
+
storage.remove("aperture_approved");
|
|
103
|
+
storage.remove("aperture_approved_at");
|
|
104
|
+
storage.remove("aperture_capabilities");
|
|
105
|
+
storage.remove("aperture_ttl_ms");
|
|
106
|
+
this.approved = false;
|
|
107
|
+
this.denied = false;
|
|
108
|
+
this.capabilities = [];
|
|
109
|
+
}
|
|
110
|
+
connect() {
|
|
111
|
+
const url = new URL("/mcp", this.config.serverUrl);
|
|
112
|
+
url.searchParams.set("type", "browser");
|
|
113
|
+
this.updateBadge("connecting");
|
|
114
|
+
this.ws = new WebSocket(url.toString());
|
|
115
|
+
this.ws.onopen = () => {
|
|
116
|
+
const customToolsPayload = this.config.customTools
|
|
117
|
+
? Object.entries(this.config.customTools).map(([name, def]) => ({
|
|
118
|
+
name,
|
|
119
|
+
description: def.description,
|
|
120
|
+
inputSchema: def.inputSchema,
|
|
121
|
+
}))
|
|
122
|
+
: [];
|
|
123
|
+
this.send({
|
|
124
|
+
type: "register",
|
|
125
|
+
url: window.location.href,
|
|
126
|
+
title: document.title,
|
|
127
|
+
customTools: customToolsPayload,
|
|
128
|
+
});
|
|
129
|
+
console.log("[Aperture] Connected to server");
|
|
130
|
+
this.updateBadge("connected");
|
|
131
|
+
if (this.approved) {
|
|
132
|
+
this.send({
|
|
133
|
+
type: "approval",
|
|
134
|
+
approved: this.approved,
|
|
135
|
+
capabilities: this.capabilities,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
this.reportFocusState();
|
|
139
|
+
if (typeof window !== "undefined" && !this.focusListenersAdded) {
|
|
140
|
+
this.focusListenersAdded = true;
|
|
141
|
+
window.addEventListener("focus", () => this.reportFocusState());
|
|
142
|
+
window.addEventListener("blur", () => this.reportFocusState());
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
this.ws.onmessage = async (event) => {
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(event.data);
|
|
148
|
+
if (msg.type === "tool_call") {
|
|
149
|
+
await this.handleToolCall(msg);
|
|
150
|
+
}
|
|
151
|
+
else if (msg.type === "agent_connected") {
|
|
152
|
+
await this.getOrWaitApproval();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
};
|
|
157
|
+
this.ws.onclose = () => {
|
|
158
|
+
console.log("[Aperture] Disconnected. Retrying in 3s...");
|
|
159
|
+
this.updateBadge("disconnected");
|
|
160
|
+
this.stopScreenCapture();
|
|
161
|
+
setTimeout(() => this.connect(), 3000);
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
disconnect() {
|
|
165
|
+
if (this.ws) {
|
|
166
|
+
this.ws.onclose = null;
|
|
167
|
+
this.ws.close();
|
|
168
|
+
this.ws = null;
|
|
169
|
+
}
|
|
170
|
+
this.stopScreenCapture();
|
|
171
|
+
if (this.badgeElement) {
|
|
172
|
+
this.badgeElement.remove();
|
|
173
|
+
this.badgeElement = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
reportFocusState() {
|
|
177
|
+
if (typeof document === "undefined")
|
|
178
|
+
return;
|
|
179
|
+
const focused = document.hasFocus();
|
|
180
|
+
this.send({ type: "focus", focused });
|
|
181
|
+
}
|
|
182
|
+
send(msg) {
|
|
183
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
184
|
+
this.ws.send(JSON.stringify(msg));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
updateBadge(status) {
|
|
188
|
+
if (typeof document === "undefined")
|
|
189
|
+
return;
|
|
190
|
+
if (this.isBadgeHidden()) {
|
|
191
|
+
if (this.badgeElement) {
|
|
192
|
+
this.badgeElement.remove();
|
|
193
|
+
this.badgeElement = null;
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!this.badgeElement) {
|
|
198
|
+
this.badgeElement = document.createElement("div");
|
|
199
|
+
this.badgeElement.id = "aperture-badge";
|
|
200
|
+
this.badgeElement.title = "Manage Aperture session (Ctrl+Shift+A)";
|
|
201
|
+
const dot = document.createElement("span");
|
|
202
|
+
dot.className = "dot";
|
|
203
|
+
this.badgeElement.appendChild(dot);
|
|
204
|
+
this.badgeElement.addEventListener("click", () => {
|
|
205
|
+
this.openStatusDialog();
|
|
206
|
+
});
|
|
207
|
+
document.body.appendChild(this.badgeElement);
|
|
208
|
+
}
|
|
209
|
+
const dot = this.badgeElement.querySelector(".dot");
|
|
210
|
+
if (dot) {
|
|
211
|
+
dot.className = `dot ${status}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async handleToolCall(msg) {
|
|
215
|
+
if (this.denied) {
|
|
216
|
+
this.send({
|
|
217
|
+
type: "result",
|
|
218
|
+
requestId: msg.requestId,
|
|
219
|
+
result: { error: "User denied browser access" },
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!this.approved) {
|
|
224
|
+
await this.getOrWaitApproval();
|
|
225
|
+
if (!this.approved) {
|
|
226
|
+
this.send({
|
|
227
|
+
type: "result",
|
|
228
|
+
requestId: msg.requestId,
|
|
229
|
+
result: {
|
|
230
|
+
error: this.denied
|
|
231
|
+
? "User denied browser access"
|
|
232
|
+
: "User dismissed the approval request",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const builtInHandler = TOOL_HANDLERS[msg.tool];
|
|
239
|
+
const customHandler = this.config.customTools?.[msg.tool]?.handler;
|
|
240
|
+
const handler = builtInHandler || customHandler;
|
|
241
|
+
if (!handler) {
|
|
242
|
+
this.send({
|
|
243
|
+
type: "result",
|
|
244
|
+
requestId: msg.requestId,
|
|
245
|
+
result: { error: `Unknown tool: ${msg.tool}` },
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const result = await handler(this, msg.args);
|
|
251
|
+
this.send({ type: "result", requestId: msg.requestId, result });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
this.send({
|
|
255
|
+
type: "result",
|
|
256
|
+
requestId: msg.requestId,
|
|
257
|
+
result: { error: err instanceof Error ? err.message : String(err) },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
approvalPendingPromise = null;
|
|
262
|
+
getOrWaitApproval() {
|
|
263
|
+
if (this.approved || this.denied)
|
|
264
|
+
return Promise.resolve();
|
|
265
|
+
if (this.approvalPendingPromise)
|
|
266
|
+
return this.approvalPendingPromise;
|
|
267
|
+
this.approvalPendingPromise = (async () => {
|
|
268
|
+
const decision = await this.getApproval("MCP Agent");
|
|
269
|
+
if (decision.dismissed) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.approved = decision.approved;
|
|
273
|
+
this.denied = !decision.approved;
|
|
274
|
+
this.capabilities = decision.capabilities;
|
|
275
|
+
this.saveApproval(this.approved, this.capabilities, decision.ttlMs);
|
|
276
|
+
this.send({
|
|
277
|
+
type: "approval",
|
|
278
|
+
approved: this.approved,
|
|
279
|
+
capabilities: this.capabilities,
|
|
280
|
+
});
|
|
281
|
+
})();
|
|
282
|
+
this.approvalPendingPromise.finally(() => {
|
|
283
|
+
this.approvalPendingPromise = null;
|
|
284
|
+
});
|
|
285
|
+
return this.approvalPendingPromise;
|
|
286
|
+
}
|
|
287
|
+
async getApproval(agentName) {
|
|
288
|
+
if (this.config.onApprovalRequest) {
|
|
289
|
+
const result = await this.config.onApprovalRequest(agentName);
|
|
290
|
+
return { ...result, dismissed: false };
|
|
291
|
+
}
|
|
292
|
+
return showApprovalDialog(agentName, (state) => {
|
|
293
|
+
if (state.stream !== undefined) {
|
|
294
|
+
this.screenCaptureStream = state.stream;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async captureScreenshotFromStream() {
|
|
299
|
+
if (!this.screenCaptureStream?.active) {
|
|
300
|
+
if (!this.approved || !this.capabilities.includes("screenshot")) {
|
|
301
|
+
throw new Error("Screenshot access was not granted. Please approve the connection with screenshot capability enabled.");
|
|
302
|
+
}
|
|
303
|
+
const stream = await this.requestScreenCapture();
|
|
304
|
+
if (!stream) {
|
|
305
|
+
throw new Error("Failed to acquire screen capture. Please try again or re-approve the connection.");
|
|
306
|
+
}
|
|
307
|
+
this.screenCaptureStream = stream;
|
|
308
|
+
}
|
|
309
|
+
const track = this.screenCaptureStream.getVideoTracks()[0];
|
|
310
|
+
if (!track) {
|
|
311
|
+
throw new Error("No video tracks found in screen capture stream.");
|
|
312
|
+
}
|
|
313
|
+
const video = document.createElement("video");
|
|
314
|
+
video.srcObject = this.screenCaptureStream;
|
|
315
|
+
video.autoplay = true;
|
|
316
|
+
video.playsInline = true;
|
|
317
|
+
video.muted = true;
|
|
318
|
+
await new Promise((resolve) => {
|
|
319
|
+
video.onloadedmetadata = () => resolve(null);
|
|
320
|
+
});
|
|
321
|
+
await video.play().catch(() => null);
|
|
322
|
+
await new Promise((resolve) => {
|
|
323
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
324
|
+
video.requestVideoFrameCallback(() => resolve());
|
|
325
|
+
setTimeout(() => resolve(), 1000);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
setTimeout(() => resolve(), 300);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
const canvas = document.createElement("canvas");
|
|
332
|
+
canvas.width = video.videoWidth || 800;
|
|
333
|
+
canvas.height = video.videoHeight || 600;
|
|
334
|
+
const ctx = canvas.getContext("2d");
|
|
335
|
+
if (!ctx) {
|
|
336
|
+
throw new Error("Could not get 2D context from canvas");
|
|
337
|
+
}
|
|
338
|
+
ctx.drawImage(video, 0, 0);
|
|
339
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
340
|
+
video.srcObject = null;
|
|
341
|
+
video.load();
|
|
342
|
+
return dataUrl;
|
|
343
|
+
}
|
|
344
|
+
stopScreenCapture() {
|
|
345
|
+
if (this.screenCaptureStream) {
|
|
346
|
+
for (const track of this.screenCaptureStream.getTracks()) {
|
|
347
|
+
track.stop();
|
|
348
|
+
}
|
|
349
|
+
this.screenCaptureStream = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
setMediaStream(stream) {
|
|
353
|
+
this.screenCaptureStream = stream;
|
|
354
|
+
}
|
|
355
|
+
async requestScreenCapture() {
|
|
356
|
+
return requestDisplayMedia();
|
|
357
|
+
}
|
|
358
|
+
openStatusDialog() {
|
|
359
|
+
showStatusDialog({
|
|
360
|
+
wsReadyState: this.ws?.readyState ?? WebSocket.CLOSED,
|
|
361
|
+
approved: this.approved,
|
|
362
|
+
denied: this.denied,
|
|
363
|
+
capabilities: this.capabilities,
|
|
364
|
+
isBadgeHidden: () => this.isBadgeHidden(),
|
|
365
|
+
showBadge: () => this.showBadge(),
|
|
366
|
+
hideBadgeFor24h: () => this.hideBadgeFor24h(),
|
|
367
|
+
revokeApproval: () => this.revokeApproval(),
|
|
368
|
+
onApprovalStateChange: (state) => {
|
|
369
|
+
this.approved = state.approved;
|
|
370
|
+
if (state.approved) {
|
|
371
|
+
this.denied = false;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
this.denied = true;
|
|
375
|
+
}
|
|
376
|
+
this.capabilities = state.capabilities;
|
|
377
|
+
if (state.stream !== undefined) {
|
|
378
|
+
if (state.stream === null) {
|
|
379
|
+
this.stopScreenCapture();
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
this.screenCaptureStream = state.stream;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.saveApproval(this.approved, this.capabilities);
|
|
386
|
+
this.send({
|
|
387
|
+
type: "approval",
|
|
388
|
+
approved: this.approved,
|
|
389
|
+
capabilities: this.capabilities,
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
export function initAperture(options) {
|
|
396
|
+
if (typeof window === "undefined")
|
|
397
|
+
return;
|
|
398
|
+
const isDev = location.hostname === "localhost" ||
|
|
399
|
+
location.hostname === "127.0.0.1" ||
|
|
400
|
+
location.hostname.endsWith(".localhost") ||
|
|
401
|
+
!!window.__vite_inject__;
|
|
402
|
+
if (!isDev)
|
|
403
|
+
return;
|
|
404
|
+
const port = options?.port || window.__APERTURE_PORT__ || 3456;
|
|
405
|
+
const serverUrl = options?.serverUrl || `ws://localhost:${port}`;
|
|
406
|
+
const existing = window.__apertureInstance__;
|
|
407
|
+
if (existing && typeof existing.disconnect === "function") {
|
|
408
|
+
existing.disconnect();
|
|
409
|
+
}
|
|
410
|
+
const client = new ApertureClient({
|
|
411
|
+
serverUrl,
|
|
412
|
+
customTools: options?.customTools,
|
|
413
|
+
});
|
|
414
|
+
client.connect();
|
|
415
|
+
window.__apertureInstance__ = client;
|
|
416
|
+
return client;
|
|
417
|
+
}
|
|
418
|
+
if (typeof window !== "undefined") {
|
|
419
|
+
const isDev = location.hostname === "localhost" ||
|
|
420
|
+
location.hostname === "127.0.0.1" ||
|
|
421
|
+
location.hostname.endsWith(".localhost") ||
|
|
422
|
+
!!window.__vite_inject__;
|
|
423
|
+
if (isDev) {
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
if (!window.__apertureInstance__) {
|
|
426
|
+
const port = window.__APERTURE_PORT__ || 3456;
|
|
427
|
+
const serverUrl = window.__APERTURE_URL__ || `ws://localhost:${port}`;
|
|
428
|
+
console.log("[Aperture] No manual initialization detected. Auto-connecting...");
|
|
429
|
+
const client = new ApertureClient({ serverUrl });
|
|
430
|
+
client.connect();
|
|
431
|
+
window.__apertureInstance__ = client;
|
|
432
|
+
}
|
|
433
|
+
}, 500);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ensureApertureServer } from "./shared.js";
|
|
2
|
+
export function withAperture(nextConfig = {}, options) {
|
|
3
|
+
const port = options?.port || 3456;
|
|
4
|
+
ensureApertureServer(port).catch(console.error);
|
|
5
|
+
const config = { ...nextConfig };
|
|
6
|
+
const serverExternalPackages = config.serverExternalPackages || [];
|
|
7
|
+
if (!serverExternalPackages.includes("@ericmhalvorsen/aperture")) {
|
|
8
|
+
serverExternalPackages.push("@ericmhalvorsen/aperture");
|
|
9
|
+
}
|
|
10
|
+
config.serverExternalPackages = serverExternalPackages;
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ensureApertureServer(port?: number): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
let serverStarting = false;
|
|
5
|
+
let serverStarted = false;
|
|
6
|
+
function getBinPath() {
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
return path.join(__dirname, "..", "bin.js");
|
|
9
|
+
}
|
|
10
|
+
function isPortFree(port) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const socket = createConnection(port, "127.0.0.1");
|
|
13
|
+
socket.once("connect", () => {
|
|
14
|
+
socket.destroy();
|
|
15
|
+
resolve(false);
|
|
16
|
+
});
|
|
17
|
+
socket.once("error", () => {
|
|
18
|
+
socket.destroy();
|
|
19
|
+
resolve(true);
|
|
20
|
+
});
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
resolve(true);
|
|
24
|
+
}, 300);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function ensureApertureServer(port = 3456) {
|
|
28
|
+
if (serverStarting || serverStarted)
|
|
29
|
+
return;
|
|
30
|
+
const free = await isPortFree(port);
|
|
31
|
+
if (!free) {
|
|
32
|
+
serverStarted = true;
|
|
33
|
+
console.log(`[Aperture] Server already running on port ${port}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
serverStarting = true;
|
|
37
|
+
const binPath = getBinPath();
|
|
38
|
+
const cp = await import(/* webpackIgnore: true */ "node:child_process");
|
|
39
|
+
const child = cp.spawn("node", [binPath], {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
env: { ...process.env, APERTURE_PORT: String(port) },
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
process.on("exit", () => {
|
|
45
|
+
try {
|
|
46
|
+
child.kill();
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
});
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
51
|
+
const nowFree = await isPortFree(port);
|
|
52
|
+
if (!nowFree) {
|
|
53
|
+
serverStarted = true;
|
|
54
|
+
serverStarting = false;
|
|
55
|
+
console.log(`[Aperture] Started server on port ${port}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
serverStarting = false;
|
|
59
|
+
console.error(`[Aperture] Failed to start server on port ${port}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ensureApertureServer } from "./shared.js";
|
|
2
|
+
export function aperture(options) {
|
|
3
|
+
const port = options?.port || Number(process.env.APERTURE_PORT) || 3456;
|
|
4
|
+
let started = false;
|
|
5
|
+
return {
|
|
6
|
+
name: "aperture",
|
|
7
|
+
apply: "serve",
|
|
8
|
+
config() {
|
|
9
|
+
return {
|
|
10
|
+
define: {
|
|
11
|
+
"window.__APERTURE_PORT__": JSON.stringify(port),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
async configureServer() {
|
|
16
|
+
if (started)
|
|
17
|
+
return;
|
|
18
|
+
started = true;
|
|
19
|
+
await ensureApertureServer(port);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { CustomToolDefinition } from "./client.js";
|
|
2
|
+
export { ApertureClient } from "./client.js";
|
|
3
|
+
export { withAperture } from "./frameworks/next.js";
|
|
4
|
+
export { Aperture } from "./react.js";
|
|
5
|
+
export { ApertureServer } from "./server.js";
|
|
6
|
+
export type { BrowserToolName } from "./tools.js";
|
|
7
|
+
export { BROWSER_TOOLS } from "./tools.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { BrowserSession } from "./types.js";
|
|
3
|
+
export interface SharedServerState {
|
|
4
|
+
mcpInitialized: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function createApertureMcpServer(sessions: Map<string, BrowserSession>, pendingRequests: Map<string, (result: unknown) => void>, sharedState: SharedServerState): Server<{
|
|
7
|
+
method: string;
|
|
8
|
+
params?: {
|
|
9
|
+
[x: string]: unknown;
|
|
10
|
+
_meta?: {
|
|
11
|
+
[x: string]: unknown;
|
|
12
|
+
progressToken?: string | number | undefined;
|
|
13
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
14
|
+
taskId: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
} | undefined;
|
|
17
|
+
} | undefined;
|
|
18
|
+
}, {
|
|
19
|
+
method: string;
|
|
20
|
+
params?: {
|
|
21
|
+
[x: string]: unknown;
|
|
22
|
+
_meta?: {
|
|
23
|
+
[x: string]: unknown;
|
|
24
|
+
progressToken?: string | number | undefined;
|
|
25
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
26
|
+
taskId: string;
|
|
27
|
+
} | undefined;
|
|
28
|
+
} | undefined;
|
|
29
|
+
} | undefined;
|
|
30
|
+
}, {
|
|
31
|
+
[x: string]: unknown;
|
|
32
|
+
_meta?: {
|
|
33
|
+
[x: string]: unknown;
|
|
34
|
+
progressToken?: string | number | undefined;
|
|
35
|
+
"io.modelcontextprotocol/related-task"?: {
|
|
36
|
+
taskId: string;
|
|
37
|
+
} | undefined;
|
|
38
|
+
} | undefined;
|
|
39
|
+
}>;
|