@frontmcp/plugin-dashboard 0.0.1 → 0.7.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/esm/index.mjs ADDED
@@ -0,0 +1,1281 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+
12
+ // plugins/plugin-dashboard/src/dashboard.plugin.ts
13
+ import { DynamicPlugin, Plugin } from "@frontmcp/sdk";
14
+
15
+ // plugins/plugin-dashboard/src/dashboard.types.ts
16
+ import { z } from "zod";
17
+ var cdnConfigSchema = z.object({
18
+ /** Base URL for the dashboard UI bundle (e.g., https://esm.sh/@frontmcp/dashboard-ui@1.0.0) */
19
+ entrypoint: z.string().optional(),
20
+ /** React CDN URL */
21
+ react: z.string().default("https://esm.sh/react@19"),
22
+ /** React DOM CDN URL */
23
+ reactDom: z.string().default("https://esm.sh/react-dom@19"),
24
+ /** React DOM client CDN URL */
25
+ reactDomClient: z.string().default("https://esm.sh/react-dom@19/client"),
26
+ /** React JSX runtime CDN URL */
27
+ reactJsxRuntime: z.string().default("https://esm.sh/react@19/jsx-runtime"),
28
+ /** React Router CDN URL */
29
+ reactRouter: z.string().default("https://esm.sh/react-router-dom@7"),
30
+ /** XYFlow (React Flow) CDN URL */
31
+ xyflow: z.string().default("https://esm.sh/@xyflow/react@12?external=react,react-dom"),
32
+ /** Dagre layout library CDN URL */
33
+ dagre: z.string().default("https://esm.sh/dagre@0.8.5"),
34
+ /** XYFlow CSS URL */
35
+ xyflowCss: z.string().default("https://esm.sh/@xyflow/react@12/dist/style.css")
36
+ });
37
+ var dashboardAuthSchema = z.object({
38
+ /** Enable authentication (default: false) */
39
+ enabled: z.boolean().default(false),
40
+ /** Secret token for query param authentication (?token=xxx) */
41
+ token: z.string().optional()
42
+ });
43
+ var dashboardPluginOptionsSchema = z.object({
44
+ /** Enable/disable dashboard (undefined = auto: enabled in dev, disabled in prod) */
45
+ enabled: z.boolean().optional(),
46
+ /** Base path for dashboard routes (default: /dashboard) */
47
+ basePath: z.string().default("/dashboard"),
48
+ /** Authentication configuration */
49
+ auth: dashboardAuthSchema.optional().transform((v) => dashboardAuthSchema.parse(v ?? {})),
50
+ /** CDN configuration for UI loading */
51
+ cdn: cdnConfigSchema.optional().transform((v) => cdnConfigSchema.parse(v ?? {}))
52
+ });
53
+ var defaultDashboardPluginOptions = {
54
+ basePath: "/dashboard",
55
+ auth: { enabled: false },
56
+ cdn: {}
57
+ };
58
+ function isDashboardEnabled(options) {
59
+ if (options.enabled !== void 0) {
60
+ return options.enabled;
61
+ }
62
+ return process.env["NODE_ENV"] !== "production";
63
+ }
64
+
65
+ // plugins/plugin-dashboard/src/dashboard.symbol.ts
66
+ var DashboardConfigToken = /* @__PURE__ */ Symbol("DashboardConfig");
67
+ var GraphDataProviderToken = /* @__PURE__ */ Symbol("GraphDataProvider");
68
+ var ParentScopeToken = /* @__PURE__ */ Symbol("ParentScope");
69
+
70
+ // plugins/plugin-dashboard/src/dashboard.plugin.ts
71
+ var DashboardPlugin = class extends DynamicPlugin {
72
+ options;
73
+ constructor(options = {}) {
74
+ super();
75
+ this.options = dashboardPluginOptionsSchema.parse({
76
+ ...defaultDashboardPluginOptions,
77
+ ...options
78
+ });
79
+ }
80
+ /**
81
+ * Dynamic providers allow configuration of the dashboard with custom options.
82
+ * This injects the parsed options into the DI container so all dashboard
83
+ * components can access the configuration.
84
+ */
85
+ static dynamicProviders(options) {
86
+ const parsedOptions = dashboardPluginOptionsSchema.parse({
87
+ ...defaultDashboardPluginOptions,
88
+ ...options
89
+ });
90
+ return [
91
+ {
92
+ name: "dashboard:config",
93
+ provide: DashboardConfigToken,
94
+ useValue: parsedOptions
95
+ }
96
+ ];
97
+ }
98
+ };
99
+ DashboardPlugin = __decorateClass([
100
+ Plugin({
101
+ name: "dashboard",
102
+ description: "Visual dashboard for FrontMCP server monitoring and visualization",
103
+ providers: []
104
+ })
105
+ ], DashboardPlugin);
106
+
107
+ // plugins/plugin-dashboard/src/app/dashboard.app.ts
108
+ import {
109
+ App,
110
+ Plugin as Plugin2,
111
+ DynamicPlugin as DynamicPlugin2,
112
+ ScopeEntry as ScopeEntry4,
113
+ FrontMcpConfig,
114
+ FrontMcpServer
115
+ } from "@frontmcp/sdk";
116
+
117
+ // plugins/plugin-dashboard/src/providers/graph-data.provider.ts
118
+ import { Provider } from "@frontmcp/sdk";
119
+ var GraphDataProvider = class {
120
+ constructor(scope, serverName, serverVersion) {
121
+ this.scope = scope;
122
+ this.serverName = serverName;
123
+ this.serverVersion = serverVersion;
124
+ }
125
+ /** Cached graph data to avoid re-extraction on every request */
126
+ cachedData = null;
127
+ /** Timestamp of when the cache was last updated */
128
+ cacheTimestamp = 0;
129
+ /** Cache time-to-live in milliseconds */
130
+ cacheTTL = 5e3;
131
+ /**
132
+ * Get graph data for the current scope.
133
+ * Uses caching to avoid expensive extraction on every request.
134
+ */
135
+ async getGraphData() {
136
+ const now = Date.now();
137
+ if (this.cachedData && now - this.cacheTimestamp < this.cacheTTL) {
138
+ return this.cachedData;
139
+ }
140
+ this.cachedData = this.extractGraphData();
141
+ this.cacheTimestamp = now;
142
+ return this.cachedData;
143
+ }
144
+ /**
145
+ * Invalidate the cache to force fresh extraction.
146
+ */
147
+ invalidateCache() {
148
+ this.cachedData = null;
149
+ this.cacheTimestamp = 0;
150
+ }
151
+ /**
152
+ * Get all scopes from the ScopeRegistry, excluding the dashboard scope.
153
+ * Traverses up the provider hierarchy to find the ScopeRegistry if not found locally.
154
+ */
155
+ getMonitoredScopes() {
156
+ try {
157
+ let scopeRegistries = this.scope.providers.getRegistries("ScopeRegistry");
158
+ if (!scopeRegistries || scopeRegistries.length === 0) {
159
+ let currentProviders = this.scope.providers.parentProviders;
160
+ while (currentProviders && (!scopeRegistries || scopeRegistries.length === 0)) {
161
+ scopeRegistries = currentProviders.getRegistries?.("ScopeRegistry") || [];
162
+ currentProviders = currentProviders.parentProviders;
163
+ }
164
+ }
165
+ if (!scopeRegistries || scopeRegistries.length === 0) {
166
+ return [this.scope];
167
+ }
168
+ const scopeRegistry = scopeRegistries[0];
169
+ const allScopes = scopeRegistry.getScopes();
170
+ return allScopes.filter((s) => s.id !== "dashboard");
171
+ } catch {
172
+ return [this.scope];
173
+ }
174
+ }
175
+ /**
176
+ * Extract graph data from all monitored scopes.
177
+ */
178
+ extractGraphData() {
179
+ const nodes = [];
180
+ const edges = [];
181
+ const addedNodeIds = /* @__PURE__ */ new Set();
182
+ const addNode = (node) => {
183
+ if (!addedNodeIds.has(node.id)) {
184
+ nodes.push(node);
185
+ addedNodeIds.add(node.id);
186
+ }
187
+ };
188
+ const serverId = `server:${this.serverName}`;
189
+ addNode({
190
+ id: serverId,
191
+ type: "server",
192
+ label: this.serverName,
193
+ data: {
194
+ name: this.serverName,
195
+ description: `FrontMCP Server v${this.serverVersion || "unknown"}`
196
+ }
197
+ });
198
+ const scopes = this.getMonitoredScopes();
199
+ for (const scope of scopes) {
200
+ const scopeId = `scope:${scope.id}`;
201
+ addNode({
202
+ id: scopeId,
203
+ type: "scope",
204
+ label: scope.id,
205
+ data: {
206
+ name: scope.id
207
+ }
208
+ });
209
+ edges.push({
210
+ id: `${serverId}->${scopeId}`,
211
+ source: serverId,
212
+ target: scopeId,
213
+ type: "contains"
214
+ });
215
+ this.extractToolsFromScope(scope, scopeId, edges, addNode);
216
+ this.extractResourcesFromScope(scope, scopeId, edges, addNode);
217
+ this.extractResourceTemplatesFromScope(scope, scopeId, edges, addNode);
218
+ this.extractPromptsFromScope(scope, scopeId, edges, addNode);
219
+ this.extractAppsFromScope(scope, scopeId, edges, addNode);
220
+ }
221
+ return {
222
+ nodes,
223
+ edges,
224
+ metadata: this.createMetadata(nodes.length, edges.length)
225
+ };
226
+ }
227
+ extractToolsFromScope(scope, scopeId, edges, addNode) {
228
+ try {
229
+ const tools = scope.tools?.getTools?.(true) || [];
230
+ for (const tool of tools) {
231
+ if (tool.fullName?.startsWith("dashboard:")) {
232
+ continue;
233
+ }
234
+ const toolId = `tool:${tool.fullName}`;
235
+ addNode({
236
+ id: toolId,
237
+ type: "tool",
238
+ label: tool.name,
239
+ data: {
240
+ name: tool.fullName,
241
+ description: tool.metadata?.description,
242
+ inputSchema: tool.inputSchema,
243
+ outputSchema: tool.outputSchema,
244
+ tags: tool.metadata?.tags,
245
+ annotations: tool.metadata?.annotations
246
+ }
247
+ });
248
+ edges.push({
249
+ id: `${scopeId}->${toolId}`,
250
+ source: scopeId,
251
+ target: toolId,
252
+ type: "provides"
253
+ });
254
+ }
255
+ } catch {
256
+ }
257
+ }
258
+ extractResourcesFromScope(scope, scopeId, edges, addNode) {
259
+ try {
260
+ const resources = scope.resources?.getResources?.(true) || [];
261
+ for (const resource of resources) {
262
+ const resourceId = `resource:${resource.uri}`;
263
+ addNode({
264
+ id: resourceId,
265
+ type: "resource",
266
+ label: resource.name,
267
+ data: {
268
+ name: resource.name,
269
+ description: resource.metadata?.description,
270
+ uri: resource.uri,
271
+ mimeType: resource.metadata?.mimeType
272
+ }
273
+ });
274
+ edges.push({
275
+ id: `${scopeId}->${resourceId}`,
276
+ source: scopeId,
277
+ target: resourceId,
278
+ type: "provides"
279
+ });
280
+ }
281
+ } catch {
282
+ }
283
+ }
284
+ extractResourceTemplatesFromScope(scope, scopeId, edges, addNode) {
285
+ try {
286
+ const templates = scope.resources?.getResourceTemplates?.() || [];
287
+ for (const template of templates) {
288
+ const templateId = `resource-template:${template.uriTemplate}`;
289
+ addNode({
290
+ id: templateId,
291
+ type: "resource-template",
292
+ label: template.name,
293
+ data: {
294
+ name: template.name,
295
+ description: template.metadata?.description,
296
+ uri: template.uriTemplate,
297
+ mimeType: template.metadata?.mimeType
298
+ }
299
+ });
300
+ edges.push({
301
+ id: `${scopeId}->${templateId}`,
302
+ source: scopeId,
303
+ target: templateId,
304
+ type: "provides"
305
+ });
306
+ }
307
+ } catch {
308
+ }
309
+ }
310
+ extractPromptsFromScope(scope, scopeId, edges, addNode) {
311
+ try {
312
+ const prompts = scope.prompts?.getPrompts?.(true) || [];
313
+ for (const prompt of prompts) {
314
+ const promptId = `prompt:${prompt.name}`;
315
+ addNode({
316
+ id: promptId,
317
+ type: "prompt",
318
+ label: prompt.name,
319
+ data: {
320
+ name: prompt.name,
321
+ description: prompt.metadata?.description,
322
+ arguments: prompt.metadata?.arguments
323
+ }
324
+ });
325
+ edges.push({
326
+ id: `${scopeId}->${promptId}`,
327
+ source: scopeId,
328
+ target: promptId,
329
+ type: "provides"
330
+ });
331
+ }
332
+ } catch {
333
+ }
334
+ }
335
+ extractAppsFromScope(scope, scopeId, edges, addNode) {
336
+ try {
337
+ const apps = scope.apps?.getApps?.() || [];
338
+ for (const app of apps) {
339
+ const appName = app.metadata?.name || "unknown";
340
+ if (appName === "dashboard") {
341
+ continue;
342
+ }
343
+ const appId = `app:${appName}`;
344
+ addNode({
345
+ id: appId,
346
+ type: "app",
347
+ label: appName,
348
+ data: {
349
+ name: appName,
350
+ description: app.metadata?.description
351
+ }
352
+ });
353
+ edges.push({
354
+ id: `${scopeId}->${appId}`,
355
+ source: scopeId,
356
+ target: appId,
357
+ type: "contains"
358
+ });
359
+ }
360
+ } catch {
361
+ }
362
+ }
363
+ createMetadata(nodeCount, edgeCount) {
364
+ return {
365
+ serverName: this.serverName,
366
+ serverVersion: this.serverVersion,
367
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
368
+ entryFile: "runtime",
369
+ nodeCount,
370
+ edgeCount
371
+ };
372
+ }
373
+ };
374
+ GraphDataProvider = __decorateClass([
375
+ Provider({
376
+ name: "graph-data-provider"
377
+ })
378
+ ], GraphDataProvider);
379
+
380
+ // plugins/plugin-dashboard/src/tools/graph.tool.ts
381
+ import { Tool, ToolContext } from "@frontmcp/sdk";
382
+ import { z as z2 } from "zod";
383
+ var graphToolInputSchema = {
384
+ includeSchemas: z2.boolean().optional().default(false).describe("Include full input/output schemas in the response"),
385
+ refresh: z2.boolean().optional().default(false).describe("Force refresh the graph data (bypass cache)")
386
+ };
387
+ var GraphTool = class extends ToolContext {
388
+ async execute(input) {
389
+ const graphProvider = this.get(GraphDataProvider);
390
+ if (input.refresh) {
391
+ graphProvider.invalidateCache();
392
+ }
393
+ const graphData = await graphProvider.getGraphData();
394
+ if (!input.includeSchemas) {
395
+ return {
396
+ ...graphData,
397
+ nodes: graphData.nodes.map((node) => ({
398
+ ...node,
399
+ data: {
400
+ ...node.data,
401
+ inputSchema: void 0,
402
+ outputSchema: void 0
403
+ }
404
+ }))
405
+ };
406
+ }
407
+ return graphData;
408
+ }
409
+ };
410
+ GraphTool = __decorateClass([
411
+ Tool({
412
+ name: "dashboard:graph",
413
+ description: "Get the server graph showing all registered tools, resources, prompts, apps, and their relationships. Returns nodes and edges that can be visualized.",
414
+ inputSchema: graphToolInputSchema,
415
+ annotations: {
416
+ readOnlyHint: true
417
+ }
418
+ })
419
+ ], GraphTool);
420
+
421
+ // plugins/plugin-dashboard/src/tools/list-tools.tool.ts
422
+ import { Tool as Tool2, ToolContext as ToolContext2 } from "@frontmcp/sdk";
423
+ import { z as z3 } from "zod";
424
+
425
+ // plugins/plugin-dashboard/src/shared/safe-regex.ts
426
+ function safeRegex(pattern) {
427
+ if (pattern.length > 100) {
428
+ return null;
429
+ }
430
+ try {
431
+ const regex = new RegExp(pattern, "i");
432
+ regex.test("test");
433
+ return regex;
434
+ } catch {
435
+ return null;
436
+ }
437
+ }
438
+
439
+ // plugins/plugin-dashboard/src/tools/list-tools.tool.ts
440
+ var listToolsInputSchema = z3.object({
441
+ filter: z3.string().optional().describe("Filter tools by name pattern (regex supported)"),
442
+ includePlugins: z3.boolean().default(true).describe("Include tools from plugins"),
443
+ includeSchemas: z3.boolean().default(false).describe("Include input/output schemas in the response")
444
+ });
445
+ var listToolsOutputSchema = z3.object({
446
+ tools: z3.array(
447
+ z3.object({
448
+ name: z3.string(),
449
+ fullName: z3.string(),
450
+ description: z3.string().optional(),
451
+ tags: z3.array(z3.string()).optional(),
452
+ inputSchema: z3.unknown().optional(),
453
+ outputSchema: z3.unknown().optional()
454
+ })
455
+ ),
456
+ count: z3.number()
457
+ });
458
+ var ListToolsTool = class extends ToolContext2 {
459
+ async execute(input) {
460
+ const parentScope = this.tryGet(ParentScopeToken);
461
+ const targetScope = parentScope || this.scope;
462
+ let allTools = [];
463
+ try {
464
+ allTools = targetScope.tools?.getTools?.(input.includePlugins ?? true) || [];
465
+ } catch {
466
+ }
467
+ if (input.filter) {
468
+ const pattern = safeRegex(input.filter);
469
+ if (pattern) {
470
+ allTools = allTools.filter((t) => pattern.test(t.name) || pattern.test(t.fullName));
471
+ }
472
+ }
473
+ const tools = allTools.map((tool) => ({
474
+ name: tool.name,
475
+ fullName: tool.fullName,
476
+ description: tool.metadata?.description,
477
+ tags: tool.metadata?.tags,
478
+ ...input.includeSchemas ? {
479
+ inputSchema: tool.inputSchema,
480
+ outputSchema: tool.outputSchema
481
+ } : {}
482
+ }));
483
+ return {
484
+ tools,
485
+ count: tools.length
486
+ };
487
+ }
488
+ };
489
+ ListToolsTool = __decorateClass([
490
+ Tool2({
491
+ name: "dashboard:list-tools",
492
+ description: "List all tools registered in the monitored FrontMCP server. Returns tool names, descriptions, and optionally schemas.",
493
+ inputSchema: listToolsInputSchema,
494
+ outputSchema: listToolsOutputSchema,
495
+ annotations: {
496
+ readOnlyHint: true
497
+ }
498
+ })
499
+ ], ListToolsTool);
500
+
501
+ // plugins/plugin-dashboard/src/tools/list-resources.tool.ts
502
+ import { Tool as Tool3, ToolContext as ToolContext3 } from "@frontmcp/sdk";
503
+ import { z as z4 } from "zod";
504
+ var listResourcesInputSchema = z4.object({
505
+ filter: z4.string().optional().describe("Filter resources by name or URI pattern (regex supported)"),
506
+ includeTemplates: z4.boolean().default(true).describe("Include resource templates in the response")
507
+ });
508
+ var listResourcesOutputSchema = z4.object({
509
+ resources: z4.array(
510
+ z4.object({
511
+ name: z4.string(),
512
+ uri: z4.string(),
513
+ description: z4.string().optional(),
514
+ mimeType: z4.string().optional(),
515
+ isTemplate: z4.boolean()
516
+ })
517
+ ),
518
+ count: z4.number()
519
+ });
520
+ var ListResourcesTool = class extends ToolContext3 {
521
+ async execute(input) {
522
+ const parentScope = this.tryGet(ParentScopeToken);
523
+ const targetScope = parentScope || this.scope;
524
+ const results = [];
525
+ try {
526
+ const resources = targetScope.resources?.getResources?.(true) || [];
527
+ for (const resource of resources) {
528
+ results.push({
529
+ name: resource.name || "unnamed",
530
+ uri: resource.uri || "unknown",
531
+ description: resource.metadata?.description,
532
+ mimeType: resource.metadata?.mimeType,
533
+ isTemplate: false
534
+ });
535
+ }
536
+ } catch {
537
+ }
538
+ if (input.includeTemplates) {
539
+ try {
540
+ const templates = targetScope.resources?.getResourceTemplates?.() || [];
541
+ for (const template of templates) {
542
+ results.push({
543
+ name: template.name || "unnamed",
544
+ uri: template.uriTemplate || "unknown",
545
+ description: template.metadata?.description,
546
+ mimeType: template.metadata?.mimeType,
547
+ isTemplate: true
548
+ });
549
+ }
550
+ } catch {
551
+ }
552
+ }
553
+ let filtered = results;
554
+ if (input.filter) {
555
+ const pattern = safeRegex(input.filter);
556
+ if (pattern) {
557
+ filtered = results.filter((r) => pattern.test(r.name) || pattern.test(r.uri));
558
+ }
559
+ }
560
+ return {
561
+ resources: filtered,
562
+ count: filtered.length
563
+ };
564
+ }
565
+ };
566
+ ListResourcesTool = __decorateClass([
567
+ Tool3({
568
+ name: "dashboard:list-resources",
569
+ description: "List all resources and resource templates registered in the monitored FrontMCP server.",
570
+ inputSchema: listResourcesInputSchema,
571
+ outputSchema: listResourcesOutputSchema,
572
+ annotations: {
573
+ readOnlyHint: true
574
+ }
575
+ })
576
+ ], ListResourcesTool);
577
+
578
+ // plugins/plugin-dashboard/src/html/html.generator.ts
579
+ function escapeForJs(str) {
580
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/</g, "\\x3c").replace(/>/g, "\\x3e").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
581
+ }
582
+ function generateDashboardHtml(options) {
583
+ const cdn = options.cdn;
584
+ const basePath = options.basePath;
585
+ if (cdn.entrypoint) {
586
+ return generateExternalEntrypointHtml(options);
587
+ }
588
+ return generateInlineDashboardHtml(options);
589
+ }
590
+ function generateExternalEntrypointHtml(options) {
591
+ const { cdn, auth } = options;
592
+ const safeBasePath = escapeForJs(options.basePath);
593
+ const token = escapeForJs(auth?.token || "");
594
+ const safeReact = escapeForJs(cdn.react);
595
+ const safeReactDom = escapeForJs(cdn.reactDom);
596
+ const safeReactDomClient = escapeForJs(cdn.reactDomClient);
597
+ const safeReactJsxRuntime = escapeForJs(cdn.reactJsxRuntime);
598
+ const safeXyflow = escapeForJs(cdn.xyflow);
599
+ const safeDagre = escapeForJs(cdn.dagre);
600
+ const safeXyflowCss = escapeForJs(cdn.xyflowCss);
601
+ const safeEntrypoint = escapeForJs(cdn.entrypoint || "");
602
+ return `<!DOCTYPE html>
603
+ <html lang="en">
604
+ <head>
605
+ <meta charset="UTF-8">
606
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
607
+ <title>FrontMCP Dashboard</title>
608
+ <script type="importmap">
609
+ {
610
+ "imports": {
611
+ "react": "${safeReact}",
612
+ "react-dom": "${safeReactDom}",
613
+ "react-dom/client": "${safeReactDomClient}",
614
+ "react/jsx-runtime": "${safeReactJsxRuntime}",
615
+ "@xyflow/react": "${safeXyflow}",
616
+ "dagre": "${safeDagre}"
617
+ }
618
+ }
619
+ </script>
620
+ <link rel="stylesheet" href="${safeXyflowCss}" />
621
+ </head>
622
+ <body>
623
+ <div id="root">Loading dashboard...</div>
624
+ <script type="module">
625
+ // Escape HTML for safe innerHTML usage
626
+ function escapeHtml(str) {
627
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
628
+ }
629
+
630
+ // Dashboard configuration
631
+ window.__FRONTMCP_DASHBOARD__ = {
632
+ basePath: '${safeBasePath}',
633
+ sseUrl: '${safeBasePath}/sse${token ? `?token=${token}` : ""}',
634
+ token: '${token}',
635
+ };
636
+
637
+ // Load external dashboard UI
638
+ import('${safeEntrypoint}').then(mod => {
639
+ if (mod.mount) {
640
+ mod.mount(document.getElementById('root'), window.__FRONTMCP_DASHBOARD__);
641
+ }
642
+ }).catch(err => {
643
+ document.getElementById('root').innerHTML =
644
+ '<div style="color: red; padding: 20px;">Failed to load dashboard: ' + escapeHtml(err.message || 'Unknown error') + '</div>';
645
+ });
646
+ </script>
647
+ </body>
648
+ </html>`;
649
+ }
650
+ function generateInlineDashboardHtml(options) {
651
+ const { cdn, auth } = options;
652
+ const safeBasePath = escapeForJs(options.basePath);
653
+ const token = escapeForJs(auth?.token || "");
654
+ const sseUrl = `${safeBasePath}/sse${token ? `?token=${token}` : ""}`;
655
+ const safeReact = escapeForJs(cdn.react);
656
+ const safeReactDom = escapeForJs(cdn.reactDom);
657
+ const safeReactDomClient = escapeForJs(cdn.reactDomClient);
658
+ const safeReactJsxRuntime = escapeForJs(cdn.reactJsxRuntime);
659
+ const safeXyflow = escapeForJs(cdn.xyflow);
660
+ const safeDagre = escapeForJs(cdn.dagre);
661
+ const safeXyflowCss = escapeForJs(cdn.xyflowCss);
662
+ return `<!DOCTYPE html>
663
+ <html lang="en">
664
+ <head>
665
+ <meta charset="UTF-8">
666
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
667
+ <title>FrontMCP Dashboard</title>
668
+
669
+ <script type="importmap">
670
+ {
671
+ "imports": {
672
+ "react": "${safeReact}",
673
+ "react-dom": "${safeReactDom}",
674
+ "react-dom/client": "${safeReactDomClient}",
675
+ "react/jsx-runtime": "${safeReactJsxRuntime}",
676
+ "@xyflow/react": "${safeXyflow}",
677
+ "dagre": "${safeDagre}"
678
+ }
679
+ }
680
+ </script>
681
+ <link rel="stylesheet" href="${safeXyflowCss}" />
682
+
683
+ <style>
684
+ * { margin: 0; padding: 0; box-sizing: border-box; }
685
+ body {
686
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
687
+ background: #0f172a;
688
+ color: #e2e8f0;
689
+ }
690
+ #root { width: 100vw; height: 100vh; }
691
+
692
+ .loading {
693
+ display: flex;
694
+ flex-direction: column;
695
+ align-items: center;
696
+ justify-content: center;
697
+ height: 100vh;
698
+ gap: 16px;
699
+ }
700
+ .loading-spinner {
701
+ width: 40px;
702
+ height: 40px;
703
+ border: 3px solid #334155;
704
+ border-top-color: #3b82f6;
705
+ border-radius: 50%;
706
+ animation: spin 1s linear infinite;
707
+ }
708
+ @keyframes spin { to { transform: rotate(360deg); } }
709
+
710
+ .error {
711
+ display: flex;
712
+ flex-direction: column;
713
+ align-items: center;
714
+ justify-content: center;
715
+ height: 100vh;
716
+ gap: 12px;
717
+ color: #f87171;
718
+ }
719
+
720
+ .header {
721
+ position: fixed;
722
+ top: 20px;
723
+ left: 20px;
724
+ background: #1e293b;
725
+ padding: 16px 20px;
726
+ border-radius: 12px;
727
+ border: 1px solid #334155;
728
+ z-index: 1000;
729
+ }
730
+ .header h1 {
731
+ font-size: 18px;
732
+ font-weight: 600;
733
+ margin-bottom: 4px;
734
+ }
735
+ .header .stats {
736
+ font-size: 13px;
737
+ color: #94a3b8;
738
+ }
739
+ .header .status {
740
+ font-size: 11px;
741
+ margin-top: 8px;
742
+ display: flex;
743
+ align-items: center;
744
+ gap: 6px;
745
+ }
746
+ .status-dot {
747
+ width: 8px;
748
+ height: 8px;
749
+ border-radius: 50%;
750
+ background: #22c55e;
751
+ }
752
+ .status-dot.disconnected { background: #ef4444; }
753
+ .status-dot.connecting { background: #f59e0b; animation: pulse 1s ease-in-out infinite; }
754
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
755
+
756
+ .legend {
757
+ position: fixed;
758
+ bottom: 20px;
759
+ left: 20px;
760
+ background: #1e293b;
761
+ padding: 16px;
762
+ border-radius: 12px;
763
+ border: 1px solid #334155;
764
+ z-index: 1000;
765
+ font-size: 12px;
766
+ }
767
+ .legend h3 {
768
+ margin-bottom: 12px;
769
+ font-size: 13px;
770
+ color: #f1f5f9;
771
+ }
772
+ .legend-item {
773
+ display: flex;
774
+ align-items: center;
775
+ margin-bottom: 6px;
776
+ color: #94a3b8;
777
+ }
778
+ .legend-color {
779
+ width: 16px;
780
+ height: 16px;
781
+ border-radius: 4px;
782
+ margin-right: 8px;
783
+ border: 2px solid;
784
+ }
785
+
786
+ .custom-node {
787
+ padding: 10px 14px;
788
+ border-radius: 8px;
789
+ min-width: 140px;
790
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
791
+ }
792
+ .custom-node .node-type {
793
+ font-size: 10px;
794
+ text-transform: uppercase;
795
+ margin-bottom: 4px;
796
+ font-weight: 500;
797
+ letter-spacing: 0.5px;
798
+ }
799
+ .custom-node .node-label {
800
+ font-size: 13px;
801
+ font-weight: 600;
802
+ color: #f1f5f9;
803
+ }
804
+ .custom-node .node-desc {
805
+ font-size: 10px;
806
+ color: #94a3b8;
807
+ margin-top: 3px;
808
+ max-width: 160px;
809
+ overflow: hidden;
810
+ text-overflow: ellipsis;
811
+ white-space: nowrap;
812
+ }
813
+
814
+ .react-flow__minimap { background: #1e293b !important; }
815
+ .react-flow__controls button {
816
+ background: #1e293b !important;
817
+ border-color: #334155 !important;
818
+ color: #e2e8f0 !important;
819
+ }
820
+ .react-flow__controls button:hover {
821
+ background: #334155 !important;
822
+ }
823
+ </style>
824
+ </head>
825
+ <body>
826
+ <div id="root">
827
+ <div class="loading">
828
+ <div class="loading-spinner"></div>
829
+ <div>Connecting to server...</div>
830
+ </div>
831
+ </div>
832
+
833
+ <script type="module">
834
+ import React, { useMemo, useCallback, useState, useEffect } from 'react';
835
+ import { createRoot } from 'react-dom/client';
836
+ import {
837
+ ReactFlow,
838
+ ReactFlowProvider,
839
+ Background,
840
+ Controls,
841
+ MiniMap,
842
+ useNodesState,
843
+ useEdgesState,
844
+ Handle,
845
+ Position
846
+ } from '@xyflow/react';
847
+ import dagre from 'dagre';
848
+
849
+ // Configuration
850
+ const config = {
851
+ basePath: '${safeBasePath}',
852
+ sseUrl: '${sseUrl}',
853
+ token: '${token}',
854
+ };
855
+
856
+ // Node styling config
857
+ const nodeConfig = {
858
+ server: { fill: '#312e81', stroke: '#6366f1', icon: '\u{1F5A5}\uFE0F', textColor: '#a5b4fc' },
859
+ scope: { fill: '#1e3a5f', stroke: '#3b82f6', icon: '\u{1F4E6}', textColor: '#93c5fd' },
860
+ app: { fill: '#14532d', stroke: '#22c55e', icon: '\u{1F4F1}', textColor: '#86efac' },
861
+ plugin: { fill: '#713f12', stroke: '#f59e0b', icon: '\u{1F50C}', textColor: '#fcd34d' },
862
+ adapter: { fill: '#7f1d1d', stroke: '#ef4444', icon: '\u{1F517}', textColor: '#fca5a5' },
863
+ tool: { fill: '#164e63', stroke: '#06b6d4', icon: '\u{1F527}', textColor: '#67e8f9' },
864
+ resource: { fill: '#312e81', stroke: '#8b5cf6', icon: '\u{1F4C4}', textColor: '#c4b5fd' },
865
+ 'resource-template': { fill: '#4c1d95', stroke: '#a78bfa', icon: '\u{1F4CB}', textColor: '#ddd6fe' },
866
+ prompt: { fill: '#831843', stroke: '#ec4899', icon: '\u{1F4AC}', textColor: '#f9a8d4' },
867
+ auth: { fill: '#78350f', stroke: '#d97706', icon: '\u{1F6E1}\uFE0F', textColor: '#fcd34d' },
868
+ };
869
+
870
+ // Custom node component
871
+ function CustomNode({ data, type }) {
872
+ const cfg = nodeConfig[type] || nodeConfig.tool;
873
+
874
+ return React.createElement('div', {
875
+ className: 'custom-node',
876
+ style: { background: cfg.fill, border: '2px solid ' + cfg.stroke }
877
+ },
878
+ React.createElement(Handle, { type: 'target', position: Position.Top, style: { visibility: 'hidden' } }),
879
+ React.createElement('div', { className: 'node-type', style: { color: cfg.textColor } },
880
+ cfg.icon + ' ' + type
881
+ ),
882
+ React.createElement('div', { className: 'node-label' }, data.label),
883
+ data.description && React.createElement('div', { className: 'node-desc' }, data.description),
884
+ React.createElement(Handle, { type: 'source', position: Position.Bottom, style: { visibility: 'hidden' } })
885
+ );
886
+ }
887
+
888
+ // Create node types
889
+ const nodeTypes = {};
890
+ Object.keys(nodeConfig).forEach(type => {
891
+ nodeTypes[type] = (props) => CustomNode({ ...props, type });
892
+ });
893
+
894
+ // Layout with Dagre
895
+ function layoutGraph(graphData) {
896
+ const g = new dagre.graphlib.Graph();
897
+ g.setGraph({ rankdir: 'TB', nodesep: 50, ranksep: 80 });
898
+ g.setDefaultEdgeLabel(() => ({}));
899
+
900
+ graphData.nodes.forEach(node => {
901
+ g.setNode(node.id, { width: 160, height: 60 });
902
+ });
903
+
904
+ graphData.edges.forEach(edge => {
905
+ g.setEdge(edge.source, edge.target);
906
+ });
907
+
908
+ dagre.layout(g);
909
+
910
+ const nodes = graphData.nodes.map(node => {
911
+ const pos = g.node(node.id);
912
+ return {
913
+ id: node.id,
914
+ type: node.type,
915
+ position: { x: pos.x - 80, y: pos.y - 30 },
916
+ data: { label: node.label, ...node.data },
917
+ };
918
+ });
919
+
920
+ const edges = graphData.edges.map(edge => ({
921
+ id: edge.id,
922
+ source: edge.source,
923
+ target: edge.target,
924
+ type: 'smoothstep',
925
+ style: { stroke: '#475569', strokeWidth: 1.5 },
926
+ markerEnd: { type: 'arrowclosed', color: '#475569' },
927
+ }));
928
+
929
+ return { nodes, edges };
930
+ }
931
+
932
+ /**
933
+ * MCP Client for SSE transport communication.
934
+ * Connects to the dashboard SSE endpoint and sends JSON-RPC requests.
935
+ */
936
+ class McpClient {
937
+ constructor(sseUrl, basePath) {
938
+ this.sseUrl = sseUrl;
939
+ this.basePath = basePath;
940
+ this.messageUrl = null;
941
+ this.eventSource = null;
942
+ this.requestId = 0;
943
+ this.pendingRequests = new Map();
944
+ this.sessionId = null;
945
+ }
946
+
947
+ /**
948
+ * Connect to the SSE endpoint and wait for the message endpoint URL.
949
+ * The server sends an 'endpoint' event containing the URL for POST requests.
950
+ */
951
+ async connect() {
952
+ return new Promise((resolve, reject) => {
953
+ this.eventSource = new EventSource(this.sseUrl);
954
+
955
+ // Listen for the endpoint event to get the message URL
956
+ this.eventSource.addEventListener('endpoint', (e) => {
957
+ const endpointPath = e.data.split('?')[0].replace(this.basePath, '');
958
+ this.messageUrl = this.basePath + endpointPath;
959
+
960
+ // Extract sessionId from the endpoint URL (legacy SSE format)
961
+ const match = e.data.match(/sessionId=([^&]+)/);
962
+ this.sessionId = match ? match[1] : null;
963
+ resolve();
964
+ });
965
+
966
+ // Listen for responses to our requests
967
+ this.eventSource.addEventListener('message', (e) => {
968
+ try {
969
+ const response = JSON.parse(e.data);
970
+ if (response.id && this.pendingRequests.has(response.id)) {
971
+ const { resolve, reject } = this.pendingRequests.get(response.id);
972
+ this.pendingRequests.delete(response.id);
973
+ if (response.error) {
974
+ reject(new Error(response.error.message));
975
+ } else {
976
+ resolve(response.result);
977
+ }
978
+ }
979
+ } catch (err) {
980
+ console.error('Failed to parse SSE message:', err);
981
+ }
982
+ });
983
+
984
+ this.eventSource.onerror = () => {
985
+ if (!this.messageUrl) {
986
+ reject(new Error('SSE connection failed'));
987
+ }
988
+ };
989
+
990
+ // Connection timeout
991
+ setTimeout(() => {
992
+ if (!this.messageUrl) {
993
+ this.close();
994
+ reject(new Error('SSE connection timeout'));
995
+ }
996
+ }, 10000);
997
+ });
998
+ }
999
+
1000
+ /**
1001
+ * Call an MCP tool via JSON-RPC over HTTP POST.
1002
+ * @param {string} name - The tool name (e.g., 'dashboard:graph')
1003
+ * @param {object} args - The tool arguments
1004
+ * @returns {Promise<object>} The tool result
1005
+ */
1006
+ async callTool(name, args = {}) {
1007
+ if (!this.messageUrl) {
1008
+ throw new Error('Not connected');
1009
+ }
1010
+
1011
+ const id = ++this.requestId;
1012
+ const message = {
1013
+ jsonrpc: '2.0',
1014
+ id,
1015
+ method: 'tools/call',
1016
+ params: { name, arguments: args }
1017
+ };
1018
+
1019
+ return new Promise((resolve, reject) => {
1020
+ this.pendingRequests.set(id, { resolve, reject });
1021
+
1022
+ // Append sessionId to URL for legacy SSE transport
1023
+ const url = this.messageUrl + (this.sessionId ? '?sessionId=' + this.sessionId : '');
1024
+ fetch(url, {
1025
+ method: 'POST',
1026
+ headers: { 'Content-Type': 'application/json' },
1027
+ body: JSON.stringify(message)
1028
+ }).catch(reject);
1029
+ });
1030
+ }
1031
+
1032
+ /** Close the SSE connection */
1033
+ close() {
1034
+ if (this.eventSource) {
1035
+ this.eventSource.close();
1036
+ this.eventSource = null;
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ // Main App
1042
+ function DashboardApp() {
1043
+ const [status, setStatus] = useState('loading');
1044
+ const [error, setError] = useState(null);
1045
+ const [graphData, setGraphData] = useState(null);
1046
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
1047
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
1048
+
1049
+ useEffect(() => {
1050
+ const client = new McpClient(config.sseUrl, config.basePath);
1051
+
1052
+ async function init() {
1053
+ try {
1054
+ // Connect via SSE
1055
+ setStatus('connecting');
1056
+ await client.connect();
1057
+
1058
+ // Call dashboard:graph tool via MCP protocol
1059
+ const result = await client.callTool('dashboard:graph', {});
1060
+
1061
+ // Extract graph data from tool result
1062
+ let data;
1063
+ if (result && result.content && result.content[0]) {
1064
+ const content = result.content[0];
1065
+ if (content.type === 'text') {
1066
+ data = JSON.parse(content.text);
1067
+ }
1068
+ }
1069
+
1070
+ if (!data) {
1071
+ throw new Error('Invalid graph data response');
1072
+ }
1073
+
1074
+ setGraphData(data);
1075
+ setStatus('connected');
1076
+
1077
+ const { nodes: layoutNodes, edges: layoutEdges } = layoutGraph(data);
1078
+ setNodes(layoutNodes);
1079
+ setEdges(layoutEdges);
1080
+ } catch (err) {
1081
+ setStatus('error');
1082
+ setError(err.message);
1083
+ }
1084
+ }
1085
+
1086
+ init();
1087
+
1088
+ return () => client.close();
1089
+ }, []);
1090
+
1091
+ if (error) {
1092
+ return React.createElement('div', { className: 'error' },
1093
+ React.createElement('div', { style: { fontSize: 48 } }, '\u26A0\uFE0F'),
1094
+ React.createElement('div', { style: { fontSize: 18, fontWeight: 600 } }, 'Connection Failed'),
1095
+ React.createElement('div', { style: { color: '#94a3b8' } }, error)
1096
+ );
1097
+ }
1098
+
1099
+ if (!graphData) {
1100
+ return React.createElement('div', { className: 'loading' },
1101
+ React.createElement('div', { className: 'loading-spinner' }),
1102
+ React.createElement('div', null, 'Loading graph data...')
1103
+ );
1104
+ }
1105
+
1106
+ return React.createElement('div', { style: { width: '100%', height: '100%' } },
1107
+ React.createElement('div', { className: 'header' },
1108
+ React.createElement('h1', null, graphData.metadata.serverName),
1109
+ React.createElement('div', { className: 'stats' },
1110
+ graphData.metadata.nodeCount + ' nodes \xB7 ' + graphData.metadata.edgeCount + ' edges'
1111
+ ),
1112
+ React.createElement('div', { className: 'status' },
1113
+ React.createElement('span', {
1114
+ className: 'status-dot' + (status !== 'connected' ? ' ' + status : '')
1115
+ }),
1116
+ status === 'connected' ? 'Connected' : status
1117
+ )
1118
+ ),
1119
+
1120
+ React.createElement('div', { className: 'legend' },
1121
+ React.createElement('h3', null, 'Node Types'),
1122
+ ...Object.entries(nodeConfig).slice(0, 6).map(([type, cfg]) =>
1123
+ React.createElement('div', { key: type, className: 'legend-item' },
1124
+ React.createElement('div', {
1125
+ className: 'legend-color',
1126
+ style: { background: cfg.fill, borderColor: cfg.stroke }
1127
+ }),
1128
+ cfg.icon + ' ' + type
1129
+ )
1130
+ )
1131
+ ),
1132
+
1133
+ React.createElement(ReactFlow, {
1134
+ nodes,
1135
+ edges,
1136
+ onNodesChange,
1137
+ onEdgesChange,
1138
+ nodeTypes,
1139
+ fitView: true,
1140
+ fitViewOptions: { padding: 0.2 },
1141
+ minZoom: 0.1,
1142
+ maxZoom: 2,
1143
+ },
1144
+ React.createElement(Background, { color: '#334155', gap: 20 }),
1145
+ React.createElement(Controls),
1146
+ React.createElement(MiniMap, {
1147
+ nodeColor: (n) => nodeConfig[n.type]?.stroke || '#475569',
1148
+ maskColor: 'rgba(15, 23, 42, 0.8)'
1149
+ })
1150
+ )
1151
+ );
1152
+ }
1153
+
1154
+ // Mount
1155
+ const root = createRoot(document.getElementById('root'));
1156
+ root.render(
1157
+ React.createElement(ReactFlowProvider, null,
1158
+ React.createElement(DashboardApp)
1159
+ )
1160
+ );
1161
+ </script>
1162
+ </body>
1163
+ </html>`;
1164
+ }
1165
+
1166
+ // plugins/plugin-dashboard/src/app/dashboard.app.ts
1167
+ var DashboardMiddlewareToken = /* @__PURE__ */ Symbol("dashboard:middleware");
1168
+ function createDashboardMiddleware(options) {
1169
+ const html = generateDashboardHtml(options);
1170
+ return async (req, res, next) => {
1171
+ if (!isDashboardEnabled(options)) {
1172
+ return next();
1173
+ }
1174
+ const urlPath = req.path || req.url || "/";
1175
+ const method = (req.method || "GET").toUpperCase();
1176
+ if (method === "GET" && (urlPath === "/" || urlPath === "")) {
1177
+ res.setHeader?.(
1178
+ "Content-Type",
1179
+ "text/html"
1180
+ );
1181
+ res.status(200).send(html);
1182
+ return;
1183
+ }
1184
+ return next();
1185
+ };
1186
+ }
1187
+ var DashboardHttpPlugin = class extends DynamicPlugin2 {
1188
+ options;
1189
+ constructor(options = {}) {
1190
+ super();
1191
+ this.options = dashboardPluginOptionsSchema.parse({
1192
+ ...defaultDashboardPluginOptions,
1193
+ ...options
1194
+ });
1195
+ }
1196
+ /**
1197
+ * Provide the dashboard config and middleware registration via DI.
1198
+ */
1199
+ static dynamicProviders(options) {
1200
+ const parsedOptions = dashboardPluginOptionsSchema.parse({
1201
+ ...defaultDashboardPluginOptions,
1202
+ ...options
1203
+ });
1204
+ return [
1205
+ {
1206
+ name: "dashboard:config",
1207
+ provide: DashboardConfigToken,
1208
+ useValue: parsedOptions
1209
+ },
1210
+ // Register middleware for HTML serving (must be in dynamic providers to access config)
1211
+ {
1212
+ name: "dashboard:middleware",
1213
+ provide: DashboardMiddlewareToken,
1214
+ inject: () => [FrontMcpServer],
1215
+ useFactory: (server) => {
1216
+ const middleware = createDashboardMiddleware(parsedOptions);
1217
+ server.registerMiddleware(parsedOptions.basePath, middleware);
1218
+ return { registered: true };
1219
+ }
1220
+ }
1221
+ ];
1222
+ }
1223
+ };
1224
+ DashboardHttpPlugin = __decorateClass([
1225
+ Plugin2({
1226
+ name: "dashboard:http",
1227
+ description: "Dashboard HTTP handler for serving UI HTML"
1228
+ })
1229
+ ], DashboardHttpPlugin);
1230
+ var DashboardApp = class {
1231
+ };
1232
+ DashboardApp = __decorateClass([
1233
+ App({
1234
+ name: "dashboard",
1235
+ description: "FrontMCP Dashboard for visualization and monitoring",
1236
+ providers: [
1237
+ // Provide parent scope reference (same as current scope when standalone: false)
1238
+ {
1239
+ name: "dashboard:parent-scope",
1240
+ provide: ParentScopeToken,
1241
+ inject: () => [ScopeEntry4],
1242
+ useFactory: (scope) => {
1243
+ return scope;
1244
+ }
1245
+ },
1246
+ // Graph data provider for extracting server structure
1247
+ {
1248
+ name: "dashboard:graph-data",
1249
+ provide: GraphDataProvider,
1250
+ inject: () => [ScopeEntry4, FrontMcpConfig],
1251
+ useFactory: (scope, config) => {
1252
+ const serverName = config.info?.name || "FrontMCP Server";
1253
+ const serverVersion = config.info?.version;
1254
+ return new GraphDataProvider(scope, serverName, serverVersion);
1255
+ }
1256
+ }
1257
+ ],
1258
+ plugins: [DashboardHttpPlugin.init({})],
1259
+ tools: [GraphTool, ListToolsTool, ListResourcesTool],
1260
+ auth: {
1261
+ mode: "public"
1262
+ },
1263
+ standalone: true
1264
+ // Dashboard is part of root scope so GraphDataProvider can access all tools/resources
1265
+ })
1266
+ ], DashboardApp);
1267
+ export {
1268
+ DashboardApp,
1269
+ DashboardConfigToken,
1270
+ DashboardPlugin,
1271
+ GraphDataProvider,
1272
+ GraphDataProviderToken,
1273
+ GraphTool,
1274
+ ListResourcesTool,
1275
+ ListToolsTool,
1276
+ ParentScopeToken,
1277
+ dashboardPluginOptionsSchema,
1278
+ DashboardPlugin as default,
1279
+ defaultDashboardPluginOptions,
1280
+ isDashboardEnabled
1281
+ };