@eventcatalog/core 3.39.5 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-KVAEAYEP.js → chunk-72BKUYSR.js} +1 -1
  6. package/dist/{chunk-M4S7PORQ.js → chunk-J5CG7FRO.js} +1 -1
  7. package/dist/{chunk-S4HLJWQ7.js → chunk-K762FILQ.js} +1 -1
  8. package/dist/{chunk-TNE5QSJ4.js → chunk-UPI6QQEZ.js} +1 -1
  9. package/dist/{chunk-H5BZMNK3.js → chunk-WFNAWDCB.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/api/03-domain-api.md +16 -0
  13. package/dist/docs/api/04-service-api.md +16 -0
  14. package/dist/docs/api/05-command-api.md +16 -0
  15. package/dist/docs/api/06-event-api.md +16 -0
  16. package/dist/docs/api/06-query-api.md +16 -0
  17. package/dist/docs/api/08-channel-api.md +16 -0
  18. package/dist/docs/api/09-flow-api.md +16 -0
  19. package/dist/docs/api/10-entity-api.md +15 -0
  20. package/dist/docs/api/12-data-product-api.md +17 -0
  21. package/dist/docs/development/01-fundamentals.md +7 -0
  22. package/dist/docs/development/01-getting-started/installation.md +8 -0
  23. package/dist/docs/development/01-getting-started/project-structure.md +2 -0
  24. package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +72 -0
  25. package/dist/docs/development/bring-your-own-documentation/01-introduction.md +3 -1
  26. package/dist/docs/development/customization/customize-sidebars/00-application-sidebar.md +45 -5
  27. package/dist/docs/editor/00-overview.md +73 -0
  28. package/dist/docs/editor/01-first-edit.md +124 -0
  29. package/dist/docs/editor/_category_.json +12 -0
  30. package/dist/docs/editor/explanation/_category_.json +11 -0
  31. package/dist/docs/editor/explanation/beta-status-feedback.md +48 -0
  32. package/dist/docs/editor/explanation/how-it-works.md +49 -0
  33. package/dist/docs/editor/explanation/markdown-mdx-git.md +58 -0
  34. package/dist/docs/editor/how-to/_category_.json +11 -0
  35. package/dist/docs/editor/how-to/add-schemas-and-specifications.md +66 -0
  36. package/dist/docs/editor/how-to/edit-resource.md +88 -0
  37. package/dist/docs/editor/how-to/invite-editors.md +68 -0
  38. package/dist/docs/editor/how-to/open-catalog.md +44 -0
  39. package/dist/docs/editor/how-to/preview-changes.md +55 -0
  40. package/dist/docs/editor/how-to/revert-local-changes.md +43 -0
  41. package/dist/docs/editor/how-to/review-and-commit-changes.md +57 -0
  42. package/dist/docs/editor/how-to/run-locally.md +71 -0
  43. package/dist/docs/editor/how-to/use-flow-editor.md +66 -0
  44. package/dist/docs/editor/how-to/use-slash-commands.md +67 -0
  45. package/dist/docs/editor/reference/_category_.json +11 -0
  46. package/dist/docs/editor/reference/cli.md +61 -0
  47. package/dist/docs/editor/reference/supported-content.md +81 -0
  48. package/dist/docs/editor/reference/supported-resources.md +51 -0
  49. package/dist/docs/editor/reference/troubleshooting.md +76 -0
  50. package/dist/eventcatalog.cjs +1 -1
  51. package/dist/eventcatalog.config.d.cts +42 -0
  52. package/dist/eventcatalog.config.d.ts +42 -0
  53. package/dist/eventcatalog.js +5 -5
  54. package/dist/generate.cjs +1 -1
  55. package/dist/generate.js +3 -3
  56. package/dist/utils/cli-logger.cjs +1 -1
  57. package/dist/utils/cli-logger.js +2 -2
  58. package/eventcatalog/src/components/Badge.astro +50 -0
  59. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +1 -0
  60. package/eventcatalog/src/components/Tables/Discover/columns.tsx +35 -13
  61. package/eventcatalog/src/components/Tables/Table.tsx +1 -0
  62. package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +24 -11
  63. package/eventcatalog/src/content.config-shared-collections.ts +1 -0
  64. package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +6 -1
  65. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +2 -18
  66. package/eventcatalog/src/enterprise/feature.ts +2 -0
  67. package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +12 -0
  68. package/eventcatalog/src/enterprise/mcp/mcp-auth.ts +266 -0
  69. package/eventcatalog/src/enterprise/mcp/mcp-server.ts +13 -0
  70. package/eventcatalog/src/enterprise/mcp/oauth-protected-resource.ts +25 -0
  71. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +2 -14
  72. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +2 -14
  73. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +3 -15
  74. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +2 -18
  75. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +2 -18
  76. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +2 -11
  77. package/eventcatalog/src/utils/badge-styles.ts +31 -0
  78. package/eventcatalog/src/utils/feature.ts +1 -0
  79. package/package.json +4 -4
@@ -13,8 +13,8 @@ import {
13
13
  } from "./chunk-K3ZVEX2Y.js";
14
14
  import {
15
15
  log_build_default
16
- } from "./chunk-KVAEAYEP.js";
17
- import "./chunk-H5BZMNK3.js";
16
+ } from "./chunk-72BKUYSR.js";
17
+ import "./chunk-WFNAWDCB.js";
18
18
  import "./chunk-4UVFXLPI.js";
