arc-1 0.9.25 → 1.0.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/README.md +29 -12
- package/dist/adt/abapgit.d.ts +19 -6
- package/dist/adt/abapgit.d.ts.map +1 -1
- package/dist/adt/abapgit.js +141 -36
- package/dist/adt/abapgit.js.map +1 -1
- package/dist/adt/authorization-trace.d.ts +23 -0
- package/dist/adt/authorization-trace.d.ts.map +1 -0
- package/dist/adt/authorization-trace.js +196 -0
- package/dist/adt/authorization-trace.js.map +1 -0
- package/dist/adt/client.d.ts +21 -8
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +47 -7
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/config.d.ts +15 -0
- package/dist/adt/config.d.ts.map +1 -1
- package/dist/adt/config.js.map +1 -1
- package/dist/adt/crud.d.ts +7 -2
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +12 -4
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/ddic-xml.d.ts +27 -17
- package/dist/adt/ddic-xml.d.ts.map +1 -1
- package/dist/adt/ddic-xml.js +45 -34
- package/dist/adt/ddic-xml.js.map +1 -1
- package/dist/adt/devtools.d.ts +13 -0
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +120 -50
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +1 -1
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +1 -0
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +8 -8
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +30 -11
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts +17 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +102 -19
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/oauth.d.ts +2 -2
- package/dist/adt/oauth.d.ts.map +1 -1
- package/dist/adt/oauth.js +5 -3
- package/dist/adt/oauth.js.map +1 -1
- package/dist/adt/package-hierarchy.d.ts.map +1 -1
- package/dist/adt/package-hierarchy.js.map +1 -1
- package/dist/adt/rap-generate.js.map +1 -1
- package/dist/adt/rap-handlers.js.map +1 -1
- package/dist/adt/rap-preflight.js.map +1 -1
- package/dist/adt/safety.d.ts +13 -13
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/semaphore.d.ts.map +1 -1
- package/dist/adt/server-driven.d.ts +154 -34
- package/dist/adt/server-driven.d.ts.map +1 -1
- package/dist/adt/server-driven.js +178 -53
- package/dist/adt/server-driven.js.map +1 -1
- package/dist/adt/structure-hierarchy.js.map +1 -1
- package/dist/adt/transport.d.ts +6 -6
- package/dist/adt/transport.d.ts.map +1 -1
- package/dist/adt/transport.js +23 -34
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +94 -6
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/version-diff.js.map +1 -1
- package/dist/adt/xml-parser.d.ts +52 -12
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +120 -10
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/aff/validator.js.map +1 -1
- package/dist/authz/policy.d.ts +8 -0
- package/dist/authz/policy.d.ts.map +1 -1
- package/dist/authz/policy.js +24 -0
- package/dist/authz/policy.js.map +1 -1
- package/dist/cache/cache.d.ts +4 -36
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +4 -5
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/caching-layer.d.ts +6 -22
- package/dist/cache/caching-layer.d.ts.map +1 -1
- package/dist/cache/caching-layer.js +5 -29
- package/dist/cache/caching-layer.js.map +1 -1
- package/dist/cache/inactive-list-cache.d.ts.map +1 -1
- package/dist/cache/memory.d.ts +2 -13
- package/dist/cache/memory.d.ts.map +1 -1
- package/dist/cache/memory.js +1 -70
- package/dist/cache/memory.js.map +1 -1
- package/dist/cache/sqlite.d.ts +3 -10
- package/dist/cache/sqlite.d.ts.map +1 -1
- package/dist/cache/sqlite.js +9 -85
- package/dist/cache/sqlite.js.map +1 -1
- package/dist/cli.js +6 -3
- package/dist/cli.js.map +1 -1
- package/dist/context/compressor.js.map +1 -1
- package/dist/context/grep.js.map +1 -1
- package/dist/context/unit-surgery.d.ts +28 -0
- package/dist/context/unit-surgery.d.ts.map +1 -0
- package/dist/context/unit-surgery.js +150 -0
- package/dist/context/unit-surgery.js.map +1 -0
- package/dist/extract-sap-cookies.js.map +1 -1
- package/dist/handlers/activate.d.ts.map +1 -1
- package/dist/handlers/activate.js +44 -6
- package/dist/handlers/activate.js.map +1 -1
- package/dist/handlers/context.d.ts +1 -1
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/context.js +98 -28
- package/dist/handlers/context.js.map +1 -1
- package/dist/handlers/diagnose.d.ts.map +1 -1
- package/dist/handlers/diagnose.js +74 -31
- package/dist/handlers/diagnose.js.map +1 -1
- package/dist/handlers/dispatch.d.ts +12 -1
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +160 -25
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/feature-cache.d.ts +27 -14
- package/dist/handlers/feature-cache.d.ts.map +1 -1
- package/dist/handlers/feature-cache.js +63 -25
- package/dist/handlers/feature-cache.js.map +1 -1
- package/dist/handlers/function-processing.d.ts +61 -0
- package/dist/handlers/function-processing.d.ts.map +1 -0
- package/dist/handlers/function-processing.js +45 -0
- package/dist/handlers/function-processing.js.map +1 -0
- package/dist/handlers/git.d.ts.map +1 -1
- package/dist/handlers/git.js +44 -19
- package/dist/handlers/git.js.map +1 -1
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +2 -3
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/lint.d.ts.map +1 -1
- package/dist/handlers/lint.js +7 -7
- package/dist/handlers/lint.js.map +1 -1
- package/dist/handlers/manage.d.ts.map +1 -1
- package/dist/handlers/manage.js +47 -28
- package/dist/handlers/manage.js.map +1 -1
- package/dist/handlers/navigate.d.ts.map +1 -1
- package/dist/handlers/navigate.js +27 -90
- package/dist/handlers/navigate.js.map +1 -1
- package/dist/handlers/object-types.d.ts +2 -0
- package/dist/handlers/object-types.d.ts.map +1 -1
- package/dist/handlers/object-types.js +10 -0
- package/dist/handlers/object-types.js.map +1 -1
- package/dist/handlers/query-errors.d.ts +11 -0
- package/dist/handlers/query-errors.d.ts.map +1 -0
- package/dist/handlers/query-errors.js +216 -0
- package/dist/handlers/query-errors.js.map +1 -0
- package/dist/handlers/query.d.ts.map +1 -1
- package/dist/handlers/query.js +62 -67
- package/dist/handlers/query.js.map +1 -1
- package/dist/handlers/read.d.ts.map +1 -1
- package/dist/handlers/read.js +64 -41
- package/dist/handlers/read.js.map +1 -1
- package/dist/handlers/schemas.d.ts +285 -234
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +126 -32
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/search.d.ts.map +1 -1
- package/dist/handlers/search.js +8 -7
- package/dist/handlers/search.js.map +1 -1
- package/dist/handlers/shared.d.ts +13 -0
- package/dist/handlers/shared.d.ts.map +1 -1
- package/dist/handlers/shared.js +15 -0
- package/dist/handlers/shared.js.map +1 -1
- package/dist/handlers/tool-descriptions.d.ts +4 -0
- package/dist/handlers/tool-descriptions.d.ts.map +1 -0
- package/dist/handlers/tool-descriptions.js +52 -0
- package/dist/handlers/tool-descriptions.js.map +1 -0
- package/dist/handlers/tool-registry.d.ts +20 -6
- package/dist/handlers/tool-registry.d.ts.map +1 -1
- package/dist/handlers/tool-registry.js +18 -0
- package/dist/handlers/tool-registry.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +124 -92
- package/dist/handlers/tools.js.map +1 -1
- package/dist/handlers/transport.d.ts +1 -1
- package/dist/handlers/transport.d.ts.map +1 -1
- package/dist/handlers/transport.js +37 -10
- package/dist/handlers/transport.js.map +1 -1
- package/dist/handlers/where-used.d.ts +25 -0
- package/dist/handlers/where-used.d.ts.map +1 -0
- package/dist/handlers/where-used.js +94 -0
- package/dist/handlers/where-used.js.map +1 -0
- package/dist/handlers/write/class-surgery.d.ts.map +1 -1
- package/dist/handlers/write/class-surgery.js +11 -12
- package/dist/handlers/write/class-surgery.js.map +1 -1
- package/dist/handlers/write/create.d.ts.map +1 -1
- package/dist/handlers/write/create.js +194 -84
- package/dist/handlers/write/create.js.map +1 -1
- package/dist/handlers/write/rap.d.ts.map +1 -1
- package/dist/handlers/write/rap.js +9 -9
- package/dist/handlers/write/rap.js.map +1 -1
- package/dist/handlers/write/unit-surgery.d.ts +5 -0
- package/dist/handlers/write/unit-surgery.d.ts.map +1 -0
- package/dist/handlers/write/unit-surgery.js +47 -0
- package/dist/handlers/write/unit-surgery.js.map +1 -0
- package/dist/handlers/write/update-delete.js +8 -8
- package/dist/handlers/write/update-delete.js.map +1 -1
- package/dist/handlers/write-helpers.d.ts +7 -4
- package/dist/handlers/write-helpers.d.ts.map +1 -1
- package/dist/handlers/write-helpers.js +118 -63
- package/dist/handlers/write-helpers.js.map +1 -1
- package/dist/handlers/write.d.ts.map +1 -1
- package/dist/handlers/write.js +35 -13
- package/dist/handlers/write.js.map +1 -1
- package/dist/lint/config-builder.js.map +1 -1
- package/dist/lint/lint.d.ts.map +1 -1
- package/dist/lint/lint.js +4 -2
- package/dist/lint/lint.js.map +1 -1
- package/dist/probe/catalog.js +1 -1
- package/dist/probe/catalog.js.map +1 -1
- package/dist/public/testing.js.map +1 -1
- package/dist/public/ui/app.js +1 -14
- package/dist/registry/tool-registry.d.ts.map +1 -1
- package/dist/server/audit.d.ts +27 -15
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/auth-rate-limit.d.ts +18 -9
- package/dist/server/auth-rate-limit.d.ts.map +1 -1
- package/dist/server/auth-rate-limit.js +31 -6
- package/dist/server/auth-rate-limit.js.map +1 -1
- package/dist/server/auth-summary.d.ts +4 -0
- package/dist/server/auth-summary.d.ts.map +1 -0
- package/dist/server/auth-summary.js +67 -0
- package/dist/server/auth-summary.js.map +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +110 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/context.d.ts +12 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js.map +1 -1
- package/dist/server/destination-discovery.d.ts +36 -0
- package/dist/server/destination-discovery.d.ts.map +1 -0
- package/dist/server/destination-discovery.js +130 -0
- package/dist/server/destination-discovery.js.map +1 -0
- package/dist/server/destination-registry.d.ts +111 -0
- package/dist/server/destination-registry.d.ts.map +1 -0
- package/dist/server/destination-registry.js +449 -0
- package/dist/server/destination-registry.js.map +1 -0
- package/dist/server/http.d.ts +43 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +283 -73
- package/dist/server/http.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/logger.js +58 -13
- package/dist/server/logger.js.map +1 -1
- package/dist/server/mcp-rate-limit.d.ts +6 -4
- package/dist/server/mcp-rate-limit.d.ts.map +1 -1
- package/dist/server/mcp-rate-limit.js +6 -4
- package/dist/server/mcp-rate-limit.js.map +1 -1
- package/dist/server/multi-target-basic-auth.d.ts +49 -0
- package/dist/server/multi-target-basic-auth.d.ts.map +1 -0
- package/dist/server/multi-target-basic-auth.js +246 -0
- package/dist/server/multi-target-basic-auth.js.map +1 -0
- package/dist/server/multi-target-catalog.d.ts +33 -0
- package/dist/server/multi-target-catalog.d.ts.map +1 -0
- package/dist/server/multi-target-catalog.js +130 -0
- package/dist/server/multi-target-catalog.js.map +1 -0
- package/dist/server/multi-target-destination-config.d.ts +12 -0
- package/dist/server/multi-target-destination-config.d.ts.map +1 -0
- package/dist/server/multi-target-destination-config.js +37 -0
- package/dist/server/multi-target-destination-config.js.map +1 -0
- package/dist/server/multi-target-destination-runtime.d.ts +20 -0
- package/dist/server/multi-target-destination-runtime.d.ts.map +1 -0
- package/dist/server/multi-target-destination-runtime.js +191 -0
- package/dist/server/multi-target-destination-runtime.js.map +1 -0
- package/dist/server/multi-target-feature-state.d.ts +6 -0
- package/dist/server/multi-target-feature-state.d.ts.map +1 -0
- package/dist/server/multi-target-feature-state.js +41 -0
- package/dist/server/multi-target-feature-state.js.map +1 -0
- package/dist/server/multi-target-identity.d.ts +10 -0
- package/dist/server/multi-target-identity.d.ts.map +1 -0
- package/dist/server/multi-target-identity.js +18 -0
- package/dist/server/multi-target-identity.js.map +1 -0
- package/dist/server/multi-target-runtime.d.ts +27 -0
- package/dist/server/multi-target-runtime.d.ts.map +1 -0
- package/dist/server/multi-target-runtime.js +110 -0
- package/dist/server/multi-target-runtime.js.map +1 -0
- package/dist/server/multi-target-server.d.ts +53 -0
- package/dist/server/multi-target-server.d.ts.map +1 -0
- package/dist/server/multi-target-server.js +434 -0
- package/dist/server/multi-target-server.js.map +1 -0
- package/dist/server/multi-target-shared-auth-state.d.ts +82 -0
- package/dist/server/multi-target-shared-auth-state.d.ts.map +1 -0
- package/dist/server/multi-target-shared-auth-state.js +226 -0
- package/dist/server/multi-target-shared-auth-state.js.map +1 -0
- package/dist/server/multi-target-tools.d.ts +20 -0
- package/dist/server/multi-target-tools.d.ts.map +1 -0
- package/dist/server/multi-target-tools.js +191 -0
- package/dist/server/multi-target-tools.js.map +1 -0
- package/dist/server/oauth-session-recovery.d.ts +22 -0
- package/dist/server/oauth-session-recovery.d.ts.map +1 -0
- package/dist/server/oauth-session-recovery.js +69 -0
- package/dist/server/oauth-session-recovery.js.map +1 -0
- package/dist/server/server.d.ts +47 -25
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +490 -301
- package/dist/server/server.js.map +1 -1
- package/dist/server/sinks/btp-auditlog.d.ts.map +1 -1
- package/dist/server/sinks/btp-auditlog.js +83 -11
- package/dist/server/sinks/btp-auditlog.js.map +1 -1
- package/dist/server/sinks/file.d.ts.map +1 -1
- package/dist/server/sinks/file.js.map +1 -1
- package/dist/server/sinks/stderr.d.ts.map +1 -1
- package/dist/server/sinks/stderr.js.map +1 -1
- package/dist/server/tool-auth.d.ts +10 -0
- package/dist/server/tool-auth.d.ts.map +1 -0
- package/dist/server/tool-auth.js +75 -0
- package/dist/server/tool-auth.js.map +1 -0
- package/dist/server/trace-context.d.ts +51 -0
- package/dist/server/trace-context.d.ts.map +1 -0
- package/dist/server/trace-context.js +100 -0
- package/dist/server/trace-context.js.map +1 -0
- package/dist/server/types.d.ts +28 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +5 -2
- package/dist/server/types.js.map +1 -1
- package/dist/server/ui-log-buffer.d.ts.map +1 -1
- package/dist/server/ui-log-buffer.js.map +1 -1
- package/dist/server/ui-state.d.ts +0 -2
- package/dist/server/ui-state.d.ts.map +1 -1
- package/dist/server/ui-state.js +2 -6
- package/dist/server/ui-state.js.map +1 -1
- package/dist/server/ui.d.ts.map +1 -1
- package/dist/server/ui.js +8 -9
- package/dist/server/ui.js.map +1 -1
- package/package.json +21 -18
- package/dist/cache/warmup.d.ts +0 -41
- package/dist/cache/warmup.d.ts.map +0 -1
- package/dist/cache/warmup.js +0 -337
- package/dist/cache/warmup.js.map +0 -1
- package/dist/server/elicit.d.ts +0 -43
- package/dist/server/elicit.d.ts.map +0 -1
- package/dist/server/elicit.js +0 -183
- package/dist/server/elicit.js.map +0 -1
package/dist/server/http.d.ts
CHANGED
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { XsuaaCredentials } from '@arc-mcp/xsuaa-auth';
|
|
28
28
|
import type { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
|
|
29
|
+
import type { Request, Response } from 'express';
|
|
29
30
|
import express from 'express';
|
|
31
|
+
import type { DestinationRegistry, TargetDescriptor } from './destination-registry.js';
|
|
30
32
|
import type { ServerConfig } from './types.js';
|
|
31
33
|
import { type UiServerDeps } from './ui.js';
|
|
32
34
|
/**
|
|
@@ -53,10 +55,50 @@ import { type UiServerDeps } from './ui.js';
|
|
|
53
55
|
* Exported for unit tests; also called from `startHttpServer` below.
|
|
54
56
|
*/
|
|
55
57
|
export declare function applySecurityMiddleware(app: express.Application, allowedOrigins: string[]): void;
|
|
58
|
+
/**
|
|
59
|
+
* Create an Express handler that processes MCP requests.
|
|
60
|
+
* Each request gets a fresh Server + Transport pair.
|
|
61
|
+
* Exported for the era-contract regression tests (tests/unit/server/mcp-era-contract.test.ts).
|
|
62
|
+
*/
|
|
63
|
+
export declare function createMcpHandler(serverFactory: () => McpServer): (req: Request, res: Response) => Promise<void>;
|
|
64
|
+
export interface MultiTargetRouting {
|
|
65
|
+
registry: DestinationRegistry;
|
|
66
|
+
aggregateFactory: () => McpServer;
|
|
67
|
+
createPinnedServer: (target: TargetDescriptor) => McpServer;
|
|
68
|
+
}
|
|
69
|
+
export declare function createPinnedTargetMcpHandler(multi: MultiTargetRouting): (req: Request, res: Response) => Promise<void>;
|
|
70
|
+
export declare function createAggregateMcpHandler(multi: MultiTargetRouting): (req: Request, res: Response) => Promise<void>;
|
|
71
|
+
export declare function resolveMcpHttpRateLimit(config: Pick<ServerConfig, 'authRateLimit' | 'mcpHttpRateLimit'>): number;
|
|
72
|
+
export declare function multiTargetHealthStatus(registry: DestinationRegistry): 'ready' | 'error';
|
|
73
|
+
export declare const MULTI_TARGET_SCOPES_SUPPORTED: readonly string[];
|
|
56
74
|
/**
|
|
57
75
|
* Start the HTTP Streamable server.
|
|
58
76
|
*/
|
|
59
|
-
export declare function startHttpServer(serverFactory: () => McpServer, config: ServerConfig, xsuaaCredentials?: XsuaaCredentials, uiDeps?: UiServerDeps): Promise<void>;
|
|
77
|
+
export declare function startHttpServer(serverFactory: (() => McpServer) | undefined, config: ServerConfig, xsuaaCredentials?: XsuaaCredentials, uiDeps?: UiServerDeps, multiTargets?: MultiTargetRouting): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Build the RFC 9728 document ARC-1 publishes in OIDC mode.
|
|
80
|
+
*
|
|
81
|
+
* ARC-1 is only the resource server here: `authorization_servers` points at the
|
|
82
|
+
* external IdP, and clients discover its endpoints themselves (RFC 8414 with the
|
|
83
|
+
* OIDC-discovery fallbacks — Entra only answers the append form). `resource` is
|
|
84
|
+
* the canonical MCP endpoint URI; MCP clients reject a document whose `resource`
|
|
85
|
+
* is not same-origin with the server they called.
|
|
86
|
+
*
|
|
87
|
+
* `scopes_supported` is IdP-specific (`api://<client-id>/access_as_user` on Entra,
|
|
88
|
+
* arbitrary elsewhere) and cannot be derived from `SAP_OIDC_AUDIENCE` — it is
|
|
89
|
+
* omitted unless the admin sets `SAP_OIDC_SCOPES`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function buildOidcResourceMetadata(config: ServerConfig, publicBase: string): import('@modelcontextprotocol/sdk/shared/auth.js').OAuthProtectedResourceMetadata;
|
|
92
|
+
/**
|
|
93
|
+
* Mount the protected-resource metadata routes and return the URL that belongs in
|
|
94
|
+
* the `WWW-Authenticate` challenge. Caller gates on `oidcIssuer && oidcDiscovery`.
|
|
95
|
+
*
|
|
96
|
+
* URLs come from `getAppUrl()` (ARC1_PUBLIC_URL / VCAP), never from the request
|
|
97
|
+
* `Host` header, so a spoofed Host cannot redirect clients at an attacker's IdP.
|
|
98
|
+
* The SDK's `metadataHandler` supplies `cors()` + GET/OPTIONS-only, the same
|
|
99
|
+
* treatment the XSUAA branch's metadata gets — the document is public by design.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mountOidcResourceMetadata(app: express.Application, config: ServerConfig, bindHost: string, port: number): Promise<string>;
|
|
60
102
|
/**
|
|
61
103
|
* Create a token verifier for standard auth mode (API key + OIDC).
|
|
62
104
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,KAAK,EAAE,MAAM,IAAI,SAAS,EAAE,MAAM,2CAA2C,CAAC;AAGrF,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,OAAO,MAAM,SAAS,CAAC;AAK9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAevF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AA2B3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,IAAI,CAiEhG;AA4CD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,SAAS,SAC1C,OAAO,OAAO,QAAQ,mBAU1C;AAID,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gBAAgB,EAAE,MAAM,SAAS,CAAC;IAClC,kBAAkB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,SAAS,CAAC;CAC7D;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,kBAAkB,SACjD,OAAO,OAAO,QAAQ,mBAmB1C;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,kBAAkB,SAC9C,OAAO,OAAO,QAAQ,mBAM1C;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,eAAe,GAAG,kBAAkB,CAAC,GAAG,MAAM,CAMhH;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,GAAG,OAAO,CAIxF;AAED,eAAO,MAAM,6BAA6B,mBAAkD,CAAC;AAE7F;;GAEG;AACH,wBAAsB,eAAe,CACnC,aAAa,EAAE,CAAC,MAAM,SAAS,CAAC,GAAG,SAAS,EAC5C,MAAM,EAAE,YAAY,EACpB,gBAAgB,CAAC,EAAE,gBAAgB,EACnC,MAAM,CAAC,EAAE,YAAY,EACrB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,IAAI,CAAC,CAggBf;AAkDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,0CAA0C,EAAE,8BAA8B,CAQnF;AAED;;;;;;;;GAQG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,OAAO,CAAC,WAAW,EACxB,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,qBAAqB,EAAE,QAAQ,CAAC,CAOlH"}
|
package/dist/server/http.js
CHANGED
|
@@ -28,10 +28,14 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
28
28
|
import cors from 'cors';
|
|
29
29
|
import express from 'express';
|
|
30
30
|
import helmet from 'helmet';
|
|
31
|
-
import { expandScopes } from '../authz/policy.js';
|
|
31
|
+
import { expandScopes, hasRequiredScope } from '../authz/policy.js';
|
|
32
32
|
import { API_KEY_PROFILES } from './config.js';
|
|
33
|
+
import { generateRequestId, requestContext } from './context.js';
|
|
33
34
|
import { authLibLogger, logger } from './logger.js';
|
|
35
|
+
import { PINNED_MCP_PATH_PATTERN, PINNED_RESOURCE_METADATA_PATH_PATTERN, targetFromPinnedMcpPath, } from './multi-target-identity.js';
|
|
36
|
+
import { buildXsuaaSessionRefreshUrl, createOAuthLoggedOutHandler, OAUTH_LOGGED_OUT_PATH, withInvalidScopeSessionRecovery, } from './oauth-session-recovery.js';
|
|
34
37
|
import { VERSION } from './server.js';
|
|
38
|
+
import { sanitizeClientAgent, validateTraceparent, validateTracestate } from './trace-context.js';
|
|
35
39
|
import { mountUiRoutes } from './ui.js';
|
|
36
40
|
// ─── API Key Entry Mapping ───────────────────────────────────────────
|
|
37
41
|
/**
|
|
@@ -113,7 +117,11 @@ export function applySecurityMiddleware(app, allowedOrigins) {
|
|
|
113
117
|
callback(null, allowed.has(origin));
|
|
114
118
|
},
|
|
115
119
|
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
|
|
116
|
-
|
|
120
|
+
// mcp-protocol-version: sent by every SDK client on post-initialize requests (spec 2025-06-18+).
|
|
121
|
+
// last-event-id: sent on SSE reconnects (fetch-based, so preflighted). 2026-07-28 headers
|
|
122
|
+
// (Mcp-Method/Mcp-Name/Mcp-Param-*) deliberately absent — v2 clients default to legacy mode,
|
|
123
|
+
// and Mcp-Param-* is dynamically named (can't be allow-listed under credentials). ADR-0006.
|
|
124
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'mcp-session-id', 'mcp-protocol-version', 'last-event-id'],
|
|
117
125
|
exposedHeaders: ['mcp-session-id'],
|
|
118
126
|
credentials: true,
|
|
119
127
|
}));
|
|
@@ -138,11 +146,49 @@ export function applySecurityMiddleware(app, allowedOrigins) {
|
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
// ─── MCP Request Handler ─────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Serve one MCP request with a fresh Server + Transport pair.
|
|
151
|
+
*
|
|
152
|
+
* IMPORTANT: Passes req.body as pre-parsed body (3rd argument).
|
|
153
|
+
* express.json() middleware consumes the raw request stream. Without this,
|
|
154
|
+
* the MCP SDK's transport tries to re-read the stream, gets nothing, and
|
|
155
|
+
* returns "Parse error: Invalid JSON" (-32700). The SDK explicitly supports
|
|
156
|
+
* this pattern — see their docs/comments in
|
|
157
|
+
* StreamableHTTPServerTransport.handleRequest().
|
|
158
|
+
*/
|
|
159
|
+
async function serveMcpRequest(serverFactory, req, res) {
|
|
160
|
+
try {
|
|
161
|
+
const server = serverFactory();
|
|
162
|
+
const transport = new StreamableHTTPServerTransport({
|
|
163
|
+
sessionIdGenerator: undefined, // Stateless mode
|
|
164
|
+
});
|
|
165
|
+
await server.connect(transport);
|
|
166
|
+
// Seed the request context from the HTTP edge: trace context to forward to SAP, and the
|
|
167
|
+
// calling agent. handleToolCall opens its own nested context for the tool call and inherits
|
|
168
|
+
// both — but events emitted before it (auth, multi-target prep) are covered only here.
|
|
169
|
+
// `User-Agent` is the only agent signal available on this transport: stateless mode means the
|
|
170
|
+
// per-request Server never saw `initialize`, so `getClientVersion()` is always undefined.
|
|
171
|
+
const traceparent = validateTraceparent(req.headers.traceparent);
|
|
172
|
+
await requestContext.run({
|
|
173
|
+
requestId: generateRequestId(),
|
|
174
|
+
traceparent,
|
|
175
|
+
tracestate: validateTracestate(req.headers.tracestate, traceparent),
|
|
176
|
+
clientAgent: sanitizeClientAgent(req.headers['user-agent']),
|
|
177
|
+
}, () => transport.handleRequest(req, res, req.body));
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
logger.error('MCP request error', { error: err instanceof Error ? err.message : String(err) });
|
|
181
|
+
if (!res.headersSent) {
|
|
182
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
141
186
|
/**
|
|
142
187
|
* Create an Express handler that processes MCP requests.
|
|
143
188
|
* Each request gets a fresh Server + Transport pair.
|
|
189
|
+
* Exported for the era-contract regression tests (tests/unit/server/mcp-era-contract.test.ts).
|
|
144
190
|
*/
|
|
145
|
-
function createMcpHandler(serverFactory) {
|
|
191
|
+
export function createMcpHandler(serverFactory) {
|
|
146
192
|
return async (req, res) => {
|
|
147
193
|
logger.debug('MCP handler invoked', {
|
|
148
194
|
method: req.method,
|
|
@@ -151,32 +197,56 @@ function createMcpHandler(serverFactory) {
|
|
|
151
197
|
bodyMethod: req.body?.method,
|
|
152
198
|
bodyId: req.body?.id,
|
|
153
199
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// Without this, the MCP SDK's transport tries to re-read the stream,
|
|
163
|
-
// gets nothing, and returns "Parse error: Invalid JSON" (-32700).
|
|
164
|
-
// The SDK explicitly supports this pattern — see their docs/comments
|
|
165
|
-
// in StreamableHTTPServerTransport.handleRequest().
|
|
166
|
-
await transport.handleRequest(req, res, req.body);
|
|
200
|
+
await serveMcpRequest(serverFactory, req, res);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function createPinnedTargetMcpHandler(multi) {
|
|
204
|
+
return async (req, res) => {
|
|
205
|
+
if (!multi.registry.available) {
|
|
206
|
+
res.status(503).json({ error: 'Multi-target registry unavailable' });
|
|
207
|
+
return;
|
|
167
208
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
209
|
+
const targetId = targetFromPinnedMcpPath(req.path);
|
|
210
|
+
const target = targetId ? multi.registry.get(targetId) : undefined;
|
|
211
|
+
if (!target) {
|
|
212
|
+
res.status(404).json({ error: 'Not found' });
|
|
213
|
+
return;
|
|
173
214
|
}
|
|
215
|
+
logger.debug('MCP handler invoked (pinned target)', {
|
|
216
|
+
target: target.target,
|
|
217
|
+
method: req.method,
|
|
218
|
+
bodyMethod: req.body?.method,
|
|
219
|
+
bodyId: req.body?.id,
|
|
220
|
+
});
|
|
221
|
+
await serveMcpRequest(() => multi.createPinnedServer(target), req, res);
|
|
174
222
|
};
|
|
175
223
|
}
|
|
224
|
+
export function createAggregateMcpHandler(multi) {
|
|
225
|
+
return async (req, res) => {
|
|
226
|
+
// Keep the aggregate MCP transport reachable when discovery fails so an
|
|
227
|
+
// administrator can call SAPTargets and inspect secret-safe diagnostics.
|
|
228
|
+
// Other tool calls return a structured registry-unavailable result.
|
|
229
|
+
await serveMcpRequest(multi.aggregateFactory, req, res);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export function resolveMcpHttpRateLimit(config) {
|
|
233
|
+
if (config.mcpHttpRateLimit !== undefined)
|
|
234
|
+
return config.mcpHttpRateLimit;
|
|
235
|
+
// OAuth and MCP traffic are separate edge buckets. Disabling only the OAuth
|
|
236
|
+
// bucket must not also remove MCP protection; ARC1_MCP_HTTP_RATE_LIMIT=0 is
|
|
237
|
+
// the explicit opt-out for MCP traffic.
|
|
238
|
+
return Math.max(config.authRateLimit * 30, 600);
|
|
239
|
+
}
|
|
240
|
+
export function multiTargetHealthStatus(registry) {
|
|
241
|
+
// Zero targets and individually quarantined destinations are valid snapshots.
|
|
242
|
+
// Only a registry-wide failure makes multi-target routing unavailable.
|
|
243
|
+
return registry.available ? 'ready' : 'error';
|
|
244
|
+
}
|
|
245
|
+
export const MULTI_TARGET_SCOPES_SUPPORTED = Object.freeze(['read', 'data', 'sql', 'admin']);
|
|
176
246
|
/**
|
|
177
247
|
* Start the HTTP Streamable server.
|
|
178
248
|
*/
|
|
179
|
-
export async function startHttpServer(serverFactory, config, xsuaaCredentials, uiDeps) {
|
|
249
|
+
export async function startHttpServer(serverFactory, config, xsuaaCredentials, uiDeps, multiTargets) {
|
|
180
250
|
const [host, portStr] = config.httpAddr.split(':');
|
|
181
251
|
const port = Number.parseInt(portStr || '8080', 10);
|
|
182
252
|
const bindHost = host || '0.0.0.0';
|
|
@@ -186,28 +256,41 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
186
256
|
app.set('trust proxy', 1);
|
|
187
257
|
applySecurityMiddleware(app, config.allowedOrigins);
|
|
188
258
|
// ─── Layer 1: HTTP-edge rate limiter helper ──────────────────────────
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
259
|
+
// `ARC1_AUTH_RATE_LIMIT` (default 20/min/IP) controls OAuth endpoints uniformly.
|
|
260
|
+
// MCP traffic derives `max(value × 30, 600)/min/IP` unless the operator sets the
|
|
261
|
+
// independent `ARC1_MCP_HTTP_RATE_LIMIT`; this keeps pre-bearer probing gated even
|
|
262
|
+
// when OAuth limiting is delegated to an upstream proxy.
|
|
193
263
|
// See docs_page/rate-limiting.md (Layer 1) and ADR-0004.
|
|
194
264
|
//
|
|
195
265
|
// Implementation note: the limiter is mounted DIRECTLY via createAuthRateLimiter →
|
|
196
266
|
// express-rate-limit. The disabled path skips the mount entirely rather than going
|
|
197
267
|
// through a noop indirection — this keeps the dataflow `rateLimit({...}) → app.use`
|
|
198
268
|
// direct and makes CodeQL's `js/missing-rate-limiting` query close cleanly.
|
|
199
|
-
const { createAuthRateLimiter, isCopilotJsonRpc } = await import('./auth-rate-limit.js');
|
|
200
|
-
const
|
|
201
|
-
const mcpRatePerMinute =
|
|
269
|
+
const { createAuthRateLimiter, isCopilotJsonRpc, isMcpHttpTraffic } = await import('./auth-rate-limit.js');
|
|
270
|
+
const authRateLimitEnabled = config.authRateLimit > 0;
|
|
271
|
+
const mcpRatePerMinute = resolveMcpHttpRateLimit(config);
|
|
272
|
+
const mcpRateLimitEnabled = mcpRatePerMinute > 0;
|
|
202
273
|
logger.info('Auth rate limiting', {
|
|
203
274
|
perMinute: config.authRateLimit,
|
|
204
275
|
mcpPerMinute: mcpRatePerMinute,
|
|
205
|
-
endpoints:
|
|
206
|
-
|
|
276
|
+
endpoints: [
|
|
277
|
+
...(authRateLimitEnabled ? ['/register', '/authorize', '/token', '/revoke'] : []),
|
|
278
|
+
...(mcpRateLimitEnabled ? ['/mcp'] : []),
|
|
279
|
+
],
|
|
280
|
+
disabled: !authRateLimitEnabled && !mcpRateLimitEnabled,
|
|
207
281
|
});
|
|
208
282
|
app.use(express.json());
|
|
209
283
|
app.use(express.urlencoded({ extended: false }));
|
|
210
|
-
|
|
284
|
+
if (mcpRateLimitEnabled) {
|
|
285
|
+
// One middleware instance means one MemoryStore and therefore one per-IP
|
|
286
|
+
// bucket across single-target, pinned, aggregate, and Copilot routes.
|
|
287
|
+
// Mount after body parsing so the Copilot JSON-RPC predicate can inspect
|
|
288
|
+
// `req.body`, but before every authentication and route handler.
|
|
289
|
+
app.use(createAuthRateLimiter('/mcp', mcpRatePerMinute, { skip: (req) => !isMcpHttpTraffic(req) }));
|
|
290
|
+
}
|
|
291
|
+
const mcpHandler = serverFactory ? createMcpHandler(serverFactory) : undefined;
|
|
292
|
+
const pinnedMcpHandler = multiTargets ? createPinnedTargetMcpHandler(multiTargets) : undefined;
|
|
293
|
+
const aggregateMcpHandler = multiTargets ? createAggregateMcpHandler(multiTargets) : undefined;
|
|
211
294
|
const hasAdminApiKey = config.apiKeys?.some((entry) => entry.profile === 'admin') ?? false;
|
|
212
295
|
if (uiDeps && !(hasAdminApiKey || config.oidcIssuer || (config.xsuaaAuth && xsuaaCredentials))) {
|
|
213
296
|
throw new Error('ARC1_UI=web requires an admin API key, OIDC, or XSUAA HTTP authentication.');
|
|
@@ -230,7 +313,15 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
230
313
|
// they're talking to the CORRECT process (not a zombie from a previous deploy).
|
|
231
314
|
const startedAt = new Date().toISOString();
|
|
232
315
|
app.get('/health', (_req, res) => {
|
|
233
|
-
res.json({
|
|
316
|
+
res.json({
|
|
317
|
+
status: 'ok',
|
|
318
|
+
version: VERSION,
|
|
319
|
+
startedAt,
|
|
320
|
+
pid: process.pid,
|
|
321
|
+
...(multiTargets
|
|
322
|
+
? { components: { multiTarget: { status: multiTargetHealthStatus(multiTargets.registry) } } }
|
|
323
|
+
: {}),
|
|
324
|
+
});
|
|
234
325
|
});
|
|
235
326
|
// ─── XSUAA OAuth Proxy Mode ──────────────────────────────
|
|
236
327
|
if (config.xsuaaAuth && xsuaaCredentials) {
|
|
@@ -252,6 +343,8 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
252
343
|
// URL sent to XSUAA as redirect_uri; the Express route is mounted at the root
|
|
253
344
|
// `/oauth/callback` below since the proxy strips the prefix before forwarding.
|
|
254
345
|
const oauthCallbackUrl = `${oauthFullBase}/oauth/callback`;
|
|
346
|
+
const oauthLoggedOutUrl = `${oauthFullBase}${OAUTH_LOGGED_OUT_PATH}`;
|
|
347
|
+
const xsuaaSessionRefreshUrl = buildXsuaaSessionRefreshUrl(xsuaaCredentials, oauthLoggedOutUrl);
|
|
255
348
|
// Create XSUAA provider + chained verifier.
|
|
256
349
|
//
|
|
257
350
|
// The package defaults its client_id prefix + KDF labels to `mcp-*`. ARC-1
|
|
@@ -286,6 +379,42 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
286
379
|
verifier: { verifyAccessToken: chainedVerifier },
|
|
287
380
|
resourceMetadataUrl,
|
|
288
381
|
});
|
|
382
|
+
// Multi-target routes accept XSUAA identities only. Define this once and
|
|
383
|
+
// reuse it for explicit routes and the Copilot `/authorize` compatibility
|
|
384
|
+
// alias so multi mode cannot fall back to API-key/OIDC or a single target.
|
|
385
|
+
const multiBearerAuth = multiTargets && pinnedMcpHandler && aggregateMcpHandler
|
|
386
|
+
? (req, res, next) => {
|
|
387
|
+
// `app.use('/authorize', ...)` temporarily trims req.path to `/`.
|
|
388
|
+
// originalUrl preserves the public route, so the Copilot alias can
|
|
389
|
+
// advertise the aggregate PRM instead of a non-existent root PRM.
|
|
390
|
+
const originalPath = req.originalUrl?.split('?', 1)[0] || req.path;
|
|
391
|
+
const lowerPath = originalPath.toLowerCase();
|
|
392
|
+
const resourcePath = lowerPath === '/authorize' ? '/multi/mcp' : originalPath;
|
|
393
|
+
const multiResourceMetadataUrl = `${oauthFullBase}/.well-known/oauth-protected-resource${resourcePath}`;
|
|
394
|
+
const authenticate = requireBearerAuth({
|
|
395
|
+
verifier: { verifyAccessToken: xsuaaVerifier },
|
|
396
|
+
resourceMetadataUrl: multiResourceMetadataUrl,
|
|
397
|
+
});
|
|
398
|
+
authenticate(req, res, (error) => {
|
|
399
|
+
if (error) {
|
|
400
|
+
next(error);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// Keep route membership behind the global read scope without putting
|
|
404
|
+
// `scope=read` in the initial 401 challenge. MCP clients can therefore
|
|
405
|
+
// request the four mutation-free scopes advertised by the route PRM;
|
|
406
|
+
// XSUAA still grants only the subset assigned to the authenticated user.
|
|
407
|
+
if (!req.auth || !hasRequiredScope(req.auth.scopes, 'read')) {
|
|
408
|
+
res
|
|
409
|
+
.set('WWW-Authenticate', `Bearer error="insufficient_scope", error_description="Insufficient scope", scope="read", resource_metadata="${multiResourceMetadataUrl}"`)
|
|
410
|
+
.status(403)
|
|
411
|
+
.json({ error: 'insufficient_scope', error_description: 'Insufficient scope' });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
next();
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
: undefined;
|
|
289
418
|
if (uiDeps) {
|
|
290
419
|
const uiBearerAuth = requireBearerAuth({
|
|
291
420
|
verifier: { verifyAccessToken: chainedVerifier },
|
|
@@ -300,33 +429,19 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
300
429
|
// rate-limited — they're cheap, cacheable, and legitimate clients hit them on
|
|
301
430
|
// every reconnect. See docs_page/rate-limiting.md.
|
|
302
431
|
//
|
|
303
|
-
// Every `app.use(path, …)` here receives a fresh `rateLimit({...})` middleware
|
|
432
|
+
// Every OAuth `app.use(path, …)` here receives a fresh `rateLimit({...})` middleware
|
|
304
433
|
// DIRECTLY. No conditional dispatchers, no helper wrappers. CodeQL's
|
|
305
434
|
// `js/missing-rate-limiting` query only recognises that exact pattern; going
|
|
306
435
|
// through an inline arrow function with branch-based delegation makes it
|
|
307
436
|
// re-open the alert (verified — see PR #276 review history).
|
|
308
437
|
//
|
|
309
|
-
// Copilot Studio
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
// 2. /mcp cap, with `skip` returning true for everything BUT Copilot JSON-RPC.
|
|
314
|
-
// Each request hits one bucket — the OAuth bucket for real OAuth flows, the
|
|
315
|
-
// higher /mcp bucket for Copilot. The `isCopilotJsonRpc` predicate is shared
|
|
316
|
-
// with auth-rate-limit.ts so the two mounts can never drift.
|
|
317
|
-
//
|
|
318
|
-
// Trade-off: the /authorize-JSON-RPC bucket is a separate store from the
|
|
319
|
-
// direct /mcp bucket. An attacker alternating routes effectively gets
|
|
320
|
-
// `mcpCap + mcpCap = 2 × mcpCap`/min/IP. At default config that's still
|
|
321
|
-
// 1200/min, well below abuse thresholds. Sharing the store would require
|
|
322
|
-
// injecting a custom MemoryStore into both `rateLimit({...})` calls — not
|
|
323
|
-
// worth the complexity for a 2× headroom on an already loose cap.
|
|
324
|
-
if (rateLimitEnabled) {
|
|
438
|
+
// Copilot Studio JSON-RPC on `/authorize` skips the low OAuth bucket. The
|
|
439
|
+
// root-mounted MCP limiter above counts it in the same higher-cap bucket as
|
|
440
|
+
// every other MCP endpoint style.
|
|
441
|
+
if (authRateLimitEnabled) {
|
|
325
442
|
app.use('/register', createAuthRateLimiter('/register', config.authRateLimit));
|
|
326
443
|
// /authorize OAuth limiter — skips Copilot Studio MCP JSON-RPC traffic.
|
|
327
444
|
app.use('/authorize', createAuthRateLimiter('/authorize', config.authRateLimit, { skip: isCopilotJsonRpc }));
|
|
328
|
-
// /authorize MCP limiter — only applies to Copilot Studio JSON-RPC; uses /mcp cap.
|
|
329
|
-
app.use('/authorize', createAuthRateLimiter('/mcp', mcpRatePerMinute, { skip: (req) => !isCopilotJsonRpc(req) }));
|
|
330
445
|
app.use('/token', createAuthRateLimiter('/token', config.authRateLimit));
|
|
331
446
|
app.use('/revoke', createAuthRateLimiter('/revoke', config.authRateLimit));
|
|
332
447
|
// /oauth/callback is unauthenticated and does an HMAC verify per hit —
|
|
@@ -337,7 +452,7 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
337
452
|
// Copilot Studio sends MCP JSON-RPC requests to /authorize instead of
|
|
338
453
|
// /mcp after completing the OAuth flow. When we detect a JSON-RPC body
|
|
339
454
|
// (has "jsonrpc" field) on POST /authorize, we bypass the OAuth handler
|
|
340
|
-
// and route directly to
|
|
455
|
+
// and route directly to the applicable bearer verifier + MCP handler.
|
|
341
456
|
//
|
|
342
457
|
// For normal OAuth requests, merge query params into body as fallback
|
|
343
458
|
// (some clients send POST /authorize with params in query string).
|
|
@@ -352,13 +467,24 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
352
467
|
id: req.body.id,
|
|
353
468
|
userAgent: req.headers['user-agent']?.slice(0, 60),
|
|
354
469
|
});
|
|
355
|
-
//
|
|
356
|
-
|
|
470
|
+
// The compatibility alias is necessarily target-ambiguous. Whenever
|
|
471
|
+
// multi-target mode is enabled, resolve it to the mutation-free
|
|
472
|
+
// aggregate route and its XSUAA-only verifier, even if a separately
|
|
473
|
+
// configured (and potentially writable) single-target /mcp coexists.
|
|
474
|
+
// Only a single-target-only deployment may use the chained verifier.
|
|
475
|
+
const useAggregateAlias = !!(aggregateMcpHandler && multiBearerAuth);
|
|
476
|
+
const authorizeBearerAuth = useAggregateAlias ? multiBearerAuth : bearerAuth;
|
|
477
|
+
authorizeBearerAuth(req, res, (err) => {
|
|
357
478
|
if (err) {
|
|
358
479
|
next(err);
|
|
359
480
|
return;
|
|
360
481
|
}
|
|
361
|
-
mcpHandler
|
|
482
|
+
const handler = useAggregateAlias ? aggregateMcpHandler : mcpHandler;
|
|
483
|
+
if (!handler) {
|
|
484
|
+
res.status(404).json({ error: 'Not found' });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
handler(req, res);
|
|
362
488
|
});
|
|
363
489
|
return;
|
|
364
490
|
}
|
|
@@ -408,7 +534,8 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
408
534
|
// XSUAA as oauthCallbackUrl. A strip-prefix proxy maps the public path
|
|
409
535
|
// back to this root route. Handler is extracted (exported) so the
|
|
410
536
|
// state-round-trip contract is unit-testable without a live XSUAA.
|
|
411
|
-
app.get(
|
|
537
|
+
app.get(OAUTH_LOGGED_OUT_PATH, createOAuthLoggedOutHandler());
|
|
538
|
+
app.get('/oauth/callback', withInvalidScopeSessionRecovery(createOAuthCallbackHandler(stateCodec, clientStore, { logger: authLibLogger }), xsuaaSessionRefreshUrl));
|
|
412
539
|
// ─── Path-prefix-aware OAuth metadata override ────────────────
|
|
413
540
|
// The MCP SDK's `mcpAuthRouter` builds endpoint URLs with
|
|
414
541
|
// `new URL("/authorize", baseUrl).href`, which strips any path component
|
|
@@ -464,6 +591,27 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
464
591
|
basePath,
|
|
465
592
|
});
|
|
466
593
|
}
|
|
594
|
+
// Registry-independent multi-target PRM routes must precede the SDK router.
|
|
595
|
+
// Syntactically valid unknown targets intentionally receive the same metadata as known ones.
|
|
596
|
+
if (multiTargets && pinnedMcpHandler && aggregateMcpHandler) {
|
|
597
|
+
app.get(/^\/\.well-known\/oauth-protected-resource\/multi\/mcp$/, (_req, res) => {
|
|
598
|
+
res.json({
|
|
599
|
+
resource: `${oauthFullBase}/multi/mcp`,
|
|
600
|
+
authorization_servers: [`${oauthFullBase}/`],
|
|
601
|
+
scopes_supported: MULTI_TARGET_SCOPES_SUPPORTED,
|
|
602
|
+
resource_name: 'ARC-1 SAP MCP Server (multi-target)',
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
app.get(PINNED_RESOURCE_METADATA_PATH_PATTERN, (req, res) => {
|
|
606
|
+
const match = req.path.match(PINNED_RESOURCE_METADATA_PATH_PATTERN);
|
|
607
|
+
res.json({
|
|
608
|
+
resource: `${oauthFullBase}/${match?.[1]}/${match?.[2]}/mcp`,
|
|
609
|
+
authorization_servers: [`${oauthFullBase}/`],
|
|
610
|
+
scopes_supported: MULTI_TARGET_SCOPES_SUPPORTED,
|
|
611
|
+
resource_name: 'ARC-1 SAP MCP Server (pinned target)',
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
467
615
|
// Install MCP SDK auth router at root (OAuth endpoints + DCR).
|
|
468
616
|
// For root-path deployments (no basePath) the SDK's metadata is correct
|
|
469
617
|
// as-is and serves both well-known endpoints. For prefix deployments the
|
|
@@ -478,14 +626,13 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
478
626
|
scopesSupported,
|
|
479
627
|
resourceName: 'ARC-1 SAP MCP Server',
|
|
480
628
|
}));
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (rateLimitEnabled) {
|
|
485
|
-
app.use('/mcp', createAuthRateLimiter('/mcp', mcpRatePerMinute));
|
|
629
|
+
if (multiTargets && pinnedMcpHandler && aggregateMcpHandler && multiBearerAuth) {
|
|
630
|
+
app.all(/^\/multi\/mcp$/, multiBearerAuth, aggregateMcpHandler);
|
|
631
|
+
app.all(PINNED_MCP_PATH_PATTERN, multiBearerAuth, pinnedMcpHandler);
|
|
486
632
|
}
|
|
487
633
|
// Protected MCP endpoint with chained token verification
|
|
488
|
-
|
|
634
|
+
if (mcpHandler)
|
|
635
|
+
app.all('/mcp', bearerAuth, mcpHandler);
|
|
489
636
|
logger.info('XSUAA OAuth proxy enabled', {
|
|
490
637
|
xsappname: xsuaaCredentials.xsappname,
|
|
491
638
|
appUrl,
|
|
@@ -496,30 +643,34 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
496
643
|
// No JWKS pre-warm needed: the package's OIDC verifier lazy-imports jose and
|
|
497
644
|
// memoizes the remote JWKS on the first verify (and retries on a transient
|
|
498
645
|
// discovery failure instead of caching the rejection).
|
|
499
|
-
// Layer 1 on /mcp also applies outside XSUAA mode — API-key / OIDC / no-auth
|
|
500
|
-
// deployments get the same anonymous-probing protection. OAuth endpoints don't
|
|
501
|
-
// exist in non-XSUAA mode so only /mcp needs mounting here.
|
|
502
|
-
if (rateLimitEnabled) {
|
|
503
|
-
app.use('/mcp', createAuthRateLimiter('/mcp', mcpRatePerMinute));
|
|
504
|
-
}
|
|
505
646
|
if (config.apiKeys || config.oidcIssuer) {
|
|
506
647
|
// Use requireBearerAuth so that authInfo is populated on the MCP request context.
|
|
507
648
|
// This enables scope enforcement, per-request safety, and principal propagation.
|
|
508
649
|
const { requireBearerAuth } = await import('@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js');
|
|
509
650
|
const verifier = await createStandardVerifier(config);
|
|
510
|
-
|
|
651
|
+
// ─── RFC 9728 protected-resource metadata (OIDC mode) ─────────
|
|
652
|
+
// ARC-1 is a pure resource server here: it publishes WHERE the external
|
|
653
|
+
// IdP lives and mints no tokens. Skipped for api-key-only mode (no
|
|
654
|
+
// authorization server to advertise) and when the admin opted out.
|
|
655
|
+
const resourceMetadataUrl = config.oidcIssuer && config.oidcDiscovery
|
|
656
|
+
? await mountOidcResourceMetadata(app, config, bindHost, port)
|
|
657
|
+
: undefined;
|
|
658
|
+
const bearerAuth = requireBearerAuth({ verifier: { verifyAccessToken: verifier }, resourceMetadataUrl });
|
|
511
659
|
if (uiDeps) {
|
|
512
660
|
const uiBearerAuth = requireBearerAuth({
|
|
513
661
|
verifier: { verifyAccessToken: verifier },
|
|
514
662
|
requiredScopes: ['admin'],
|
|
663
|
+
resourceMetadataUrl,
|
|
515
664
|
});
|
|
516
665
|
mountUiRoutes(app, uiDeps, uiBearerAuth);
|
|
517
666
|
}
|
|
518
|
-
|
|
667
|
+
if (mcpHandler)
|
|
668
|
+
app.all('/mcp', bearerAuth, mcpHandler);
|
|
519
669
|
}
|
|
520
670
|
else {
|
|
521
671
|
// No auth configured — open access
|
|
522
|
-
|
|
672
|
+
if (mcpHandler)
|
|
673
|
+
app.all('/mcp', mcpHandler);
|
|
523
674
|
}
|
|
524
675
|
}
|
|
525
676
|
// ─── 404 for anything else ─────────────────────────────────
|
|
@@ -541,7 +692,8 @@ export async function startHttpServer(serverFactory, config, xsuaaCredentials, u
|
|
|
541
692
|
logger.info('ARC-1 HTTP server started', {
|
|
542
693
|
addr: `${bindHost}:${port}`,
|
|
543
694
|
health: `http://${bindHost}:${port}/health`,
|
|
544
|
-
mcp: `http://${bindHost}:${port}/mcp
|
|
695
|
+
mcp: serverFactory ? `http://${bindHost}:${port}/mcp` : undefined,
|
|
696
|
+
multi: multiTargets ? `http://${bindHost}:${port}/multi/mcp` : undefined,
|
|
545
697
|
ui: uiDeps ? `http://${bindHost}:${port}/ui/` : undefined,
|
|
546
698
|
auth: authMode,
|
|
547
699
|
});
|
|
@@ -598,6 +750,64 @@ function buildPackageOidcVerifier(config, createOidcVerifier) {
|
|
|
598
750
|
logger: authLibLogger,
|
|
599
751
|
});
|
|
600
752
|
}
|
|
753
|
+
// ─── OIDC Protected-Resource Metadata (RFC 9728) ────────────────────
|
|
754
|
+
/**
|
|
755
|
+
* Build the RFC 9728 document ARC-1 publishes in OIDC mode.
|
|
756
|
+
*
|
|
757
|
+
* ARC-1 is only the resource server here: `authorization_servers` points at the
|
|
758
|
+
* external IdP, and clients discover its endpoints themselves (RFC 8414 with the
|
|
759
|
+
* OIDC-discovery fallbacks — Entra only answers the append form). `resource` is
|
|
760
|
+
* the canonical MCP endpoint URI; MCP clients reject a document whose `resource`
|
|
761
|
+
* is not same-origin with the server they called.
|
|
762
|
+
*
|
|
763
|
+
* `scopes_supported` is IdP-specific (`api://<client-id>/access_as_user` on Entra,
|
|
764
|
+
* arbitrary elsewhere) and cannot be derived from `SAP_OIDC_AUDIENCE` — it is
|
|
765
|
+
* omitted unless the admin sets `SAP_OIDC_SCOPES`.
|
|
766
|
+
*/
|
|
767
|
+
export function buildOidcResourceMetadata(config, publicBase) {
|
|
768
|
+
return {
|
|
769
|
+
resource: `${publicBase}/mcp`,
|
|
770
|
+
authorization_servers: [config.oidcIssuer],
|
|
771
|
+
bearer_methods_supported: ['header'],
|
|
772
|
+
resource_name: 'ARC-1 SAP MCP Server',
|
|
773
|
+
...(config.oidcScopes?.length ? { scopes_supported: config.oidcScopes } : {}),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Mount the protected-resource metadata routes and return the URL that belongs in
|
|
778
|
+
* the `WWW-Authenticate` challenge. Caller gates on `oidcIssuer && oidcDiscovery`.
|
|
779
|
+
*
|
|
780
|
+
* URLs come from `getAppUrl()` (ARC1_PUBLIC_URL / VCAP), never from the request
|
|
781
|
+
* `Host` header, so a spoofed Host cannot redirect clients at an attacker's IdP.
|
|
782
|
+
* The SDK's `metadataHandler` supplies `cors()` + GET/OPTIONS-only, the same
|
|
783
|
+
* treatment the XSUAA branch's metadata gets — the document is public by design.
|
|
784
|
+
*/
|
|
785
|
+
export async function mountOidcResourceMetadata(app, config, bindHost, port) {
|
|
786
|
+
const { metadataHandler } = await import('@modelcontextprotocol/sdk/server/auth/handlers/metadata.js');
|
|
787
|
+
const { getAppUrl } = await import('./app-url.js');
|
|
788
|
+
const parsed = new URL(getAppUrl() ?? `http://${bindHost}:${port}`);
|
|
789
|
+
const basePath = parsed.pathname.replace(/\/$/, ''); // '' for root, '/arc1' behind a prefix proxy
|
|
790
|
+
const publicBase = `${parsed.origin}${basePath}`;
|
|
791
|
+
const metadata = buildOidcResourceMetadata(config, publicBase);
|
|
792
|
+
// Order matters: '/.well-known/oauth-protected-resource' is a mount PREFIX, so
|
|
793
|
+
// it must come after the '/mcp' route or it would swallow it.
|
|
794
|
+
app.use('/.well-known/oauth-protected-resource/mcp', metadataHandler(metadata)); // RFC 9728 §3.1 path insertion
|
|
795
|
+
if (basePath)
|
|
796
|
+
app.use(`/.well-known/oauth-protected-resource${basePath}/mcp`, metadataHandler(metadata));
|
|
797
|
+
// Root fallback (spec 2025-11-25 lists it; the SDK client probes sub-path → root).
|
|
798
|
+
// As a prefix mount it also answers unknown sub-paths — harmless: clients validate
|
|
799
|
+
// `resource` against the URL they called, and OIDC mode has no other MCP routes.
|
|
800
|
+
app.use('/.well-known/oauth-protected-resource', metadataHandler(metadata));
|
|
801
|
+
const resourceMetadataUrl = `${publicBase}/.well-known/oauth-protected-resource/mcp`;
|
|
802
|
+
// Key is `issuer`, not `authorizationServer` — the logger redacts any key
|
|
803
|
+
// containing "authorization", which would hide the one value operators need.
|
|
804
|
+
logger.info('OIDC protected-resource metadata enabled', {
|
|
805
|
+
resource: metadata.resource,
|
|
806
|
+
issuer: config.oidcIssuer,
|
|
807
|
+
scopesSupported: config.oidcScopes?.length ?? 0,
|
|
808
|
+
});
|
|
809
|
+
return resourceMetadataUrl;
|
|
810
|
+
}
|
|
601
811
|
// ─── Standard Mode Verifier ─────────────────────────────────────────
|
|
602
812
|
/**
|
|
603
813
|
* Create a token verifier for standard auth mode (API key + OIDC).
|