@cyanmycelium/mcp-broker 0.4.0 → 1.2.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/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
- package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
- package/.mcp-broker.example/README.md +8 -3
- package/.mcp-broker.example/config.json +86 -0
- package/README.md +122 -3
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +180 -208
- package/dist/bin.js.map +1 -1
- package/dist/chunk-FTDKH2C4.js +3670 -0
- package/dist/chunk-FTDKH2C4.js.map +1 -0
- package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
- package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
- package/dist/index.d.ts +1790 -18
- package/dist/index.js +2 -14
- package/dist/index.js.map +1 -1
- package/package.json +14 -8
- package/scripts/copy-assets.mjs +16 -10
- package/scripts/gen-cert.mjs +5 -5
- package/scripts/pack-mcpb.mjs +5 -5
- package/scripts/sign-bundle.mjs +6 -6
- package/src/auth/auth.config.ts +98 -0
- package/src/auth/auth.types.ts +120 -0
- package/src/auth/http.auth.ts +128 -0
- package/src/auth/index.ts +34 -0
- package/src/auth/jwt.validator.ts +63 -0
- package/src/auth/provider.auth.ts +114 -0
- package/src/auth/resource.metadata.ts +34 -0
- package/src/authorization/audit.ts +35 -0
- package/src/authorization/capability.classifier.ts +114 -0
- package/src/authorization/index.ts +37 -0
- package/src/authorization/policy.engine.ts +212 -0
- package/src/authorization/policy.types.ts +105 -0
- package/src/authorization/resource.path.ts +126 -0
- package/src/authorization/runtime.ts +56 -0
- package/src/authorization/slot.resource.ts +50 -0
- package/src/authorization/subject.mapper.ts +81 -0
- package/src/bin.ts +90 -7
- package/src/broker/adapters/broker.adapter.info.ts +3 -3
- package/src/broker/adapters/broker.adapter.providers.ts +3 -3
- package/src/broker/aggregate/aggregate.catalog.ts +49 -21
- package/src/broker/aggregate/aggregate.server.ts +149 -19
- package/src/broker/aggregate/provider.client.session.ts +22 -22
- package/src/broker/behaviors/broker.behavior.info.ts +4 -4
- package/src/broker/behaviors/broker.behavior.providers.ts +6 -6
- package/src/broker/broker.context.ts +10 -4
- package/src/broker/broker.grammars.ts +16 -13
- package/src/broker/broker.server.ts +12 -9
- package/src/broker/grammars/claude/en.json +1 -1
- package/src/broker/grammars/claude/fr.json +1 -1
- package/src/broker/index.ts +9 -9
- package/src/config.ts +81 -8
- package/src/index.ts +121 -20
- package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +24 -21
- package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
- package/src/remote.transports.ts +11 -8
- package/src/remote.upstream.ts +11 -8
- package/src/stdio.upstream.ts +9 -6
- package/src/upstream.ts +6 -3
- package/src/ws/ws.interfaces.ts +357 -0
- package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +112 -9
- package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +646 -457
- package/web/README.md +94 -0
- package/web/assets/logo.png +0 -0
- package/web/broker-self-mcp.html +338 -0
- package/web/css/styles.css +580 -0
- package/web/demos/DemoPlaceholder.html +258 -0
- package/web/demos/broker-explorer/css/app.css +393 -0
- package/web/demos/broker-explorer/index.html +94 -0
- package/web/demos/broker-explorer/js/app.js +271 -0
- package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
- package/web/demos/oauth-lab/README.md +94 -0
- package/web/demos/oauth-lab/config.json +117 -0
- package/web/demos/oauth-lab/css/app.css +1097 -0
- package/web/demos/oauth-lab/index.html +323 -0
- package/web/demos/oauth-lab/js/app.js +654 -0
- package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
- package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
- package/web/demos/oauth-lab/server/smoke-test.mjs +308 -0
- package/web/demos/oauth-lab/server/start.mjs +106 -0
- package/web/demos/provider-tunnel/css/app.css +384 -0
- package/web/demos/provider-tunnel/index.html +99 -0
- package/web/demos/provider-tunnel/js/app.js +226 -0
- package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
- package/web/index.html +558 -0
- package/web/js/lib/broker-tunnel.js +173 -0
- package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
- package/dist/broker/adapters/broker.adapter.info.js +0 -43
- package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
- package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
- package/dist/broker/adapters/broker.adapter.providers.js +0 -61
- package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
- package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -54
- package/dist/broker/aggregate/aggregate.catalog.js +0 -105
- package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
- package/dist/broker/aggregate/aggregate.server.d.ts +0 -47
- package/dist/broker/aggregate/aggregate.server.js +0 -151
- package/dist/broker/aggregate/aggregate.server.js.map +0 -1
- package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
- package/dist/broker/aggregate/provider.client.session.js +0 -140
- package/dist/broker/aggregate/provider.client.session.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
- package/dist/broker/behaviors/broker.behavior.info.js +0 -41
- package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
- package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
- package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
- package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
- package/dist/broker/broker.context.d.ts +0 -59
- package/dist/broker/broker.context.js +0 -2
- package/dist/broker/broker.context.js.map +0 -1
- package/dist/broker/broker.grammars.d.ts +0 -130
- package/dist/broker/broker.grammars.js +0 -229
- package/dist/broker/broker.grammars.js.map +0 -1
- package/dist/broker/broker.server.d.ts +0 -66
- package/dist/broker/broker.server.js +0 -73
- package/dist/broker/broker.server.js.map +0 -1
- package/dist/broker/index.d.ts +0 -9
- package/dist/broker/index.js +0 -7
- package/dist/broker/index.js.map +0 -1
- package/dist/config.d.ts +0 -136
- package/dist/config.js +0 -61
- package/dist/config.js.map +0 -1
- package/dist/mcpb.loader.d.ts +0 -24
- package/dist/mcpb.loader.js +0 -161
- package/dist/mcpb.loader.js.map +0 -1
- package/dist/mcpb.unzip.d.ts +0 -6
- package/dist/mcpb.unzip.js +0 -95
- package/dist/mcpb.unzip.js.map +0 -1
- package/dist/remote.transports.d.ts +0 -16
- package/dist/remote.transports.js +0 -297
- package/dist/remote.transports.js.map +0 -1
- package/dist/remote.upstream.d.ts +0 -36
- package/dist/remote.upstream.js +0 -52
- package/dist/remote.upstream.js.map +0 -1
- package/dist/stdio.upstream.d.ts +0 -45
- package/dist/stdio.upstream.js +0 -85
- package/dist/stdio.upstream.js.map +0 -1
- package/dist/upstream.d.ts +0 -33
- package/dist/upstream.js +0 -2
- package/dist/upstream.js.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.js +0 -9
- package/dist/version.js.map +0 -1
- package/dist/ws.tunnel.builder.d.ts +0 -139
- package/dist/ws.tunnel.builder.js +0 -205
- package/dist/ws.tunnel.builder.js.map +0 -1
- package/dist/ws.tunnel.d.ts +0 -373
- package/dist/ws.tunnel.js +0 -1090
- package/dist/ws.tunnel.js.map +0 -1
- /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
- /package/dist/{broker/grammars → grammars}/default/zh.json +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/* =============================================================
|
|
2
|
+
Provider Tunnel Demo, app-specific overrides
|
|
3
|
+
Base: ../../css/styles.css
|
|
4
|
+
============================================================= */
|
|
5
|
+
|
|
6
|
+
/* Extra tokens not in styles.css */
|
|
7
|
+
:root {
|
|
8
|
+
--amber: #ffb86b;
|
|
9
|
+
--red: #f78c6c;
|
|
10
|
+
--bg-code: #0a0e13;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* App shell: full-viewport, no scroll */
|
|
14
|
+
html, body {
|
|
15
|
+
height: 100dvh;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
font-size: 15px;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
code {
|
|
26
|
+
font-family: var(--font-mono);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Topbar: direct-flex layout (styles.css flexes on .topbar-inner) */
|
|
30
|
+
.topbar {
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
height: 56px;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 16px;
|
|
36
|
+
padding: 0 22px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Brand: override link color and suppress hover underline */
|
|
40
|
+
.brand {
|
|
41
|
+
color: var(--fg);
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
}
|
|
44
|
+
.brand:hover {
|
|
45
|
+
text-decoration: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Brand mark: 26px (styles.css uses 24px) */
|
|
49
|
+
.brand-mark {
|
|
50
|
+
width: 26px;
|
|
51
|
+
height: 26px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.topbar-tag {
|
|
55
|
+
font-family: var(--font-mono);
|
|
56
|
+
font-size: 11px;
|
|
57
|
+
letter-spacing: 0.14em;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
color: var(--blue);
|
|
60
|
+
padding: 3px 9px;
|
|
61
|
+
border: 1px solid var(--rule-strong);
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.topbar-spacer {
|
|
66
|
+
flex: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.topbar-link {
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
color: var(--fg-muted);
|
|
72
|
+
text-decoration: none;
|
|
73
|
+
}
|
|
74
|
+
.topbar-link:hover {
|
|
75
|
+
color: var(--fg);
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Status badge (replaces docs .badge definition) */
|
|
80
|
+
.badge {
|
|
81
|
+
font-family: var(--font-mono);
|
|
82
|
+
font-size: 11px;
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
letter-spacing: 0.08em;
|
|
85
|
+
text-transform: uppercase;
|
|
86
|
+
padding: 4px 11px;
|
|
87
|
+
border-radius: 99px;
|
|
88
|
+
border: 1px solid var(--rule-strong);
|
|
89
|
+
height: auto;
|
|
90
|
+
background: none;
|
|
91
|
+
color: var(--fg-muted);
|
|
92
|
+
}
|
|
93
|
+
.badge.disconnected {
|
|
94
|
+
color: var(--red);
|
|
95
|
+
background: rgba(247, 140, 108, 0.08);
|
|
96
|
+
border-color: rgba(247, 140, 108, 0.3);
|
|
97
|
+
}
|
|
98
|
+
.badge.connecting {
|
|
99
|
+
color: var(--amber);
|
|
100
|
+
background: rgba(255, 184, 107, 0.08);
|
|
101
|
+
border-color: rgba(255, 184, 107, 0.3);
|
|
102
|
+
}
|
|
103
|
+
.badge.connected {
|
|
104
|
+
color: var(--lime);
|
|
105
|
+
background: rgba(184, 224, 96, 0.08);
|
|
106
|
+
border-color: rgba(184, 224, 96, 0.32);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Buttons: 38px (styles.css uses 44px) */
|
|
110
|
+
.btn {
|
|
111
|
+
height: 38px;
|
|
112
|
+
padding: 0 16px;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
}
|
|
115
|
+
.btn:disabled {
|
|
116
|
+
opacity: 0.4;
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
}
|
|
119
|
+
.btn-primary:not(:disabled):hover {
|
|
120
|
+
background: var(--blue-bright);
|
|
121
|
+
border-color: var(--blue-bright);
|
|
122
|
+
color: var(--blue-deep);
|
|
123
|
+
}
|
|
124
|
+
.btn-secondary:not(:disabled):hover {
|
|
125
|
+
border-color: var(--fg-muted);
|
|
126
|
+
background: var(--bg-elev-1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Layout */
|
|
130
|
+
.layout {
|
|
131
|
+
flex: 1;
|
|
132
|
+
display: flex;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
.left-panel {
|
|
136
|
+
width: 360px;
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
border-right: 1px solid var(--rule);
|
|
139
|
+
}
|
|
140
|
+
.right-column {
|
|
141
|
+
flex: 1;
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
}
|
|
146
|
+
.panel {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
overflow: hidden;
|
|
150
|
+
}
|
|
151
|
+
.panel-title {
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
font-family: var(--font-mono);
|
|
154
|
+
font-size: 11px;
|
|
155
|
+
font-weight: 500;
|
|
156
|
+
letter-spacing: 0.14em;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
color: var(--blue);
|
|
159
|
+
padding: 11px 18px;
|
|
160
|
+
background: var(--bg-elev-1);
|
|
161
|
+
border-bottom: 1px solid var(--rule);
|
|
162
|
+
}
|
|
163
|
+
.panel-body {
|
|
164
|
+
flex: 1;
|
|
165
|
+
overflow-y: auto;
|
|
166
|
+
padding: 18px;
|
|
167
|
+
}
|
|
168
|
+
.catalog-panel {
|
|
169
|
+
flex: 1;
|
|
170
|
+
border-bottom: 1px solid var(--rule);
|
|
171
|
+
}
|
|
172
|
+
.console-panel {
|
|
173
|
+
height: 240px;
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Form controls */
|
|
178
|
+
.field {
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
gap: 6px;
|
|
182
|
+
margin-bottom: 16px;
|
|
183
|
+
}
|
|
184
|
+
.field label {
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
color: var(--fg-dim);
|
|
187
|
+
}
|
|
188
|
+
input[type="text"] {
|
|
189
|
+
width: 100%;
|
|
190
|
+
background: var(--bg-code);
|
|
191
|
+
border: 1px solid var(--rule-strong);
|
|
192
|
+
border-radius: 6px;
|
|
193
|
+
color: var(--fg);
|
|
194
|
+
font-family: var(--font-mono);
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
padding: 8px 11px;
|
|
197
|
+
}
|
|
198
|
+
input[type="text"]:focus {
|
|
199
|
+
outline: none;
|
|
200
|
+
border-color: var(--blue-dim);
|
|
201
|
+
}
|
|
202
|
+
input[readonly] {
|
|
203
|
+
color: var(--blue-bright);
|
|
204
|
+
cursor: text;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.btn-row {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
flex-wrap: wrap;
|
|
211
|
+
gap: 9px;
|
|
212
|
+
margin-top: 4px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Aggregate opt-in checkbox + hover tooltip */
|
|
216
|
+
.check {
|
|
217
|
+
position: relative;
|
|
218
|
+
margin-left: auto;
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
gap: 6px;
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
color: var(--fg-muted);
|
|
224
|
+
cursor: pointer;
|
|
225
|
+
user-select: none;
|
|
226
|
+
}
|
|
227
|
+
.check input {
|
|
228
|
+
margin: 0;
|
|
229
|
+
accent-color: var(--blue);
|
|
230
|
+
cursor: pointer;
|
|
231
|
+
}
|
|
232
|
+
.check input:disabled {
|
|
233
|
+
cursor: not-allowed;
|
|
234
|
+
}
|
|
235
|
+
.check-tip {
|
|
236
|
+
position: absolute;
|
|
237
|
+
top: calc(100% + 8px);
|
|
238
|
+
right: 0;
|
|
239
|
+
width: 220px;
|
|
240
|
+
padding: 9px 11px;
|
|
241
|
+
background: var(--bg-elev-2);
|
|
242
|
+
border: 1px solid var(--rule-strong);
|
|
243
|
+
border-radius: 6px;
|
|
244
|
+
font-size: 12px;
|
|
245
|
+
line-height: 1.5;
|
|
246
|
+
color: var(--fg-muted);
|
|
247
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
248
|
+
opacity: 0;
|
|
249
|
+
visibility: hidden;
|
|
250
|
+
transition: opacity 120ms;
|
|
251
|
+
z-index: 20;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
}
|
|
254
|
+
.check:hover .check-tip,
|
|
255
|
+
.check:focus-within .check-tip {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
visibility: visible;
|
|
258
|
+
}
|
|
259
|
+
.check-tip code {
|
|
260
|
+
font-family: var(--font-mono);
|
|
261
|
+
font-size: 0.9em;
|
|
262
|
+
color: var(--blue);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Endpoints */
|
|
266
|
+
.endpoints {
|
|
267
|
+
margin-top: 20px;
|
|
268
|
+
padding-top: 16px;
|
|
269
|
+
border-top: 1px solid var(--rule);
|
|
270
|
+
}
|
|
271
|
+
.endpoints .field {
|
|
272
|
+
margin-bottom: 12px;
|
|
273
|
+
}
|
|
274
|
+
.endpoints .field label {
|
|
275
|
+
font-family: var(--font-mono);
|
|
276
|
+
font-size: 10px;
|
|
277
|
+
letter-spacing: 0.1em;
|
|
278
|
+
text-transform: uppercase;
|
|
279
|
+
color: var(--blue);
|
|
280
|
+
}
|
|
281
|
+
.endpoints .field:last-child {
|
|
282
|
+
margin-bottom: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.hint {
|
|
286
|
+
margin-top: 18px;
|
|
287
|
+
font-size: 13px;
|
|
288
|
+
line-height: 1.6;
|
|
289
|
+
color: var(--fg-dim);
|
|
290
|
+
}
|
|
291
|
+
.hint code {
|
|
292
|
+
font-size: 0.86em;
|
|
293
|
+
color: var(--blue);
|
|
294
|
+
background: var(--bg-inline);
|
|
295
|
+
border: 1px solid var(--rule);
|
|
296
|
+
padding: 1px 5px;
|
|
297
|
+
border-radius: 4px;
|
|
298
|
+
}
|
|
299
|
+
.hint strong {
|
|
300
|
+
color: var(--fg-muted);
|
|
301
|
+
font-weight: 600;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Catalog */
|
|
305
|
+
.cat-group {
|
|
306
|
+
margin-bottom: 20px;
|
|
307
|
+
}
|
|
308
|
+
.cat-group:last-child {
|
|
309
|
+
margin-bottom: 0;
|
|
310
|
+
}
|
|
311
|
+
.cat-group-title {
|
|
312
|
+
font-family: var(--font-mono);
|
|
313
|
+
font-size: 11px;
|
|
314
|
+
font-weight: 600;
|
|
315
|
+
letter-spacing: 0.12em;
|
|
316
|
+
text-transform: uppercase;
|
|
317
|
+
color: var(--fg-dim);
|
|
318
|
+
margin-bottom: 10px;
|
|
319
|
+
}
|
|
320
|
+
.cat-item {
|
|
321
|
+
background: var(--bg-elev-1);
|
|
322
|
+
border: 1px solid var(--rule);
|
|
323
|
+
border-radius: 8px;
|
|
324
|
+
padding: 10px 13px;
|
|
325
|
+
margin-bottom: 9px;
|
|
326
|
+
transition: border-color 140ms, background 140ms;
|
|
327
|
+
}
|
|
328
|
+
.cat-item:hover {
|
|
329
|
+
border-color: var(--blue-dim);
|
|
330
|
+
background: var(--bg-elev-2);
|
|
331
|
+
}
|
|
332
|
+
.cat-item-name {
|
|
333
|
+
font-family: var(--font-mono);
|
|
334
|
+
font-size: 13px;
|
|
335
|
+
font-weight: 600;
|
|
336
|
+
color: var(--blue);
|
|
337
|
+
}
|
|
338
|
+
.cat-item-name.cat-item-uri {
|
|
339
|
+
color: var(--lime);
|
|
340
|
+
}
|
|
341
|
+
.cat-item-desc {
|
|
342
|
+
font-size: 13px;
|
|
343
|
+
color: var(--fg-muted);
|
|
344
|
+
margin-top: 3px;
|
|
345
|
+
line-height: 1.5;
|
|
346
|
+
}
|
|
347
|
+
.cat-item-params {
|
|
348
|
+
font-family: var(--font-mono);
|
|
349
|
+
font-size: 12px;
|
|
350
|
+
color: var(--fg-dim);
|
|
351
|
+
margin-top: 5px;
|
|
352
|
+
}
|
|
353
|
+
.cat-item-params .pname {
|
|
354
|
+
color: var(--amber);
|
|
355
|
+
}
|
|
356
|
+
.cat-item-params .preq {
|
|
357
|
+
color: var(--red);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* Console */
|
|
361
|
+
#console-output {
|
|
362
|
+
font-family: var(--font-mono);
|
|
363
|
+
font-size: 12.5px;
|
|
364
|
+
line-height: 1.65;
|
|
365
|
+
background: var(--bg-code);
|
|
366
|
+
}
|
|
367
|
+
.log-line {
|
|
368
|
+
display: flex;
|
|
369
|
+
gap: 10px;
|
|
370
|
+
}
|
|
371
|
+
.log-line .ts {
|
|
372
|
+
color: var(--fg-dim);
|
|
373
|
+
user-select: none;
|
|
374
|
+
flex-shrink: 0;
|
|
375
|
+
}
|
|
376
|
+
.log-line .msg {
|
|
377
|
+
min-width: 0;
|
|
378
|
+
overflow-wrap: anywhere;
|
|
379
|
+
}
|
|
380
|
+
.log-line.info .msg { color: var(--fg); }
|
|
381
|
+
.log-line.ok .msg { color: var(--lime); }
|
|
382
|
+
.log-line.warn .msg { color: var(--amber); }
|
|
383
|
+
.log-line.error .msg { color: var(--red); }
|
|
384
|
+
.log-line.dim .msg { color: var(--fg-dim); }
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>MCP Broker, Provider Tunnel Demo</title>
|
|
7
|
+
<link rel="icon" type="image/png" href="../../assets/logo.png" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
|
+
<link
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
13
|
+
/>
|
|
14
|
+
<link rel="stylesheet" href="../../css/styles.css" />
|
|
15
|
+
<link rel="stylesheet" href="css/app.css" />
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<!-- Topbar -->
|
|
19
|
+
<header class="topbar">
|
|
20
|
+
<a class="brand" href="https://github.com/pandaGaume/mcp-broker" target="_blank" rel="noopener">
|
|
21
|
+
<img class="brand-mark" src="../../assets/logo.png" alt="" aria-hidden="true" />
|
|
22
|
+
<span>@cyanmycelium/mcp-broker</span>
|
|
23
|
+
</a>
|
|
24
|
+
<span class="topbar-tag">Provider Tunnel Demo</span>
|
|
25
|
+
<span class="topbar-spacer"></span>
|
|
26
|
+
<a class="topbar-link" href="https://github.com/pandaGaume/mcp-broker" target="_blank" rel="noopener">
|
|
27
|
+
GitHub →
|
|
28
|
+
</a>
|
|
29
|
+
<span id="status-badge" class="badge disconnected">Disconnected</span>
|
|
30
|
+
</header>
|
|
31
|
+
|
|
32
|
+
<div class="layout">
|
|
33
|
+
<!-- Left: tunnel connection -->
|
|
34
|
+
<aside class="left-panel panel">
|
|
35
|
+
<div class="panel-title">Tunnel connection</div>
|
|
36
|
+
<div class="panel-body">
|
|
37
|
+
<div class="field">
|
|
38
|
+
<label for="ws-url">Broker provider base URL</label>
|
|
39
|
+
<input type="text" id="ws-url" />
|
|
40
|
+
</div>
|
|
41
|
+
<div class="field">
|
|
42
|
+
<label for="server-name">Provider slot name</label>
|
|
43
|
+
<input type="text" id="server-name" value="demo-toolbox" />
|
|
44
|
+
</div>
|
|
45
|
+
<div class="btn-row">
|
|
46
|
+
<button id="start-btn" class="btn btn-primary">Connect</button>
|
|
47
|
+
<button id="stop-btn" class="btn btn-secondary" disabled>Disconnect</button>
|
|
48
|
+
<label class="check">
|
|
49
|
+
<input type="checkbox" id="aggregate-check" />
|
|
50
|
+
<span>aggregate</span>
|
|
51
|
+
<span class="check-tip" role="tooltip">
|
|
52
|
+
Joins the broker's <code>_all</code> slot: this server's tools and prompts are
|
|
53
|
+
merged into one aggregate MCP endpoint alongside every other aggregated
|
|
54
|
+
provider, so a single client reaches them all.
|
|
55
|
+
</span>
|
|
56
|
+
</label>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="endpoints" id="endpoints" hidden>
|
|
60
|
+
<div class="field">
|
|
61
|
+
<label for="ep-http">Streamable HTTP (MCP Inspector)</label>
|
|
62
|
+
<input type="text" id="ep-http" readonly />
|
|
63
|
+
</div>
|
|
64
|
+
<div class="field">
|
|
65
|
+
<label for="ep-sse">Legacy SSE</label>
|
|
66
|
+
<input type="text" id="ep-sse" readonly />
|
|
67
|
+
</div>
|
|
68
|
+
<div class="field">
|
|
69
|
+
<label for="ep-ws">Raw WebSocket</label>
|
|
70
|
+
<input type="text" id="ep-ws" readonly />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<p class="hint">
|
|
75
|
+
This page hosts an MCP <strong>server</strong> built with the official
|
|
76
|
+
<code>@modelcontextprotocol/sdk</code>. Connecting opens an outbound WebSocket to the broker's
|
|
77
|
+
<code>/provider/<name></code> endpoint: the broker then relays any MCP client that
|
|
78
|
+
targets the slot above. The broker-specific code is isolated in
|
|
79
|
+
<code>js/lib/broker-tunnel.js</code>.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
</aside>
|
|
83
|
+
|
|
84
|
+
<!-- Right: catalog + console -->
|
|
85
|
+
<div class="right-column">
|
|
86
|
+
<section class="panel catalog-panel">
|
|
87
|
+
<div class="panel-title">Server catalog, what this MCP server exposes</div>
|
|
88
|
+
<div class="panel-body" id="catalog"></div>
|
|
89
|
+
</section>
|
|
90
|
+
<section class="panel console-panel">
|
|
91
|
+
<div class="panel-title">Console</div>
|
|
92
|
+
<div class="panel-body" id="console-output"></div>
|
|
93
|
+
</section>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<script type="module" src="js/app.js"></script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app.js, UI wiring for the provider tunnel demo.
|
|
3
|
+
*
|
|
4
|
+
* Owns the DOM: console log, status badge, catalog panel, and the connect /
|
|
5
|
+
* disconnect lifecycle. The two pieces of real logic are imported:
|
|
6
|
+
*
|
|
7
|
+
* - js/lib/broker-tunnel.js, broker connection (reusable, zero-dependency,
|
|
8
|
+
* shared across demos, lives at the web root).
|
|
9
|
+
* - ./toolbox-server.js : the demo MCP server (plain MCP, broker-agnostic).
|
|
10
|
+
*/
|
|
11
|
+
import { BrokerTunnelTransport, describeTunnelClose } from "../../../js/lib/broker-tunnel.js";
|
|
12
|
+
import { TOOLS, RESOURCES, createToolboxServer } from "./toolbox-server.js";
|
|
13
|
+
|
|
14
|
+
// =====================================================================
|
|
15
|
+
// Console + status helpers
|
|
16
|
+
// =====================================================================
|
|
17
|
+
const consoleEl = document.getElementById("console-output");
|
|
18
|
+
|
|
19
|
+
function log(msg, level = "info") {
|
|
20
|
+
const ts = new Date().toLocaleTimeString("en", {
|
|
21
|
+
hour12: false,
|
|
22
|
+
hour: "2-digit",
|
|
23
|
+
minute: "2-digit",
|
|
24
|
+
second: "2-digit",
|
|
25
|
+
});
|
|
26
|
+
const div = document.createElement("div");
|
|
27
|
+
div.className = `log-line ${level}`;
|
|
28
|
+
div.innerHTML = `<span class="ts">${ts}</span><span class="msg"></span>`;
|
|
29
|
+
div.querySelector(".msg").textContent = msg;
|
|
30
|
+
consoleEl.appendChild(div);
|
|
31
|
+
consoleEl.scrollTop = consoleEl.scrollHeight;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const badgeEl = document.getElementById("status-badge");
|
|
35
|
+
let state = "disconnected";
|
|
36
|
+
|
|
37
|
+
function setStatus(next) {
|
|
38
|
+
state = next;
|
|
39
|
+
badgeEl.className = `badge ${next}`;
|
|
40
|
+
badgeEl.textContent =
|
|
41
|
+
{ disconnected: "Disconnected", connecting: "Connecting…", connected: "Connected" }[next] ?? next;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =====================================================================
|
|
45
|
+
// Catalog panel, rendered from the toolbox server's exported definitions.
|
|
46
|
+
// =====================================================================
|
|
47
|
+
function renderCatalog() {
|
|
48
|
+
const el = document.getElementById("catalog");
|
|
49
|
+
el.innerHTML = "";
|
|
50
|
+
|
|
51
|
+
const toolGroup = document.createElement("div");
|
|
52
|
+
toolGroup.className = "cat-group";
|
|
53
|
+
toolGroup.innerHTML = `<div class="cat-group-title">Tools (${TOOLS.length})</div>`;
|
|
54
|
+
for (const tool of TOOLS) {
|
|
55
|
+
const item = document.createElement("div");
|
|
56
|
+
item.className = "cat-item";
|
|
57
|
+
const props = tool.inputSchema.properties ?? {};
|
|
58
|
+
const required = tool.inputSchema.required ?? [];
|
|
59
|
+
const params = Object.entries(props)
|
|
60
|
+
.map(([pn, pd]) => {
|
|
61
|
+
const req = required.includes(pn);
|
|
62
|
+
return `<span class="pname">${pn}</span>${req ? '<span class="preq">*</span>' : ""}: ${pd.type}`;
|
|
63
|
+
})
|
|
64
|
+
.join(", ");
|
|
65
|
+
item.innerHTML =
|
|
66
|
+
`<div class="cat-item-name">${tool.name}</div>` +
|
|
67
|
+
`<div class="cat-item-desc"></div>` +
|
|
68
|
+
(params ? `<div class="cat-item-params">(${params})</div>` : "");
|
|
69
|
+
item.querySelector(".cat-item-desc").textContent = tool.description;
|
|
70
|
+
toolGroup.appendChild(item);
|
|
71
|
+
}
|
|
72
|
+
el.appendChild(toolGroup);
|
|
73
|
+
|
|
74
|
+
const resGroup = document.createElement("div");
|
|
75
|
+
resGroup.className = "cat-group";
|
|
76
|
+
resGroup.innerHTML = `<div class="cat-group-title">Resources (${RESOURCES.length})</div>`;
|
|
77
|
+
for (const res of RESOURCES) {
|
|
78
|
+
const item = document.createElement("div");
|
|
79
|
+
item.className = "cat-item";
|
|
80
|
+
item.innerHTML = `<div class="cat-item-name cat-item-uri"></div>` + `<div class="cat-item-desc"></div>`;
|
|
81
|
+
item.querySelector(".cat-item-uri").textContent = res.uri;
|
|
82
|
+
item.querySelector(".cat-item-desc").textContent = res.description;
|
|
83
|
+
resGroup.appendChild(item);
|
|
84
|
+
}
|
|
85
|
+
el.appendChild(resGroup);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =====================================================================
|
|
89
|
+
// Lifecycle
|
|
90
|
+
// =====================================================================
|
|
91
|
+
let server = null;
|
|
92
|
+
let transport = null;
|
|
93
|
+
let intentionalClose = false;
|
|
94
|
+
let connectGraceTimer = null;
|
|
95
|
+
let tunnelWasOpen = false;
|
|
96
|
+
|
|
97
|
+
const startBtn = document.getElementById("start-btn");
|
|
98
|
+
const stopBtn = document.getElementById("stop-btn");
|
|
99
|
+
const wsUrlInput = document.getElementById("ws-url");
|
|
100
|
+
const nameInput = document.getElementById("server-name");
|
|
101
|
+
const endpointsEl = document.getElementById("endpoints");
|
|
102
|
+
const aggregateCheck = document.getElementById("aggregate-check");
|
|
103
|
+
|
|
104
|
+
// Derive the provider WebSocket base from the page origin: ws:// over HTTP,
|
|
105
|
+
// wss:// over HTTPS, avoids mixed-content errors under TLS.
|
|
106
|
+
(function deriveProviderUrl() {
|
|
107
|
+
const scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
108
|
+
wsUrlInput.value = `${scheme}//${window.location.host}/provider`;
|
|
109
|
+
})();
|
|
110
|
+
|
|
111
|
+
function teardown() {
|
|
112
|
+
server = null;
|
|
113
|
+
transport = null;
|
|
114
|
+
tunnelWasOpen = false;
|
|
115
|
+
setStatus("disconnected");
|
|
116
|
+
startBtn.disabled = false;
|
|
117
|
+
stopBtn.disabled = true;
|
|
118
|
+
aggregateCheck.disabled = false;
|
|
119
|
+
endpointsEl.hidden = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function showEndpoints(slotName) {
|
|
123
|
+
const httpScheme = window.location.protocol === "https:" ? "https:" : "http:";
|
|
124
|
+
const wsScheme = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
125
|
+
const host = window.location.host;
|
|
126
|
+
const enc = encodeURIComponent(slotName);
|
|
127
|
+
document.getElementById("ep-http").value = `${httpScheme}//${host}/${enc}/mcp`;
|
|
128
|
+
document.getElementById("ep-sse").value = `${httpScheme}//${host}/${enc}/sse`;
|
|
129
|
+
document.getElementById("ep-ws").value = `${wsScheme}//${host}/${enc}`;
|
|
130
|
+
endpointsEl.hidden = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The WS "handshake check": classify how the tunnel socket closed.
|
|
134
|
+
// describeTunnelClose (lib/broker-tunnel.js) maps the close code to a
|
|
135
|
+
// diagnosis, broker rejection (1008), unreachable, or dropped.
|
|
136
|
+
function handleTunnelClose(code, reason) {
|
|
137
|
+
// A pending success banner must not fire once the slot is rejected.
|
|
138
|
+
if (connectGraceTimer) {
|
|
139
|
+
clearTimeout(connectGraceTimer);
|
|
140
|
+
connectGraceTimer = null;
|
|
141
|
+
}
|
|
142
|
+
if (intentionalClose) {
|
|
143
|
+
intentionalClose = false;
|
|
144
|
+
teardown();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const { level, message } = describeTunnelClose(code, reason, tunnelWasOpen);
|
|
148
|
+
log(message, level);
|
|
149
|
+
teardown();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
startBtn.addEventListener("click", async () => {
|
|
153
|
+
const base = wsUrlInput.value.trim().replace(/\/+$/, "");
|
|
154
|
+
const slotName = nameInput.value.trim() || "demo-toolbox";
|
|
155
|
+
if (!base) {
|
|
156
|
+
log("Enter the broker provider base URL first.", "error");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const url = `${base}/${encodeURIComponent(slotName)}`;
|
|
160
|
+
|
|
161
|
+
const aggregate = aggregateCheck.checked;
|
|
162
|
+
|
|
163
|
+
setStatus("connecting");
|
|
164
|
+
startBtn.disabled = true;
|
|
165
|
+
aggregateCheck.disabled = true;
|
|
166
|
+
intentionalClose = false;
|
|
167
|
+
tunnelWasOpen = false;
|
|
168
|
+
log(`Opening tunnel WebSocket to ${url} …`, "dim");
|
|
169
|
+
if (aggregate) {
|
|
170
|
+
log("Aggregate mode on: this server will join the broker's _all slot.", "dim");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
transport = new BrokerTunnelTransport(url, { aggregate });
|
|
174
|
+
transport.onTunnelOpen = () => {
|
|
175
|
+
tunnelWasOpen = true;
|
|
176
|
+
};
|
|
177
|
+
transport.onTunnelClose = handleTunnelClose;
|
|
178
|
+
transport.onTunnelError = (err) => log(`Transport error: ${err.message}`, "error");
|
|
179
|
+
|
|
180
|
+
server = createToolboxServer({ name: slotName, onActivity: log });
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// connect() awaits transport.start(), resolves when the broker accepts
|
|
184
|
+
// the WebSocket upgrade. The MCP initialize handshake happens later,
|
|
185
|
+
// when a real client targets the slot.
|
|
186
|
+
await server.connect(transport);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log(`Failed to open tunnel: ${err.message}`, "error");
|
|
189
|
+
// handleTunnelClose may already have run; teardown is idempotent.
|
|
190
|
+
teardown();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The broker accepts the WebSocket upgrade first, then may still reject the
|
|
195
|
+
// slot at the application layer with a 1008 close (name already taken, or
|
|
196
|
+
// reserved) within a few milliseconds. Hold a short grace window before
|
|
197
|
+
// declaring success, handleTunnelClose cancels this timer if a rejection
|
|
198
|
+
// lands first.
|
|
199
|
+
connectGraceTimer = setTimeout(() => {
|
|
200
|
+
connectGraceTimer = null;
|
|
201
|
+
setStatus("connected");
|
|
202
|
+
stopBtn.disabled = false;
|
|
203
|
+
showEndpoints(slotName);
|
|
204
|
+
log(`Tunnel open. Provider slot "${slotName}" is now routable through the broker.`, "ok");
|
|
205
|
+
log("Point an MCP client (e.g. MCP Inspector) at the Streamable HTTP URL.", "dim");
|
|
206
|
+
}, 250);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
stopBtn.addEventListener("click", async () => {
|
|
210
|
+
if (!server) return;
|
|
211
|
+
intentionalClose = true;
|
|
212
|
+
stopBtn.disabled = true;
|
|
213
|
+
try {
|
|
214
|
+
await server.close(); // closes the transport, which closes the WS
|
|
215
|
+
log("Tunnel closed. Provider slot released.", "warn");
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log(`Error while closing: ${err.message}`, "error");
|
|
218
|
+
}
|
|
219
|
+
teardown();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// =====================================================================
|
|
223
|
+
// Boot
|
|
224
|
+
// =====================================================================
|
|
225
|
+
renderCatalog();
|
|
226
|
+
log("Ready. Press Connect to tunnel this MCP server to the broker.", "dim");
|