@dereekb/firebase-server 13.35.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.
package/mcp/index.cjs.js CHANGED
@@ -2,15 +2,14 @@
2
2
 
3
3
  var common = require('@nestjs/common');
4
4
  var node_fs = require('node:fs');
5
- var mcp_js = require('@modelcontextprotocol/sdk/server/mcp.js');
6
- var types_js = require('@modelcontextprotocol/sdk/types.js');
5
+ var server = require('@modelcontextprotocol/server');
7
6
  var firebase = require('@dereekb/firebase');
8
7
  var oidc = require('@dereekb/firebase-server/oidc');
9
8
  var util = require('@dereekb/util');
10
9
  var firebaseServer = require('@dereekb/firebase-server');
11
10
  var node_util = require('node:util');
12
11
  var model = require('@dereekb/model');
13
- var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
12
+ var node = require('@modelcontextprotocol/node');
14
13
 
15
14
  function _class_call_check$5(instance, Constructor) {
16
15
  if (!(instance instanceof Constructor)) {
@@ -102,11 +101,12 @@ function _define_property$9(obj, key, value) {
102
101
  * protected-resource discovery document's `scopes_supported` (RFC 9728 §2).
103
102
  *
104
103
  * The base list is pulled automatically from the OIDC provider config via the
105
- * injected {@link OidcProviderConfigService.scopesSupported} (i.e.
106
- * `Object.keys(providerConfig.claims)`), so the MCP resource advertises exactly
107
- * the scopes the issuer supports without the app restating them. Provide this
108
- * only to narrow that set — it receives every scope the provider issues and
109
- * returns the subset to advertise. When unset, all of them are advertised.
104
+ * injected {@link OidcProviderConfigService.clientRequestableScopesSupported}, so
105
+ * the MCP resource advertises the scopes the issuer grants an arbitrary client
106
+ * without the app restating them. Scopes only an admin-assigned provider profile
107
+ * unlocks are already excluded — requesting one is fatal at the consent unlock
108
+ * gate. Provide this only to narrow the set further; it returns the subset to
109
+ * advertise. When unset, the whole base list is advertised.
110
110
  *
111
111
  * Advertising these matters because dynamic-registration MCP clients (the Claude
112
112
  * Code CLI) read `scopes_supported` to decide which scopes to request on the
@@ -2038,8 +2038,8 @@ function modelGetToolHandler(args, ctx, deps) {
2038
2038
  })();
2039
2039
  }
2040
2040
  function parseModelGetInput(args) {
2041
- var modelType = args.modelType;
2042
- var keys = args.keys;
2041
+ var modelType = args['modelType'];
2042
+ var keys = args['keys'];
2043
2043
  if (typeof modelType !== 'string' || modelType.length === 0) {
2044
2044
  throw new Error('model-get: "modelType" is required and must be a non-empty string.');
2045
2045
  }
@@ -2940,7 +2940,7 @@ var MODEL_ROW_SCHEMA = {
2940
2940
  }
2941
2941
  };
2942
2942
  // MCP's `tools/list` validator requires `outputSchema.type === 'object'` at the root
2943
- // (see @modelcontextprotocol/sdk Zod schema), so the variants are expressed via a top-level
2943
+ // (see the MCP SDK's `ToolSchema`), so the variants are expressed via a top-level
2944
2944
  // object whose nested `oneOf` discriminates by the `mode` literal.
2945
2945
  var MODEL_INFO_OUTPUT_SCHEMA = {
2946
2946
  type: 'object',
@@ -3742,7 +3742,7 @@ function whoamiToolHandler(args, ctx, deps) {
3742
3742
  }
3743
3743
  // Coerces the `detail` input into a WhoamiDetailLevel, defaulting unknown or absent values to `summary`.
3744
3744
  function parseDetailLevel(args) {
3745
- var value = args.detail;
3745
+ var value = args['detail'];
3746
3746
  return value === 'claims' || value === 'full' ? value : 'summary';
3747
3747
  }
3748
3748
  function buildResult(output, text) {
@@ -4337,30 +4337,30 @@ function urlModelsToolHandler(args, ctx, deps) {
4337
4337
  })();
4338
4338
  }
4339
4339
  function parseUrlModelsInput(args) {
4340
- var url = args.url;
4340
+ var url = args['url'];
4341
4341
  if (typeof url !== 'string' || url.length === 0) {
4342
4342
  throw new Error('url-models: "url" is required and must be a non-empty string.');
4343
4343
  }
4344
- var keysOnly = args.keysOnly === true;
4345
- var load = args.load === true;
4344
+ var keysOnly = args['keysOnly'] === true;
4345
+ var load = args['load'] === true;
4346
4346
  if (keysOnly && load) {
4347
4347
  throw new Error('url-models: "keysOnly" and "load" cannot be combined — "keysOnly" returns keys without loading documents.');
4348
4348
  }
4349
4349
  var models;
4350
- if (args.models !== undefined) {
4351
- if (!Array.isArray(args.models) || args.models.some(function(m) {
4350
+ if (args['models'] !== undefined) {
4351
+ if (!Array.isArray(args['models']) || args['models'].some(function(m) {
4352
4352
  return typeof m !== 'string';
4353
4353
  })) {
4354
4354
  throw new Error('url-models: "models" must be an array of model-type strings.');
4355
4355
  }
4356
- models = args.models;
4356
+ models = args['models'];
4357
4357
  }
4358
4358
  var currentUserUid;
4359
- if (args.currentUserUid !== undefined) {
4360
- if (typeof args.currentUserUid !== 'string' || args.currentUserUid.length === 0) {
4359
+ if (args['currentUserUid'] !== undefined) {
4360
+ if (typeof args['currentUserUid'] !== 'string' || args['currentUserUid'].length === 0) {
4361
4361
  throw new Error('url-models: "currentUserUid" must be a non-empty string.');
4362
4362
  }
4363
- currentUserUid = args.currentUserUid;
4363
+ currentUserUid = args['currentUserUid'];
4364
4364
  }
4365
4365
  return _object_spread$2(_object_spread_props$1(_object_spread$2({
4366
4366
  url: url
@@ -5308,19 +5308,19 @@ function batchExecuteToolHandler(args, ctx, deps) {
5308
5308
  // MARK: Input parsing
5309
5309
  function parseBatchExecuteInput(args) {
5310
5310
  var _args_format, _args_maxParallel;
5311
- var uploadPath = args.uploadPath;
5311
+ var uploadPath = args['uploadPath'];
5312
5312
  if (typeof uploadPath !== 'string' || uploadPath.length === 0) {
5313
5313
  throw new Error('batch-execute: "uploadPath" is required and must be a non-empty string.');
5314
5314
  }
5315
- var bucketId = args.bucketId;
5315
+ var bucketId = args['bucketId'];
5316
5316
  if (bucketId != null && (typeof bucketId !== 'string' || bucketId.length === 0)) {
5317
5317
  throw new Error('batch-execute: "bucketId" must be a non-empty string when provided.');
5318
5318
  }
5319
- var format = (_args_format = args.format) !== null && _args_format !== void 0 ? _args_format : 'ndjson';
5319
+ var format = (_args_format = args['format']) !== null && _args_format !== void 0 ? _args_format : 'ndjson';
5320
5320
  if (format !== 'json' && format !== 'ndjson') {
5321
5321
  throw new Error('batch-execute: "format" must be "json" or "ndjson".');
5322
5322
  }
5323
- var maxParallel = (_args_maxParallel = args.maxParallel) !== null && _args_maxParallel !== void 0 ? _args_maxParallel : DEFAULT_BATCH_EXECUTE_MAX_PARALLEL;
5323
+ var maxParallel = (_args_maxParallel = args['maxParallel']) !== null && _args_maxParallel !== void 0 ? _args_maxParallel : DEFAULT_BATCH_EXECUTE_MAX_PARALLEL;
5324
5324
  if (typeof maxParallel !== 'number' || !Number.isInteger(maxParallel) || maxParallel < 1) {
5325
5325
  throw new Error('batch-execute: "maxParallel" must be a positive integer.');
5326
5326
  }
@@ -5329,8 +5329,8 @@ function parseBatchExecuteInput(args) {
5329
5329
  bucketId: bucketId !== null && bucketId !== void 0 ? bucketId : undefined,
5330
5330
  format: format,
5331
5331
  maxParallel: maxParallel,
5332
- stopOnError: args.stopOnError === true,
5333
- deleteUploadOnSuccess: args.deleteUploadOnSuccess !== false
5332
+ stopOnError: args['stopOnError'] === true,
5333
+ deleteUploadOnSuccess: args['deleteUploadOnSuccess'] !== false
5334
5334
  };
5335
5335
  }
5336
5336
  function readOperationsFile(input) {
@@ -5411,9 +5411,9 @@ function validateOperation(value, index) {
5411
5411
  throw new Error("batch-execute: operation[".concat(index, "] must be an object."));
5412
5412
  }
5413
5413
  var candidate = value;
5414
- var modelType = candidate.modelType;
5415
- var call = candidate.call;
5416
- var specifier = candidate.specifier;
5414
+ var modelType = candidate['modelType'];
5415
+ var call = candidate['call'];
5416
+ var specifier = candidate['specifier'];
5417
5417
  if (typeof modelType !== 'string' || modelType.length === 0) {
5418
5418
  throw new Error("batch-execute: operation[".concat(index, '] is missing a non-empty "modelType".'));
5419
5419
  }
@@ -5427,7 +5427,7 @@ function validateOperation(value, index) {
5427
5427
  call: call,
5428
5428
  modelType: modelType,
5429
5429
  specifier: specifier !== null && specifier !== void 0 ? specifier : undefined,
5430
- data: candidate.data
5430
+ data: candidate['data']
5431
5431
  };
5432
5432
  }
5433
5433
  function collectPreflightErrors(operations, authorizeOperation) {
@@ -6055,9 +6055,10 @@ var McpServerFactoryService_1;
6055
6055
  * Injectable factory that builds {@link McpServer} instances pre-wired to the
6056
6056
  * call model dispatch chain.
6057
6057
  *
6058
- * The factory is invoked per Streamable HTTP request — `@modelcontextprotocol/sdk`
6059
- * recommends a fresh `McpServer` + transport pair per stateless JSON-RPC request,
6060
- * which sidesteps session bookkeeping for the common Claude-connector case.
6058
+ * The factory is invoked per Streamable HTTP request. That is the shape the MCP
6059
+ * SDK's 2026-07-28 entry (`createMcpHandler`) requires — it builds a fresh server
6060
+ * per request and has no session concept at all — and it is equally what the SDK
6061
+ * recommends for stateless 2025-era serving, so one factory backs both eras.
6061
6062
  */ exports.McpServerFactoryService = McpServerFactoryService_1 = /*#__PURE__*/ function() {
6062
6063
  function McpServerFactoryService(mcpConfig, dispatchService, modelApiGetService, roleReader, analyticsService, storageService) {
6063
6064
  _class_call_check$3(this, McpServerFactoryService);
@@ -6100,13 +6101,13 @@ var McpServerFactoryService_1;
6100
6101
  var _this_mcpConfig_serverName, _this_mcpConfig_serverVersion, _this_mcpConfig_serverInstructions;
6101
6102
  var baseServerName = (_this_mcpConfig_serverName = this.mcpConfig.serverName) !== null && _this_mcpConfig_serverName !== void 0 ? _this_mcpConfig_serverName : DEFAULT_MCP_SERVER_NAME;
6102
6103
  var serverName = this.mcpConfig.readOnly === true ? "".concat(baseServerName, " (read-only)") : baseServerName;
6103
- var server = new mcp_js.McpServer({
6104
+ var server$1 = new server.McpServer({
6104
6105
  name: serverName,
6105
6106
  version: (_this_mcpConfig_serverVersion = this.mcpConfig.serverVersion) !== null && _this_mcpConfig_serverVersion !== void 0 ? _this_mcpConfig_serverVersion : '0.0.0'
6106
6107
  }, {
6107
6108
  instructions: (_this_mcpConfig_serverInstructions = this.mcpConfig.serverInstructions) !== null && _this_mcpConfig_serverInstructions !== void 0 ? _this_mcpConfig_serverInstructions : DEFAULT_MCP_SERVER_INSTRUCTIONS
6108
6109
  });
6109
- server.server.registerCapabilities({
6110
+ server$1.server.registerCapabilities({
6110
6111
  tools: {}
6111
6112
  });
6112
6113
  var toolGeneration = this._resolveToolDefinitions();
@@ -6135,7 +6136,7 @@ var McpServerFactoryService_1;
6135
6136
  batchTool
6136
6137
  ]);
6137
6138
  }
6138
- server.server.setRequestHandler(types_js.ListToolsRequestSchema, function() {
6139
+ server$1.server.setRequestHandler('tools/list', function() {
6139
6140
  return _async_to_generator$2(function() {
6140
6141
  var _this;
6141
6142
  return _ts_generator$2(this, function(_state) {
@@ -6143,6 +6144,10 @@ var McpServerFactoryService_1;
6143
6144
  return [
6144
6145
  2,
6145
6146
  {
6147
+ // McpToolListEntry types its schemas as opaque `object`: they are read from a manifest file
6148
+ // or produced by arktype's JSON Schema export, so the `type: 'object'` root the SDK's wire
6149
+ // type mandates holds at runtime but cannot be proven statically. Every schema reaching here
6150
+ // either came from arktypeToJsonSchemaForExport or defaulted to `{ type: 'object' }`.
6146
6151
  tools: listedTools.map(function(tool) {
6147
6152
  return _this._buildToolListEntry(tool, ctx, scopes);
6148
6153
  })
@@ -6151,7 +6156,7 @@ var McpServerFactoryService_1;
6151
6156
  });
6152
6157
  }).call(_this);
6153
6158
  });
6154
- server.server.setRequestHandler(types_js.CallToolRequestSchema, function(request) {
6159
+ server$1.server.setRequestHandler('tools/call', function(request) {
6155
6160
  return _async_to_generator$2(function() {
6156
6161
  return _ts_generator$2(this, function(_state) {
6157
6162
  return [
@@ -6161,7 +6166,7 @@ var McpServerFactoryService_1;
6161
6166
  });
6162
6167
  }).call(_this);
6163
6168
  });
6164
- return server;
6169
+ return server$1;
6165
6170
  }
6166
6171
  },
6167
6172
  {
@@ -7236,29 +7241,168 @@ function _ts_generator$1(thisArg, body) {
7236
7241
  }
7237
7242
  }
7238
7243
  /**
7239
- * Handles a single Streamable HTTP JSON-RPC request against the supplied MCP server.
7244
+ * Handles a single Streamable HTTP JSON-RPC request, building a fresh MCP server for it.
7240
7245
  *
7241
- * Each call creates a fresh `StreamableHTTPServerTransport` in stateless mode — the
7242
- * transport binds the server, processes one request, and is discarded. Stateless
7243
- * mode is adequate for Claude custom-connector usage and avoids the bookkeeping
7244
- * cost of session-tracked transports for the common case.
7246
+ * Both protocol eras are served, each by its own leg:
7247
+ *
7248
+ * - **2026-07-28 (modern)** — `createMcpHandler`, which is per-request and stateless by
7249
+ * construction: there is no `Mcp-Session-Id` and no transport bookkeeping.
7250
+ * - **2025-era (legacy)** — a `NodeStreamableHTTPServerTransport` in stateless mode, which is
7251
+ * byte-for-byte what this endpoint served before the SDK v2 upgrade.
7252
+ *
7253
+ * The legacy leg is wired by hand (the SDK's documented `isLegacyRequest` composition) rather
7254
+ * than left to `createMcpHandler`'s built-in `legacy: 'stateless'` fallback, because that
7255
+ * fallback builds its transport with `sessionIdGenerator` alone and therefore answers in
7256
+ * `text/event-stream`. See {@link handleLegacyMcpRequest} for why that framing is not viable
7257
+ * here.
7245
7258
  *
7246
7259
  * The caller is expected to have already validated the bearer token (via the
7247
7260
  * OIDC bearer middleware) before this function runs.
7248
7261
  *
7249
7262
  * @param req - The Express request carrying the JSON-RPC body.
7250
7263
  * @param res - The Express response.
7264
+ * @param buildServer - Factory producing the MCP server that backs this request.
7265
+ */ function handleStreamableHttpMcpRequest(req, res, buildServer) {
7266
+ return _async_to_generator$1(function() {
7267
+ var probe;
7268
+ return _ts_generator$1(this, function(_state) {
7269
+ switch(_state.label){
7270
+ case 0:
7271
+ // Hand-wired compositions must reject non-JSON POST bodies themselves — neither the modern
7272
+ // entry's routing nor the legacy transport does it on our behalf once we classify up front.
7273
+ if (!server.isJsonContentType(req.headers['content-type'])) {
7274
+ res.status(415).json({
7275
+ statusCode: 415,
7276
+ message: 'Unsupported Media Type'
7277
+ });
7278
+ return [
7279
+ 2
7280
+ ];
7281
+ }
7282
+ return [
7283
+ 4,
7284
+ node.toWebRequest(req, req.body)
7285
+ ];
7286
+ case 1:
7287
+ probe = _state.sent();
7288
+ return [
7289
+ 4,
7290
+ server.isLegacyRequest(probe)
7291
+ ];
7292
+ case 2:
7293
+ if (!_state.sent()) return [
7294
+ 3,
7295
+ 4
7296
+ ];
7297
+ return [
7298
+ 4,
7299
+ handleLegacyMcpRequest(req, res, buildServer())
7300
+ ];
7301
+ case 3:
7302
+ _state.sent();
7303
+ return [
7304
+ 3,
7305
+ 6
7306
+ ];
7307
+ case 4:
7308
+ return [
7309
+ 4,
7310
+ handleModernMcpRequest(req, res, buildServer)
7311
+ ];
7312
+ case 5:
7313
+ _state.sent();
7314
+ _state.label = 6;
7315
+ case 6:
7316
+ return [
7317
+ 2
7318
+ ];
7319
+ }
7320
+ });
7321
+ })();
7322
+ }
7323
+ /**
7324
+ * Serves one 2026-07-28 request through `createMcpHandler`.
7325
+ *
7326
+ * `legacy: 'reject'` keeps this leg modern-only — 2025-era traffic never reaches it, having
7327
+ * already been routed to {@link handleLegacyMcpRequest}.
7328
+ *
7329
+ * `responseMode: 'json'` is the modern-leg equivalent of the legacy transport's
7330
+ * `enableJsonResponse`, keeping the endpoint on a single `application/json` body. Safe here
7331
+ * because no tool handler emits mid-call notifications, which are the only thing `'json'` drops.
7332
+ *
7333
+ * @param req - The Express request carrying the JSON-RPC body.
7334
+ * @param res - The Express response.
7335
+ * @param buildServer - Factory producing the MCP server that backs this request.
7336
+ */ function handleModernMcpRequest(req, res, buildServer) {
7337
+ return _async_to_generator$1(function() {
7338
+ var handler;
7339
+ return _ts_generator$1(this, function(_state) {
7340
+ switch(_state.label){
7341
+ case 0:
7342
+ handler = server.createMcpHandler(buildServer, {
7343
+ legacy: 'reject',
7344
+ responseMode: 'json'
7345
+ });
7346
+ _state.label = 1;
7347
+ case 1:
7348
+ _state.trys.push([
7349
+ 1,
7350
+ ,
7351
+ 3,
7352
+ 5
7353
+ ]);
7354
+ return [
7355
+ 4,
7356
+ node.toNodeHandler(handler)(req, res, req.body)
7357
+ ];
7358
+ case 2:
7359
+ _state.sent();
7360
+ return [
7361
+ 3,
7362
+ 5
7363
+ ];
7364
+ case 3:
7365
+ return [
7366
+ 4,
7367
+ handler.close()
7368
+ ];
7369
+ case 4:
7370
+ _state.sent();
7371
+ return [
7372
+ 7
7373
+ ];
7374
+ case 5:
7375
+ return [
7376
+ 2
7377
+ ];
7378
+ }
7379
+ });
7380
+ })();
7381
+ }
7382
+ /**
7383
+ * Serves one 2025-era request through a stateless `NodeStreamableHTTPServerTransport`.
7384
+ *
7385
+ * `sessionIdGenerator: undefined` selects stateless mode: the transport binds the server,
7386
+ * processes one request, and is discarded.
7387
+ *
7388
+ * `enableJsonResponse: true` replies with a single `application/json` body instead of framing it
7389
+ * as `text/event-stream`. Stateless mode answers exactly one JSON-RPC message per request and the
7390
+ * controller only implements POST (there is no standalone SSE stream to keep open), so SSE framing
7391
+ * adds nothing while making the endpoint fragile behind proxies that buffer or rewrite streamed
7392
+ * responses — the failure that originally pushed the demo app off its dev proxy.
7393
+ *
7394
+ * @param req - The Express request carrying the JSON-RPC body.
7395
+ * @param res - The Express response.
7251
7396
  * @param server - The MCP server instance to back the transport.
7252
- * @param server.connect - The MCP server's connect method that binds the transport before request handling.
7253
- */ function handleStreamableHttpMcpRequest(req, res, server) {
7397
+ */ function handleLegacyMcpRequest(req, res, server) {
7254
7398
  return _async_to_generator$1(function() {
7255
7399
  var transport;
7256
7400
  return _ts_generator$1(this, function(_state) {
7257
7401
  switch(_state.label){
7258
7402
  case 0:
7259
- // sessionIdGenerator: undefined → stateless mode.
7260
- transport = new streamableHttp_js.StreamableHTTPServerTransport({
7261
- sessionIdGenerator: undefined
7403
+ transport = new node.NodeStreamableHTTPServerTransport({
7404
+ sessionIdGenerator: undefined,
7405
+ enableJsonResponse: true
7262
7406
  });
7263
7407
  res.on('close', function() {
7264
7408
  void transport.close();
@@ -7450,9 +7594,11 @@ var McpController_1;
7450
7594
  * which must include `'/mcp'` in its `protectedPaths`. By the time the request reaches
7451
7595
  * this controller, `req.auth` is populated with the authenticated user's data.
7452
7596
  *
7453
- * Each request gets a fresh transport + MCP server pair (stateless mode), which is
7454
- * adequate for Claude custom-connector style usage. A session-tracked variant can
7455
- * be layered on later if streaming tool output becomes a requirement.
7597
+ * Each request gets a fresh MCP server (stateless mode), which is adequate for Claude
7598
+ * custom-connector style usage. Both protocol eras are served — 2026-07-28 through the
7599
+ * SDK's per-request `createMcpHandler` entry, and 2025-era through a stateless Streamable
7600
+ * HTTP transport. A session-tracked variant can be layered on later if streaming tool
7601
+ * output becomes a requirement.
7456
7602
  */ exports.McpController = McpController_1 = /*#__PURE__*/ function() {
7457
7603
  function McpController(factory) {
7458
7604
  _class_call_check$2(this, McpController);
@@ -7465,15 +7611,12 @@ var McpController_1;
7465
7611
  key: "handleMcpRequest",
7466
7612
  value: function handleMcpRequest(req, res) {
7467
7613
  return _async_to_generator(function() {
7468
- var auth, server, error;
7614
+ var _this, auth, error;
7469
7615
  return _ts_generator(this, function(_state) {
7470
7616
  switch(_state.label){
7471
7617
  case 0:
7618
+ _this = this;
7472
7619
  auth = req.auth;
7473
- server = this.factory.createServer({
7474
- auth: auth,
7475
- rawRequest: req
7476
- });
7477
7620
  _state.label = 1;
7478
7621
  case 1:
7479
7622
  _state.trys.push([
@@ -7484,7 +7627,12 @@ var McpController_1;
7484
7627
  ]);
7485
7628
  return [
7486
7629
  4,
7487
- handleStreamableHttpMcpRequest(req, res, server)
7630
+ handleStreamableHttpMcpRequest(req, res, function() {
7631
+ return _this.factory.createServer({
7632
+ auth: auth,
7633
+ rawRequest: req
7634
+ });
7635
+ })
7488
7636
  ];
7489
7637
  case 2:
7490
7638
  _state.sent();
@@ -7513,6 +7661,45 @@ var McpController_1;
7513
7661
  });
7514
7662
  }).call(this);
7515
7663
  }
7664
+ },
7665
+ {
7666
+ /**
7667
+ * Rejects the Streamable HTTP transport's optional `GET` method, which opens a standalone SSE
7668
+ * stream. Not applicable in stateless mode. Separate from {@link handleUnsupportedDelete} because
7669
+ * NestJS binds one HTTP method per handler — stacking route decorators would silently drop one.
7670
+ *
7671
+ * @param res - The Express response to write the rejection to.
7672
+ */ key: "handleUnsupportedGet",
7673
+ value: function handleUnsupportedGet(res) {
7674
+ this._rejectUnsupportedMethod(res);
7675
+ }
7676
+ },
7677
+ {
7678
+ /**
7679
+ * Rejects the Streamable HTTP transport's optional `DELETE` method, which tears down a session.
7680
+ * Not applicable in stateless mode — no session is ever issued.
7681
+ *
7682
+ * @param res - The Express response to write the rejection to.
7683
+ */ key: "handleUnsupportedDelete",
7684
+ value: function handleUnsupportedDelete(res) {
7685
+ this._rejectUnsupportedMethod(res);
7686
+ }
7687
+ },
7688
+ {
7689
+ /**
7690
+ * Answers with a spec-conformant `405 Method Not Allowed` + `Allow` header. Without these
7691
+ * handlers NestJS answers `404`, which some clients treat as a hard failure rather than
7692
+ * "the server doesn't offer this".
7693
+ *
7694
+ * @param res - The Express response to write the rejection to.
7695
+ */ key: "_rejectUnsupportedMethod",
7696
+ value: function _rejectUnsupportedMethod(res) {
7697
+ res.setHeader('Allow', 'POST');
7698
+ res.status(common.HttpStatus.METHOD_NOT_ALLOWED).json({
7699
+ statusCode: common.HttpStatus.METHOD_NOT_ALLOWED,
7700
+ message: 'Method Not Allowed'
7701
+ });
7702
+ }
7516
7703
  }
7517
7704
  ]);
7518
7705
  return McpController;
@@ -7522,6 +7709,14 @@ __decorate([
7522
7709
  __param(0, common.Req()),
7523
7710
  __param(1, common.Res())
7524
7711
  ], exports.McpController.prototype, "handleMcpRequest", null);
7712
+ __decorate([
7713
+ common.Get(),
7714
+ __param(0, common.Res())
7715
+ ], exports.McpController.prototype, "handleUnsupportedGet", null);
7716
+ __decorate([
7717
+ common.Delete(),
7718
+ __param(0, common.Res())
7719
+ ], exports.McpController.prototype, "handleUnsupportedDelete", null);
7525
7720
  exports.McpController = McpController_1 = __decorate([
7526
7721
  common.Controller('mcp'),
7527
7722
  __param(0, common.Inject(exports.McpServerFactoryService))
@@ -7607,6 +7802,13 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7607
7802
  * URIs must live at the host root. Apps need to exclude `.well-known/{*path}`
7608
7803
  * from any global API route prefix (see `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`
7609
7804
  * in `firebase-server/oidc` for the corresponding OIDC excludes).
7805
+ *
7806
+ * Because the document must be reachable at the origin root, the MCP endpoint has to be
7807
+ * exposed through a host that serves the app at `/` — Firebase Hosting, or the hosting
7808
+ * emulator in development. Pointing a client straight at the Functions emulator origin
7809
+ * (`http://localhost:<port>/<project>/<region>/<function>/mcp`) breaks discovery: that
7810
+ * runtime only routes `/<project>/<region>/<function>/…`, so nothing answers at the root
7811
+ * and a client that has not yet seen a 401 challenge cannot find the issuer.
7610
7812
  */ exports.McpWellKnownController = /*#__PURE__*/ function() {
7611
7813
  function McpWellKnownController(mcpConfig, oidcProviderConfigService) {
7612
7814
  _class_call_check$1(this, McpWellKnownController);
@@ -7617,12 +7819,26 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7617
7819
  }
7618
7820
  _create_class$1(McpWellKnownController, [
7619
7821
  {
7620
- key: "getProtectedResourceMetadata",
7822
+ /**
7823
+ * Serves both RFC 9728 discovery URLs — the primary form (§3.1), which inserts the resource's
7824
+ * path after the well-known prefix (`/.well-known/oauth-protected-resource/mcp`), and the bare
7825
+ * form (`/.well-known/oauth-protected-resource`) that clients probe as a fallback.
7826
+ *
7827
+ * The document is identical either way; the resource identity comes from
7828
+ * {@link McpModuleConfig.mcpUrl}, not from the request path. Serving the primary form means
7829
+ * discovery succeeds on a client's first probe rather than depending on it implementing the
7830
+ * fallback.
7831
+ *
7832
+ * @returns The protected-resource metadata document.
7833
+ */ key: "getProtectedResourceMetadata",
7621
7834
  value: function getProtectedResourceMetadata() {
7622
7835
  var _this_mcpConfig = this.mcpConfig, mcpUrl = _this_mcpConfig.mcpUrl, oidcIssuer = _this_mcpConfig.oidcIssuer, scopesFilter = _this_mcpConfig.scopesSupported;
7623
- // Base scope list is the OIDC provider's own `scopes_supported`, so the resource
7624
- // advertises exactly what the issuer grants; the optional filter narrows it.
7625
- var providerScopes = this.oidcProviderConfigService.scopesSupported;
7836
+ // Base scope list is what the issuer grants an arbitrary client, NOT its full
7837
+ // `scopes_supported`: a dynamic-registration client copies this list verbatim onto
7838
+ // `/authorize`, and a provider-profile-gated scope in it would be hard-rejected at the
7839
+ // consent unlock gate (`access_denied`) for every client lacking that profile assignment.
7840
+ // The optional filter narrows the list further.
7841
+ var providerScopes = this.oidcProviderConfigService.clientRequestableScopesSupported;
7626
7842
  var scopes = scopesFilter ? scopesFilter(providerScopes) : providerScopes;
7627
7843
  return _object_spread({
7628
7844
  resource: mcpUrl,
@@ -7638,7 +7854,10 @@ function _unsupported_iterable_to_array$2(o, minLen) {
7638
7854
  return McpWellKnownController;
7639
7855
  }();
7640
7856
  __decorate([
7641
- common.Get('oauth-protected-resource')
7857
+ common.Get([
7858
+ 'oauth-protected-resource',
7859
+ 'oauth-protected-resource/{*path}'
7860
+ ])
7642
7861
  ], exports.McpWellKnownController.prototype, "getProtectedResourceMetadata", null);
7643
7862
  exports.McpWellKnownController = __decorate([
7644
7863
  common.Controller('.well-known'),