19
19
  import {
20
20
  catalogToAstro
@@ -28,13 +28,13 @@ import {
28
28
  } from "./chunk-ULZYHF3V.js";
29
29
  import {
30
30
  generate
31
- } from "./chunk-S4HLJWQ7.js";
31
+ } from "./chunk-K762FILQ.js";
32
32
  import {
33
33
  logger
34
- } from "./chunk-M4S7PORQ.js";
34
+ } from "./chunk-J5CG7FRO.js";
35
35
  import {
36
36
  VERSION
37
- } from "./chunk-TNE5QSJ4.js";
37
+ } from "./chunk-UPI6QQEZ.js";
38
38
  import {
39
39
  getEventCatalogConfigFile,
40
40
  verifyRequiredFieldsAreInCatalogConfigFile
package/dist/generate.cjs CHANGED
@@ -78,7 +78,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
78
78
  var import_picocolors = __toESM(require("picocolors"), 1);
79
79
 
80
80
  // package.json
81
- var version = "3.39.5";
81
+ var version = "3.40.0";
82
82
 
83
83
  // src/constants.ts
84
84
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-S4HLJWQ7.js";
4
- import "./chunk-M4S7PORQ.js";
5
- import "./chunk-TNE5QSJ4.js";
3
+ } from "./chunk-K762FILQ.js";
4
+ import "./chunk-J5CG7FRO.js";
5
+ import "./chunk-UPI6QQEZ.js";
6
6
  import "./chunk-5T63CXKU.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "3.39.5";
39
+ var version = "3.40.0";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-M4S7PORQ.js";
4
- import "../chunk-TNE5QSJ4.js";
3
+ } from "../chunk-J5CG7FRO.js";
4
+ import "../chunk-UPI6QQEZ.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -0,0 +1,50 @@
1
+ ---
2
+ import { getBadgeHref, getBadgeStyle } from '@utils/badge-styles';
3
+ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
4
+
5
+ type Badge = {
6
+ id?: string;
7
+ content: string;
8
+ backgroundColor?: string;
9
+ textColor?: string;
10
+ icon?: any;
11
+ iconComponent?: any;
12
+ iconURL?: string;
13
+ class?: string;
14
+ url?: string;
15
+ };
16
+
17
+ type Props = {
18
+ badge: Badge;
19
+ className?: string;
20
+ };
21
+
22
+ const { badge, className = '' } = Astro.props;
23
+ const href = getBadgeHref(badge);
24
+ const Icon = badge.icon || badge.iconComponent;
25
+ const classes = `
26
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
27
+ ${badge.backgroundColor ? 'bg-[rgb(var(--ec-content-hover))]' : 'bg-transparent'} border border-[rgb(var(--ec-page-border))]
28
+ text-[rgb(var(--ec-page-text))]
29
+ shadow-xs
30
+ ${badge.class ? badge.class : ''}
31
+ ${className}
32
+ `;
33
+ ---
34
+
35
+ {
36
+ href ? (
37
+ <a id={badge.id || undefined} href={href} class={classes} style={getBadgeStyle(badge)} title={badge.content}>
38
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
39
+ {badge.iconURL && <img src={badge.iconURL} class="w-4 h-4 flex-shrink-0 opacity-80" alt="" />}
40
+ <span>{badge.content}</span>
41
+ <ArrowTopRightOnSquareIcon className="w-3.5 h-3.5 flex-shrink-0 opacity-70" aria-hidden="true" />
42
+ </a>
43
+ ) : (
44
+ <span id={badge.id || undefined} class={classes} style={getBadgeStyle(badge)} title={badge.content}>
45
+ {Icon && <Icon className="w-4 h-4 flex-shrink-0" />}
46
+ {badge.iconURL && <img src={badge.iconURL} class="w-4 h-4 flex-shrink-0 opacity-80" alt="" />}
47
+ <span>{badge.content}</span>
48
+ </span>
49
+ )
50
+ }
@@ -52,6 +52,7 @@ export interface DiscoverTableData {
52
52
  content: string;
53
53
  backgroundColor?: string;
54
54
  textColor?: string;
55
+ url?: string;
55
56
  }>;
