mcp-page-bridge 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 +21 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.js +7 -0
- package/dist/chunk-7BFUNK5V.js +1178 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +41 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eray Ates
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { Tool, Prompt, Resource } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
|
|
6
|
+
interface ProviderMeta {
|
|
7
|
+
url?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
userAgent?: string;
|
|
10
|
+
tabId?: number;
|
|
11
|
+
providerId?: string;
|
|
12
|
+
}
|
|
13
|
+
/** A connected browser MCP server (one per WebSocket connection). */
|
|
14
|
+
interface Provider {
|
|
15
|
+
id: string;
|
|
16
|
+
/** Unique, tool-name-safe namespace label. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Raw serverInfo.name reported by the browser. */
|
|
19
|
+
rawName: string;
|
|
20
|
+
version: string;
|
|
21
|
+
client: Client;
|
|
22
|
+
tools: Tool[];
|
|
23
|
+
prompts: Prompt[];
|
|
24
|
+
resources: Resource[];
|
|
25
|
+
meta: ProviderMeta;
|
|
26
|
+
connectedAt: number;
|
|
27
|
+
}
|
|
28
|
+
type PublicProvider = Omit<Provider, "client">;
|
|
29
|
+
interface Bridge {
|
|
30
|
+
/** Agent-facing MCP server (connect this to a stdio/http transport). */
|
|
31
|
+
server: Server;
|
|
32
|
+
wss: WebSocketServer;
|
|
33
|
+
/** Actual port the WS server bound to (useful when port 0 is requested). */
|
|
34
|
+
port: number;
|
|
35
|
+
listProviders(): PublicProvider[];
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
interface BridgeOptions {
|
|
39
|
+
port?: number;
|
|
40
|
+
host?: string;
|
|
41
|
+
/** If set, browsers must connect with `?token=<token>` or they're rejected. */
|
|
42
|
+
token?: string;
|
|
43
|
+
}
|
|
44
|
+
declare function createBridge(opts?: BridgeOptions): Promise<Bridge>;
|
|
45
|
+
|
|
46
|
+
export { type Bridge, type BridgeOptions, type PublicProvider, createBridge };
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bridge.ts
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { createServer as createHttpServer } from "http";
|
|
6
|
+
import { WebSocketServer } from "ws";
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
EmptyResultSchema,
|
|
12
|
+
GetPromptRequestSchema,
|
|
13
|
+
ListPromptsRequestSchema,
|
|
14
|
+
ListResourcesRequestSchema,
|
|
15
|
+
ListToolsRequestSchema,
|
|
16
|
+
LoggingMessageNotificationSchema,
|
|
17
|
+
PromptListChangedNotificationSchema,
|
|
18
|
+
ReadResourceRequestSchema,
|
|
19
|
+
ResourceListChangedNotificationSchema,
|
|
20
|
+
ToolListChangedNotificationSchema
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
|
|
23
|
+
// ../protocol/src/index.ts
|
|
24
|
+
var MCP_PAGE_BRIDGE_VERSION = "0.1.0";
|
|
25
|
+
var WS_SUBPROTOCOL = "mcp";
|
|
26
|
+
var DEFAULT_PORT = 8787;
|
|
27
|
+
var NAMESPACE_SEP = "__";
|
|
28
|
+
var DISALLOWED = /[^a-zA-Z0-9_-]/g;
|
|
29
|
+
function sanitizeLabel(input) {
|
|
30
|
+
const base = (input ?? "").trim().toLowerCase().replace(/[\s.]+/g, "-").replace(DISALLOWED, "").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 40);
|
|
31
|
+
return base || "browser";
|
|
32
|
+
}
|
|
33
|
+
function namespaceName(label, name) {
|
|
34
|
+
return `${label}${NAMESPACE_SEP}${name}`;
|
|
35
|
+
}
|
|
36
|
+
var MCP_PAGE_BRIDGE_DASHBOARD_ACTIVATE_TAB = "mcpPageBridge/activateTab";
|
|
37
|
+
var MCP_PAGE_BRIDGE_DASHBOARD_CLOSE_TAB = "mcpPageBridge/closeTab";
|
|
38
|
+
|
|
39
|
+
// src/dashboard.ts
|
|
40
|
+
var FAVICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><polygon points="22.39,6.00 22.39,18.00 12.00,24.00 1.61,18.00 1.61,6.00 12.00,0.00" fill="#E63946"/><svg x="3" y="3" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/></svg></svg>';
|
|
41
|
+
var DASHBOARD_HTML = `<!doctype html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8" />
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
47
|
+
<title>mcp-page-bridge dashboard</title>
|
|
48
|
+
<style>
|
|
49
|
+
:root {
|
|
50
|
+
color-scheme: dark;
|
|
51
|
+
--bg: #0b1017;
|
|
52
|
+
--panel: #111922;
|
|
53
|
+
--panel-2: #0e151d;
|
|
54
|
+
--panel-3: #141e29;
|
|
55
|
+
--fg: #e8eef6;
|
|
56
|
+
--muted: #91a0af;
|
|
57
|
+
--quiet: #687786;
|
|
58
|
+
--line: #273443;
|
|
59
|
+
--line-strong: #3a4a5c;
|
|
60
|
+
--brand: #e63946;
|
|
61
|
+
--brand-soft: rgba(230, 57, 70, 0.14);
|
|
62
|
+
--green: #3fce7a;
|
|
63
|
+
--green-soft: rgba(63, 206, 122, 0.12);
|
|
64
|
+
--blue: #6da2ff;
|
|
65
|
+
--blue-soft: rgba(109, 162, 255, 0.11);
|
|
66
|
+
--shadow: 0 24px 70px rgba(0, 0, 0, 0.28);
|
|
67
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
68
|
+
--sans: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
69
|
+
}
|
|
70
|
+
@media (prefers-color-scheme: light) {
|
|
71
|
+
:root {
|
|
72
|
+
color-scheme: light;
|
|
73
|
+
--bg: #f4f6f9;
|
|
74
|
+
--panel: #ffffff;
|
|
75
|
+
--panel-2: #f8fafc;
|
|
76
|
+
--panel-3: #f1f5f9;
|
|
77
|
+
--fg: #141922;
|
|
78
|
+
--muted: #5f6f80;
|
|
79
|
+
--quiet: #7b8794;
|
|
80
|
+
--line: #d8e0e8;
|
|
81
|
+
--line-strong: #bcc8d4;
|
|
82
|
+
--brand-soft: rgba(230, 57, 70, 0.09);
|
|
83
|
+
--green-soft: rgba(46, 155, 78, 0.1);
|
|
84
|
+
--blue-soft: rgba(47, 111, 237, 0.09);
|
|
85
|
+
--shadow: 0 18px 48px rgba(20, 25, 34, 0.08);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
* { box-sizing: border-box; }
|
|
89
|
+
html { min-height: 100%; }
|
|
90
|
+
body {
|
|
91
|
+
min-height: 100%;
|
|
92
|
+
margin: 0;
|
|
93
|
+
color: var(--fg);
|
|
94
|
+
background:
|
|
95
|
+
radial-gradient(circle at 16% 0%, rgba(230, 57, 70, 0.13), transparent 28rem),
|
|
96
|
+
radial-gradient(circle at 86% 6%, rgba(109, 162, 255, 0.12), transparent 30rem),
|
|
97
|
+
var(--bg);
|
|
98
|
+
font-family: var(--sans);
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
line-height: 1.45;
|
|
101
|
+
}
|
|
102
|
+
button { font: inherit; }
|
|
103
|
+
.shell { max-width: 1280px; margin: 0 auto; padding: 24px clamp(16px, 3vw, 36px) 30px; }
|
|
104
|
+
.topbar {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
gap: 18px;
|
|
109
|
+
padding-bottom: 18px;
|
|
110
|
+
border-bottom: 1px solid var(--line);
|
|
111
|
+
}
|
|
112
|
+
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
113
|
+
.brand img { width: 34px; height: 34px; display: block; }
|
|
114
|
+
.eyebrow { color: var(--muted); font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; }
|
|
115
|
+
h1 { margin: 1px 0 0; font-size: clamp(22px, 3vw, 32px); line-height: 1; letter-spacing: -0.04em; }
|
|
116
|
+
.endpoint {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-wrap: wrap;
|
|
119
|
+
justify-content: flex-end;
|
|
120
|
+
gap: 8px;
|
|
121
|
+
color: var(--muted);
|
|
122
|
+
font-family: var(--mono);
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
}
|
|
125
|
+
.chip {
|
|
126
|
+
border: 1px solid var(--line);
|
|
127
|
+
background: var(--panel-2);
|
|
128
|
+
color: var(--muted);
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
padding: 5px 8px;
|
|
131
|
+
}
|
|
132
|
+
.status-row {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 12px;
|
|
137
|
+
margin: 18px 0;
|
|
138
|
+
}
|
|
139
|
+
.status { display: flex; align-items: center; gap: 9px; color: var(--muted); }
|
|
140
|
+
.dot { width: 8px; height: 8px; background: var(--brand); box-shadow: 0 0 0 4px var(--brand-soft); }
|
|
141
|
+
.dot.on { background: var(--green); box-shadow: 0 0 0 4px var(--green-soft); }
|
|
142
|
+
.refresh { color: var(--quiet); font-size: 12px; font-family: var(--mono); }
|
|
143
|
+
.stats {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
146
|
+
gap: 10px;
|
|
147
|
+
margin-bottom: 18px;
|
|
148
|
+
}
|
|
149
|
+
.stat {
|
|
150
|
+
border: 1px solid var(--line);
|
|
151
|
+
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
padding: 12px 13px;
|
|
154
|
+
}
|
|
155
|
+
.stat span { display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
|
|
156
|
+
.stat strong { display: block; margin-top: 4px; font-size: 24px; line-height: 1; letter-spacing: -0.04em; }
|
|
157
|
+
.layout {
|
|
158
|
+
display: grid;
|
|
159
|
+
grid-template-columns: minmax(360px, 0.95fr) minmax(430px, 1.05fr);
|
|
160
|
+
gap: 18px;
|
|
161
|
+
align-items: start;
|
|
162
|
+
}
|
|
163
|
+
.panel-title {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: space-between;
|
|
167
|
+
margin: 0 0 9px;
|
|
168
|
+
color: var(--muted);
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
letter-spacing: 0.08em;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
}
|
|
173
|
+
.card {
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
margin-bottom: 12px;
|
|
176
|
+
border: 1px solid var(--line);
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
background: color-mix(in srgb, var(--panel) 94%, transparent);
|
|
179
|
+
box-shadow: var(--shadow);
|
|
180
|
+
}
|
|
181
|
+
.provider-card:not([open]) .provider-head { border-bottom: 0; }
|
|
182
|
+
.provider-head {
|
|
183
|
+
display: block;
|
|
184
|
+
list-style: none;
|
|
185
|
+
padding: 14px 14px 12px;
|
|
186
|
+
border-bottom: 1px solid var(--line);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
user-select: none;
|
|
189
|
+
}
|
|
190
|
+
.provider-head::-webkit-details-marker { display: none; }
|
|
191
|
+
.provider-main { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
192
|
+
.provider-title { min-width: 0; }
|
|
193
|
+
.provider-name { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
194
|
+
.provider-caret {
|
|
195
|
+
width: 8px;
|
|
196
|
+
height: 8px;
|
|
197
|
+
flex: 0 0 auto;
|
|
198
|
+
border-top: 1.5px solid var(--muted);
|
|
199
|
+
border-right: 1.5px solid var(--muted);
|
|
200
|
+
transform: rotate(45deg);
|
|
201
|
+
transition: transform 0.15s ease;
|
|
202
|
+
}
|
|
203
|
+
.provider-card[open] .provider-caret { transform: rotate(135deg); }
|
|
204
|
+
.label {
|
|
205
|
+
max-width: 170px;
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
text-overflow: ellipsis;
|
|
208
|
+
white-space: nowrap;
|
|
209
|
+
border: 1px solid color-mix(in srgb, var(--brand) 42%, var(--line));
|
|
210
|
+
background: var(--brand-soft);
|
|
211
|
+
color: var(--brand);
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
padding: 3px 7px;
|
|
214
|
+
font-family: var(--mono);
|
|
215
|
+
font-size: 12px;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
}
|
|
218
|
+
.ptitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 650; }
|
|
219
|
+
.url { margin-top: 6px; color: var(--quiet); font-family: var(--mono); font-size: 11px; word-break: break-all; }
|
|
220
|
+
.provider-side { display: grid; justify-items: end; gap: 8px; flex-shrink: 0; }
|
|
221
|
+
.metrics { display: flex; gap: 6px; }
|
|
222
|
+
.metric { border: 1px solid var(--line); background: var(--panel-2); color: var(--muted); border-radius: 4px; padding: 3px 6px; font-family: var(--mono); font-size: 11px; }
|
|
223
|
+
.actions { display: flex; gap: 6px; }
|
|
224
|
+
.action-btn {
|
|
225
|
+
appearance: none;
|
|
226
|
+
border: 1px solid var(--line);
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
background: var(--panel-2);
|
|
229
|
+
color: var(--muted);
|
|
230
|
+
padding: 5px 8px;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
font-size: 12px;
|
|
233
|
+
}
|
|
234
|
+
.action-btn:hover { color: var(--fg); border-color: var(--line-strong); background: var(--panel-3); }
|
|
235
|
+
.action-btn:disabled { opacity: 0.55; cursor: wait; }
|
|
236
|
+
.action-btn.danger { color: var(--brand); border-color: color-mix(in srgb, var(--brand) 35%, var(--line)); background: var(--brand-soft); }
|
|
237
|
+
.action-btn.danger:hover { border-color: var(--brand); }
|
|
238
|
+
.card-body { padding: 10px; }
|
|
239
|
+
.group {
|
|
240
|
+
margin: 0 0 9px;
|
|
241
|
+
border: 1px solid var(--line);
|
|
242
|
+
border-radius: 5px;
|
|
243
|
+
background: var(--panel-2);
|
|
244
|
+
}
|
|
245
|
+
.group:last-child { margin-bottom: 0; }
|
|
246
|
+
.group summary {
|
|
247
|
+
list-style: none;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 8px;
|
|
251
|
+
min-height: 38px;
|
|
252
|
+
padding: 9px 10px;
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
color: var(--muted);
|
|
255
|
+
user-select: none;
|
|
256
|
+
}
|
|
257
|
+
.group summary::-webkit-details-marker { display: none; }
|
|
258
|
+
.chev {
|
|
259
|
+
width: 7px;
|
|
260
|
+
height: 7px;
|
|
261
|
+
border-top: 1.5px solid currentColor;
|
|
262
|
+
border-right: 1.5px solid currentColor;
|
|
263
|
+
transform: rotate(45deg);
|
|
264
|
+
transition: transform 0.15s ease;
|
|
265
|
+
}
|
|
266
|
+
.group[open] .chev { transform: rotate(135deg); }
|
|
267
|
+
.group-title { flex: 1; font-size: 12px; font-weight: 650; letter-spacing: 0.02em; }
|
|
268
|
+
.group-count { color: var(--quiet); font-family: var(--mono); font-size: 11px; }
|
|
269
|
+
.group-list { display: grid; gap: 4px; padding: 5px; border-top: 1px solid var(--line); }
|
|
270
|
+
.item {
|
|
271
|
+
display: grid;
|
|
272
|
+
grid-template-columns: minmax(0, 1fr) minmax(110px, 0.8fr);
|
|
273
|
+
gap: 10px;
|
|
274
|
+
width: 100%;
|
|
275
|
+
min-height: 40px;
|
|
276
|
+
border: 1px solid transparent;
|
|
277
|
+
border-radius: 4px;
|
|
278
|
+
background: transparent;
|
|
279
|
+
color: inherit;
|
|
280
|
+
text-align: left;
|
|
281
|
+
padding: 8px 9px;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
}
|
|
284
|
+
.item:hover { background: var(--panel-3); border-color: var(--line); }
|
|
285
|
+
.item.active { background: var(--blue-soft); border-color: color-mix(in srgb, var(--blue) 45%, var(--line)); }
|
|
286
|
+
.nm { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); font-size: 12.5px; }
|
|
287
|
+
.full { display: block; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--quiet); font-family: var(--mono); font-size: 10.5px; }
|
|
288
|
+
.ds { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 12px; align-self: center; }
|
|
289
|
+
.detail {
|
|
290
|
+
position: sticky;
|
|
291
|
+
top: 18px;
|
|
292
|
+
min-height: 360px;
|
|
293
|
+
border: 1px solid var(--line);
|
|
294
|
+
border-radius: 6px;
|
|
295
|
+
background: color-mix(in srgb, var(--panel) 96%, transparent);
|
|
296
|
+
box-shadow: var(--shadow);
|
|
297
|
+
}
|
|
298
|
+
.detail-inner { padding: 18px; }
|
|
299
|
+
.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
|
|
300
|
+
.kind {
|
|
301
|
+
border: 1px solid color-mix(in srgb, var(--blue) 40%, var(--line));
|
|
302
|
+
background: var(--blue-soft);
|
|
303
|
+
color: var(--blue);
|
|
304
|
+
border-radius: 4px;
|
|
305
|
+
padding: 3px 7px;
|
|
306
|
+
font-size: 11px;
|
|
307
|
+
font-weight: 700;
|
|
308
|
+
letter-spacing: 0.08em;
|
|
309
|
+
text-transform: uppercase;
|
|
310
|
+
}
|
|
311
|
+
.from { color: var(--quiet); font-family: var(--mono); font-size: 11px; text-align: right; }
|
|
312
|
+
.detail h2 { margin: 0 0 6px; font-family: var(--mono); font-size: clamp(18px, 2vw, 22px); line-height: 1.2; letter-spacing: -0.04em; word-break: break-all; }
|
|
313
|
+
.desc { margin: 0 0 16px; color: var(--muted); }
|
|
314
|
+
.section-title { margin: 16px 0 7px; color: var(--muted); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
315
|
+
table { width: 100%; border-collapse: collapse; overflow: hidden; border: 1px solid var(--line); font-size: 13px; }
|
|
316
|
+
th, td { text-align: left; padding: 8px 9px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
317
|
+
tr:last-child td { border-bottom: 0; }
|
|
318
|
+
th { background: var(--panel-2); color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
319
|
+
.pname { font-family: var(--mono); }
|
|
320
|
+
.type { font-family: var(--mono); color: var(--brand); font-size: 12px; }
|
|
321
|
+
.req { color: var(--brand); font-weight: 800; }
|
|
322
|
+
.raw { margin-top: 14px; border: 1px solid var(--line); border-radius: 5px; background: var(--panel-2); }
|
|
323
|
+
.raw summary { list-style: none; cursor: pointer; padding: 9px 10px; color: var(--muted); font-size: 12px; }
|
|
324
|
+
.raw summary::-webkit-details-marker { display: none; }
|
|
325
|
+
pre { margin: 0; padding: 12px; overflow: auto; border-top: 1px solid var(--line); font-family: var(--mono); font-size: 12px; line-height: 1.45; }
|
|
326
|
+
.empty {
|
|
327
|
+
display: grid;
|
|
328
|
+
place-items: center;
|
|
329
|
+
min-height: 180px;
|
|
330
|
+
padding: 20px;
|
|
331
|
+
color: var(--muted);
|
|
332
|
+
text-align: center;
|
|
333
|
+
}
|
|
334
|
+
.empty.small { min-height: 96px; border: 1px dashed var(--line); border-radius: 5px; background: var(--panel-2); }
|
|
335
|
+
.placeholder { color: var(--muted); }
|
|
336
|
+
code { border: 1px solid var(--line); background: var(--panel-2); padding: 1px 5px; border-radius: 3px; font-family: var(--mono); }
|
|
337
|
+
footer { margin-top: 20px; color: var(--quiet); font-size: 12px; }
|
|
338
|
+
@media (max-width: 900px) {
|
|
339
|
+
.topbar, .status-row { align-items: flex-start; flex-direction: column; }
|
|
340
|
+
.endpoint { justify-content: flex-start; }
|
|
341
|
+
.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
342
|
+
.layout { grid-template-columns: 1fr; }
|
|
343
|
+
.detail { position: static; }
|
|
344
|
+
}
|
|
345
|
+
@media (max-width: 560px) {
|
|
346
|
+
.shell { padding-inline: 12px; }
|
|
347
|
+
.stats { grid-template-columns: 1fr; }
|
|
348
|
+
.provider-main { flex-direction: column; }
|
|
349
|
+
.provider-side { justify-items: start; }
|
|
350
|
+
.item { grid-template-columns: 1fr; }
|
|
351
|
+
.ds { align-self: start; }
|
|
352
|
+
}
|
|
353
|
+
</style>
|
|
354
|
+
</head>
|
|
355
|
+
<body>
|
|
356
|
+
<div class="shell">
|
|
357
|
+
<header class="topbar">
|
|
358
|
+
<div class="brand">
|
|
359
|
+
<img src="/favicon.svg" alt="" />
|
|
360
|
+
<div>
|
|
361
|
+
<div class="eyebrow">browser MCP bridge</div>
|
|
362
|
+
<h1>mcp-page-bridge dashboard</h1>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="endpoint">
|
|
366
|
+
<span class="chip" id="version">v0.0.0</span>
|
|
367
|
+
<span class="chip" id="endpoint">ws://127.0.0.1:8787</span>
|
|
368
|
+
<span class="chip">GET /api/providers</span>
|
|
369
|
+
</div>
|
|
370
|
+
</header>
|
|
371
|
+
|
|
372
|
+
<div class="status-row">
|
|
373
|
+
<div class="status">
|
|
374
|
+
<span class="dot" id="dot"></span>
|
|
375
|
+
<span id="statusText">connecting</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="refresh">auto refresh: 1.5s</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<section class="stats" aria-label="Connected provider summary">
|
|
381
|
+
<div class="stat"><span>Providers</span><strong id="providerCount">0</strong></div>
|
|
382
|
+
<div class="stat"><span>Tools</span><strong id="toolCount">0</strong></div>
|
|
383
|
+
</section>
|
|
384
|
+
|
|
385
|
+
<main class="layout">
|
|
386
|
+
<section>
|
|
387
|
+
<div class="panel-title"><span>Providers</span><span id="listCount">0 connected</span></div>
|
|
388
|
+
<div id="list"></div>
|
|
389
|
+
</section>
|
|
390
|
+
<aside class="detail" id="detail">
|
|
391
|
+
<div class="detail-inner"><div class="empty placeholder">Select an item to inspect its schema and source.</div></div>
|
|
392
|
+
</aside>
|
|
393
|
+
</main>
|
|
394
|
+
|
|
395
|
+
<footer>Dashboard is served by the local bridge on the same port as WebSocket transport.</footer>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<script>
|
|
399
|
+
const $ = (id) => document.getElementById(id);
|
|
400
|
+
const esc = (s) =>
|
|
401
|
+
String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
402
|
+
|
|
403
|
+
const EMPTY = "No tools published.";
|
|
404
|
+
const BUILTIN_TOOLS = new Set([
|
|
405
|
+
"eval",
|
|
406
|
+
"dom_query",
|
|
407
|
+
"get_html",
|
|
408
|
+
"get_page_info",
|
|
409
|
+
"click",
|
|
410
|
+
"set_value",
|
|
411
|
+
"scroll",
|
|
412
|
+
"wait_for",
|
|
413
|
+
"console_logs",
|
|
414
|
+
"screenshot",
|
|
415
|
+
"navigate",
|
|
416
|
+
"reload",
|
|
417
|
+
]);
|
|
418
|
+
const BROWSER_TOOLS = new Set(["list_tabs", "open_tab", "activate_tab", "navigate_tab", "close_tab"]);
|
|
419
|
+
|
|
420
|
+
let data = { version: "", port: 8787, providers: [] };
|
|
421
|
+
let selected = null; // { kind, provider, key }
|
|
422
|
+
const openProviders = Object.create(null);
|
|
423
|
+
const openGroups = Object.create(null);
|
|
424
|
+
|
|
425
|
+
function shortName(provider, key) {
|
|
426
|
+
const raw = String(key || "");
|
|
427
|
+
const prefix = provider.label + "__";
|
|
428
|
+
return raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function counts(provider) {
|
|
432
|
+
return {
|
|
433
|
+
tool: (provider.tools || []).length,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function totals() {
|
|
438
|
+
return data.providers.reduce(
|
|
439
|
+
(acc, p) => {
|
|
440
|
+
acc.providers += 1;
|
|
441
|
+
acc.tools += (p.tools || []).length;
|
|
442
|
+
return acc;
|
|
443
|
+
},
|
|
444
|
+
{ providers: 0, tools: 0 },
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function itemList(provider) {
|
|
449
|
+
return provider.tools || [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function itemKey(item) {
|
|
453
|
+
return item.name;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function itemDescription(item) {
|
|
457
|
+
return item.description || "";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function groupId(provider, title) {
|
|
461
|
+
return provider.label + "|tools|" + title;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function classifyTool(provider, item) {
|
|
465
|
+
const local = shortName(provider, item.name);
|
|
466
|
+
if (provider.label === "browser" || BROWSER_TOOLS.has(local)) return "Browser control";
|
|
467
|
+
if (BUILTIN_TOOLS.has(local)) return "Built-in page tools";
|
|
468
|
+
return "Page tools";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function groupsFor(provider) {
|
|
472
|
+
const buckets = new Map();
|
|
473
|
+
for (const item of itemList(provider)) {
|
|
474
|
+
const title = classifyTool(provider, item);
|
|
475
|
+
if (!buckets.has(title)) buckets.set(title, []);
|
|
476
|
+
buckets.get(title).push(item);
|
|
477
|
+
}
|
|
478
|
+
return [...buckets.entries()].map(([title, items]) => ({ title, items }));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function renderItem(provider, item) {
|
|
482
|
+
const key = itemKey(item);
|
|
483
|
+
const active = selected && selected.kind === "tool" && selected.provider === provider.label && selected.key === key;
|
|
484
|
+
const local = shortName(provider, key);
|
|
485
|
+
return (
|
|
486
|
+
'<button class="item' +
|
|
487
|
+
(active ? " active" : "") +
|
|
488
|
+
'" data-kind="tool" data-provider="' +
|
|
489
|
+
esc(provider.label) +
|
|
490
|
+
'" data-key="' +
|
|
491
|
+
esc(key) +
|
|
492
|
+
'">' +
|
|
493
|
+
'<span><span class="nm">' +
|
|
494
|
+
esc(local) +
|
|
495
|
+
'</span><span class="full">' +
|
|
496
|
+
esc(key) +
|
|
497
|
+
"</span></span>" +
|
|
498
|
+
'<span class="ds">' +
|
|
499
|
+
esc(itemDescription(item)) +
|
|
500
|
+
"</span></button>"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderGroups(provider) {
|
|
505
|
+
const groups = groupsFor(provider);
|
|
506
|
+
if (!groups.length) return '<div class="empty small">' + EMPTY + "</div>";
|
|
507
|
+
return groups
|
|
508
|
+
.map((group) => {
|
|
509
|
+
const id = groupId(provider, group.title);
|
|
510
|
+
const open = openGroups[id] !== false;
|
|
511
|
+
return (
|
|
512
|
+
'<details class="group" data-group="' +
|
|
513
|
+
esc(id) +
|
|
514
|
+
'"' +
|
|
515
|
+
(open ? " open" : "") +
|
|
516
|
+
'><summary><span class="chev"></span><span class="group-title">' +
|
|
517
|
+
esc(group.title) +
|
|
518
|
+
'</span><span class="group-count">' +
|
|
519
|
+
group.items.length +
|
|
520
|
+
'</span></summary><div class="group-list">' +
|
|
521
|
+
group.items.map((item) => renderItem(provider, item)).join("") +
|
|
522
|
+
"</div></details>"
|
|
523
|
+
);
|
|
524
|
+
})
|
|
525
|
+
.join("");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function renderProvider(provider) {
|
|
529
|
+
const c = counts(provider);
|
|
530
|
+
const title = provider.title || provider.name || provider.label;
|
|
531
|
+
const open = openProviders[provider.label] === true;
|
|
532
|
+
const actions =
|
|
533
|
+
provider.tabId === undefined
|
|
534
|
+
? ""
|
|
535
|
+
: '<div class="actions"><button class="action-btn provider-action" data-action="activate" data-provider="' +
|
|
536
|
+
esc(provider.label) +
|
|
537
|
+
'">Focus tab</button><button class="action-btn danger provider-action" data-action="close" data-provider="' +
|
|
538
|
+
esc(provider.label) +
|
|
539
|
+
'">Close</button></div>';
|
|
540
|
+
return (
|
|
541
|
+
'<details class="card provider-card" data-provider="' +
|
|
542
|
+
esc(provider.label) +
|
|
543
|
+
'"' +
|
|
544
|
+
(open ? " open" : "") +
|
|
545
|
+
'><summary class="provider-head"><div class="provider-main"><div class="provider-title">' +
|
|
546
|
+
'<div class="provider-name"><span class="label">' +
|
|
547
|
+
esc(provider.label) +
|
|
548
|
+
'</span><span class="provider-caret"></span>' +
|
|
549
|
+
'<span class="ptitle">' +
|
|
550
|
+
esc(title) +
|
|
551
|
+
"</span></div>" +
|
|
552
|
+
(provider.url ? '<div class="url">' + esc(provider.url) + "</div>" : "") +
|
|
553
|
+
'</div><div class="provider-side"><div class="metrics"><span class="metric">Tools ' +
|
|
554
|
+
c.tool +
|
|
555
|
+
"</span></div>" +
|
|
556
|
+
actions +
|
|
557
|
+
"</div></div></summary>" +
|
|
558
|
+
'<div class="card-body">' +
|
|
559
|
+
renderGroups(provider) +
|
|
560
|
+
"</div></details>"
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function allItems() {
|
|
565
|
+
const items = [];
|
|
566
|
+
for (const p of data.providers) {
|
|
567
|
+
for (const t of p.tools || []) items.push({ kind: "tool", provider: p, key: t.name, item: t });
|
|
568
|
+
}
|
|
569
|
+
return items;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function callProviderAction(provider, action) {
|
|
573
|
+
if (action === "close" && !confirm("Close tab for provider " + provider + "?")) return;
|
|
574
|
+
const res = await fetch("/api/providers/" + encodeURIComponent(provider) + "/" + action, {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: { "x-mcp-page-bridge-dashboard": "1" },
|
|
577
|
+
});
|
|
578
|
+
const body = await res.json().catch(() => ({}));
|
|
579
|
+
if (!res.ok || body.ok === false) throw new Error(body.error || "action failed");
|
|
580
|
+
if (action === "close" && selected?.provider === provider) selected = null;
|
|
581
|
+
await tick();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderList() {
|
|
585
|
+
const list = $("list");
|
|
586
|
+
if (!data.providers.length) {
|
|
587
|
+
list.innerHTML =
|
|
588
|
+
'<div class="card"><div class="empty">No browsers connected yet.<br/>Enable the mcp-page-bridge extension on a tab.</div></div>';
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
list.innerHTML = data.providers.map(renderProvider).join("");
|
|
592
|
+
|
|
593
|
+
for (const card of document.querySelectorAll(".provider-card")) {
|
|
594
|
+
card.addEventListener("toggle", () => {
|
|
595
|
+
openProviders[card.dataset.provider] = card.open;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
for (const btn of document.querySelectorAll(".provider-action")) {
|
|
599
|
+
btn.addEventListener("click", (event) => {
|
|
600
|
+
event.preventDefault();
|
|
601
|
+
event.stopPropagation();
|
|
602
|
+
btn.disabled = true;
|
|
603
|
+
callProviderAction(btn.dataset.provider, btn.dataset.action)
|
|
604
|
+
.catch((error) => {
|
|
605
|
+
$("statusText").textContent = error instanceof Error ? error.message : String(error);
|
|
606
|
+
})
|
|
607
|
+
.finally(() => {
|
|
608
|
+
btn.disabled = false;
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
for (const group of document.querySelectorAll(".group")) {
|
|
613
|
+
group.addEventListener("toggle", () => {
|
|
614
|
+
openGroups[group.dataset.group] = group.open;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
for (const btn of document.querySelectorAll(".item")) {
|
|
618
|
+
btn.addEventListener("click", () => {
|
|
619
|
+
selected = { kind: btn.dataset.kind, provider: btn.dataset.provider, key: btn.dataset.key };
|
|
620
|
+
renderList();
|
|
621
|
+
renderDetail();
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function schemaTable(schema) {
|
|
627
|
+
if (!schema || typeof schema !== "object" || !schema.properties) {
|
|
628
|
+
return '<p class="placeholder">No parameters.</p>';
|
|
629
|
+
}
|
|
630
|
+
const required = new Set(schema.required || []);
|
|
631
|
+
const rows = Object.entries(schema.properties)
|
|
632
|
+
.map(([name, def]) => {
|
|
633
|
+
const d = def || {};
|
|
634
|
+
const type = d.type || (d.enum ? "enum" : d.oneOf ? "oneOf" : d.anyOf ? "anyOf" : "any");
|
|
635
|
+
return (
|
|
636
|
+
"<tr><td><span class='pname'>" +
|
|
637
|
+
esc(name) +
|
|
638
|
+
"</span></td><td><span class='type'>" +
|
|
639
|
+
esc(type) +
|
|
640
|
+
"</span></td><td>" +
|
|
641
|
+
(required.has(name) ? "<span class='req'>yes</span>" : "-") +
|
|
642
|
+
"</td><td>" +
|
|
643
|
+
esc(d.description || "") +
|
|
644
|
+
"</td></tr>"
|
|
645
|
+
);
|
|
646
|
+
})
|
|
647
|
+
.join("");
|
|
648
|
+
return (
|
|
649
|
+
"<table><thead><tr><th>Param</th><th>Type</th><th>Req</th><th>Description</th></tr></thead><tbody>" +
|
|
650
|
+
rows +
|
|
651
|
+
"</tbody></table>"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function renderDetail() {
|
|
656
|
+
const el = $("detail");
|
|
657
|
+
if (!selected) {
|
|
658
|
+
el.innerHTML = '<div class="detail-inner"><div class="empty placeholder">Select an item to inspect its schema and source.</div></div>';
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const found = allItems().find(
|
|
662
|
+
(x) => x.kind === selected.kind && x.provider.label === selected.provider && x.key === selected.key,
|
|
663
|
+
);
|
|
664
|
+
if (!found) {
|
|
665
|
+
el.innerHTML = '<div class="detail-inner"><div class="empty placeholder">That item is no longer connected.</div></div>';
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const { kind, provider, item, key } = found;
|
|
670
|
+
const source = provider.label + (provider.title ? " / " + provider.title : "");
|
|
671
|
+
const body =
|
|
672
|
+
(item.description ? '<p class="desc">' + esc(item.description) + "</p>" : "") +
|
|
673
|
+
'<div class="section-title">Input</div>' +
|
|
674
|
+
schemaTable(item.inputSchema) +
|
|
675
|
+
(item.inputSchema
|
|
676
|
+
? '<details class="raw"><summary>Raw input schema</summary><pre>' +
|
|
677
|
+
esc(JSON.stringify(item.inputSchema, null, 2)) +
|
|
678
|
+
"</pre></details>"
|
|
679
|
+
: "");
|
|
680
|
+
|
|
681
|
+
el.innerHTML =
|
|
682
|
+
'<div class="detail-inner"><div class="detail-head"><span class="kind">' +
|
|
683
|
+
kind +
|
|
684
|
+
'</span><span class="from">' +
|
|
685
|
+
esc(source) +
|
|
686
|
+
"</span></div><h2>" +
|
|
687
|
+
esc(key) +
|
|
688
|
+
"</h2>" +
|
|
689
|
+
body +
|
|
690
|
+
"</div>";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function updateStats() {
|
|
694
|
+
const t = totals();
|
|
695
|
+
$("providerCount").textContent = t.providers;
|
|
696
|
+
$("toolCount").textContent = t.tools;
|
|
697
|
+
$("listCount").textContent = t.providers + (t.providers === 1 ? " connected" : " connected");
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function tick() {
|
|
701
|
+
try {
|
|
702
|
+
const res = await fetch("/api/providers", { cache: "no-store" });
|
|
703
|
+
data = await res.json();
|
|
704
|
+
$("dot").className = "dot on";
|
|
705
|
+
$("version").textContent = "v" + data.version;
|
|
706
|
+
$("endpoint").textContent = "ws://127.0.0.1:" + data.port;
|
|
707
|
+
const n = data.providers.length;
|
|
708
|
+
$("statusText").textContent = n ? "bridge online, browser providers connected" : "bridge online, waiting for browser tabs";
|
|
709
|
+
updateStats();
|
|
710
|
+
renderList();
|
|
711
|
+
renderDetail();
|
|
712
|
+
} catch {
|
|
713
|
+
data = { version: "", port: 8787, providers: [] };
|
|
714
|
+
$("dot").className = "dot";
|
|
715
|
+
$("statusText").textContent = "bridge not reachable";
|
|
716
|
+
updateStats();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
tick();
|
|
721
|
+
setInterval(tick, 1500);
|
|
722
|
+
</script>
|
|
723
|
+
</body>
|
|
724
|
+
</html>
|
|
725
|
+
`;
|
|
726
|
+
|
|
727
|
+
// src/ws-transport.ts
|
|
728
|
+
import { JSONRPCMessageSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
729
|
+
function rawToString(data, isBinary) {
|
|
730
|
+
if (typeof data === "string") return data;
|
|
731
|
+
if (Buffer.isBuffer(data)) return data.toString("utf8");
|
|
732
|
+
if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
|
|
733
|
+
return Buffer.from(data).toString("utf8");
|
|
734
|
+
}
|
|
735
|
+
var WebSocketServerTransport = class {
|
|
736
|
+
constructor(socket) {
|
|
737
|
+
this.socket = socket;
|
|
738
|
+
this.socket.on("message", (data, isBinary) => {
|
|
739
|
+
let message;
|
|
740
|
+
try {
|
|
741
|
+
message = JSONRPCMessageSchema.parse(JSON.parse(rawToString(data, isBinary)));
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.onerror?.(error);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (!this.started || !this.onmessage) {
|
|
747
|
+
this.buffer.push(message);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
this.onmessage(message);
|
|
751
|
+
});
|
|
752
|
+
this.socket.on("close", () => this.onclose?.());
|
|
753
|
+
this.socket.on("error", (error) => this.onerror?.(error));
|
|
754
|
+
}
|
|
755
|
+
socket;
|
|
756
|
+
onclose;
|
|
757
|
+
onerror;
|
|
758
|
+
onmessage;
|
|
759
|
+
sessionId;
|
|
760
|
+
started = false;
|
|
761
|
+
buffer = [];
|
|
762
|
+
async start() {
|
|
763
|
+
this.started = true;
|
|
764
|
+
const pending = this.buffer;
|
|
765
|
+
this.buffer = [];
|
|
766
|
+
for (const message of pending) this.onmessage?.(message);
|
|
767
|
+
}
|
|
768
|
+
async send(message) {
|
|
769
|
+
this.socket.send(JSON.stringify(message));
|
|
770
|
+
}
|
|
771
|
+
async close() {
|
|
772
|
+
this.socket.close();
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
// src/bridge.ts
|
|
777
|
+
var META_LIST_CLIENTS = "mcp_page_bridge_list_clients";
|
|
778
|
+
async function createBridge(opts = {}) {
|
|
779
|
+
const host = opts.host ?? "127.0.0.1";
|
|
780
|
+
const providers = /* @__PURE__ */ new Map();
|
|
781
|
+
const toolRoutes = /* @__PURE__ */ new Map();
|
|
782
|
+
const promptRoutes = /* @__PURE__ */ new Map();
|
|
783
|
+
const resourceRoutes = /* @__PURE__ */ new Map();
|
|
784
|
+
const server = new Server(
|
|
785
|
+
{ name: "mcp-page-bridge", version: MCP_PAGE_BRIDGE_VERSION },
|
|
786
|
+
{
|
|
787
|
+
capabilities: {
|
|
788
|
+
tools: { listChanged: true },
|
|
789
|
+
prompts: { listChanged: true },
|
|
790
|
+
resources: { listChanged: true },
|
|
791
|
+
logging: {}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
function metaTools() {
|
|
796
|
+
return [
|
|
797
|
+
{
|
|
798
|
+
name: META_LIST_CLIENTS,
|
|
799
|
+
description: "List browser MCP providers connected to mcp-page-bridge (label, source page, tool/prompt/resource counts).",
|
|
800
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false }
|
|
801
|
+
}
|
|
802
|
+
];
|
|
803
|
+
}
|
|
804
|
+
function providerContext(p) {
|
|
805
|
+
const bits = [p.label];
|
|
806
|
+
if (p.meta.title) {
|
|
807
|
+
bits.push(p.meta.title);
|
|
808
|
+
} else if (p.meta.url) {
|
|
809
|
+
try {
|
|
810
|
+
bits.push(new URL(p.meta.url).host);
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return bits.join(" \xB7 ");
|
|
815
|
+
}
|
|
816
|
+
function exposedTools() {
|
|
817
|
+
const out = [...metaTools()];
|
|
818
|
+
for (const p of providers.values()) {
|
|
819
|
+
const ctx = providerContext(p);
|
|
820
|
+
for (const t of p.tools) {
|
|
821
|
+
out.push({
|
|
822
|
+
...t,
|
|
823
|
+
name: namespaceName(p.label, t.name),
|
|
824
|
+
title: `${p.label}: ${t.title ?? t.name}`,
|
|
825
|
+
description: `[${ctx}] ${t.description ?? t.name}`
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
831
|
+
function exposedPrompts() {
|
|
832
|
+
const out = [];
|
|
833
|
+
for (const p of providers.values()) {
|
|
834
|
+
const ctx = providerContext(p);
|
|
835
|
+
for (const pr of p.prompts) {
|
|
836
|
+
out.push({
|
|
837
|
+
...pr,
|
|
838
|
+
name: namespaceName(p.label, pr.name),
|
|
839
|
+
description: `[${ctx}] ${pr.description ?? pr.name}`
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return out;
|
|
844
|
+
}
|
|
845
|
+
function exposedResources() {
|
|
846
|
+
const out = [];
|
|
847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
848
|
+
for (const p of providers.values()) {
|
|
849
|
+
for (const r of p.resources) {
|
|
850
|
+
if (seen.has(r.uri)) continue;
|
|
851
|
+
seen.add(r.uri);
|
|
852
|
+
out.push({ ...r, name: r.name ? `[${p.label}] ${r.name}` : r.uri });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return out;
|
|
856
|
+
}
|
|
857
|
+
function rebuildRoutes() {
|
|
858
|
+
toolRoutes.clear();
|
|
859
|
+
promptRoutes.clear();
|
|
860
|
+
resourceRoutes.clear();
|
|
861
|
+
for (const p of providers.values()) {
|
|
862
|
+
for (const t of p.tools) {
|
|
863
|
+
toolRoutes.set(namespaceName(p.label, t.name), { providerId: p.id, originalName: t.name });
|
|
864
|
+
}
|
|
865
|
+
for (const pr of p.prompts) {
|
|
866
|
+
promptRoutes.set(namespaceName(p.label, pr.name), {
|
|
867
|
+
providerId: p.id,
|
|
868
|
+
originalName: pr.name
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
for (const r of p.resources) {
|
|
872
|
+
if (!resourceRoutes.has(r.uri)) resourceRoutes.set(r.uri, p.id);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function notifyChanged(kind) {
|
|
877
|
+
rebuildRoutes();
|
|
878
|
+
const send = kind === "tools" ? () => server.sendToolListChanged() : kind === "prompts" ? () => server.sendPromptListChanged() : () => server.sendResourceListChanged();
|
|
879
|
+
void Promise.resolve().then(send).catch(() => {
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function uniqueLabel(base) {
|
|
883
|
+
const used = new Set([...providers.values()].map((p) => p.label));
|
|
884
|
+
if (!used.has(base)) return base;
|
|
885
|
+
let i = 2;
|
|
886
|
+
while (used.has(`${base}-${i}`)) i += 1;
|
|
887
|
+
return `${base}-${i}`;
|
|
888
|
+
}
|
|
889
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: exposedTools() }));
|
|
890
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
891
|
+
const name = req.params.name;
|
|
892
|
+
if (name === META_LIST_CLIENTS) {
|
|
893
|
+
return { content: [{ type: "text", text: JSON.stringify(providerSummary(), null, 2) }] };
|
|
894
|
+
}
|
|
895
|
+
const route = toolRoutes.get(name);
|
|
896
|
+
const provider = route && providers.get(route.providerId);
|
|
897
|
+
if (!route || !provider) {
|
|
898
|
+
return { content: [{ type: "text", text: `Unknown or disconnected tool: ${name}` }], isError: true };
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const result = await provider.client.callTool({
|
|
902
|
+
name: route.originalName,
|
|
903
|
+
arguments: req.params.arguments ?? {}
|
|
904
|
+
});
|
|
905
|
+
return result;
|
|
906
|
+
} catch (error) {
|
|
907
|
+
return {
|
|
908
|
+
content: [{ type: "text", text: `Tool call failed: ${error.message}` }],
|
|
909
|
+
isError: true
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: exposedPrompts() }));
|
|
914
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
915
|
+
const route = promptRoutes.get(req.params.name);
|
|
916
|
+
const provider = route && providers.get(route.providerId);
|
|
917
|
+
if (!route || !provider) throw new Error(`Unknown or disconnected prompt: ${req.params.name}`);
|
|
918
|
+
return provider.client.getPrompt({
|
|
919
|
+
name: route.originalName,
|
|
920
|
+
arguments: req.params.arguments
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
924
|
+
resources: exposedResources()
|
|
925
|
+
}));
|
|
926
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
927
|
+
const providerId = resourceRoutes.get(req.params.uri);
|
|
928
|
+
const provider = providerId ? providers.get(providerId) : void 0;
|
|
929
|
+
if (!provider) throw new Error(`Unknown or disconnected resource: ${req.params.uri}`);
|
|
930
|
+
return provider.client.readResource({ uri: req.params.uri });
|
|
931
|
+
});
|
|
932
|
+
function providerSummary() {
|
|
933
|
+
return [...providers.values()].map((p) => ({
|
|
934
|
+
label: p.label,
|
|
935
|
+
name: p.rawName,
|
|
936
|
+
version: p.version,
|
|
937
|
+
url: p.meta.url,
|
|
938
|
+
title: p.meta.title,
|
|
939
|
+
tabId: p.meta.tabId,
|
|
940
|
+
providerId: p.meta.providerId,
|
|
941
|
+
tools: p.tools.map((t) => ({
|
|
942
|
+
name: namespaceName(p.label, t.name),
|
|
943
|
+
description: t.description,
|
|
944
|
+
inputSchema: t.inputSchema
|
|
945
|
+
})),
|
|
946
|
+
prompts: p.prompts.map((pr) => ({
|
|
947
|
+
name: namespaceName(p.label, pr.name),
|
|
948
|
+
description: pr.description,
|
|
949
|
+
arguments: pr.arguments
|
|
950
|
+
})),
|
|
951
|
+
resources: p.resources.map((r) => ({
|
|
952
|
+
uri: r.uri,
|
|
953
|
+
name: r.name,
|
|
954
|
+
mimeType: r.mimeType
|
|
955
|
+
})),
|
|
956
|
+
connectedAt: new Date(p.connectedAt).toISOString()
|
|
957
|
+
}));
|
|
958
|
+
}
|
|
959
|
+
function requestMeta(url) {
|
|
960
|
+
try {
|
|
961
|
+
const parsed = new URL(url ?? "/", "ws://localhost");
|
|
962
|
+
const rawTabId = parsed.searchParams.get("tabId");
|
|
963
|
+
const tabId = rawTabId ? Number(rawTabId) : void 0;
|
|
964
|
+
return {
|
|
965
|
+
tabId: Number.isFinite(tabId) ? tabId : void 0,
|
|
966
|
+
providerId: parsed.searchParams.get("providerId") ?? void 0
|
|
967
|
+
};
|
|
968
|
+
} catch {
|
|
969
|
+
return {};
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function handleProviderAction(req, res, label, action) {
|
|
973
|
+
if (req.headers["x-mcp-page-bridge-dashboard"] !== "1") {
|
|
974
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
975
|
+
res.end(JSON.stringify({ ok: false, error: "missing dashboard header" }));
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const provider = [...providers.values()].find((p) => p.label === label);
|
|
979
|
+
if (!provider) {
|
|
980
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
981
|
+
res.end(JSON.stringify({ ok: false, error: `provider not found: ${label}` }));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (provider.meta.tabId === void 0) {
|
|
985
|
+
res.writeHead(409, { "content-type": "application/json" });
|
|
986
|
+
res.end(JSON.stringify({ ok: false, error: "provider is not attached to a browser tab" }));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const method = action === "activate" ? MCP_PAGE_BRIDGE_DASHBOARD_ACTIVATE_TAB : MCP_PAGE_BRIDGE_DASHBOARD_CLOSE_TAB;
|
|
990
|
+
try {
|
|
991
|
+
await provider.client.request({ method }, EmptyResultSchema);
|
|
992
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
993
|
+
res.end(JSON.stringify({ ok: true }));
|
|
994
|
+
} catch (error) {
|
|
995
|
+
res.writeHead(502, { "content-type": "application/json", "cache-control": "no-store" });
|
|
996
|
+
res.end(
|
|
997
|
+
JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const token = opts.token;
|
|
1002
|
+
const httpServer = createHttpServer((req, res) => {
|
|
1003
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
1004
|
+
const actionMatch = path.match(/^\/api\/providers\/([^/]+)\/(activate|close)$/);
|
|
1005
|
+
if (req.method === "POST" && actionMatch) {
|
|
1006
|
+
void handleProviderAction(
|
|
1007
|
+
req,
|
|
1008
|
+
res,
|
|
1009
|
+
decodeURIComponent(actionMatch[1]),
|
|
1010
|
+
actionMatch[2]
|
|
1011
|
+
);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (req.method === "GET" && (path === "/" || path === "/ui")) {
|
|
1015
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1016
|
+
res.end(DASHBOARD_HTML);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (req.method === "GET" && path === "/favicon.svg") {
|
|
1020
|
+
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "max-age=86400" });
|
|
1021
|
+
res.end(FAVICON_SVG);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (req.method === "GET" && (path === "/api/providers" || path === "/providers.json")) {
|
|
1025
|
+
res.writeHead(200, {
|
|
1026
|
+
"content-type": "application/json",
|
|
1027
|
+
"cache-control": "no-store",
|
|
1028
|
+
"access-control-allow-origin": "*"
|
|
1029
|
+
});
|
|
1030
|
+
res.end(JSON.stringify({ version: MCP_PAGE_BRIDGE_VERSION, port, providers: providerSummary() }));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
1034
|
+
res.end("not found");
|
|
1035
|
+
});
|
|
1036
|
+
const wss = new WebSocketServer({
|
|
1037
|
+
server: httpServer,
|
|
1038
|
+
handleProtocols: (protocols) => protocols.has(WS_SUBPROTOCOL) ? WS_SUBPROTOCOL : false,
|
|
1039
|
+
verifyClient: token ? (info) => {
|
|
1040
|
+
try {
|
|
1041
|
+
const url = new URL(info.req.url ?? "/", "ws://localhost");
|
|
1042
|
+
return url.searchParams.get("token") === token;
|
|
1043
|
+
} catch {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
} : void 0
|
|
1047
|
+
});
|
|
1048
|
+
await new Promise((resolve, reject) => {
|
|
1049
|
+
httpServer.once("listening", resolve);
|
|
1050
|
+
httpServer.once("error", reject);
|
|
1051
|
+
httpServer.listen(opts.port ?? DEFAULT_PORT, host);
|
|
1052
|
+
});
|
|
1053
|
+
const address = httpServer.address();
|
|
1054
|
+
const port = typeof address === "object" && address ? address.port : opts.port ?? DEFAULT_PORT;
|
|
1055
|
+
wss.on("connection", async (ws, req) => {
|
|
1056
|
+
const transport = new WebSocketServerTransport(ws);
|
|
1057
|
+
const client = new Client({ name: "mcp-page-bridge", version: MCP_PAGE_BRIDGE_VERSION }, { capabilities: {} });
|
|
1058
|
+
const id = randomUUID();
|
|
1059
|
+
try {
|
|
1060
|
+
await client.connect(transport);
|
|
1061
|
+
} catch {
|
|
1062
|
+
try {
|
|
1063
|
+
ws.close();
|
|
1064
|
+
} catch {
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const info = client.getServerVersion();
|
|
1069
|
+
const caps = client.getServerCapabilities();
|
|
1070
|
+
const rawName = info?.name ?? "browser";
|
|
1071
|
+
const provider = {
|
|
1072
|
+
id,
|
|
1073
|
+
label: uniqueLabel(sanitizeLabel(rawName)),
|
|
1074
|
+
rawName,
|
|
1075
|
+
version: info?.version ?? "0.0.0",
|
|
1076
|
+
client,
|
|
1077
|
+
tools: [],
|
|
1078
|
+
prompts: [],
|
|
1079
|
+
resources: [],
|
|
1080
|
+
meta: {
|
|
1081
|
+
title: info?.title,
|
|
1082
|
+
url: info?.websiteUrl,
|
|
1083
|
+
...requestMeta(req.url)
|
|
1084
|
+
},
|
|
1085
|
+
connectedAt: Date.now()
|
|
1086
|
+
};
|
|
1087
|
+
providers.set(id, provider);
|
|
1088
|
+
const refreshTools = async () => {
|
|
1089
|
+
if (!caps?.tools) return;
|
|
1090
|
+
try {
|
|
1091
|
+
provider.tools = (await client.listTools()).tools;
|
|
1092
|
+
} catch {
|
|
1093
|
+
provider.tools = [];
|
|
1094
|
+
}
|
|
1095
|
+
notifyChanged("tools");
|
|
1096
|
+
};
|
|
1097
|
+
const refreshPrompts = async () => {
|
|
1098
|
+
if (!caps?.prompts) return;
|
|
1099
|
+
try {
|
|
1100
|
+
provider.prompts = (await client.listPrompts()).prompts;
|
|
1101
|
+
} catch {
|
|
1102
|
+
provider.prompts = [];
|
|
1103
|
+
}
|
|
1104
|
+
notifyChanged("prompts");
|
|
1105
|
+
};
|
|
1106
|
+
const refreshResources = async () => {
|
|
1107
|
+
if (!caps?.resources) return;
|
|
1108
|
+
try {
|
|
1109
|
+
provider.resources = (await client.listResources()).resources;
|
|
1110
|
+
} catch {
|
|
1111
|
+
provider.resources = [];
|
|
1112
|
+
}
|
|
1113
|
+
notifyChanged("resources");
|
|
1114
|
+
};
|
|
1115
|
+
if (caps?.tools?.listChanged) {
|
|
1116
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, () => void refreshTools());
|
|
1117
|
+
}
|
|
1118
|
+
if (caps?.prompts?.listChanged) {
|
|
1119
|
+
client.setNotificationHandler(PromptListChangedNotificationSchema, () => void refreshPrompts());
|
|
1120
|
+
}
|
|
1121
|
+
if (caps?.resources?.listChanged) {
|
|
1122
|
+
client.setNotificationHandler(
|
|
1123
|
+
ResourceListChangedNotificationSchema,
|
|
1124
|
+
() => void refreshResources()
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (caps?.logging) {
|
|
1128
|
+
client.setNotificationHandler(LoggingMessageNotificationSchema, (note) => {
|
|
1129
|
+
void Promise.resolve().then(
|
|
1130
|
+
() => server.sendLoggingMessage({
|
|
1131
|
+
...note.params,
|
|
1132
|
+
logger: `${provider.label}/${note.params.logger ?? "page"}`
|
|
1133
|
+
})
|
|
1134
|
+
).catch(() => {
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
const cleanup = () => {
|
|
1139
|
+
if (providers.delete(id)) {
|
|
1140
|
+
rebuildRoutes();
|
|
1141
|
+
notifyChanged("tools");
|
|
1142
|
+
notifyChanged("prompts");
|
|
1143
|
+
notifyChanged("resources");
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
client.onclose = cleanup;
|
|
1147
|
+
ws.on("close", cleanup);
|
|
1148
|
+
await Promise.all([refreshTools(), refreshPrompts(), refreshResources()]);
|
|
1149
|
+
});
|
|
1150
|
+
return {
|
|
1151
|
+
server,
|
|
1152
|
+
wss,
|
|
1153
|
+
port,
|
|
1154
|
+
listProviders() {
|
|
1155
|
+
return [...providers.values()].map(({ client: _client, ...rest }) => rest);
|
|
1156
|
+
},
|
|
1157
|
+
async close() {
|
|
1158
|
+
for (const p of providers.values()) {
|
|
1159
|
+
try {
|
|
1160
|
+
await p.client.close();
|
|
1161
|
+
} catch {
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
await new Promise((resolve) => wss.close(() => resolve()));
|
|
1165
|
+
await new Promise((resolve) => httpServer.close(() => resolve()));
|
|
1166
|
+
try {
|
|
1167
|
+
await server.close();
|
|
1168
|
+
} catch {
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export {
|
|
1175
|
+
MCP_PAGE_BRIDGE_VERSION,
|
|
1176
|
+
DEFAULT_PORT,
|
|
1177
|
+
createBridge
|
|
1178
|
+
};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PORT,
|
|
4
|
+
MCP_PAGE_BRIDGE_VERSION,
|
|
5
|
+
createBridge
|
|
6
|
+
} from "./chunk-7BFUNK5V.js";
|
|
7
|
+
|
|
8
|
+
// src/cli.ts
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
function parseFlag(argv, flag) {
|
|
11
|
+
const idx = argv.indexOf(flag);
|
|
12
|
+
if (idx >= 0 && argv[idx + 1]) return argv[idx + 1];
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
function parsePort(argv) {
|
|
16
|
+
const fromFlag = parseFlag(argv, "--port") ?? process.env.MCP_PAGE_BRIDGE_PORT;
|
|
17
|
+
const n = Number(fromFlag);
|
|
18
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_PORT;
|
|
19
|
+
}
|
|
20
|
+
async function main() {
|
|
21
|
+
const argv = process.argv.slice(2);
|
|
22
|
+
const port = parsePort(argv);
|
|
23
|
+
const token = parseFlag(argv, "--token") ?? process.env.MCP_PAGE_BRIDGE_TOKEN;
|
|
24
|
+
const bridge = await createBridge({ port, token });
|
|
25
|
+
console.error(
|
|
26
|
+
`[mcp-page-bridge] v${MCP_PAGE_BRIDGE_VERSION} \u2014 ws://127.0.0.1:${bridge.port} \xB7 dashboard http://127.0.0.1:${bridge.port}/` + (token ? " (token required)" : "")
|
|
27
|
+
);
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await bridge.server.connect(transport);
|
|
30
|
+
console.error("[mcp-page-bridge] MCP stdio server ready (waiting for agent + browser connections)");
|
|
31
|
+
const shutdown = async () => {
|
|
32
|
+
await bridge.close();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
process.on("SIGINT", shutdown);
|
|
36
|
+
process.on("SIGTERM", shutdown);
|
|
37
|
+
}
|
|
38
|
+
main().catch((error) => {
|
|
39
|
+
console.error("[mcp-page-bridge] fatal:", error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-page-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP bridge that aggregates live browser-page MCP servers and exposes them to a coding agent over stdio",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Eray Ates",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rytsh/r-mcp.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"browser",
|
|
16
|
+
"websocket",
|
|
17
|
+
"agent",
|
|
18
|
+
"opencode"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"mcp-page-bridge": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./bridge": {
|
|
25
|
+
"types": "./dist/bridge.d.ts",
|
|
26
|
+
"import": "./dist/bridge.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
|
+
"ws": "^8.21.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.10.2",
|
|
44
|
+
"@types/ws": "^8.5.13",
|
|
45
|
+
"tsup": "^8.3.5",
|
|
46
|
+
"tsx": "^4.19.2",
|
|
47
|
+
"typescript": "^5.7.3",
|
|
48
|
+
"vitest": "^2.1.8",
|
|
49
|
+
"zod": "^3.25.0",
|
|
50
|
+
"@mcp-page-bridge/protocol": "0.1.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"start": "tsx src/cli.ts",
|
|
54
|
+
"dev": "tsx watch src/cli.ts",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
58
|
+
"build": "tsup"
|
|
59
|
+
}
|
|
60
|
+
}
|