@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/app/dashboard.app.d.ts +45 -0
- package/app/dashboard.app.d.ts.map +1 -0
- package/app/index.d.ts +2 -0
- package/app/index.d.ts.map +1 -0
- package/dashboard.plugin.d.ts +65 -0
- package/dashboard.plugin.d.ts.map +1 -0
- package/dashboard.symbol.d.ts +10 -0
- package/dashboard.symbol.d.ts.map +1 -0
- package/dashboard.types.d.ts +85 -0
- package/dashboard.types.d.ts.map +1 -0
- package/esm/index.mjs +1281 -0
- package/esm/package.json +53 -0
- package/html/html.generator.d.ts +10 -0
- package/html/html.generator.d.ts.map +1 -0
- package/html/index.d.ts +2 -0
- package/html/index.d.ts.map +1 -0
- package/index.d.ts +10 -0
- package/index.d.ts.map +1 -0
- package/index.js +1309 -0
- package/package.json +1 -1
- package/providers/graph-data.provider.d.ts +59 -0
- package/providers/graph-data.provider.d.ts.map +1 -0
- package/providers/index.d.ts +2 -0
- package/providers/index.d.ts.map +1 -0
- package/shared/index.d.ts +2 -0
- package/shared/index.d.ts.map +1 -0
- package/shared/safe-regex.d.ts +9 -0
- package/shared/safe-regex.d.ts.map +1 -0
- package/shared/types.d.ts +170 -0
- package/shared/types.d.ts.map +1 -0
- package/tools/graph.tool.d.ts +18 -0
- package/tools/graph.tool.d.ts.map +1 -0
- package/tools/index.d.ts +4 -0
- package/tools/index.d.ts.map +1 -0
- package/tools/list-resources.tool.d.ts +31 -0
- package/tools/list-resources.tool.d.ts.map +1 -0
- package/tools/list-tools.tool.d.ts +33 -0
- package/tools/list-tools.tool.d.ts.map +1 -0
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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
|
+
};
|