56
57
  producers?: Array<any>;
57
58
  consumers?: Array<any>;
@@ -1,11 +1,17 @@
1
1
  import { createColumnHelper } from '@tanstack/react-table';
2
2
  import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { DocumentTextIcon, MapIcon } from '@heroicons/react/24/solid';
4
- import { ArrowDownIcon, ArrowUpIcon, EllipsisVerticalIcon, StarIcon } from '@heroicons/react/24/outline';
4
+ import {
5
+ ArrowDownIcon,
6
+ ArrowTopRightOnSquareIcon,
7
+ ArrowUpIcon,
8
+ EllipsisVerticalIcon,
9
+ StarIcon,
10
+ } from '@heroicons/react/24/outline';
5
11
  import { buildUrl } from '@utils/url-builder';
6
12
  import { getColorAndIconForCollection } from '@utils/collections/icons';
7
13
  import { getCollectionTextColorClass } from '@utils/collection-colors';
8
- import { getBadgeReactStyle } from '@utils/badge-styles';
14
+ import { getBadgeHref, getBadgeReactStyle } from '@utils/badge-styles';
9
15
  import { isIconPath, resolveIconUrl } from '@utils/icon';
10
16
  import { useStore } from '@nanostores/react';
11
17
  import { favoritesStore, toggleFavorite, type FavoriteItem } from '../../../stores/favorites-store';
@@ -15,7 +21,11 @@ import type { TableConfiguration } from '@types';
15
21
  const columnHelper = createColumnHelper<DiscoverTableData>();
16
22
 
17
23
  // Badge cell component (proper React component to use hooks)
