@frontmcp/plugin-dashboard 1.8.2 → 1.8.4

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.
@@ -1,16 +1,30 @@
1
- import { DynamicPlugin, type ProviderType } from '@frontmcp/sdk';
1
+ import { DynamicPlugin, type FlowCtxOf, type ProviderType } from '@frontmcp/sdk';
2
2
  import { type DashboardPluginOptions, type DashboardPluginOptionsInput } from '../dashboard.types';
3
3
  /**
4
4
  * Internal Dashboard HTTP Plugin.
5
5
  *
6
- * Handles HTTP requests for serving the dashboard HTML.
7
- * The SSE transport and MCP protocol are handled by FrontMCP's built-in transport layer.
6
+ * Serves the dashboard HTML, and gates the dashboard's MCP endpoint. The SSE transport
7
+ * and MCP protocol are handled by FrontMCP's built-in transport layer.
8
8
  */
9
9
  declare class DashboardHttpPlugin extends DynamicPlugin<DashboardPluginOptions, DashboardPluginOptionsInput> {
10
10
  options: DashboardPluginOptions;
11
11
  constructor(options?: DashboardPluginOptionsInput);
12
12
  /**
13
- * Provide the dashboard config and middleware registration via DI.
13
+ * Gate every request the dashboard's MCP endpoint is about to handle.
14
+ *
15
+ * `enabled` and `auth` used to reach only the page middleware, so with the dashboard
16
+ * turned off, or without its token, `POST /dashboard` still answered `initialize` and
17
+ * `dashboard:graph` returned the whole server's inventory. This hook runs in the
18
+ * dashboard scope's `http:request` flow, on Node and on the fetch handler alike, after
19
+ * the router picked an MCP transport and before any of them runs. The server's own
20
+ * authentication (the `checkAuthorization` stage) still applies first; the dashboard
21
+ * token is required on top of it.
22
+ */
23
+ gateDashboardMcp(flowCtx: FlowCtxOf<'http:request'>): Promise<void>;
24
+ /** `this.get`, or `undefined` when the token isn't provided. */
25
+ private resolve;
26
+ /**
27
+ * Provide the dashboard config and page middleware registration via DI.
14
28
  */
15
29
  static dynamicProviders(options: DashboardPluginOptionsInput): ProviderType[];
16
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard.app.d.ts","sourceRoot":"","sources":["../../src/app/dashboard.app.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAOb,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AAOvB,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EACjC,MAAM,oBAAoB,CAAC;AAmE5B;;;;;GAKG;AACH,cAIM,mBAAoB,SAAQ,aAAa,CAAC,sBAAsB,EAAE,2BAA2B,CAAC;IAClG,OAAO,EAAE,sBAAsB,CAAC;gBAEpB,OAAO,GAAE,2BAAgC;IAQrD;;OAEG;WACa,gBAAgB,CAAC,OAAO,EAAE,2BAA2B,GAAG,YAAY,EAAE;CA+BvF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAmCa,YAAY;CAAG;AAG5B,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
1
+ {"version":3,"file":"dashboard.app.d.ts","sourceRoot":"","sources":["../../src/app/dashboard.app.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAOb,KAAK,SAAS,EAGd,KAAK,YAAY,EAIlB,MAAM,eAAe,CAAC;AAYvB,OAAO,EAIL,KAAK,sBAAsB,EAC3B,KAAK,2BAA2B,EACjC,MAAM,oBAAoB,CAAC;AAsF5B;;;;;GAKG;AACH,cAIM,mBAAoB,SAAQ,aAAa,CAAC,sBAAsB,EAAE,2BAA2B,CAAC;IAClG,OAAO,EAAE,sBAAsB,CAAC;gBAEpB,OAAO,GAAE,2BAAgC;IAQrD;;;;;;;;;;OAUG;IAEG,gBAAgB,CAAC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBzE,gEAAgE;IAChE,OAAO,CAAC,OAAO;IAQf;;OAEG;WACa,gBAAgB,CAAC,OAAO,EAAE,2BAA2B,GAAG,YAAY,EAAE;CAgCvF;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAmCa,YAAY;CAAG;AAG5B,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { PublicMcpError, type ToolContext } from '@frontmcp/sdk';
2
+ import { type DashboardPluginOptions } from '../dashboard.types';
3
+ /** Context methods the gate marker needs (a `FrontMcpContext`). */
4
+ interface ContextStore {
5
+ set<T>(key: symbol, value: T): void;
6
+ get<T>(key: symbol): T | undefined;
7
+ }
8
+ /** Record that the HTTP gate checked this request's dashboard token. */
9
+ export declare function markDashboardGatePassed(ctx: ContextStore | undefined): void;
10
+ /** The dashboard is turned off (`enabled: false`, or the production default): its MCP endpoint and tools refuse. */
11
+ export declare class DashboardDisabledError extends PublicMcpError {
12
+ readonly mcpErrorCode: -32003;
13
+ constructor();
14
+ }
15
+ /** `auth` is on and the caller has not shown the dashboard token. */
16
+ export declare class DashboardUnauthorizedError extends PublicMcpError {
17
+ readonly mcpErrorCode: -32001;
18
+ constructor();
19
+ }
20
+ /**
21
+ * Refuse a dashboard tool call that the dashboard's own options don't allow.
22
+ *
23
+ * The HTTP gate (a hook on the dashboard scope's `http:request` flow) already refuses such
24
+ * requests; this check makes the tools refuse them wherever they are served from (a
25
+ * direct or stdio server, another app they were added to), so `enabled` and `auth` hold
26
+ * for every transport, not only for the web page.
27
+ *
28
+ * @param options - the dashboard's effective options
29
+ * @param ctx - the request's context: the gate's mark, and its `x-frontmcp-*` headers
30
+ */
31
+ export declare function assertDashboardAccess(options: DashboardPluginOptions, ctx: (ContextStore & {
32
+ metadata?: {
33
+ customHeaders?: Readonly<Record<string, string>>;
34
+ };
35
+ }) | undefined): void;
36
+ /**
37
+ * {@link assertDashboardAccess} for one of the dashboard's tools: the options come from the
38
+ * dashboard's config provider (the ones its HTTP gate uses), else the published options.
39
+ */
40
+ export declare function assertDashboardToolAccess(tool: ToolContext): void;
41
+ export {};
42
+ //# sourceMappingURL=dashboard-access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-access.d.ts","sourceRoot":"","sources":["../../src/auth/dashboard-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAIlF,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AASrF,mEAAmE;AACnE,UAAU,YAAY;IACpB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CACpC;AAED,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,GAAG,SAAS,GAAG,IAAI,CAE3E;AAED,oHAAoH;AACpH,qBAAa,sBAAuB,SAAQ,cAAc;IACxD,QAAQ,CAAC,YAAY,SAA6B;;CAKnD;AAED,qEAAqE;AACrE,qBAAa,0BAA2B,SAAQ,cAAc;IAC5D,QAAQ,CAAC,YAAY,SAAgC;;CAUtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,EAAE,CAAC,YAAY,GAAG;IAAE,QAAQ,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;KAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GACpG,IAAI,CAaN;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAGjE"}
@@ -13,10 +13,55 @@ export interface DashboardAuthRequest {
13
13
  headers?: Record<string, string | string[] | undefined> | undefined;
14
14
  query?: Record<string, string | string[] | undefined> | undefined;
15
15
  }
16
+ /**
17
+ * Header an MCP client sends the dashboard token in. `Authorization` belongs to the
18
+ * server's own authentication, so on an authenticated server the dashboard token needs
19
+ * a channel of its own.
20
+ */
21
+ export declare const DASHBOARD_TOKEN_HEADER = "x-frontmcp-dashboard-token";
22
+ /**
23
+ * Cookie the dashboard page gets once its token was checked, so the page's own MCP
24
+ * client (an `EventSource`, which cannot send headers) reaches the MCP endpoint without
25
+ * putting the token in a URL.
26
+ */
27
+ export declare const DASHBOARD_SESSION_COOKIE = "frontmcp_dashboard";
28
+ /**
29
+ * Which part of the dashboard a request is for.
30
+ *
31
+ * - `page`: the HTML page. Accepts `Authorization: Bearer`, `?token=` (the documented
32
+ * link) and the session cookie.
33
+ * - `mcp`: the dashboard's MCP endpoint. Accepts `Authorization: Bearer`, the
34
+ * `x-frontmcp-dashboard-token` header and the session cookie, but not `?token=`: a URL
35
+ * token lands in access logs, `Referer` headers and browser history.
36
+ */
37
+ export type DashboardAuthSurface = 'page' | 'mcp';
16
38
  export interface DashboardAuthResult {
17
39
  authorized: boolean;
18
40
  status?: 401;
19
41
  message?: string;
20
42
  }
21
- export declare function createDashboardAuthValidator(auth: DashboardAuth | undefined): ((req: DashboardAuthRequest) => DashboardAuthResult) | undefined;
43
+ /**
44
+ * Constant-time token comparison.
45
+ *
46
+ * Both sides are hashed to fixed-length digests first, so neither the comparison
47
+ * time nor `timingSafeEqual`'s equal-length requirement leaks the token length.
48
+ * This is the same idiom the DCR client registry uses for client secrets.
49
+ */
50
+ export declare function tokenMatches(presented: string, expected: string): boolean;
51
+ /**
52
+ * The session cookie's value: an HMAC of a fixed label keyed by the token, so the cookie
53
+ * never holds the token itself and cannot be computed without it.
54
+ */
55
+ export declare function deriveDashboardCookieValue(token: string): string;
56
+ /**
57
+ * Create the dashboard's HTTP auth check for one surface (see {@link DashboardAuthSurface}).
58
+ *
59
+ * Returns `undefined` when authentication is not enabled, so the caller can skip
60
+ * the check entirely rather than branch on a permissive validator.
61
+ *
62
+ * `Authorization: Bearer <token>` is accepted on both surfaces; the page also takes
63
+ * `?token=<token>` (the documented link), the MCP endpoint also takes
64
+ * `x-frontmcp-dashboard-token`, and both take the cookie the page sets.
65
+ */
66
+ export declare function createDashboardAuthValidator(auth: DashboardAuth | undefined, surface?: DashboardAuthSurface): ((req: DashboardAuthRequest) => DashboardAuthResult) | undefined;
22
67
  //# sourceMappingURL=dashboard-auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dashboard-auth.d.ts","sourceRoot":"","sources":["../../src/auth/dashboard-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CACnE;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyDD,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,aAAa,GAAG,SAAS,GAC9B,CAAC,CAAC,GAAG,EAAE,oBAAoB,KAAK,mBAAmB,CAAC,GAAG,SAAS,CAqBlE"}
1
+ {"version":3,"file":"dashboard-auth.d.ts","sourceRoot":"","sources":["../../src/auth/dashboard-auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CACnE;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,+BAA+B,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,uBAAuB,CAAC;AAE7D;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,KAAK,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGzE;AAID;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AA+BD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,OAAO,GAAE,oBAA6B,GACrC,CAAC,CAAC,GAAG,EAAE,oBAAoB,KAAK,mBAAmB,CAAC,GAAG,SAAS,CA8BlE"}
package/esm/index.mjs CHANGED
@@ -421,11 +421,16 @@ import {
421
421
  App,
422
422
  DynamicPlugin as DynamicPlugin2,
423
423
  FrontMcpConfig,
424
+ FrontMcpContextStorage,
424
425
  FrontMcpServer,
426
+ HttpHook,
425
427
  Plugin as Plugin2,
426
428
  ScopeEntry as ScopeEntry2
427
429
  } from "@frontmcp/sdk";
428
430
 
431
+ // plugins/plugin-dashboard/src/auth/dashboard-access.ts
432
+ import { MCP_ERROR_CODES, PublicMcpError } from "@frontmcp/sdk";
433
+
429
434
  // libs/utils/dist/esm/index.mjs
430
435
  import { createRequire as __frontmcpCreateRequire } from "module";
431
436
  import { z as z2 } from "@frontmcp/lazy-zod";
@@ -2606,6 +2611,9 @@ function randomBytes(length) {
2606
2611
  function sha256(data) {
2607
2612
  return getCrypto().sha256(data);
2608
2613
  }
2614
+ function hmacSha256(key, data) {
2615
+ return getCrypto().hmacSha256(key, data);
2616
+ }
2609
2617
  function encryptAesGcm(key, plaintext, iv) {
2610
2618
  if (key.length !== 32) {
2611
2619
  throw new Error(`AES-256-GCM requires a 32-byte key, got ${key.length} bytes`);
@@ -3796,6 +3804,8 @@ init_crypto();
3796
3804
  init_runtime();
3797
3805
 
3798
3806
  // plugins/plugin-dashboard/src/auth/dashboard-auth.ts
3807
+ var DASHBOARD_TOKEN_HEADER = "x-frontmcp-dashboard-token";
3808
+ var DASHBOARD_SESSION_COOKIE = "frontmcp_dashboard";
3799
3809
  var AUTHORIZED = { authorized: true };
3800
3810
  function readHeader(headers, name) {
3801
3811
  if (!headers) return void 0;
@@ -3811,6 +3821,19 @@ function tokenMatches(presented, expected) {
3811
3821
  if (presented.length === 0 || expected.length === 0) return false;
3812
3822
  return timingSafeEqual(sha256(presented), sha256(expected));
3813
3823
  }
3824
+ var encoder = new TextEncoder();
3825
+ function deriveDashboardCookieValue(token) {
3826
+ return base64urlEncode(hmacSha256(encoder.encode(token), encoder.encode("frontmcp-dashboard-session:v1")));
3827
+ }
3828
+ function readCookie(header, name) {
3829
+ if (!header) return void 0;
3830
+ for (const part of header.split(";")) {
3831
+ const separator = part.indexOf("=");
3832
+ if (separator === -1) continue;
3833
+ if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
3834
+ }
3835
+ return void 0;
3836
+ }
3814
3837
  function readBearerToken(header) {
3815
3838
  if (!header) return void 0;
3816
3839
  const separator = header.indexOf(" ");
@@ -3819,36 +3842,81 @@ function readBearerToken(header) {
3819
3842
  const token = header.slice(separator + 1).trim();
3820
3843
  return token.length > 0 ? token : void 0;
3821
3844
  }
3822
- function createDashboardAuthValidator(auth) {
3845
+ function createDashboardAuthValidator(auth, surface = "page") {
3823
3846
  if (!auth?.enabled) return void 0;
3824
3847
  const expected = auth.token;
3848
+ const expectedCookie = expected ? deriveDashboardCookieValue(expected) : void 0;
3825
3849
  return (req) => {
3826
- if (!expected) {
3850
+ if (!expected || !expectedCookie) {
3827
3851
  return { authorized: false, status: 401, message: "Dashboard authentication is misconfigured" };
3828
3852
  }
3829
3853
  const bearer = readBearerToken(readHeader(req.headers, "authorization"));
3830
3854
  if (bearer && tokenMatches(bearer, expected)) return AUTHORIZED;
3831
- const queryToken = readQuery(req.query, "token");
3832
- if (queryToken && tokenMatches(queryToken, expected)) return AUTHORIZED;
3855
+ if (surface === "mcp") {
3856
+ const headerToken = readHeader(req.headers, DASHBOARD_TOKEN_HEADER);
3857
+ if (headerToken && tokenMatches(headerToken, expected)) return AUTHORIZED;
3858
+ } else {
3859
+ const queryToken = readQuery(req.query, "token");
3860
+ if (queryToken && tokenMatches(queryToken, expected)) return AUTHORIZED;
3861
+ }
3862
+ const cookie = readCookie(readHeader(req.headers, "cookie"), DASHBOARD_SESSION_COOKIE);
3863
+ if (cookie && tokenMatches(cookie, expectedCookie)) return AUTHORIZED;
3833
3864
  return { authorized: false, status: 401, message: "Unauthorized" };
3834
3865
  };
3835
3866
  }
3836
3867
 
3868
+ // plugins/plugin-dashboard/src/auth/dashboard-access.ts
3869
+ var GATE_PASSED = /* @__PURE__ */ Symbol("frontmcp:dashboard:gate-passed");
3870
+ function markDashboardGatePassed(ctx) {
3871
+ ctx?.set(GATE_PASSED, true);
3872
+ }
3873
+ var DashboardDisabledError = class extends PublicMcpError {
3874
+ mcpErrorCode = MCP_ERROR_CODES.FORBIDDEN;
3875
+ constructor() {
3876
+ super("The FrontMCP dashboard is disabled.", "DASHBOARD_DISABLED", 404);
3877
+ }
3878
+ };
3879
+ var DashboardUnauthorizedError = class extends PublicMcpError {
3880
+ mcpErrorCode = MCP_ERROR_CODES.UNAUTHORIZED;
3881
+ constructor() {
3882
+ super(
3883
+ `The dashboard token is required. Send it in the ${DASHBOARD_TOKEN_HEADER} header.`,
3884
+ "DASHBOARD_UNAUTHORIZED",
3885
+ 401,
3886
+ 'Bearer realm="frontmcp-dashboard"'
3887
+ );
3888
+ }
3889
+ };
3890
+ function assertDashboardAccess(options, ctx) {
3891
+ if (!isDashboardEnabled(options)) {
3892
+ throw new DashboardDisabledError();
3893
+ }
3894
+ if (!options.auth?.enabled) return;
3895
+ if (ctx?.get(GATE_PASSED) === true) return;
3896
+ const expected = options.auth.token;
3897
+ const presented = ctx?.metadata?.customHeaders?.[DASHBOARD_TOKEN_HEADER];
3898
+ if (expected && presented && tokenMatches(presented, expected)) return;
3899
+ throw new DashboardUnauthorizedError();
3900
+ }
3901
+ function assertDashboardToolAccess(tool) {
3902
+ const configured = tool.tryGet(DashboardConfigToken);
3903
+ assertDashboardAccess(configured ?? resolveDashboardOptions(), tool.tryGetContext());
3904
+ }
3905
+
3837
3906
  // plugins/plugin-dashboard/src/html/html.generator.ts
3838
3907
  function escapeForJs(str) {
3839
3908
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/</g, "\\x3c").replace(/>/g, "\\x3e").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
3840
3909
  }
3841
- function generateDashboardHtml(options) {
3842
- const cdn = options.cdn;
3843
- const basePath = options.basePath;
3844
- if (cdn.entrypoint) {
3845
- return generateExternalEntrypointHtml(options);
3910
+ function generateDashboardHtml(options, mcpPath = options.basePath) {
3911
+ if (options.cdn.entrypoint) {
3912
+ return generateExternalEntrypointHtml(options, mcpPath);
3846
3913
  }
3847
- return generateInlineDashboardHtml(options);
3914
+ return generateInlineDashboardHtml(options, mcpPath);
3848
3915
  }
3849
- function generateExternalEntrypointHtml(options) {
3916
+ function generateExternalEntrypointHtml(options, mcpPath) {
3850
3917
  const { cdn } = options;
3851
3918
  const safeBasePath = escapeForJs(options.basePath);
3919
+ const safeMcpPath = escapeForJs(mcpPath);
3852
3920
  const safeReact = escapeForJs(cdn.react);
3853
3921
  const safeReactDom = escapeForJs(cdn.reactDom);
3854
3922
  const safeReactDomClient = escapeForJs(cdn.reactDomClient);
@@ -3888,7 +3956,8 @@ function generateExternalEntrypointHtml(options) {
3888
3956
  // Dashboard configuration
3889
3957
  window.__FRONTMCP_DASHBOARD__ = {
3890
3958
  basePath: '${safeBasePath}',
3891
- sseUrl: '${safeBasePath}/sse',
3959
+ mcpPath: '${safeMcpPath}',
3960
+ sseUrl: '${safeMcpPath}/sse',
3892
3961
  };
3893
3962
 
3894
3963
  // Load external dashboard UI
@@ -3904,10 +3973,11 @@ function generateExternalEntrypointHtml(options) {
3904
3973
  </body>
3905
3974
  </html>`;
3906
3975
  }
3907
- function generateInlineDashboardHtml(options) {
3976
+ function generateInlineDashboardHtml(options, mcpPath) {
3908
3977
  const { cdn } = options;
3909
3978
  const safeBasePath = escapeForJs(options.basePath);
3910
- const sseUrl = `${safeBasePath}/sse`;
3979
+ const safeMcpPath = escapeForJs(mcpPath);
3980
+ const sseUrl = `${safeMcpPath}/sse`;
3911
3981
  const safeReact = escapeForJs(cdn.react);
3912
3982
  const safeReactDom = escapeForJs(cdn.reactDom);
3913
3983
  const safeReactDomClient = escapeForJs(cdn.reactDomClient);
@@ -4105,6 +4175,7 @@ function generateInlineDashboardHtml(options) {
4105
4175
  // Configuration
4106
4176
  const config = {
4107
4177
  basePath: '${safeBasePath}',
4178
+ mcpPath: '${safeMcpPath}',
4108
4179
  sseUrl: '${sseUrl}',
4109
4180
  };
4110
4181
 
@@ -4302,7 +4373,7 @@ function generateInlineDashboardHtml(options) {
4302
4373
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
4303
4374
 
4304
4375
  useEffect(() => {
4305
- const client = new McpClient(config.sseUrl, config.basePath);
4376
+ const client = new McpClient(config.sseUrl, config.mcpPath);
4306
4377
 
4307
4378
  async function init() {
4308
4379
  try {
@@ -4693,6 +4764,7 @@ var graphToolInputSchema = {
4693
4764
  };
4694
4765
  var GraphTool = class extends ToolContext {
4695
4766
  async execute(input) {
4767
+ assertDashboardToolAccess(this);
4696
4768
  const graphProvider = this.get(GraphDataProvider);
4697
4769
  if (input.refresh) {
4698
4770
  graphProvider.invalidateCache();
@@ -4762,6 +4834,7 @@ var listResourcesOutputSchema = z5.object({
4762
4834
  });
4763
4835
  var ListResourcesTool = class extends ToolContext2 {
4764
4836
  async execute(input) {
4837
+ assertDashboardToolAccess(this);
4765
4838
  const parentScope = this.tryGet(ParentScopeToken);
4766
4839
  const targetScope = parentScope || this.scope;
4767
4840
  const results = [];
@@ -4841,6 +4914,7 @@ var listToolsOutputSchema = z6.object({
4841
4914
  });
4842
4915
  var ListToolsTool = class extends ToolContext3 {
4843
4916
  async execute(input) {
4917
+ assertDashboardToolAccess(this);
4844
4918
  const parentScope = this.tryGet(ParentScopeToken);
4845
4919
  const targetScope = parentScope || this.scope;
4846
4920
  let allTools = [];
@@ -4884,9 +4958,23 @@ ListToolsTool = __decorateClass([
4884
4958
 
4885
4959
  // plugins/plugin-dashboard/src/app/dashboard.app.ts
4886
4960
  var DashboardMiddlewareToken = /* @__PURE__ */ Symbol("dashboard:middleware");
4887
- function createDashboardMiddleware(options) {
4888
- const html = generateDashboardHtml(options);
4889
- const authorize = createDashboardAuthValidator(options.auth);
4961
+ function sessionCookies(options, paths, secure) {
4962
+ const token = options.auth.token;
4963
+ if (!options.auth.enabled || !token) return [];
4964
+ const value = deriveDashboardCookieValue(token);
4965
+ return paths.map(
4966
+ (path) => `${DASHBOARD_SESSION_COOKIE}=${value}; Path=${path}; HttpOnly; SameSite=Strict${secure ? "; Secure" : ""}`
4967
+ );
4968
+ }
4969
+ function isHttps(req) {
4970
+ const socket = req.socket;
4971
+ const forwarded = req.headers?.["x-forwarded-proto"];
4972
+ return socket?.encrypted === true || typeof forwarded === "string" && forwarded.split(",")[0]?.trim() === "https";
4973
+ }
4974
+ function createDashboardMiddleware(options, mcpPath) {
4975
+ const html = generateDashboardHtml(options, mcpPath);
4976
+ const authorize = createDashboardAuthValidator(options.auth, "page");
4977
+ const cookiePaths = [.../* @__PURE__ */ new Set([mcpPath, options.basePath])];
4890
4978
  return async (req, res, next) => {
4891
4979
  if (!isDashboardEnabled(options)) {
4892
4980
  return next();
@@ -4894,7 +4982,10 @@ function createDashboardMiddleware(options) {
4894
4982
  const urlPath = req.path || req.url || "/";
4895
4983
  const method = (req.method || "GET").toUpperCase();
4896
4984
  const isPageRequest = method === "GET" && (urlPath === "/" || urlPath === "");
4897
- if (isPageRequest && authorize) {
4985
+ if (!isPageRequest) {
4986
+ return next();
4987
+ }
4988
+ if (authorize) {
4898
4989
  const result = authorize({
4899
4990
  headers: req.headers,
4900
4991
  query: req.query
@@ -4904,15 +4995,11 @@ function createDashboardMiddleware(options) {
4904
4995
  return;
4905
4996
  }
4906
4997
  }
4907
- if (isPageRequest) {
4908
- res.setHeader?.(
4909
- "Content-Type",
4910
- "text/html"
4911
- );
4912
- res.status(200).send(html);
4913
- return;
4914
- }
4915
- return next();
4998
+ const writer = res;
4999
+ writer.setHeader?.("Content-Type", "text/html");
5000
+ const cookies = sessionCookies(options, cookiePaths, isHttps(req));
5001
+ if (cookies.length > 0) writer.setHeader?.("Set-Cookie", cookies);
5002
+ res.status(200).send(html);
4916
5003
  };
4917
5004
  }
4918
5005
  var DashboardHttpPlugin = class extends DynamicPlugin2 {
@@ -4924,8 +5011,33 @@ var DashboardHttpPlugin = class extends DynamicPlugin2 {
4924
5011
  ...options
4925
5012
  });
4926
5013
  }
5014
+ async gateDashboardMcp(flowCtx) {
5015
+ const options = this.resolve(DashboardConfigToken) ?? resolveDashboardOptions();
5016
+ if (!isDashboardEnabled(options)) {
5017
+ throw new DashboardDisabledError();
5018
+ }
5019
+ const authorize = createDashboardAuthValidator(options.auth, "mcp");
5020
+ if (!authorize) return;
5021
+ const request = flowCtx.rawInput.request;
5022
+ const result = authorize({
5023
+ headers: request.headers,
5024
+ query: request.query
5025
+ });
5026
+ if (!result.authorized) {
5027
+ throw new DashboardUnauthorizedError();
5028
+ }
5029
+ markDashboardGatePassed(this.resolve(FrontMcpContextStorage)?.getStore());
5030
+ }
5031
+ /** `this.get`, or `undefined` when the token isn't provided. */
5032
+ resolve(token) {
5033
+ try {
5034
+ return this.get(token);
5035
+ } catch {
5036
+ return void 0;
5037
+ }
5038
+ }
4927
5039
  /**
4928
- * Provide the dashboard config and middleware registration via DI.
5040
+ * Provide the dashboard config and page middleware registration via DI.
4929
5041
  */
4930
5042
  static dynamicProviders(options) {
4931
5043
  return [
@@ -4939,17 +5051,20 @@ var DashboardHttpPlugin = class extends DynamicPlugin2 {
4939
5051
  {
4940
5052
  name: "dashboard:middleware",
4941
5053
  provide: DashboardMiddlewareToken,
4942
- inject: () => [FrontMcpServer],
4943
- useFactory: (server) => {
5054
+ inject: () => [FrontMcpServer, ScopeEntry2],
5055
+ useFactory: (server, scope) => {
4944
5056
  const effectiveOptions = resolveDashboardOptions(options);
4945
- const middleware = createDashboardMiddleware(effectiveOptions);
4946
- server.registerMiddleware(effectiveOptions.basePath, middleware);
4947
- return { registered: true };
5057
+ const mcpPath = scope.fullPath;
5058
+ server.registerMiddleware(effectiveOptions.basePath, createDashboardMiddleware(effectiveOptions, mcpPath));
5059
+ return { registered: true, mcpPath };
4948
5060
  }
4949
5061
  }
4950
5062
  ];
4951
5063
  }
4952
5064
  };
5065
+ __decorateClass([
5066
+ HttpHook.Will("handleMcp2026", { priority: 1e3 })
5067
+ ], DashboardHttpPlugin.prototype, "gateDashboardMcp", 1);
4953
5068
  DashboardHttpPlugin = __decorateClass([
4954
5069
  Plugin2({
4955
5070
  name: "dashboard:http",
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-dashboard",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Dashboard plugin for FrontMCP - visual monitoring and introspection of MCP servers",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -47,12 +47,12 @@
47
47
  }
48
48
  },
49
49
  "dependencies": {
50
- "@frontmcp/sdk": "1.8.2"
50
+ "@frontmcp/sdk": "1.8.4"
51
51
  },
52
52
  "devDependencies": {
53
53
  "reflect-metadata": "^0.2.2"
54
54
  },
55
55
  "peerDependencies": {
56
- "@frontmcp/lazy-zod": "1.8.2"
56
+ "@frontmcp/lazy-zod": "1.8.4"
57
57
  }
58
58
  }
@@ -11,9 +11,12 @@ import type { DashboardPluginOptions } from '../dashboard.types';
11
11
  *
12
12
  * The configured `auth.token` is deliberately NOT embedded here, and not put in
13
13
  * the SSE query string: a URL token lands in access logs, `Referer` headers and
14
- * browser history, and the page's own MCP POSTs never carried it anyway
15
- * (GHSA-rgxj-434m-vxh3). The token gates the page; the dashboard's MCP scope
16
- * uses the server's normal authentication.
14
+ * browser history (GHSA-rgxj-434m-vxh3). When `auth` is on, the page's MCP
15
+ * client authenticates with the HttpOnly cookie the server sets with the page.
16
+ *
17
+ * @param mcpPath - where the dashboard's MCP endpoint is served (its scope's
18
+ * route, `/dashboard` unless the server has an `entryPath`), which `basePath`
19
+ * does not move. Defaults to `basePath`.
17
20
  */
18
- export declare function generateDashboardHtml(options: DashboardPluginOptions): string;
21
+ export declare function generateDashboardHtml(options: DashboardPluginOptions, mcpPath?: string): string;
19
22
  //# sourceMappingURL=html.generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"html.generator.d.ts","sourceRoot":"","sources":["../../src/html/html.generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAiBjE;;;;;;GAMG;AACH;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAW7E"}
1
+ {"version":3,"file":"html.generator.d.ts","sourceRoot":"","sources":["../../src/html/html.generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAiBjE;;;;;;GAMG;AACH;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,EAAE,OAAO,GAAE,MAAyB,GAAG,MAAM,CAQjH"}
package/index.js CHANGED
@@ -437,7 +437,10 @@ DashboardPlugin = __decorateClass([
437
437
  ], DashboardPlugin);
438
438
 
439
439
  // plugins/plugin-dashboard/src/app/dashboard.app.ts
440
- var import_sdk6 = require("@frontmcp/sdk");
440
+ var import_sdk7 = require("@frontmcp/sdk");
441
+
442
+ // plugins/plugin-dashboard/src/auth/dashboard-access.ts
443
+ var import_sdk2 = require("@frontmcp/sdk");
441
444
 
442
445
  // libs/utils/dist/esm/index.mjs
443
446
  var import_module = require("module");
@@ -2620,6 +2623,9 @@ function randomBytes(length) {
2620
2623
  function sha256(data) {
2621
2624
  return getCrypto().sha256(data);
2622
2625
  }
2626
+ function hmacSha256(key, data) {
2627
+ return getCrypto().hmacSha256(key, data);
2628
+ }
2623
2629
  function encryptAesGcm(key, plaintext, iv) {
2624
2630
  if (key.length !== 32) {
2625
2631
  throw new Error(`AES-256-GCM requires a 32-byte key, got ${key.length} bytes`);
@@ -3810,6 +3816,8 @@ init_crypto();
3810
3816
  init_runtime();
3811
3817
 
3812
3818
  // plugins/plugin-dashboard/src/auth/dashboard-auth.ts
3819
+ var DASHBOARD_TOKEN_HEADER = "x-frontmcp-dashboard-token";
3820
+ var DASHBOARD_SESSION_COOKIE = "frontmcp_dashboard";
3813
3821
  var AUTHORIZED = { authorized: true };
3814
3822
  function readHeader(headers, name) {
3815
3823
  if (!headers) return void 0;
@@ -3825,6 +3833,19 @@ function tokenMatches(presented, expected) {
3825
3833
  if (presented.length === 0 || expected.length === 0) return false;
3826
3834
  return timingSafeEqual(sha256(presented), sha256(expected));
3827
3835
  }
3836
+ var encoder = new TextEncoder();
3837
+ function deriveDashboardCookieValue(token) {
3838
+ return base64urlEncode(hmacSha256(encoder.encode(token), encoder.encode("frontmcp-dashboard-session:v1")));
3839
+ }
3840
+ function readCookie(header, name) {
3841
+ if (!header) return void 0;
3842
+ for (const part of header.split(";")) {
3843
+ const separator = part.indexOf("=");
3844
+ if (separator === -1) continue;
3845
+ if (part.slice(0, separator).trim() === name) return part.slice(separator + 1).trim();
3846
+ }
3847
+ return void 0;
3848
+ }
3828
3849
  function readBearerToken(header) {
3829
3850
  if (!header) return void 0;
3830
3851
  const separator = header.indexOf(" ");
@@ -3833,36 +3854,81 @@ function readBearerToken(header) {
3833
3854
  const token = header.slice(separator + 1).trim();
3834
3855
  return token.length > 0 ? token : void 0;
3835
3856
  }
3836
- function createDashboardAuthValidator(auth) {
3857
+ function createDashboardAuthValidator(auth, surface = "page") {
3837
3858
  if (!auth?.enabled) return void 0;
3838
3859
  const expected = auth.token;
3860
+ const expectedCookie = expected ? deriveDashboardCookieValue(expected) : void 0;
3839
3861
  return (req) => {
3840
- if (!expected) {
3862
+ if (!expected || !expectedCookie) {
3841
3863
  return { authorized: false, status: 401, message: "Dashboard authentication is misconfigured" };
3842
3864
  }
3843
3865
  const bearer = readBearerToken(readHeader(req.headers, "authorization"));
3844
3866
  if (bearer && tokenMatches(bearer, expected)) return AUTHORIZED;
3845
- const queryToken = readQuery(req.query, "token");
3846
- if (queryToken && tokenMatches(queryToken, expected)) return AUTHORIZED;
3867
+ if (surface === "mcp") {
3868
+ const headerToken = readHeader(req.headers, DASHBOARD_TOKEN_HEADER);
3869
+ if (headerToken && tokenMatches(headerToken, expected)) return AUTHORIZED;
3870
+ } else {
3871
+ const queryToken = readQuery(req.query, "token");
3872
+ if (queryToken && tokenMatches(queryToken, expected)) return AUTHORIZED;
3873
+ }
3874
+ const cookie = readCookie(readHeader(req.headers, "cookie"), DASHBOARD_SESSION_COOKIE);
3875
+ if (cookie && tokenMatches(cookie, expectedCookie)) return AUTHORIZED;
3847
3876
  return { authorized: false, status: 401, message: "Unauthorized" };
3848
3877
  };
3849
3878
  }
3850
3879
 
3880
+ // plugins/plugin-dashboard/src/auth/dashboard-access.ts
3881
+ var GATE_PASSED = /* @__PURE__ */ Symbol("frontmcp:dashboard:gate-passed");
3882
+ function markDashboardGatePassed(ctx) {
3883
+ ctx?.set(GATE_PASSED, true);
3884
+ }
3885
+ var DashboardDisabledError = class extends import_sdk2.PublicMcpError {
3886
+ mcpErrorCode = import_sdk2.MCP_ERROR_CODES.FORBIDDEN;
3887
+ constructor() {
3888
+ super("The FrontMCP dashboard is disabled.", "DASHBOARD_DISABLED", 404);
3889
+ }
3890
+ };
3891
+ var DashboardUnauthorizedError = class extends import_sdk2.PublicMcpError {
3892
+ mcpErrorCode = import_sdk2.MCP_ERROR_CODES.UNAUTHORIZED;
3893
+ constructor() {
3894
+ super(
3895
+ `The dashboard token is required. Send it in the ${DASHBOARD_TOKEN_HEADER} header.`,
3896
+ "DASHBOARD_UNAUTHORIZED",
3897
+ 401,
3898
+ 'Bearer realm="frontmcp-dashboard"'
3899
+ );
3900
+ }
3901
+ };
3902
+ function assertDashboardAccess(options, ctx) {
3903
+ if (!isDashboardEnabled(options)) {
3904
+ throw new DashboardDisabledError();
3905
+ }
3906
+ if (!options.auth?.enabled) return;
3907
+ if (ctx?.get(GATE_PASSED) === true) return;
3908
+ const expected = options.auth.token;
3909
+ const presented = ctx?.metadata?.customHeaders?.[DASHBOARD_TOKEN_HEADER];
3910
+ if (expected && presented && tokenMatches(presented, expected)) return;
3911
+ throw new DashboardUnauthorizedError();
3912
+ }
3913
+ function assertDashboardToolAccess(tool) {
3914
+ const configured = tool.tryGet(DashboardConfigToken);
3915
+ assertDashboardAccess(configured ?? resolveDashboardOptions(), tool.tryGetContext());
3916
+ }
3917
+
3851
3918
  // plugins/plugin-dashboard/src/html/html.generator.ts
3852
3919
  function escapeForJs(str) {
3853
3920
  return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/</g, "\\x3c").replace(/>/g, "\\x3e").replace(/\n/g, "\\n").replace(/\r/g, "\\r");
3854
3921
  }
3855
- function generateDashboardHtml(options) {
3856
- const cdn = options.cdn;
3857
- const basePath = options.basePath;
3858
- if (cdn.entrypoint) {
3859
- return generateExternalEntrypointHtml(options);
3922
+ function generateDashboardHtml(options, mcpPath = options.basePath) {
3923
+ if (options.cdn.entrypoint) {
3924
+ return generateExternalEntrypointHtml(options, mcpPath);
3860
3925
  }
3861
- return generateInlineDashboardHtml(options);
3926
+ return generateInlineDashboardHtml(options, mcpPath);
3862
3927
  }
3863
- function generateExternalEntrypointHtml(options) {
3928
+ function generateExternalEntrypointHtml(options, mcpPath) {
3864
3929
  const { cdn } = options;
3865
3930
  const safeBasePath = escapeForJs(options.basePath);
3931
+ const safeMcpPath = escapeForJs(mcpPath);
3866
3932
  const safeReact = escapeForJs(cdn.react);
3867
3933
  const safeReactDom = escapeForJs(cdn.reactDom);
3868
3934
  const safeReactDomClient = escapeForJs(cdn.reactDomClient);
@@ -3902,7 +3968,8 @@ function generateExternalEntrypointHtml(options) {
3902
3968
  // Dashboard configuration
3903
3969
  window.__FRONTMCP_DASHBOARD__ = {
3904
3970
  basePath: '${safeBasePath}',
3905
- sseUrl: '${safeBasePath}/sse',
3971
+ mcpPath: '${safeMcpPath}',
3972
+ sseUrl: '${safeMcpPath}/sse',
3906
3973
  };
3907
3974
 
3908
3975
  // Load external dashboard UI
@@ -3918,10 +3985,11 @@ function generateExternalEntrypointHtml(options) {
3918
3985
  </body>
3919
3986
  </html>`;
3920
3987
  }
3921
- function generateInlineDashboardHtml(options) {
3988
+ function generateInlineDashboardHtml(options, mcpPath) {
3922
3989
  const { cdn } = options;
3923
3990
  const safeBasePath = escapeForJs(options.basePath);
3924
- const sseUrl = `${safeBasePath}/sse`;
3991
+ const safeMcpPath = escapeForJs(mcpPath);
3992
+ const sseUrl = `${safeMcpPath}/sse`;
3925
3993
  const safeReact = escapeForJs(cdn.react);
3926
3994
  const safeReactDom = escapeForJs(cdn.reactDom);
3927
3995
  const safeReactDomClient = escapeForJs(cdn.reactDomClient);
@@ -4119,6 +4187,7 @@ function generateInlineDashboardHtml(options) {
4119
4187
  // Configuration
4120
4188
  const config = {
4121
4189
  basePath: '${safeBasePath}',
4190
+ mcpPath: '${safeMcpPath}',
4122
4191
  sseUrl: '${sseUrl}',
4123
4192
  };
4124
4193
 
@@ -4316,7 +4385,7 @@ function generateInlineDashboardHtml(options) {
4316
4385
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
4317
4386
 
4318
4387
  useEffect(() => {
4319
- const client = new McpClient(config.sseUrl, config.basePath);
4388
+ const client = new McpClient(config.sseUrl, config.mcpPath);
4320
4389
 
4321
4390
  async function init() {
4322
4391
  try {
@@ -4433,7 +4502,7 @@ function generateInlineDashboardHtml(options) {
4433
4502
  }
4434
4503
 
4435
4504
  // plugins/plugin-dashboard/src/providers/graph-data.provider.ts
4436
- var import_sdk2 = require("@frontmcp/sdk");
4505
+ var import_sdk3 = require("@frontmcp/sdk");
4437
4506
  var GraphDataProvider = class {
4438
4507
  constructor(scope, serverName, serverVersion) {
4439
4508
  this.scope = scope;
@@ -4693,20 +4762,21 @@ var GraphDataProvider = class {
4693
4762
  }
4694
4763
  };
4695
4764
  GraphDataProvider = __decorateClass([
4696
- (0, import_sdk2.Provider)({
4765
+ (0, import_sdk3.Provider)({
4697
4766
  name: "graph-data-provider"
4698
4767
  })
4699
4768
  ], GraphDataProvider);
4700
4769
 
4701
4770
  // plugins/plugin-dashboard/src/tools/graph.tool.ts
4702
4771
  var import_lazy_zod5 = require("@frontmcp/lazy-zod");
4703
- var import_sdk3 = require("@frontmcp/sdk");
4772
+ var import_sdk4 = require("@frontmcp/sdk");
4704
4773
  var graphToolInputSchema = {
4705
4774
  includeSchemas: import_lazy_zod5.z.boolean().optional().default(false).describe("Include full input/output schemas in the response"),
4706
4775
  refresh: import_lazy_zod5.z.boolean().optional().default(false).describe("Force refresh the graph data (bypass cache)")
4707
4776
  };
4708
- var GraphTool = class extends import_sdk3.ToolContext {
4777
+ var GraphTool = class extends import_sdk4.ToolContext {
4709
4778
  async execute(input) {
4779
+ assertDashboardToolAccess(this);
4710
4780
  const graphProvider = this.get(GraphDataProvider);
4711
4781
  if (input.refresh) {
4712
4782
  graphProvider.invalidateCache();
@@ -4729,7 +4799,7 @@ var GraphTool = class extends import_sdk3.ToolContext {
4729
4799
  }
4730
4800
  };
4731
4801
  GraphTool = __decorateClass([
4732
- (0, import_sdk3.Tool)({
4802
+ (0, import_sdk4.Tool)({
4733
4803
  name: "dashboard:graph",
4734
4804
  description: "Get the server graph showing all registered tools, resources, prompts, apps, and their relationships. Returns nodes and edges that can be visualized.",
4735
4805
  inputSchema: graphToolInputSchema,
@@ -4741,7 +4811,7 @@ GraphTool = __decorateClass([
4741
4811
 
4742
4812
  // plugins/plugin-dashboard/src/tools/list-resources.tool.ts
4743
4813
  var import_lazy_zod6 = require("@frontmcp/lazy-zod");
4744
- var import_sdk4 = require("@frontmcp/sdk");
4814
+ var import_sdk5 = require("@frontmcp/sdk");
4745
4815
 
4746
4816
  // plugins/plugin-dashboard/src/shared/safe-regex.ts
4747
4817
  function safeRegex(pattern) {
@@ -4774,8 +4844,9 @@ var listResourcesOutputSchema = import_lazy_zod6.z.object({
4774
4844
  ),
4775
4845
  count: import_lazy_zod6.z.number()
4776
4846
  });
4777
- var ListResourcesTool = class extends import_sdk4.ToolContext {
4847
+ var ListResourcesTool = class extends import_sdk5.ToolContext {
4778
4848
  async execute(input) {
4849
+ assertDashboardToolAccess(this);
4779
4850
  const parentScope = this.tryGet(ParentScopeToken);
4780
4851
  const targetScope = parentScope || this.scope;
4781
4852
  const results = [];
@@ -4821,7 +4892,7 @@ var ListResourcesTool = class extends import_sdk4.ToolContext {
4821
4892
  }
4822
4893
  };
4823
4894
  ListResourcesTool = __decorateClass([
4824
- (0, import_sdk4.Tool)({
4895
+ (0, import_sdk5.Tool)({
4825
4896
  name: "dashboard:list-resources",
4826
4897
  description: "List all resources and resource templates registered in the monitored FrontMCP server.",
4827
4898
  inputSchema: listResourcesInputSchema,
@@ -4834,7 +4905,7 @@ ListResourcesTool = __decorateClass([
4834
4905
 
4835
4906
  // plugins/plugin-dashboard/src/tools/list-tools.tool.ts
4836
4907
  var import_lazy_zod7 = require("@frontmcp/lazy-zod");
4837
- var import_sdk5 = require("@frontmcp/sdk");
4908
+ var import_sdk6 = require("@frontmcp/sdk");
4838
4909
  var listToolsInputSchema = {
4839
4910
  filter: import_lazy_zod7.z.string().optional().describe("Filter tools by name pattern (regex supported)"),
4840
4911
  includePlugins: import_lazy_zod7.z.boolean().default(true).describe("Include tools from plugins"),
@@ -4853,8 +4924,9 @@ var listToolsOutputSchema = import_lazy_zod7.z.object({
4853
4924
  ),
4854
4925
  count: import_lazy_zod7.z.number()
4855
4926
  });
4856
- var ListToolsTool = class extends import_sdk5.ToolContext {
4927
+ var ListToolsTool = class extends import_sdk6.ToolContext {
4857
4928
  async execute(input) {
4929
+ assertDashboardToolAccess(this);
4858
4930
  const parentScope = this.tryGet(ParentScopeToken);
4859
4931
  const targetScope = parentScope || this.scope;
4860
4932
  let allTools = [];
@@ -4885,7 +4957,7 @@ var ListToolsTool = class extends import_sdk5.ToolContext {
4885
4957
  }
4886
4958
  };
4887
4959
  ListToolsTool = __decorateClass([
4888
- (0, import_sdk5.Tool)({
4960
+ (0, import_sdk6.Tool)({
4889
4961
  name: "dashboard:list-tools",
4890
4962
  description: "List all tools registered in the monitored FrontMCP server. Returns tool names, descriptions, and optionally schemas.",
4891
4963
  inputSchema: listToolsInputSchema,
@@ -4898,9 +4970,23 @@ ListToolsTool = __decorateClass([
4898
4970
 
4899
4971
  // plugins/plugin-dashboard/src/app/dashboard.app.ts
4900
4972
  var DashboardMiddlewareToken = /* @__PURE__ */ Symbol("dashboard:middleware");
4901
- function createDashboardMiddleware(options) {
4902
- const html = generateDashboardHtml(options);
4903
- const authorize = createDashboardAuthValidator(options.auth);
4973
+ function sessionCookies(options, paths, secure) {
4974
+ const token = options.auth.token;
4975
+ if (!options.auth.enabled || !token) return [];
4976
+ const value = deriveDashboardCookieValue(token);
4977
+ return paths.map(
4978
+ (path) => `${DASHBOARD_SESSION_COOKIE}=${value}; Path=${path}; HttpOnly; SameSite=Strict${secure ? "; Secure" : ""}`
4979
+ );
4980
+ }
4981
+ function isHttps(req) {
4982
+ const socket = req.socket;
4983
+ const forwarded = req.headers?.["x-forwarded-proto"];
4984
+ return socket?.encrypted === true || typeof forwarded === "string" && forwarded.split(",")[0]?.trim() === "https";
4985
+ }
4986
+ function createDashboardMiddleware(options, mcpPath) {
4987
+ const html = generateDashboardHtml(options, mcpPath);
4988
+ const authorize = createDashboardAuthValidator(options.auth, "page");
4989
+ const cookiePaths = [.../* @__PURE__ */ new Set([mcpPath, options.basePath])];
4904
4990
  return async (req, res, next) => {
4905
4991
  if (!isDashboardEnabled(options)) {
4906
4992
  return next();
@@ -4908,7 +4994,10 @@ function createDashboardMiddleware(options) {
4908
4994
  const urlPath = req.path || req.url || "/";
4909
4995
  const method = (req.method || "GET").toUpperCase();
4910
4996
  const isPageRequest = method === "GET" && (urlPath === "/" || urlPath === "");
4911
- if (isPageRequest && authorize) {
4997
+ if (!isPageRequest) {
4998
+ return next();
4999
+ }
5000
+ if (authorize) {
4912
5001
  const result = authorize({
4913
5002
  headers: req.headers,
4914
5003
  query: req.query
@@ -4918,18 +5007,14 @@ function createDashboardMiddleware(options) {
4918
5007
  return;
4919
5008
  }
4920
5009
  }
4921
- if (isPageRequest) {
4922
- res.setHeader?.(
4923
- "Content-Type",
4924
- "text/html"
4925
- );
4926
- res.status(200).send(html);
4927
- return;
4928
- }
4929
- return next();
5010
+ const writer = res;
5011
+ writer.setHeader?.("Content-Type", "text/html");
5012
+ const cookies = sessionCookies(options, cookiePaths, isHttps(req));
5013
+ if (cookies.length > 0) writer.setHeader?.("Set-Cookie", cookies);
5014
+ res.status(200).send(html);
4930
5015
  };
4931
5016
  }
4932
- var DashboardHttpPlugin = class extends import_sdk6.DynamicPlugin {
5017
+ var DashboardHttpPlugin = class extends import_sdk7.DynamicPlugin {
4933
5018
  options;
4934
5019
  constructor(options = {}) {
4935
5020
  super();
@@ -4938,8 +5023,33 @@ var DashboardHttpPlugin = class extends import_sdk6.DynamicPlugin {
4938
5023
  ...options
4939
5024
  });
4940
5025
  }
5026
+ async gateDashboardMcp(flowCtx) {
5027
+ const options = this.resolve(DashboardConfigToken) ?? resolveDashboardOptions();
5028
+ if (!isDashboardEnabled(options)) {
5029
+ throw new DashboardDisabledError();
5030
+ }
5031
+ const authorize = createDashboardAuthValidator(options.auth, "mcp");
5032
+ if (!authorize) return;
5033
+ const request = flowCtx.rawInput.request;
5034
+ const result = authorize({
5035
+ headers: request.headers,
5036
+ query: request.query
5037
+ });
5038
+ if (!result.authorized) {
5039
+ throw new DashboardUnauthorizedError();
5040
+ }
5041
+ markDashboardGatePassed(this.resolve(import_sdk7.FrontMcpContextStorage)?.getStore());
5042
+ }
5043
+ /** `this.get`, or `undefined` when the token isn't provided. */
5044
+ resolve(token) {
5045
+ try {
5046
+ return this.get(token);
5047
+ } catch {
5048
+ return void 0;
5049
+ }
5050
+ }
4941
5051
  /**
4942
- * Provide the dashboard config and middleware registration via DI.
5052
+ * Provide the dashboard config and page middleware registration via DI.
4943
5053
  */
4944
5054
  static dynamicProviders(options) {
4945
5055
  return [
@@ -4953,19 +5063,22 @@ var DashboardHttpPlugin = class extends import_sdk6.DynamicPlugin {
4953
5063
  {
4954
5064
  name: "dashboard:middleware",
4955
5065
  provide: DashboardMiddlewareToken,
4956
- inject: () => [import_sdk6.FrontMcpServer],
4957
- useFactory: (server) => {
5066
+ inject: () => [import_sdk7.FrontMcpServer, import_sdk7.ScopeEntry],
5067
+ useFactory: (server, scope) => {
4958
5068
  const effectiveOptions = resolveDashboardOptions(options);
4959
- const middleware = createDashboardMiddleware(effectiveOptions);
4960
- server.registerMiddleware(effectiveOptions.basePath, middleware);
4961
- return { registered: true };
5069
+ const mcpPath = scope.fullPath;
5070
+ server.registerMiddleware(effectiveOptions.basePath, createDashboardMiddleware(effectiveOptions, mcpPath));
5071
+ return { registered: true, mcpPath };
4962
5072
  }
4963
5073
  }
4964
5074
  ];
4965
5075
  }
4966
5076
  };
5077
+ __decorateClass([
5078
+ import_sdk7.HttpHook.Will("handleMcp2026", { priority: 1e3 })
5079
+ ], DashboardHttpPlugin.prototype, "gateDashboardMcp", 1);
4967
5080
  DashboardHttpPlugin = __decorateClass([
4968
- (0, import_sdk6.Plugin)({
5081
+ (0, import_sdk7.Plugin)({
4969
5082
  name: "dashboard:http",
4970
5083
  description: "Dashboard HTTP handler for serving UI HTML"
4971
5084
  })
@@ -4973,7 +5086,7 @@ DashboardHttpPlugin = __decorateClass([
4973
5086
  var DashboardApp = class {
4974
5087
  };
4975
5088
  DashboardApp = __decorateClass([
4976
- (0, import_sdk6.App)({
5089
+ (0, import_sdk7.App)({
4977
5090
  name: "dashboard",
4978
5091
  description: "FrontMCP Dashboard for visualization and monitoring",
4979
5092
  providers: [
@@ -4981,7 +5094,7 @@ DashboardApp = __decorateClass([
4981
5094
  {
4982
5095
  name: "dashboard:parent-scope",
4983
5096
  provide: ParentScopeToken,
4984
- inject: () => [import_sdk6.ScopeEntry],
5097
+ inject: () => [import_sdk7.ScopeEntry],
4985
5098
  useFactory: (scope) => {
4986
5099
  return scope;
4987
5100
  }
@@ -4990,7 +5103,7 @@ DashboardApp = __decorateClass([
4990
5103
  {
4991
5104
  name: "dashboard:graph-data",
4992
5105
  provide: GraphDataProvider,
4993
- inject: () => [import_sdk6.ScopeEntry, import_sdk6.FrontMcpConfig],
5106
+ inject: () => [import_sdk7.ScopeEntry, import_sdk7.FrontMcpConfig],
4994
5107
  useFactory: (scope, config) => {
4995
5108
  const serverName = config.info?.name || "FrontMCP Server";
4996
5109
  const serverVersion = config.info?.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-dashboard",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Dashboard plugin for FrontMCP - visual monitoring and introspection of MCP servers",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "license": "Apache-2.0",
@@ -47,12 +47,12 @@
47
47
  }
48
48
  },
49
49
  "dependencies": {
50
- "@frontmcp/sdk": "1.8.2"
50
+ "@frontmcp/sdk": "1.8.4"
51
51
  },
52
52
  "devDependencies": {
53
53
  "reflect-metadata": "^0.2.2"
54
54
  },
55
55
  "peerDependencies": {
56
- "@frontmcp/lazy-zod": "1.8.2"
56
+ "@frontmcp/lazy-zod": "1.8.4"
57
57
  }
58
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph.tool.d.ts","sourceRoot":"","sources":["../../src/tools/graph.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;CAGhC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAE/E;;GAEG;AAUH,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAC1C,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC;CA2BzD"}
1
+ {"version":3,"file":"graph.tool.d.ts","sourceRoot":"","sources":["../../src/tools/graph.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAIlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;CAGhC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAE/E;;GAEG;AAUH,MAAM,CAAC,OAAO,OAAO,SAAU,SAAQ,WAAW;IAC1C,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,SAAS,CAAC;CA6BzD"}
@@ -1 +1 @@
1
- {"version":3,"file":"list-resources.tool.d.ts","sourceRoot":"","sources":["../../src/tools/list-resources.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAmB,MAAM,eAAe,CAAC;AAKnE;;GAEG;AACH,eAAO,MAAM,wBAAwB;;;CAGpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,wBAAwB,CAAC,CAAC,CAAC;AAExF;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;gCAWpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE7E;;GAEG;AAUH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,WAAW;IAClD,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CAwDvE"}
1
+ {"version":3,"file":"list-resources.tool.d.ts","sourceRoot":"","sources":["../../src/tools/list-resources.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAmB,MAAM,eAAe,CAAC;AAMnE;;GAEG;AACH,eAAO,MAAM,wBAAwB;;;CAGpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,wBAAwB,CAAC,CAAC,CAAC;AAExF;;GAEG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;gCAWpC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE7E;;GAEG;AAUH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,WAAW;IAClD,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CA0DvE"}
@@ -1 +1 @@
1
- {"version":3,"file":"list-tools.tool.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAmB,MAAM,eAAe,CAAC;AAKnE;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;CAIhC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAEhF;;GAEG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;gCAYhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAErE;;GAEG;AAWH,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,WAAW;IAC9C,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAgD/D"}
1
+ {"version":3,"file":"list-tools.tool.d.ts","sourceRoot":"","sources":["../../src/tools/list-tools.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,oBAAoB,CAAC;AACvC,OAAO,EAAQ,WAAW,EAAmB,MAAM,eAAe,CAAC;AAMnE;;GAEG;AACH,eAAO,MAAM,oBAAoB;;;;CAIhC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAEhF;;GAEG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;gCAYhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAErE;;GAEG;AAWH,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,WAAW;IAC9C,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAkD/D"}