@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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-KVAEAYEP.js → chunk-72BKUYSR.js} +1 -1
- package/dist/{chunk-M4S7PORQ.js → chunk-J5CG7FRO.js} +1 -1
- package/dist/{chunk-S4HLJWQ7.js → chunk-K762FILQ.js} +1 -1
- package/dist/{chunk-TNE5QSJ4.js → chunk-UPI6QQEZ.js} +1 -1
- package/dist/{chunk-H5BZMNK3.js → chunk-WFNAWDCB.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/api/03-domain-api.md +16 -0
- package/dist/docs/api/04-service-api.md +16 -0
- package/dist/docs/api/05-command-api.md +16 -0
- package/dist/docs/api/06-event-api.md +16 -0
- package/dist/docs/api/06-query-api.md +16 -0
- package/dist/docs/api/08-channel-api.md +16 -0
- package/dist/docs/api/09-flow-api.md +16 -0
- package/dist/docs/api/10-entity-api.md +15 -0
- package/dist/docs/api/12-data-product-api.md +17 -0
- package/dist/docs/development/01-fundamentals.md +7 -0
- package/dist/docs/development/01-getting-started/installation.md +8 -0
- package/dist/docs/development/01-getting-started/project-structure.md +2 -0
- package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +72 -0
- package/dist/docs/development/bring-your-own-documentation/01-introduction.md +3 -1
- package/dist/docs/development/customization/customize-sidebars/00-application-sidebar.md +45 -5
- package/dist/docs/editor/00-overview.md +73 -0
- package/dist/docs/editor/01-first-edit.md +124 -0
- package/dist/docs/editor/_category_.json +12 -0
- package/dist/docs/editor/explanation/_category_.json +11 -0
- package/dist/docs/editor/explanation/beta-status-feedback.md +48 -0
- package/dist/docs/editor/explanation/how-it-works.md +49 -0
- package/dist/docs/editor/explanation/markdown-mdx-git.md +58 -0
- package/dist/docs/editor/how-to/_category_.json +11 -0
- package/dist/docs/editor/how-to/add-schemas-and-specifications.md +66 -0
- package/dist/docs/editor/how-to/edit-resource.md +88 -0
- package/dist/docs/editor/how-to/invite-editors.md +68 -0
- package/dist/docs/editor/how-to/open-catalog.md +44 -0
- package/dist/docs/editor/how-to/preview-changes.md +55 -0
- package/dist/docs/editor/how-to/revert-local-changes.md +43 -0
- package/dist/docs/editor/how-to/review-and-commit-changes.md +57 -0
- package/dist/docs/editor/how-to/run-locally.md +71 -0
- package/dist/docs/editor/how-to/use-flow-editor.md +66 -0
- package/dist/docs/editor/how-to/use-slash-commands.md +67 -0
- package/dist/docs/editor/reference/_category_.json +11 -0
- package/dist/docs/editor/reference/cli.md +61 -0
- package/dist/docs/editor/reference/supported-content.md +81 -0
- package/dist/docs/editor/reference/supported-resources.md +51 -0
- package/dist/docs/editor/reference/troubleshooting.md +76 -0
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.config.d.cts +42 -0
- package/dist/eventcatalog.config.d.ts +42 -0
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/Badge.astro +50 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +1 -0
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +35 -13
- package/eventcatalog/src/components/Tables/Table.tsx +1 -0
- package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +24 -11
- package/eventcatalog/src/content.config-shared-collections.ts +1 -0
- package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +6 -1
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +2 -18
- package/eventcatalog/src/enterprise/feature.ts +2 -0
- package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +12 -0
- package/eventcatalog/src/enterprise/mcp/mcp-auth.ts +266 -0
- package/eventcatalog/src/enterprise/mcp/mcp-server.ts +13 -0
- package/eventcatalog/src/enterprise/mcp/oauth-protected-resource.ts +25 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +2 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +2 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +3 -15
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +2 -18
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +2 -18
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +2 -11
- package/eventcatalog/src/utils/badge-styles.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +1 -0
- package/package.json +4 -4
package/dist/eventcatalog.js
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
} from "./chunk-K3ZVEX2Y.js";
|
|
14
14
|
import {
|
|
15
15
|
log_build_default
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
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-
|
|
31
|
+
} from "./chunk-K762FILQ.js";
|
|
32
32
|
import {
|
|
33
33
|
logger
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-J5CG7FRO.js";
|
|
35
35
|
import {
|
|
36
36
|
VERSION
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-UPI6QQEZ.js";
|
|
38
38
|
import {
|
|
39
39
|
getEventCatalogConfigFile,
|
|
40
40
|
verifyRequiredFieldsAreInCatalogConfigFile
|
package/dist/generate.cjs
CHANGED
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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 {
|
|
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 = ({
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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}`}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createColumnHelper } from '@tanstack/react-table';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,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
|