@dereekb/firebase-server 13.36.0 → 13.37.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.
@@ -12,7 +12,8 @@ export interface OAuthProtectedResourceMetadata {
12
12
  /**
13
13
  * Scopes the resource accepts (RFC 9728 §2). Advertised so dynamic-registration
14
14
  * MCP clients (e.g. the Claude Code CLI) know which scopes to request on the
15
- * authorization call. Omitted when {@link McpModuleConfig.scopesSupported} is unset.
15
+ * authorization call — they request this list verbatim, so it carries only scopes
16
+ * such a client can actually be granted. Omitted when the resolved list is empty.
16
17
  */
17
18
  readonly scopes_supported?: ReadonlyArray<OidcScope>;
18
19
  }
@@ -25,10 +26,29 @@ export interface OAuthProtectedResourceMetadata {
25
26
  * URIs must live at the host root. Apps need to exclude `.well-known/{*path}`
26
27
  * from any global API route prefix (see `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`
27
28
  * in `firebase-server/oidc` for the corresponding OIDC excludes).
29
+ *
30
+ * Because the document must be reachable at the origin root, the MCP endpoint has to be
31
+ * exposed through a host that serves the app at `/` — Firebase Hosting, or the hosting
32
+ * emulator in development. Pointing a client straight at the Functions emulator origin
33
+ * (`http://localhost:<port>/<project>/<region>/<function>/mcp`) breaks discovery: that
34
+ * runtime only routes `/<project>/<region>/<function>/…`, so nothing answers at the root
35
+ * and a client that has not yet seen a 401 challenge cannot find the issuer.
28
36
  */
29
37
  export declare class McpWellKnownController {
30
38
  private readonly mcpConfig;
31
39
  private readonly oidcProviderConfigService;
32
40
  constructor(mcpConfig: McpModuleConfig, oidcProviderConfigService: OidcProviderConfigService);
41
+ /**
42
+ * Serves both RFC 9728 discovery URLs — the primary form (§3.1), which inserts the resource's
43
+ * path after the well-known prefix (`/.well-known/oauth-protected-resource/mcp`), and the bare
44
+ * form (`/.well-known/oauth-protected-resource`) that clients probe as a fallback.
45
+ *
46
+ * The document is identical either way; the resource identity comes from
47
+ * {@link McpModuleConfig.mcpUrl}, not from the request path. Serving the primary form means
48
+ * discovery succeeds on a client's first probe rather than depending on it implementing the
49
+ * fallback.
50
+ *
51
+ * @returns The protected-resource metadata document.
52
+ */
33
53
  getProtectedResourceMetadata(): OAuthProtectedResourceMetadata;
34
54
  }
@@ -83,8 +83,9 @@ export interface McpReasonParameterConfig {
83
83
  /**
84
84
  * Filter applied to the OIDC provider's advertised scope list when building the MCP
85
85
  * protected-resource metadata's `scopes_supported`. Receives every scope the provider
86
- * issues (from `OidcProviderConfigService.scopesSupported`) and returns the subset to
87
- * advertise. Supplied via {@link McpModuleConfig.scopesSupported}; when unset, all are advertised.
86
+ * issues to an arbitrary client (from `OidcProviderConfigService.clientRequestableScopesSupported`)
87
+ * and returns the subset to advertise. Supplied via {@link McpModuleConfig.scopesSupported};
88
+ * when unset, all of them are advertised.
88
89
  */
89
90
  export type McpScopesSupportedFilter = (allScopes: readonly OidcScope[]) => readonly OidcScope[];
90
91
  /**
@@ -127,11 +128,12 @@ export declare abstract class McpModuleConfig {
127
128
  * protected-resource discovery document's `scopes_supported` (RFC 9728 §2).
128
129
  *
129
130
  * The base list is pulled automatically from the OIDC provider config via the
130
- * injected {@link OidcProviderConfigService.scopesSupported} (i.e.
131
- * `Object.keys(providerConfig.claims)`), so the MCP resource advertises exactly
132
- * the scopes the issuer supports without the app restating them. Provide this
133
- * only to narrow that set — it receives every scope the provider issues and
134
- * returns the subset to advertise. When unset, all of them are advertised.
131
+ * injected {@link OidcProviderConfigService.clientRequestableScopesSupported}, so
132
+ * the MCP resource advertises the scopes the issuer grants an arbitrary client
133
+ * without the app restating them. Scopes only an admin-assigned provider profile
134
+ * unlocks are already excluded — requesting one is fatal at the consent unlock
135
+ * gate. Provide this only to narrow the set further; it returns the subset to
136
+ * advertise. When unset, the whole base list is advertised.
135
137
  *
136
138
  * Advertising these matters because dynamic-registration MCP clients (the Claude
137
139
  * Code CLI) read `scopes_supported` to decide which scopes to request on the
@@ -17,7 +17,8 @@ export interface McpModuleMetadataConfig extends Pick<ModuleMetadata, 'imports'
17
17
  * - {@link ModelApiDispatchConfig} — so the MCP server can reuse the call model dispatch chain.
18
18
  * - {@link McpModuleConfig} — issuer + resource URLs for protected-resource discovery.
19
19
  * - `OidcProviderConfigService` (from `@dereekb/firebase-server/oidc`) — the `McpWellKnownController`
20
- * reads the provider's `scopesSupported` to advertise them on the protected-resource document.
20
+ * reads the provider's `clientRequestableScopesSupported` to advertise them on the
21
+ * protected-resource document.
21
22
  *
22
23
  * In practice, downstream apps typically import their own `*ModelApiModule` first (which provides
23
24
  * `ModelApiCallModelDispatchService` + `MODEL_API_NEST_APPLICATION_CONTEXT`) and add the
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { type Request } from 'express';
3
3
  import { ModelApiCallModelDispatchService, ModelApiGetService, FirebaseServerStorageService, type FirebaseServerAuthData } from '@dereekb/firebase-server';
4
4
  import { McpModuleConfig, type McpAuthRoleReader } from '../mcp.config';
@@ -17,9 +17,10 @@ export interface McpRequestContext {
17
17
  * Injectable factory that builds {@link McpServer} instances pre-wired to the
18
18
  * call model dispatch chain.
19
19
  *
20
- * The factory is invoked per Streamable HTTP request — `@modelcontextprotocol/sdk`
21
- * recommends a fresh `McpServer` + transport pair per stateless JSON-RPC request,
22
- * which sidesteps session bookkeeping for the common Claude-connector case.
20
+ * The factory is invoked per Streamable HTTP request. That is the shape the MCP
21
+ * SDK's 2026-07-28 entry (`createMcpHandler`) requires — it builds a fresh server
22
+ * per request and has no session concept at all — and it is equally what the SDK
23
+ * recommends for stateless 2025-era serving, so one factory backs both eras.
23
24
  */
24
25
  export declare class McpServerFactoryService {
25
26
  private readonly mcpConfig;
@@ -2,7 +2,7 @@ import { type Maybe } from '@dereekb/util';
2
2
  import { type FirestoreModelType, type KnownOnCallFunctionType, type OidcModelScopeRequirement, type OidcScopeTerm } from '@dereekb/firebase';
3
3
  import { type ModelApiDetailsResult, type OnCallModelFunctionApiDetails, type FirebaseServerAuthData, type McpToolDetailsBuilder, type McpVisibilityRule } from '@dereekb/firebase-server';
4
4
  import { type Request } from 'express';
5
- import { type CallToolResult, type ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
5
+ import type { CallToolResult, ToolAnnotations } from '@modelcontextprotocol/server';
6
6
  import { type McpManifestToolEntry } from './mcp.manifest';
7
7
  import { type McpToolFilterMetadata } from './mcp.visibility';
8
8
  /**
@@ -1,7 +1,7 @@
1
1
  import { type Maybe } from '@dereekb/util';
2
2
  import { type CallModelOidcScope, type OidcScopeTerm } from '@dereekb/firebase';
3
3
  import { type McpToolVisibility, type McpVisibilityContext, type McpVisibilityRule } from '@dereekb/firebase-server';
4
- import { type ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
4
+ import type { ToolAnnotations } from '@modelcontextprotocol/server';
5
5
  /**
6
6
  * Normalized classification of a {@link McpToolVisibility} value computed at boot.
7
7
  *
@@ -1,21 +1,26 @@
1
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
1
+ import { type McpServer } from '@modelcontextprotocol/server';
2
2
  import { type Request, type Response } from 'express';
3
3
  /**
4
- * Handles a single Streamable HTTP JSON-RPC request against the supplied MCP server.
4
+ * Handles a single Streamable HTTP JSON-RPC request, building a fresh MCP server for it.
5
5
  *
6
- * Each call creates a fresh `StreamableHTTPServerTransport` in stateless mode — the
7
- * transport binds the server, processes one request, and is discarded. Stateless
8
- * mode is adequate for Claude custom-connector usage and avoids the bookkeeping
9
- * cost of session-tracked transports for the common case.
6
+ * Both protocol eras are served, each by its own leg:
7
+ *
8
+ * - **2026-07-28 (modern)** — `createMcpHandler`, which is per-request and stateless by
9
+ * construction: there is no `Mcp-Session-Id` and no transport bookkeeping.
10
+ * - **2025-era (legacy)** — a `NodeStreamableHTTPServerTransport` in stateless mode, which is
11
+ * byte-for-byte what this endpoint served before the SDK v2 upgrade.
12
+ *
13
+ * The legacy leg is wired by hand (the SDK's documented `isLegacyRequest` composition) rather
14
+ * than left to `createMcpHandler`'s built-in `legacy: 'stateless'` fallback, because that
15
+ * fallback builds its transport with `sessionIdGenerator` alone and therefore answers in
16
+ * `text/event-stream`. See {@link handleLegacyMcpRequest} for why that framing is not viable
17
+ * here.
10
18
  *
11
19
  * The caller is expected to have already validated the bearer token (via the
12
20
  * OIDC bearer middleware) before this function runs.
13
21
  *
14
22
  * @param req - The Express request carrying the JSON-RPC body.
15
23
  * @param res - The Express response.
16
- * @param server - The MCP server instance to back the transport.
17
- * @param server.connect - The MCP server's connect method that binds the transport before request handling.
24
+ * @param buildServer - Factory producing the MCP server that backs this request.
18
25
  */
19
- export declare function handleStreamableHttpMcpRequest(req: Request, res: Response, server: {
20
- connect: (transport: StreamableHTTPServerTransport) => Promise<void>;
21
- }): Promise<void>;
26
+ export declare function handleStreamableHttpMcpRequest(req: Request, res: Response, buildServer: () => McpServer): Promise<void>;
@@ -14770,7 +14770,7 @@ function enrichSignedUploadUrlInputSchema(defaultInputSchema, purposeKeys) {
14770
14770
  if (defaultInputSchema != null && (typeof defaultInputSchema === "undefined" ? "undefined" : _type_of$3(defaultInputSchema)) === 'object') {
14771
14771
  var _base_properties;
14772
14772
  var base = defaultInputSchema;
14773
- var purposeProperty = (_base_properties = base.properties) === null || _base_properties === void 0 ? void 0 : _base_properties.purpose;
14773
+ var purposeProperty = (_base_properties = base.properties) === null || _base_properties === void 0 ? void 0 : _base_properties['purpose'];
14774
14774
  if (purposeProperty != null && (typeof purposeProperty === "undefined" ? "undefined" : _type_of$3(purposeProperty)) === 'object') {
14775
14775
  var quotedPurposes = purposeKeys.map(function(purpose) {
14776
14776
  return '"'.concat(purpose, '"');
@@ -14768,7 +14768,7 @@ function enrichSignedUploadUrlInputSchema(defaultInputSchema, purposeKeys) {
14768
14768
  if (defaultInputSchema != null && (typeof defaultInputSchema === "undefined" ? "undefined" : _type_of$3(defaultInputSchema)) === 'object') {
14769
14769
  var _base_properties;
14770
14770
  var base = defaultInputSchema;
14771
- var purposeProperty = (_base_properties = base.properties) === null || _base_properties === void 0 ? void 0 : _base_properties.purpose;
14771
+ var purposeProperty = (_base_properties = base.properties) === null || _base_properties === void 0 ? void 0 : _base_properties['purpose'];
14772
14772
  if (purposeProperty != null && (typeof purposeProperty === "undefined" ? "undefined" : _type_of$3(purposeProperty)) === 'object') {
14773
14773
  var quotedPurposes = purposeKeys.map(function(purpose) {
14774
14774
  return '"'.concat(purpose, '"');
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/model",
3
- "version": "13.36.0",
3
+ "version": "13.37.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.36.0",
6
- "@dereekb/date": "13.36.0",
7
- "@dereekb/firebase": "13.36.0",
8
- "@dereekb/firebase-server": "13.36.0",
9
- "@dereekb/model": "13.36.0",
10
- "@dereekb/nestjs": "13.36.0",
11
- "@dereekb/rxjs": "13.36.0",
12
- "@dereekb/util": "13.36.0",
5
+ "@dereekb/analytics": "13.37.0",
6
+ "@dereekb/date": "13.37.0",
7
+ "@dereekb/firebase": "13.37.0",
8
+ "@dereekb/firebase-server": "13.37.0",
9
+ "@dereekb/model": "13.37.0",
10
+ "@dereekb/nestjs": "13.37.0",
11
+ "@dereekb/rxjs": "13.37.0",
12
+ "@dereekb/util": "13.37.0",
13
13
  "@nestjs/common": "^11.1.19",
14
14
  "@nestjs/config": "^4.0.4",
15
15
  "archiver": "^7.0.1",