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