18
- const BadgesCell = ({ badges }: { badges: Array<{ content: string; backgroundColor?: string; textColor?: string }> }) => {
24
+ const BadgesCell = ({
25
+ badges,
26
+ }: {
27
+ badges: Array<{ content: string; backgroundColor?: string; textColor?: string; url?: string }>;
28
+ }) => {
19
29
  const [isExpanded, setIsExpanded] = useState(false);
20
30
 
21
31
  if (!badges || badges.length === 0) return <span className="text-xs text-[rgb(var(--ec-icon-color))]">-</span>;
@@ -25,16 +35,28 @@ const BadgesCell = ({ badges }: { badges: Array<{ content: string; backgroundCol
25
35
 
26
36
  return (
27
37
  <div className="flex flex-col gap-1 items-start">
28
- {visibleItems.map((badge, index) => (
29
- <span
30
- key={`${badge.content}-${index}`}
31
- className="inline-flex items-center px-2 py-0.5 text-[11px] font-normal rounded-md max-w-[140px] truncate border border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] bg-transparent"
32
- style={getBadgeReactStyle(badge)}
33
- title={badge.content}
34
- >
35
- {badge.content}
36
- </span>
37
- ))}
38
+ {visibleItems.map((badge, index) => {
39
+ const href = getBadgeHref(badge);
40
+ const className =
41
+ 'inline-flex items-center px-2 py-0.5 text-[11px] font-normal rounded-md max-w-[140px] truncate border border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] bg-transparent';
42
+
43
+ return href ? (
44
+ <a
45
+ key={`${badge.content}-${index}`}
46
+ href={href}
47
+ className={className}
48
+ style={getBadgeReactStyle(badge)}
49
+ title={badge.content}
50
+ >
51
+ <span className="truncate">{badge.content}</span>
52
+ <ArrowTopRightOnSquareIcon className="ml-1 h-3 w-3 shrink-0 opacity-70" aria-hidden="true" />
53
+ </a>
54
+ ) : (
55
+ <span key={`${badge.content}-${index}`} className={className} style={getBadgeReactStyle(badge)} title={badge.content}>
56
+ {badge.content}
57
+ </span>
58
+ );
59
+ })}
38
60
  {hiddenCount > 0 && (
39
61
  <button onClick={() => setIsExpanded(!isExpanded)} className="text-xs text-[rgb(var(--ec-accent))] hover:underline">
40
62
  {isExpanded ? 'less' : `+${hiddenCount}`}
@@ -58,6 +58,7 @@ export type TData<T extends TCollectionTypes> = {
58
58
  backgroundColor: string;
59
59
  textColor: string;
60
60
  icon: any; // Where is it defined?
61
+ url?: string;
61
62
  }>;
62
63
  // ---------------------------------------------------------------------------
63
64
  // Domains
@@ -1,6 +1,7 @@
1
1
  import { createColumnHelper } from '@tanstack/react-table';
2
2
  import { useState } from 'react';
3
- import { getBadgeReactStyle } from '@utils/badge-styles';
3
+ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid';
4
+ import { getBadgeHref, getBadgeReactStyle } from '@utils/badge-styles';
4
5
  import { filterByBadge } from '../filters/custom-filters';
5
6
  import type { TCollectionTypes, TData } from '../Table';
6
7
  import type { TableConfiguration } from '@types';
@@ -24,16 +25,28 @@ export const createBadgesColumn = <T extends { data: Pick<TData<U>['data'], 'bad
24
25
 
25
26
  return (
26
27
  <div className="flex flex-wrap gap-1 items-center">
27
- {visibleItems.map((badge, index) => (
28
- <span
29
- key={`${badge.id}-${index}`}
30
- className="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-md border border-[rgb(var(--ec-accent)/0.5)] text-[rgb(var(--ec-page-text))] bg-transparent"
31
- style={getBadgeReactStyle(badge)}
32
- title={badge.content}
33
- >
34
- {badge.content}
35
- </span>
36
- ))}
28
+ {visibleItems.map((badge, index) => {
29
+ const href = getBadgeHref(badge);
30
+ const className =
31
+ 'inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-md border border-[rgb(var(--ec-accent)/0.5)] text-[rgb(var(--ec-page-text))] bg-transparent';
32
+
33
+ return href ? (
34
+ <a
35
+ key={`${badge.id}-${index}`}
36
+ href={href}
37
+ className={className}
38
+ style={getBadgeReactStyle(badge)}
39
+ title={badge.content}
40
+ >
41
+ <span>{badge.content}</span>
42
+ <ArrowTopRightOnSquareIcon className="ml-1 h-3 w-3 shrink-0 opacity-70" aria-hidden="true" />
43
+ </a>
44
+ ) : (
45
+ <span key={`${badge.id}-${index}`} className={className} style={getBadgeReactStyle(badge)} title={badge.content}>
46
+ {badge.content}
47
+ </span>
48
+ );
49
+ })}
37
50
  {hiddenCount > 0 && (
38
51
  <button onClick={() => setIsExpanded(!isExpanded)} className="text-xs text-[rgb(var(--ec-accent))] hover:underline">
39
52
  {isExpanded ? 'less' : `+${hiddenCount}`}
@@ -6,6 +6,7 @@ export const badge = z.object({
6
6
  backgroundColor: z.string(),
7
7
  textColor: z.string(),
8
8
  icon: z.string().optional(),
9
+ url: z.string().optional(),
9
10
  });
10
11
 
11
12
  // Create a union type for owners
@@ -6,7 +6,7 @@
6
6
  // src/middleware/auth.ts
7
7
  import type { MiddlewareHandler } from 'astro';
8
8
  import { getSession } from 'auth-astro/server';
9
- import { isAuthEnabled } from '@utils/feature';
9
+ import { isAuthEnabled, isEventCatalogMCPAuthEnabled } from '@utils/feature';
10
10
  import jwt from 'jsonwebtoken';
11
11
  import { isLLMSTxtEnabled } from '@utils/feature';
12
12
 
@@ -97,6 +97,10 @@ export function getPublicRoutes(isLLMSTextEnabled: boolean) {
97
97
  ];
98
98
  }
99
99
 
100
+ export function isMcpRoute(pathname: string) {
101
+ return pathname === '/docs/mcp' || pathname.startsWith('/docs/mcp/');
102
+ }
103
+
100
104
  export const authMiddleware: MiddlewareHandler = async (context, next) => {
101
105
  const { request, redirect, locals } = context;
102
106
  const url = new URL(request.url);
@@ -118,6 +122,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
118
122
 
119
123
  if (
120
124
  pathname.startsWith('/_') ||
125
+ (isEventCatalogMCPAuthEnabled() && isMcpRoute(pathname)) ||
121
126
  systemRoutes.some((route) => pathname.startsWith(route)) ||
122
127
  pathname.startsWith('/.well-known/') ||
123
128
  publicRoutes.some((route) => pathname.startsWith(route)) ||
@@ -5,6 +5,7 @@ import config from '@config';
5
5
  import { AlignLeftIcon, UserIcon, UsersIcon } from 'lucide-react';
6
6
 
7
7
  import mdxComponents from '@components/MDX/components';
8
+ import Badge from '@components/Badge.astro';
8
9
 
9
10
  import { getOwner } from '@utils/collections/owners';
10
11
  import { buildUrl, buildEditUrlForResource } from '@utils/url-builder';
@@ -117,24 +118,7 @@ const editUrl =
117
118
  badges && badges.length > 0 && (
118
119
  <div class="flex flex-wrap gap-3 py-2">
119
120
  {badges.map((badge: any) => {
120
- return (
121
- <a href={badge.url || '#'}>
122
- <span
123
- id={badge.id || ''}
124
- class={`
125
- inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
126
- bg-[rgb(var(--ec-content-hover))] border border-[rgb(var(--ec-page-border))]
127
- text-[rgb(var(--ec-page-text))]
128
- shadow-xs
129
- ${badge.class ? badge.class : ''}
130
- `}
131
- >
132
- {badge.icon && <badge.icon className="w-4 h-4 flex-shrink-0 text-[rgb(var(--ec-icon-color))]" />}
133
- {badge.iconURL && <img src={badge.iconURL} class="w-4 h-4 flex-shrink-0 opacity-80" alt="" />}
134
- <span>{badge.content}</span>
135
- </span>
136
- </a>
137
- );
121
+ return <Badge badge={badge} />;
138
122
  })}
139
123
  </div>
140
124
  )
@@ -66,6 +66,8 @@ export const isDiagramComparisonEnabled = () => isEventCatalogScaleEnabled();
66
66
 
67
67
  export const isEventCatalogMCPEnabled = () => isEventCatalogScaleEnabled() && isSSR();
68
68
 
69
+ export const isEventCatalogMCPAuthEnabled = () => isEventCatalogMCPEnabled() && (config?.mcp?.auth?.enabled ?? false);
70
+
69
71
  export const isIntegrationsEnabled = () => isEventCatalogScaleEnabled();
70
72
 
71
73
  export const isExportPDFEnabled = () => true;
@@ -12,6 +12,7 @@ import {
12
12
  isEventCatalogScaleEnabled,
13
13
  isEventCatalogStarterEnabled,
14
14
  isEventCatalogMCPEnabled,
15
+ isEventCatalogMCPAuthEnabled,
15
16
  isFullCatalogAPIEnabled,
16
17
  isDevMode,
17
18
  isIntegrationsEnabled,
@@ -72,6 +73,17 @@ export default function eventCatalogIntegration(): AstroIntegration {
72
73
  });
73
74
  }
74
75
 
76
+ if (isEventCatalogMCPAuthEnabled()) {
77
+ params.injectRoute({
78
+ pattern: '/.well-known/oauth-protected-resource',
79
+ entrypoint: path.join(catalogDirectory, 'src/enterprise/mcp/oauth-protected-resource.ts'),
80
+ });
81
+ params.injectRoute({
82
+ pattern: '/.well-known/oauth-protected-resource/[...path]',
83
+ entrypoint: path.join(catalogDirectory, 'src/enterprise/mcp/oauth-protected-resource.ts'),
84
+ });
85
+ }
86
+
75
87
  // Handle routes for authentication
76
88
  if (isAuthEnabled()) {
77
89
  configureAuthentication(params);
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Licensed under the EventCatalog Commercial License.
3
+ * See /packages/core/eventcatalog/src/enterprise/LICENSE
4
+ */
5
+
6
+ import jwt, { type Algorithm, type JwtPayload } from 'jsonwebtoken';
7
+ import { createPublicKey, type JsonWebKey, type KeyObject } from 'node:crypto';
8
+ import config from '../../../eventcatalog.config.js';
9
+ import type { Config } from '../../../../src/eventcatalog.config';
10
+
11
+ type ConfiguredMcpAuth = NonNullable<NonNullable<Config['mcp']>['auth']>;
12
+
13
+ export type McpAuthConfig = ConfiguredMcpAuth;
14
+
15
+ type TokenClaims = JwtPayload & {
16
+ scope?: string;
17
+ scp?: string[];
18
+ };
19
+
20
+ type JwksKey = JsonWebKey & {
21
+ kid?: string;
22
+ };
23
+
24
+ type AuthFailure = {
25
+ ok: false;
26
+ status: 401 | 403;
27
+ error: string;
28
+ description: string;
29
+ requiredScopes: string[];
30
+ metadataUrl: string;
31
+ };
32
+
33
+ export type McpAuthResult =
34
+ | {
35
+ ok: true;
36
+ claims?: TokenClaims;
37
+ }
38
+ | AuthFailure;
39
+
40
+ const jwksCache = new Map<string, { expiresAt: number; keys: JwksKey[] }>();
41
+
42
+ const quote = (value: string) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
43
+
44
+ export const getMcpAuthConfig = (): McpAuthConfig | undefined => config?.mcp?.auth;
45
+
46
+ export const isMcpAuthEnabled = (
47
+ authConfig: McpAuthConfig | undefined = getMcpAuthConfig()
48
+ ): authConfig is McpAuthConfig & { enabled: true } => authConfig?.enabled === true;
49
+
50
+ export function getMcpResourceUrl(request: Request, authConfig: McpAuthConfig | undefined = getMcpAuthConfig()) {
51
+ if (authConfig?.resource) return authConfig.resource;
52
+ return new URL('/docs/mcp', request.url).href;
53
+ }
54
+
55
+ export function getMcpProtectedResourceMetadataUrl(request: Request, authConfig: McpAuthConfig | undefined = getMcpAuthConfig()) {
56
+ if (authConfig?.protectedResourceMetadataUrl) return authConfig.protectedResourceMetadataUrl;
57
+ return new URL('/.well-known/oauth-protected-resource', request.url).href;
58
+ }
59
+
60
+ export function getMcpRequiredScopes(authConfig: McpAuthConfig | undefined = getMcpAuthConfig()) {
61
+ return authConfig?.requiredScopes ?? [];
62
+ }
63
+
64
+ export function getMcpProtectedResourceMetadata(request: Request, authConfig: McpAuthConfig | undefined = getMcpAuthConfig()) {
65
+ if (!isMcpAuthEnabled(authConfig)) return undefined;
66
+
67
+ return {
68
+ resource: getMcpResourceUrl(request, authConfig),
69
+ authorization_servers: authConfig?.authorizationServers ?? [],
70
+ scopes_supported: getMcpRequiredScopes(authConfig),
71
+ };
72
+ }
73
+
74
+ export function createWwwAuthenticateHeader(failure: AuthFailure) {
75
+ const params = [
76
+ `realm=${quote('mcp')}`,
77
+ `resource_metadata=${quote(failure.metadataUrl)}`,
78
+ failure.requiredScopes.length > 0 ? `scope=${quote(failure.requiredScopes.join(' '))}` : undefined,
79
+ failure.error ? `error=${quote(failure.error)}` : undefined,
80
+ failure.description ? `error_description=${quote(failure.description)}` : undefined,
81
+ ].filter(Boolean);
82
+
83
+ return `Bearer ${params.join(', ')}`;
84
+ }
85
+
86
+ export function createMcpAuthErrorResponse(failure: AuthFailure) {
87
+ return new Response(JSON.stringify({ error: failure.error, message: failure.description }), {
88
+ status: failure.status,
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'WWW-Authenticate': createWwwAuthenticateHeader(failure),
92
+ },
93
+ });
94
+ }
95
+
96
+ export async function validateMcpRequest(
97
+ request: Request,
98
+ authConfig: McpAuthConfig | undefined = getMcpAuthConfig()
99
+ ): Promise<McpAuthResult> {
100
+ if (!isMcpAuthEnabled(authConfig)) return { ok: true };
101
+
102
+ const metadataUrl = getMcpProtectedResourceMetadataUrl(request, authConfig);
103
+ const requiredScopes = getMcpRequiredScopes(authConfig);
104
+ const authHeader = request.headers.get('Authorization');
105
+ const bearerMatch = authHeader?.match(/^Bearer\s+(.*)$/i);
106
+
107
+ if (!bearerMatch) {
108
+ return {
109
+ ok: false,
110
+ status: 401,
111
+ error: 'invalid_token',
112
+ description: 'Missing Bearer access token',
113
+ requiredScopes,
114
+ metadataUrl,
115
+ };
116
+ }
117
+
118
+ const token = bearerMatch[1].trim();
119
+
120
+ if (!token) {
121
+ return {
122
+ ok: false,
123
+ status: 401,
124
+ error: 'invalid_token',
125
+ description: 'Missing Bearer access token',
126
+ requiredScopes,
127
+ metadataUrl,
128
+ };
129
+ }
130
+
131
+ try {
132
+ const claims = await verifyAccessToken(token, request, authConfig);
133
+ const missingScopes = getMissingScopes(claims, requiredScopes);
134
+
135
+ if (missingScopes.length > 0) {
136
+ return {
137
+ ok: false,
138
+ status: 403,
139
+ error: 'insufficient_scope',
140
+ description: `Missing required scope${missingScopes.length > 1 ? 's' : ''}: ${missingScopes.join(' ')}`,
141
+ requiredScopes,
142
+ metadataUrl,
143
+ };
144
+ }
145
+
146
+ return { ok: true, claims };
147
+ } catch {
148
+ return {
149
+ ok: false,
150
+ status: 401,
151
+ error: 'invalid_token',
152
+ description: 'Invalid or expired Bearer access token',
153
+ requiredScopes,
154
+ metadataUrl,
155
+ };
156
+ }
157
+ }
158
+
159
+ async function verifyAccessToken(token: string, request: Request, authConfig: McpAuthConfig): Promise<TokenClaims> {
160
+ const decoded = jwt.decode(token, { complete: true });
161
+ const algorithm = decoded && typeof decoded === 'object' ? (decoded.header.alg as Algorithm | undefined) : undefined;
162
+
163
+ if (!algorithm || algorithm === 'none') {
164
+ throw new Error('Unsupported JWT algorithm');
165
+ }
166
+
167
+ const key = await getVerificationKey(token, authConfig);
168
+ const audience = getJwtAudience(authConfig.audience ?? getMcpResourceUrl(request, authConfig));
169
+
170
+ const payload = jwt.verify(token, key, {
171
+ audience,
172
+ issuer: authConfig.issuer,
173
+ algorithms: [algorithm],
174
+ clockTolerance: 5,
175
+ });
176
+
177
+ if (!payload || typeof payload === 'string') {
178
+ throw new Error('Invalid JWT payload');
179
+ }
180
+
181
+ return payload as TokenClaims;
182
+ }
183
+
184
+ async function getVerificationKey(token: string, authConfig: McpAuthConfig): Promise<string | Buffer | KeyObject> {
185
+ const sharedSecret = getConfiguredValue(authConfig.sharedSecret, authConfig.sharedSecretEnvVar);
186
+ if (sharedSecret) return sharedSecret;
187
+
188
+ const publicKey = getConfiguredValue(authConfig.publicKey, authConfig.publicKeyEnvVar);
189
+ if (publicKey) return publicKey;
190
+
191
+ if (authConfig.jwksUri) {
192
+ return getJwksVerificationKey(token, authConfig.jwksUri);
193
+ }
194
+
195
+ throw new Error('MCP auth requires jwksUri, publicKey, publicKeyEnvVar, sharedSecret, or sharedSecretEnvVar');
196
+ }
197
+
198
+ function getConfiguredValue(value: string | undefined, envVar: string | undefined) {
199
+ if (value) return value;
200
+ if (envVar) return process.env[envVar];
201
+ return undefined;
202
+ }
203
+
204
+ async function getJwksVerificationKey(token: string, jwksUri: string) {
205
+ const decoded = jwt.decode(token, { complete: true });
206
+ const kid = decoded && typeof decoded === 'object' ? decoded.header.kid : undefined;
207
+ const keys = await getJwksKeys(jwksUri);
208
+ const jwk = keys.find((key) => (kid ? key.kid === kid : keys.length === 1));
209
+
210
+ if (!jwk) {
211
+ throw new Error('No matching JWKS key found for token');
212
+ }
213
+
214
+ return createPublicKey({ key: jwk, format: 'jwk' });
215
+ }
216
+
217
+ function getJwtAudience(audience: string | string[]) {
218
+ if (Array.isArray(audience)) {
219
+ if (audience.length === 0) return undefined;
220
+ return audience as [string, ...string[]];
221
+ }
222
+
223
+ return audience;
224
+ }
225
+
226
+ async function getJwksKeys(jwksUri: string): Promise<JwksKey[]> {
227
+ const cached = jwksCache.get(jwksUri);
228
+ if (cached && cached.expiresAt > Date.now()) {
229
+ return cached.keys;
230
+ }
231
+
232
+ const response = await fetch(jwksUri, {
233
+ headers: {
234
+ Accept: 'application/json',
235
+ },
236
+ });
237
+
238
+ if (!response.ok) {
239
+ throw new Error(`Failed to fetch JWKS: ${response.status}`);
240
+ }
241
+
242
+ const body = (await response.json()) as { keys?: JwksKey[] };
243
+ const keys = body.keys ?? [];
244
+ jwksCache.set(jwksUri, { keys, expiresAt: Date.now() + 5 * 60 * 1000 });
245
+ return keys;
246
+ }
247
+
248
+ function getMissingScopes(claims: TokenClaims, requiredScopes: string[]) {
249
+ if (requiredScopes.length === 0) return [];
250
+
251
+ const scopes = new Set<string>();
252
+
253
+ if (typeof claims.scope === 'string') {
254
+ for (const scope of claims.scope.split(/\s+/)) {
255
+ if (scope) scopes.add(scope);
256
+ }
257
+ }
258
+
259
+ if (Array.isArray(claims.scp)) {
260
+ for (const scope of claims.scp) {
261
+ scopes.add(scope);
262
+ }
263
+ }
264
+
265
+ return requiredScopes.filter((scope) => !scopes.has(scope));
266
+ }
@@ -32,6 +32,7 @@ import {
32
32
  toolDescriptions,
33
33
  } from '@enterprise/tools/catalog-tools';
34
34
  import { getCollection } from 'astro:content';
35
+ import { createMcpAuthErrorResponse, validateMcpRequest } from './mcp-auth';
35
36
 
36
37
  const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
37
38
 
@@ -462,6 +463,12 @@ const mcpResources = [
462
463
 
463
464
  // Health check endpoint
464
465
  app.get('/', async (c: Context) => {
466
+ const auth = await validateMcpRequest(c.req.raw);
467
+
468
+ if (!auth.ok) {
469
+ return createMcpAuthErrorResponse(auth);
470
+ }
471
+
465
472
  return c.json({
466
473
  name: 'EventCatalog MCP Server',
467
474
  version: '1.0.0',
@@ -475,6 +482,12 @@ app.get('/', async (c: Context) => {
475
482
  // MCP protocol endpoint - handles POST requests for MCP protocol
476
483
  app.post('/', async (c: Context) => {
477
484
  try {
485
+ const auth = await validateMcpRequest(c.req.raw);
486
+
487
+ if (!auth.ok) {
488
+ return createMcpAuthErrorResponse(auth);
489
+ }
490
+
478
491
  // Create fresh server and transport per request — the MCP SDK's
479
492
  // WebStandardStreamableHTTPServerTransport is single-use in stateless
480
493
  // mode: it sets _hasHandledRequest=true after the first call and throws