@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.
Files changed (151) hide show
  1. package/.mcp-broker.example/CONFIGURATION-EN.md +861 -0
  2. package/.mcp-broker.example/CONFIGURATION-FR.md +871 -0
  3. package/.mcp-broker.example/README.md +8 -3
  4. package/.mcp-broker.example/config.json +86 -0
  5. package/README.md +122 -3
  6. package/dist/bin.d.ts +0 -1
  7. package/dist/bin.js +180 -208
  8. package/dist/bin.js.map +1 -1
  9. package/dist/chunk-FTDKH2C4.js +3670 -0
  10. package/dist/chunk-FTDKH2C4.js.map +1 -0
  11. package/dist/{broker/grammars → grammars}/claude/en.json +1 -1
  12. package/dist/{broker/grammars → grammars}/claude/fr.json +1 -1
  13. package/dist/index.d.ts +1790 -18
  14. package/dist/index.js +2 -14
  15. package/dist/index.js.map +1 -1
  16. package/package.json +14 -8
  17. package/scripts/copy-assets.mjs +16 -10
  18. package/scripts/gen-cert.mjs +5 -5
  19. package/scripts/pack-mcpb.mjs +5 -5
  20. package/scripts/sign-bundle.mjs +6 -6
  21. package/src/auth/auth.config.ts +98 -0
  22. package/src/auth/auth.types.ts +120 -0
  23. package/src/auth/http.auth.ts +128 -0
  24. package/src/auth/index.ts +34 -0
  25. package/src/auth/jwt.validator.ts +63 -0
  26. package/src/auth/provider.auth.ts +114 -0
  27. package/src/auth/resource.metadata.ts +34 -0
  28. package/src/authorization/audit.ts +35 -0
  29. package/src/authorization/capability.classifier.ts +114 -0
  30. package/src/authorization/index.ts +37 -0
  31. package/src/authorization/policy.engine.ts +212 -0
  32. package/src/authorization/policy.types.ts +105 -0
  33. package/src/authorization/resource.path.ts +126 -0
  34. package/src/authorization/runtime.ts +56 -0
  35. package/src/authorization/slot.resource.ts +50 -0
  36. package/src/authorization/subject.mapper.ts +81 -0
  37. package/src/bin.ts +90 -7
  38. package/src/broker/adapters/broker.adapter.info.ts +3 -3
  39. package/src/broker/adapters/broker.adapter.providers.ts +3 -3
  40. package/src/broker/aggregate/aggregate.catalog.ts +49 -21
  41. package/src/broker/aggregate/aggregate.server.ts +149 -19
  42. package/src/broker/aggregate/provider.client.session.ts +22 -22
  43. package/src/broker/behaviors/broker.behavior.info.ts +4 -4
  44. package/src/broker/behaviors/broker.behavior.providers.ts +6 -6
  45. package/src/broker/broker.context.ts +10 -4
  46. package/src/broker/broker.grammars.ts +16 -13
  47. package/src/broker/broker.server.ts +12 -9
  48. package/src/broker/grammars/claude/en.json +1 -1
  49. package/src/broker/grammars/claude/fr.json +1 -1
  50. package/src/broker/index.ts +9 -9
  51. package/src/config.ts +81 -8
  52. package/src/index.ts +121 -20
  53. package/src/{mcpb.loader.ts → mcpb/mcpb.loader.ts} +24 -21
  54. package/src/{mcpb.unzip.ts → mcpb/mcpb.unzip.ts} +2 -2
  55. package/src/remote.transports.ts +11 -8
  56. package/src/remote.upstream.ts +11 -8
  57. package/src/stdio.upstream.ts +9 -6
  58. package/src/upstream.ts +6 -3
  59. package/src/ws/ws.interfaces.ts +357 -0
  60. package/src/{ws.tunnel.builder.ts → ws/ws.tunnel.builder.ts} +112 -9
  61. package/src/{ws.tunnel.ts → ws/ws.tunnel.ts} +646 -457
  62. package/web/README.md +94 -0
  63. package/web/assets/logo.png +0 -0
  64. package/web/broker-self-mcp.html +338 -0
  65. package/web/css/styles.css +580 -0
  66. package/web/demos/DemoPlaceholder.html +258 -0
  67. package/web/demos/broker-explorer/css/app.css +393 -0
  68. package/web/demos/broker-explorer/index.html +94 -0
  69. package/web/demos/broker-explorer/js/app.js +271 -0
  70. package/web/demos/broker-explorer/js/mcp-ws-client.js +132 -0
  71. package/web/demos/oauth-lab/README.md +94 -0
  72. package/web/demos/oauth-lab/config.json +117 -0
  73. package/web/demos/oauth-lab/css/app.css +1097 -0
  74. package/web/demos/oauth-lab/index.html +323 -0
  75. package/web/demos/oauth-lab/js/app.js +654 -0
  76. package/web/demos/oauth-lab/server/auth-server.mjs +426 -0
  77. package/web/demos/oauth-lab/server/factory-provider.mjs +269 -0
  78. package/web/demos/oauth-lab/server/smoke-test.mjs +308 -0
  79. package/web/demos/oauth-lab/server/start.mjs +106 -0
  80. package/web/demos/provider-tunnel/css/app.css +384 -0
  81. package/web/demos/provider-tunnel/index.html +99 -0
  82. package/web/demos/provider-tunnel/js/app.js +226 -0
  83. package/web/demos/provider-tunnel/js/toolbox-server.js +186 -0
  84. package/web/index.html +558 -0
  85. package/web/js/lib/broker-tunnel.js +173 -0
  86. package/dist/broker/adapters/broker.adapter.info.d.ts +0 -16
  87. package/dist/broker/adapters/broker.adapter.info.js +0 -43
  88. package/dist/broker/adapters/broker.adapter.info.js.map +0 -1
  89. package/dist/broker/adapters/broker.adapter.providers.d.ts +0 -18
  90. package/dist/broker/adapters/broker.adapter.providers.js +0 -61
  91. package/dist/broker/adapters/broker.adapter.providers.js.map +0 -1
  92. package/dist/broker/aggregate/aggregate.catalog.d.ts +0 -54
  93. package/dist/broker/aggregate/aggregate.catalog.js +0 -105
  94. package/dist/broker/aggregate/aggregate.catalog.js.map +0 -1
  95. package/dist/broker/aggregate/aggregate.server.d.ts +0 -47
  96. package/dist/broker/aggregate/aggregate.server.js +0 -151
  97. package/dist/broker/aggregate/aggregate.server.js.map +0 -1
  98. package/dist/broker/aggregate/provider.client.session.d.ts +0 -52
  99. package/dist/broker/aggregate/provider.client.session.js +0 -140
  100. package/dist/broker/aggregate/provider.client.session.js.map +0 -1
  101. package/dist/broker/behaviors/broker.behavior.info.d.ts +0 -15
  102. package/dist/broker/behaviors/broker.behavior.info.js +0 -41
  103. package/dist/broker/behaviors/broker.behavior.info.js.map +0 -1
  104. package/dist/broker/behaviors/broker.behavior.providers.d.ts +0 -19
  105. package/dist/broker/behaviors/broker.behavior.providers.js +0 -69
  106. package/dist/broker/behaviors/broker.behavior.providers.js.map +0 -1
  107. package/dist/broker/broker.context.d.ts +0 -59
  108. package/dist/broker/broker.context.js +0 -2
  109. package/dist/broker/broker.context.js.map +0 -1
  110. package/dist/broker/broker.grammars.d.ts +0 -130
  111. package/dist/broker/broker.grammars.js +0 -229
  112. package/dist/broker/broker.grammars.js.map +0 -1
  113. package/dist/broker/broker.server.d.ts +0 -66
  114. package/dist/broker/broker.server.js +0 -73
  115. package/dist/broker/broker.server.js.map +0 -1
  116. package/dist/broker/index.d.ts +0 -9
  117. package/dist/broker/index.js +0 -7
  118. package/dist/broker/index.js.map +0 -1
  119. package/dist/config.d.ts +0 -136
  120. package/dist/config.js +0 -61
  121. package/dist/config.js.map +0 -1
  122. package/dist/mcpb.loader.d.ts +0 -24
  123. package/dist/mcpb.loader.js +0 -161
  124. package/dist/mcpb.loader.js.map +0 -1
  125. package/dist/mcpb.unzip.d.ts +0 -6
  126. package/dist/mcpb.unzip.js +0 -95
  127. package/dist/mcpb.unzip.js.map +0 -1
  128. package/dist/remote.transports.d.ts +0 -16
  129. package/dist/remote.transports.js +0 -297
  130. package/dist/remote.transports.js.map +0 -1
  131. package/dist/remote.upstream.d.ts +0 -36
  132. package/dist/remote.upstream.js +0 -52
  133. package/dist/remote.upstream.js.map +0 -1
  134. package/dist/stdio.upstream.d.ts +0 -45
  135. package/dist/stdio.upstream.js +0 -85
  136. package/dist/stdio.upstream.js.map +0 -1
  137. package/dist/upstream.d.ts +0 -33
  138. package/dist/upstream.js +0 -2
  139. package/dist/upstream.js.map +0 -1
  140. package/dist/version.d.ts +0 -2
  141. package/dist/version.js +0 -9
  142. package/dist/version.js.map +0 -1
  143. package/dist/ws.tunnel.builder.d.ts +0 -139
  144. package/dist/ws.tunnel.builder.js +0 -205
  145. package/dist/ws.tunnel.builder.js.map +0 -1
  146. package/dist/ws.tunnel.d.ts +0 -373
  147. package/dist/ws.tunnel.js +0 -1090
  148. package/dist/ws.tunnel.js.map +0 -1
  149. /package/dist/{broker/grammars → grammars}/default/en.json +0 -0
  150. /package/dist/{broker/grammars → grammars}/default/fr.json +0 -0
  151. /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/&lt;name&gt;</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");