@eventcatalog/core 3.39.6 → 3.40.1
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 +38 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-ORVOST63.js → chunk-4OEF5W6Y.js} +1 -1
- package/dist/{chunk-MQAZ4LXP.js → chunk-7UR72UMK.js} +40 -3
- package/dist/{chunk-LEUIMTEQ.js → chunk-BRMLU4PR.js} +1 -1
- package/dist/{chunk-IKZ5ITXP.js → chunk-HNG4KOYQ.js} +1 -1
- package/dist/{chunk-4OSFLWLG.js → chunk-OIVICT4V.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +72 -0
- package/dist/eventcatalog.cjs +38 -1
- package/dist/eventcatalog.config.d.cts +53 -0
- package/dist/eventcatalog.config.d.ts +53 -0
- package/dist/eventcatalog.js +10 -10
- 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/enterprise/analytics/components/AnalyticsHead.astro +78 -1
- package/eventcatalog/src/enterprise/analytics/components/AnalyticsTracker.astro +3 -1
- package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +6 -1
- 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/utils/feature.ts +1 -0
- package/package.json +3 -3
|
@@ -111,7 +111,7 @@ var import_axios = __toESM(require("axios"), 1);
|
|
|
111
111
|
var import_os = __toESM(require("os"), 1);
|
|
112
112
|
|
|
113
113
|
// package.json
|
|
114
|
-
var version = "3.
|
|
114
|
+
var version = "3.40.1";
|
|
115
115
|
|
|
116
116
|
// src/constants.ts
|
|
117
117
|
var VERSION = version;
|
|
@@ -187,6 +187,42 @@ var getFeatures = async (configFile) => {
|
|
|
187
187
|
output: configFile.output || "static"
|
|
188
188
|
};
|
|
189
189
|
};
|
|
190
|
+
var CLOUD_ANALYTICS_ENDPOINT = "https://api.ecingest.dev/v1/analytics/ingest";
|
|
191
|
+
var toCloudResourceCounts = (counts) => ({
|
|
192
|
+
domains: counts.domains || 0,
|
|
193
|
+
services: counts.services || 0,
|
|
194
|
+
events: counts.events || 0,
|
|
195
|
+
commands: counts.commands || 0,
|
|
196
|
+
queries: counts.queries || 0,
|
|
197
|
+
flows: counts.flows || 0,
|
|
198
|
+
channels: counts.channels || 0,
|
|
199
|
+
entities: counts.entities || 0,
|
|
200
|
+
containers: counts.containers || 0,
|
|
201
|
+
dataProducts: counts["data-products"] || 0,
|
|
202
|
+
teams: counts.teams || 0,
|
|
203
|
+
users: counts.users || 0,
|
|
204
|
+
designs: counts.designs || 0,
|
|
205
|
+
diagrams: counts.diagrams || 0,
|
|
206
|
+
ubiquitousLanguages: counts.ubiquitousLanguages || 0
|
|
207
|
+
});
|
|
208
|
+
var reportCloudResourceInventory = async (configFile, resourceCounts) => {
|
|
209
|
+
const analytics = configFile.cloud?.analytics;
|
|
210
|
+
if (!analytics?.enabled || !analytics.trackingId || !analytics.writeKey) return;
|
|
211
|
+
const endpoint = analytics.endpoint || CLOUD_ANALYTICS_ENDPOINT;
|
|
212
|
+
await fetch(endpoint, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: {
|
|
215
|
+
"Content-Type": "application/json",
|
|
216
|
+
"X-EventCatalog-Analytics-Key": analytics.writeKey
|
|
217
|
+
},
|
|
218
|
+
body: JSON.stringify({
|
|
219
|
+
trackingId: analytics.trackingId,
|
|
220
|
+
event: "catalog.resource_inventory_reported",
|
|
221
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
|
+
counts: toCloudResourceCounts(resourceCounts)
|
|
223
|
+
})
|
|
224
|
+
});
|
|
225
|
+
};
|
|
190
226
|
var main = async (projectDir, { isEventCatalogStarterEnabled, isEventCatalogScaleEnabled, isBackstagePluginEnabled }) => {
|
|
191
227
|
if (process.env.NODE_ENV === "CI") return;
|
|
192
228
|
try {
|
|
@@ -205,6 +241,7 @@ var main = async (projectDir, { isEventCatalogStarterEnabled, isEventCatalogScal
|
|
|
205
241
|
}
|
|
206
242
|
const features = await getFeatures(configFile);
|
|
207
243
|
const resourceCounts = await countResources(projectDir);
|
|
244
|
+
await reportCloudResourceInventory(configFile, resourceCounts);
|
|
208
245
|
await raiseEvent({
|
|
209
246
|
command: "build",
|
|
210
247
|
org: organizationName,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-ORVOST63.js";
|
|
3
|
+
} from "../chunk-7UR72UMK.js";
|
|
5
4
|
import "../chunk-4UVFXLPI.js";
|
|
6
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-4OEF5W6Y.js";
|
|
6
|
+
import "../chunk-HNG4KOYQ.js";
|
|
7
7
|
import "../chunk-5T63CXKU.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
raiseEvent
|
|
3
|
-
} from "./chunk-ORVOST63.js";
|
|
4
1
|
import {
|
|
5
2
|
countResources,
|
|
6
3
|
serializeCounts
|
|
7
4
|
} from "./chunk-4UVFXLPI.js";
|
|
5
|
+
import {
|
|
6
|
+
raiseEvent
|
|
7
|
+
} from "./chunk-4OEF5W6Y.js";
|
|
8
8
|
import {
|
|
9
9
|
getEventCatalogConfigFile,
|
|
10
10
|
verifyRequiredFieldsAreInCatalogConfigFile
|
|
@@ -19,6 +19,42 @@ var getFeatures = async (configFile) => {
|
|
|
19
19
|
output: configFile.output || "static"
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
var CLOUD_ANALYTICS_ENDPOINT = "https://api.ecingest.dev/v1/analytics/ingest";
|
|
23
|
+
var toCloudResourceCounts = (counts) => ({
|
|
24
|
+
domains: counts.domains || 0,
|
|
25
|
+
services: counts.services || 0,
|
|
26
|
+
events: counts.events || 0,
|
|
27
|
+
commands: counts.commands || 0,
|
|
28
|
+
queries: counts.queries || 0,
|
|
29
|
+
flows: counts.flows || 0,
|
|
30
|
+
channels: counts.channels || 0,
|
|
31
|
+
entities: counts.entities || 0,
|
|
32
|
+
containers: counts.containers || 0,
|
|
33
|
+
dataProducts: counts["data-products"] || 0,
|
|
34
|
+
teams: counts.teams || 0,
|
|
35
|
+
users: counts.users || 0,
|
|
36
|
+
designs: counts.designs || 0,
|
|
37
|
+
diagrams: counts.diagrams || 0,
|
|
38
|
+
ubiquitousLanguages: counts.ubiquitousLanguages || 0
|
|
39
|
+
});
|
|
40
|
+
var reportCloudResourceInventory = async (configFile, resourceCounts) => {
|
|
41
|
+
const analytics = configFile.cloud?.analytics;
|
|
42
|
+
if (!analytics?.enabled || !analytics.trackingId || !analytics.writeKey) return;
|
|
43
|
+
const endpoint = analytics.endpoint || CLOUD_ANALYTICS_ENDPOINT;
|
|
44
|
+
await fetch(endpoint, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"X-EventCatalog-Analytics-Key": analytics.writeKey
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
trackingId: analytics.trackingId,
|
|
52
|
+
event: "catalog.resource_inventory_reported",
|
|
53
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54
|
+
counts: toCloudResourceCounts(resourceCounts)
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
};
|
|
22
58
|
var main = async (projectDir, { isEventCatalogStarterEnabled, isEventCatalogScaleEnabled, isBackstagePluginEnabled }) => {
|
|
23
59
|
if (process.env.NODE_ENV === "CI") return;
|
|
24
60
|
try {
|
|
@@ -37,6 +73,7 @@ var main = async (projectDir, { isEventCatalogStarterEnabled, isEventCatalogScal
|
|
|
37
73
|
}
|
|
38
74
|
const features = await getFeatures(configFile);
|
|
39
75
|
const resourceCounts = await countResources(projectDir);
|
|
76
|
+
await reportCloudResourceInventory(configFile, resourceCounts);
|
|
40
77
|
await raiseEvent({
|
|
41
78
|
command: "build",
|
|
42
79
|
org: organizationName,
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
|
@@ -45,6 +45,78 @@ Visit the endpoint in your browser to verify. It returns available tools and res
|
|
|
45
45
|
}
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
### Protect with OAuth
|
|
49
|
+
|
|
50
|
+
<AddedIn version="3.40.0" />
|
|
51
|
+
|
|
52
|
+
The built-in MCP server can be protected with OAuth Bearer tokens, following the MCP authorization specification for HTTP transports.
|
|
53
|
+
|
|
54
|
+
EventCatalog acts as the OAuth protected resource server for `/docs/mcp`. Your identity provider or authorization server remains responsible for user login, consent, client registration, `/authorize`, `/oauth/token`, and token refresh.
|
|
55
|
+
|
|
56
|
+
Configure MCP authorization in `eventcatalog.config.js`:
|
|
57
|
+
|
|
58
|
+
```js title="eventcatalog.config.js"
|
|
59
|
+
module.exports = {
|
|
60
|
+
output: 'server',
|
|
61
|
+
mcp: {
|
|
62
|
+
auth: {
|
|
63
|
+
enabled: true,
|
|
64
|
+
resource: 'https://your-eventcatalog.com/docs/mcp',
|
|
65
|
+
authorizationServers: ['https://auth.example.com'],
|
|
66
|
+
issuer: 'https://auth.example.com',
|
|
67
|
+
audience: 'https://your-eventcatalog.com/docs/mcp',
|
|
68
|
+
requiredScopes: ['catalog:read'],
|
|
69
|
+
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
When enabled, EventCatalog serves protected resource metadata at `/.well-known/oauth-protected-resource`. Unauthenticated MCP clients receive a `401 Unauthorized` response with a `WWW-Authenticate` header pointing at that document. MCP clients then obtain an access token from the advertised authorization server and call `/docs/mcp` with:
|
|
76
|
+
|
|
77
|
+
```http
|
|
78
|
+
Authorization: Bearer <access-token>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The access token must be valid, unexpired, issued by the configured issuer, intended for the configured audience, and include all required scopes.
|
|
82
|
+
|
|
83
|
+
#### Key signing options
|
|
84
|
+
|
|
85
|
+
Choose one of the following strategies for token validation:
|
|
86
|
+
|
|
87
|
+
| Strategy | Config fields |
|
|
88
|
+
|---|---|
|
|
89
|
+
| JWKS endpoint (recommended) | `jwksUri` |
|
|
90
|
+
| Inline asymmetric public key | `publicKey` or `publicKeyEnvVar` |
|
|
91
|
+
| Symmetric shared secret | `sharedSecret` or `sharedSecretEnvVar` |
|
|
92
|
+
|
|
93
|
+
Prefer `publicKeyEnvVar` or `sharedSecretEnvVar` over inline values to avoid committing secrets to source control.
|
|
94
|
+
|
|
95
|
+
#### All options
|
|
96
|
+
|
|
97
|
+
| Field | Required | Description |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| `enabled` | Yes | Enables OAuth Bearer token validation |
|
|
100
|
+
| `resource` | No | Absolute URL of the MCP resource. Set this explicitly when behind a proxy |
|
|
101
|
+
| `protectedResourceMetadataUrl` | No | URL for the protected resource metadata document. Defaults to `/.well-known/oauth-protected-resource` |
|
|
102
|
+
| `authorizationServers` | No | Authorization server URLs advertised to MCP clients |
|
|
103
|
+
| `issuer` | No | Expected token issuer (`iss` claim) |
|
|
104
|
+
| `audience` | No | Expected token audience (`aud` claim). Defaults to `resource` |
|
|
105
|
+
| `requiredScopes` | No | Scopes every token must include |
|
|
106
|
+
| `jwksUri` | No | JWKS endpoint for asymmetric JWT validation |
|
|
107
|
+
| `publicKey` | No | Inline public key for asymmetric JWT validation |
|
|
108
|
+
| `publicKeyEnvVar` | No | Environment variable containing the public key |
|
|
109
|
+
| `sharedSecret` | No | Inline shared secret for symmetric JWT validation |
|
|
110
|
+
| `sharedSecretEnvVar` | No | Environment variable containing the shared secret |
|
|
111
|
+
|
|
112
|
+
:::note Existing website authentication
|
|
113
|
+
The `auth.enabled` and `eventcatalog.auth.js` settings protect the EventCatalog website with browser sessions. MCP authorization is separate because MCP clients authenticate with Bearer tokens, not browser cookies.
|
|
114
|
+
:::
|
|
115
|
+
|
|
116
|
+
:::tip Authorization server discovery
|
|
117
|
+
EventCatalog serves `/.well-known/oauth-protected-resource` for MCP client discovery. It does not serve `/.well-known/oauth-authorization-server`, `/authorize`, or `/oauth/token` -- those endpoints must be provided by the authorization server listed in `authorizationServers`. If your MCP client expects those endpoints on the catalog host, proxy the authorization server behind that host with your load balancer or reverse proxy.
|
|
118
|
+
:::
|
|
119
|
+
|
|
48
120
|
### Connect clients
|
|
49
121
|
|
|
50
122
|
<details>
|
package/dist/eventcatalog.cjs
CHANGED
|
@@ -114,7 +114,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
114
114
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
115
115
|
|
|
116
116
|
// package.json
|
|
117
|
-
var version = "3.
|
|
117
|
+
var version = "3.40.1";
|
|
118
118
|
|
|
119
119
|
// src/constants.ts
|
|
120
120
|
var VERSION = version;
|
|
@@ -282,6 +282,42 @@ var getFeatures = async (configFile) => {
|
|
|
282
282
|
output: configFile.output || "static"
|
|
283
283
|
};
|
|
284
284
|
};
|
|
285
|
+
var CLOUD_ANALYTICS_ENDPOINT = "https://api.ecingest.dev/v1/analytics/ingest";
|
|
286
|
+
var toCloudResourceCounts = (counts) => ({
|
|
287
|
+
domains: counts.domains || 0,
|
|
288
|
+
services: counts.services || 0,
|
|
289
|
+
events: counts.events || 0,
|
|
290
|
+
commands: counts.commands || 0,
|
|
291
|
+
queries: counts.queries || 0,
|
|
292
|
+
flows: counts.flows || 0,
|
|
293
|
+
channels: counts.channels || 0,
|
|
294
|
+
entities: counts.entities || 0,
|
|
295
|
+
containers: counts.containers || 0,
|
|
296
|
+
dataProducts: counts["data-products"] || 0,
|
|
297
|
+
teams: counts.teams || 0,
|
|
298
|
+
users: counts.users || 0,
|
|
299
|
+
designs: counts.designs || 0,
|
|
300
|
+
diagrams: counts.diagrams || 0,
|
|
301
|
+
ubiquitousLanguages: counts.ubiquitousLanguages || 0
|
|
302
|
+
});
|
|
303
|
+
var reportCloudResourceInventory = async (configFile, resourceCounts) => {
|
|
304
|
+
const analytics = configFile.cloud?.analytics;
|
|
305
|
+
if (!analytics?.enabled || !analytics.trackingId || !analytics.writeKey) return;
|
|
306
|
+
const endpoint = analytics.endpoint || CLOUD_ANALYTICS_ENDPOINT;
|
|
307
|
+
await fetch(endpoint, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: {
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
"X-EventCatalog-Analytics-Key": analytics.writeKey
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
trackingId: analytics.trackingId,
|
|
315
|
+
event: "catalog.resource_inventory_reported",
|
|
316
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
317
|
+
counts: toCloudResourceCounts(resourceCounts)
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
};
|
|
285
321
|
var main = async (projectDir, { isEventCatalogStarterEnabled: isEventCatalogStarterEnabled2, isEventCatalogScaleEnabled: isEventCatalogScaleEnabled2, isBackstagePluginEnabled }) => {
|
|
286
322
|
if (process.env.NODE_ENV === "CI") return;
|
|
287
323
|
try {
|
|
@@ -300,6 +336,7 @@ var main = async (projectDir, { isEventCatalogStarterEnabled: isEventCatalogStar
|
|
|
300
336
|
}
|
|
301
337
|
const features = await getFeatures(configFile);
|
|
302
338
|
const resourceCounts = await countResources(projectDir);
|
|
339
|
+
await reportCloudResourceInventory(configFile, resourceCounts);
|
|
303
340
|
await raiseEvent({
|
|
304
341
|
command: "build",
|
|
305
342
|
org: organizationName,
|
|
@@ -47,6 +47,47 @@ type PagesConfiguration = {
|
|
|
47
47
|
type AuthConfig = {
|
|
48
48
|
enabled: boolean;
|
|
49
49
|
};
|
|
50
|
+
type McpAuthConfig = {
|
|
51
|
+
/**
|
|
52
|
+
* Require OAuth Bearer tokens for the built-in MCP server.
|
|
53
|
+
* EventCatalog acts as the MCP protected resource server; the
|
|
54
|
+
* configured authorization server remains responsible for login,
|
|
55
|
+
* consent, token issuance, and client registration.
|
|
56
|
+
*/
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Absolute URL for the MCP resource. Defaults to the request origin
|
|
60
|
+
* plus `/docs/mcp`, but production deployments behind proxies should
|
|
61
|
+
* set this explicitly.
|
|
62
|
+
*/
|
|
63
|
+
resource?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Optional absolute URL for the OAuth Protected Resource Metadata
|
|
66
|
+
* document. Defaults to `/.well-known/oauth-protected-resource`.
|
|
67
|
+
*/
|
|
68
|
+
protectedResourceMetadataUrl?: string;
|
|
69
|
+
/** Authorization server issuer/base URLs advertised to MCP clients. */
|
|
70
|
+
authorizationServers?: string[];
|
|
71
|
+
/** Expected token issuer (`iss`). */
|
|
72
|
+
issuer?: string;
|
|
73
|
+
/** Expected token audience (`aud`). Defaults to `resource`. */
|
|
74
|
+
audience?: string | string[];
|
|
75
|
+
/** Scopes required to call the MCP server. */
|
|
76
|
+
requiredScopes?: string[];
|
|
77
|
+
/** JWKS endpoint used to validate asymmetric JWT access tokens. */
|
|
78
|
+
jwksUri?: string;
|
|
79
|
+
/** Inline public key for asymmetric JWT validation. */
|
|
80
|
+
publicKey?: string;
|
|
81
|
+
/** Environment variable containing the public key. */
|
|
82
|
+
publicKeyEnvVar?: string;
|
|
83
|
+
/** Inline shared secret for symmetric JWT validation. Prefer `sharedSecretEnvVar`. */
|
|
84
|
+
sharedSecret?: string;
|
|
85
|
+
/** Environment variable containing the shared secret. */
|
|
86
|
+
sharedSecretEnvVar?: string;
|
|
87
|
+
};
|
|
88
|
+
type McpConfig = {
|
|
89
|
+
auth?: McpAuthConfig;
|
|
90
|
+
};
|
|
50
91
|
type GA4Config = {
|
|
51
92
|
measurementId: string;
|
|
52
93
|
};
|
|
@@ -57,6 +98,16 @@ type PostHogConfig = {
|
|
|
57
98
|
apiKey: string;
|
|
58
99
|
apiHost?: string;
|
|
59
100
|
};
|
|
101
|
+
type EventCatalogCloudAnalyticsConfig = {
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
trackingId: string;
|
|
104
|
+
writeKey?: string;
|
|
105
|
+
endpoint?: string;
|
|
106
|
+
debug?: boolean;
|
|
107
|
+
};
|
|
108
|
+
type EventCatalogCloudConfig = {
|
|
109
|
+
analytics?: EventCatalogCloudAnalyticsConfig;
|
|
110
|
+
};
|
|
60
111
|
type IntegrationsConfig = {
|
|
61
112
|
ga4?: GA4Config;
|
|
62
113
|
gtm?: GTMConfig;
|
|
@@ -90,6 +141,7 @@ interface Config {
|
|
|
90
141
|
*/
|
|
91
142
|
theme?: CatalogTheme;
|
|
92
143
|
auth?: AuthConfig;
|
|
144
|
+
mcp?: McpConfig;
|
|
93
145
|
rss?: {
|
|
94
146
|
enabled: boolean;
|
|
95
147
|
limit: number;
|
|
@@ -201,6 +253,7 @@ interface Config {
|
|
|
201
253
|
queries?: {
|
|
202
254
|
tableConfiguration?: TableConfiguration;
|
|
203
255
|
};
|
|
256
|
+
cloud?: EventCatalogCloudConfig;
|
|
204
257
|
integrations?: IntegrationsConfig;
|
|
205
258
|
scalarConfiguration?: ScalarConfiguration;
|
|
206
259
|
}
|
|
@@ -47,6 +47,47 @@ type PagesConfiguration = {
|
|
|
47
47
|
type AuthConfig = {
|
|
48
48
|
enabled: boolean;
|
|
49
49
|
};
|
|
50
|
+
type McpAuthConfig = {
|
|
51
|
+
/**
|
|
52
|
+
* Require OAuth Bearer tokens for the built-in MCP server.
|
|
53
|
+
* EventCatalog acts as the MCP protected resource server; the
|
|
54
|
+
* configured authorization server remains responsible for login,
|
|
55
|
+
* consent, token issuance, and client registration.
|
|
56
|
+
*/
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Absolute URL for the MCP resource. Defaults to the request origin
|
|
60
|
+
* plus `/docs/mcp`, but production deployments behind proxies should
|
|
61
|
+
* set this explicitly.
|
|
62
|
+
*/
|
|
63
|
+
resource?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Optional absolute URL for the OAuth Protected Resource Metadata
|
|
66
|
+
* document. Defaults to `/.well-known/oauth-protected-resource`.
|
|
67
|
+
*/
|
|
68
|
+
protectedResourceMetadataUrl?: string;
|
|
69
|
+
/** Authorization server issuer/base URLs advertised to MCP clients. */
|
|
70
|
+
authorizationServers?: string[];
|
|
71
|
+
/** Expected token issuer (`iss`). */
|
|
72
|
+
issuer?: string;
|
|
73
|
+
/** Expected token audience (`aud`). Defaults to `resource`. */
|
|
74
|
+
audience?: string | string[];
|
|
75
|
+
/** Scopes required to call the MCP server. */
|
|
76
|
+
requiredScopes?: string[];
|
|
77
|
+
/** JWKS endpoint used to validate asymmetric JWT access tokens. */
|
|
78
|
+
jwksUri?: string;
|
|
79
|
+
/** Inline public key for asymmetric JWT validation. */
|
|
80
|
+
publicKey?: string;
|
|
81
|
+
/** Environment variable containing the public key. */
|
|
82
|
+
publicKeyEnvVar?: string;
|
|
83
|
+
/** Inline shared secret for symmetric JWT validation. Prefer `sharedSecretEnvVar`. */
|
|
84
|
+
sharedSecret?: string;
|
|
85
|
+
/** Environment variable containing the shared secret. */
|
|
86
|
+
sharedSecretEnvVar?: string;
|
|
87
|
+
};
|
|
88
|
+
type McpConfig = {
|
|
89
|
+
auth?: McpAuthConfig;
|
|
90
|
+
};
|
|
50
91
|
type GA4Config = {
|
|
51
92
|
measurementId: string;
|
|
52
93
|
};
|
|
@@ -57,6 +98,16 @@ type PostHogConfig = {
|
|
|
57
98
|
apiKey: string;
|
|
58
99
|
apiHost?: string;
|
|
59
100
|
};
|
|
101
|
+
type EventCatalogCloudAnalyticsConfig = {
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
trackingId: string;
|
|
104
|
+
writeKey?: string;
|
|
105
|
+
endpoint?: string;
|
|
106
|
+
debug?: boolean;
|
|
107
|
+
};
|
|
108
|
+
type EventCatalogCloudConfig = {
|
|
109
|
+
analytics?: EventCatalogCloudAnalyticsConfig;
|
|
110
|
+
};
|
|
60
111
|
type IntegrationsConfig = {
|
|
61
112
|
ga4?: GA4Config;
|
|
62
113
|
gtm?: GTMConfig;
|
|
@@ -90,6 +141,7 @@ interface Config {
|
|
|
90
141
|
*/
|
|
91
142
|
theme?: CatalogTheme;
|
|
92
143
|
auth?: AuthConfig;
|
|
144
|
+
mcp?: McpConfig;
|
|
93
145
|
rss?: {
|
|
94
146
|
enabled: boolean;
|
|
95
147
|
limit: number;
|
|
@@ -201,6 +253,7 @@ interface Config {
|
|
|
201
253
|
queries?: {
|
|
202
254
|
tableConfiguration?: TableConfiguration;
|
|
203
255
|
};
|
|
256
|
+
cloud?: EventCatalogCloudConfig;
|
|
204
257
|
integrations?: IntegrationsConfig;
|
|
205
258
|
scalarConfiguration?: ScalarConfiguration;
|
|
206
259
|
}
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
2
|
+
log_build_default
|
|
3
|
+
} from "./chunk-7UR72UMK.js";
|
|
4
|
+
import "./chunk-4UVFXLPI.js";
|
|
5
5
|
import {
|
|
6
6
|
resolve_catalog_dependencies_default
|
|
7
7
|
} from "./chunk-WAJIJEI3.js";
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
watch
|
|
13
13
|
} from "./chunk-K3ZVEX2Y.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
15
|
+
runMigrations
|
|
16
|
+
} from "./chunk-XUAF2H54.js";
|
|
17
|
+
import "./chunk-CA4U2JP7.js";
|
|
18
|
+
import "./chunk-4OEF5W6Y.js";
|
|
19
19
|
import {
|
|
20
20
|
catalogToAstro
|
|
21
21
|
} from "./chunk-YDXB3BD2.js";
|
|
@@ -28,13 +28,13 @@ import {
|
|
|
28
28
|
} from "./chunk-ULZYHF3V.js";
|
|
29
29
|
import {
|
|
30
30
|
generate
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-BRMLU4PR.js";
|
|
32
32
|
import {
|
|
33
33
|
logger
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-OIVICT4V.js";
|
|
35
35
|
import {
|
|
36
36
|
VERSION
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-HNG4KOYQ.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-BRMLU4PR.js";
|
|
4
|
+
import "./chunk-OIVICT4V.js";
|
|
5
|
+
import "./chunk-HNG4KOYQ.js";
|
|
6
6
|
import "./chunk-5T63CXKU.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -3,11 +3,15 @@ import config from '@config';
|
|
|
3
3
|
import { isIntegrationsEnabled } from '@utils/feature';
|
|
4
4
|
|
|
5
5
|
const integrations = config.integrations;
|
|
6
|
+
const cloudAnalytics = config.cloud?.analytics;
|
|
6
7
|
const enabled = isIntegrationsEnabled() && integrations;
|
|
7
8
|
const hasProviders = enabled && (integrations?.ga4 || integrations?.gtm || integrations?.posthog);
|
|
8
9
|
const debug = integrations?.debug ?? false;
|
|
10
|
+
const cloudDebug = cloudAnalytics?.debug ?? debug;
|
|
11
|
+
const cloudAnalyticsEndpoint = cloudAnalytics?.endpoint || 'https://api.ecingest.dev/v1/analytics/ingest';
|
|
12
|
+
const isCloudAnalyticsEnabled = cloudAnalytics?.enabled === true && !!cloudAnalytics?.trackingId;
|
|
9
13
|
// Render the manager if there are providers OR if debug mode is on (so users can verify the pipeline)
|
|
10
|
-
const shouldRender = enabled && (hasProviders || debug);
|
|
14
|
+
const shouldRender = isCloudAnalyticsEnabled || (enabled && (hasProviders || debug));
|
|
11
15
|
|
|
12
16
|
// Build script contents in frontmatter to avoid Prettier/JSX parsing issues with braces in inline scripts
|
|
13
17
|
const ga4ConfigScript = integrations?.ga4
|
|
@@ -35,6 +39,7 @@ posthog.init(${posthogApiKey}, {api_host: ${posthogApiHost}, person_profiles: 'i
|
|
|
35
39
|
const hasGA4 = !!integrations?.ga4;
|
|
36
40
|
const hasGTM = !!integrations?.gtm;
|
|
37
41
|
const hasPostHog = !!integrations?.posthog;
|
|
42
|
+
const hasCloudAnalytics = !!isCloudAnalyticsEnabled;
|
|
38
43
|
|
|
39
44
|
const managerScript = `(function() {
|
|
40
45
|
function AnalyticsManager(opts) {
|
|
@@ -57,6 +62,28 @@ AnalyticsManager.prototype.pageView = function(url, props) {
|
|
|
57
62
|
try { this.adapters[i].pageView(url, props); } catch(e) {}
|
|
58
63
|
}
|
|
59
64
|
};
|
|
65
|
+
function createId(prefix) {
|
|
66
|
+
var bytes = new Uint8Array(16);
|
|
67
|
+
if (window.crypto && window.crypto.getRandomValues) {
|
|
68
|
+
window.crypto.getRandomValues(bytes);
|
|
69
|
+
} else {
|
|
70
|
+
for (var i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
71
|
+
}
|
|
72
|
+
return prefix + '_' + Array.prototype.map.call(bytes, function(byte) {
|
|
73
|
+
return byte.toString(16).padStart(2, '0');
|
|
74
|
+
}).join('');
|
|
75
|
+
}
|
|
76
|
+
function getStoredId(storage, key, prefix) {
|
|
77
|
+
try {
|
|
78
|
+
var existing = storage.getItem(key);
|
|
79
|
+
if (existing) return existing;
|
|
80
|
+
var id = createId(prefix);
|
|
81
|
+
storage.setItem(key, id);
|
|
82
|
+
return id;
|
|
83
|
+
} catch(e) {
|
|
84
|
+
return createId(prefix);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
60
87
|
var manager = new AnalyticsManager({ debug: ${debug} });
|
|
61
88
|
${
|
|
62
89
|
hasGA4
|
|
@@ -85,6 +112,56 @@ ${
|
|
|
85
112
|
});`
|
|
86
113
|
: ''
|
|
87
114
|
}
|
|
115
|
+
${
|
|
116
|
+
hasCloudAnalytics
|
|
117
|
+
? `var eventCatalogCloudLastPath;
|
|
118
|
+
manager.register({
|
|
119
|
+
name: 'eventcatalog-cloud',
|
|
120
|
+
track: function(event, props) {
|
|
121
|
+
if (event !== 'catalog.page_viewed' && event !== 'catalog.built' && event !== 'catalog.resource_inventory_reported') return;
|
|
122
|
+
|
|
123
|
+
var payload = Object.assign({
|
|
124
|
+
trackingId: ${JSON.stringify(cloudAnalytics?.trackingId || '')},
|
|
125
|
+
event: event,
|
|
126
|
+
anonymousId: getStoredId(window.localStorage, 'eventcatalog-cloud-anonymous-id', 'anon'),
|
|
127
|
+
sessionId: getStoredId(window.sessionStorage, 'eventcatalog-cloud-session-id', 'sess'),
|
|
128
|
+
timestamp: new Date().toISOString()
|
|
129
|
+
}, props || {});
|
|
130
|
+
|
|
131
|
+
var body = JSON.stringify(payload);
|
|
132
|
+
if (${cloudDebug}) console.log('[EventCatalog Cloud Analytics] track: ' + event, payload);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
fetch(${JSON.stringify(cloudAnalyticsEndpoint)}, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: body,
|
|
139
|
+
keepalive: true,
|
|
140
|
+
credentials: 'omit'
|
|
141
|
+
}).catch(function() {});
|
|
142
|
+
} catch(e) {}
|
|
143
|
+
},
|
|
144
|
+
pageView: function(url, props) {
|
|
145
|
+
props = props || {};
|
|
146
|
+
var path = props.url || url || window.location.pathname;
|
|
147
|
+
var payload = {
|
|
148
|
+
path: path,
|
|
149
|
+
section: props.section || undefined,
|
|
150
|
+
referrer: eventCatalogCloudLastPath || document.referrer || undefined
|
|
151
|
+
};
|
|
152
|
+
if (props.resource_type && props.resource_id) {
|
|
153
|
+
payload.resource = {
|
|
154
|
+
type: props.resource_type,
|
|
155
|
+
id: props.resource_id,
|
|
156
|
+
version: props.resource_version || undefined
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
eventCatalogCloudLastPath = path;
|
|
160
|
+
this.track('catalog.page_viewed', payload);
|
|
161
|
+
}
|
|
162
|
+
});`
|
|
163
|
+
: ''
|
|
164
|
+
}
|
|
88
165
|
window.__ec_analytics = manager;
|
|
89
166
|
})();`;
|
|
90
167
|
---
|
|
@@ -3,10 +3,12 @@ import config from '@config';
|
|
|
3
3
|
import { isIntegrationsEnabled } from '@utils/feature';
|
|
4
4
|
|
|
5
5
|
const integrations = config.integrations;
|
|
6
|
+
const cloudAnalytics = config.cloud?.analytics;
|
|
6
7
|
const enabled = isIntegrationsEnabled() && integrations;
|
|
7
8
|
const hasProviders = enabled && (integrations?.ga4 || integrations?.gtm || integrations?.posthog);
|
|
8
9
|
const debug = integrations?.debug ?? false;
|
|
9
|
-
const
|
|
10
|
+
const isCloudAnalyticsEnabled = cloudAnalytics?.enabled === true && !!cloudAnalytics?.trackingId;
|
|
11
|
+
const shouldRender = isCloudAnalyticsEnabled || (enabled && (hasProviders || debug));
|
|
10
12
|
|
|
11
13
|
const base = (config.base || '/').replace(/\/$/, '');
|
|
12
14
|
const baseJson = JSON.stringify(base);
|
|
@@ -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)) ||
|
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { APIRoute } from 'astro';
|
|
7
|
+
import { getMcpProtectedResourceMetadata } from './mcp-auth';
|
|
8
|
+
|
|
9
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
10
|
+
const metadata = getMcpProtectedResourceMetadata(request);
|
|
11
|
+
|
|
12
|
+
if (!metadata) {
|
|
13
|
+
return new Response(JSON.stringify({ error: 'mcp_auth_not_configured' }), {
|
|
14
|
+
status: 404,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Response(JSON.stringify(metadata, null, 2), {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const prerender = false;
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "3.
|
|
10
|
+
"version": "3.40.1",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"uuid": "^10.0.0",
|
|
108
108
|
"zod": "^4.3.6",
|
|
109
109
|
"@eventcatalog/sdk": "2.21.2",
|
|
110
|
-
"@eventcatalog/
|
|
111
|
-
"@eventcatalog/
|
|
110
|
+
"@eventcatalog/visualiser": "^3.21.0",
|
|
111
|
+
"@eventcatalog/linter": "1.0.24"
|
|
112
112
|
},
|
|
113
113
|
"devDependencies": {
|
|
114
114
|
"@astrojs/check": "^0.9.9",
|